Commit dbb12759 by Peter Fogg

Merge peter-fogg/fix-video-captions-setting.

parents 5b9e88e2 45527bf9
...@@ -10,6 +10,8 @@ ...@@ -10,6 +10,8 @@
.AppleDouble .AppleDouble
database.sqlite database.sqlite
requirements/private.txt requirements/private.txt
lms/envs/private.py
cms/envs/private.py
courseware/static/js/mathjax/* courseware/static/js/mathjax/*
flushdb.sh flushdb.sh
build build
...@@ -27,6 +29,7 @@ conf/locale/en/LC_MESSAGES/*.po ...@@ -27,6 +29,7 @@ conf/locale/en/LC_MESSAGES/*.po
!messages.po !messages.po
lms/static/sass/*.css lms/static/sass/*.css
lms/static/sass/application.scss lms/static/sass/application.scss
lms/static/sass/course.scss
cms/static/sass/*.css cms/static/sass/*.css
lms/lib/comment_client/python lms/lib/comment_client/python
nosetests.xml nosetests.xml
......
...@@ -52,7 +52,7 @@ Feature: Problem Editor ...@@ -52,7 +52,7 @@ Feature: Problem Editor
Scenario: User cannot type out of range values in an integer number field Scenario: User cannot type out of range values in an integer number field
Given I have created a Blank Common Problem Given I have created a Blank Common Problem
And I edit and select Settings And I edit and select Settings
Then if I set the max attempts to "-3", it displays initially as "-3", and is persisted as "1" Then if I set the max attempts to "-3", it displays initially as "-3", and is persisted as "0"
Scenario: Settings changes are not saved on Cancel Scenario: Settings changes are not saved on Cancel
Given I have created a Blank Common Problem Given I have created a Blank Common Problem
......
...@@ -26,11 +26,9 @@ Feature: Create Section ...@@ -26,11 +26,9 @@ Feature: Create Section
And I save a new section release date And I save a new section release date
Then the section release date is updated Then the section release date is updated
# Skipped because Ubuntu ChromeDriver hangs on alert
@skip
Scenario: Delete section Scenario: Delete section
Given I have opened a new course in Studio Given I have opened a new course in Studio
And I have added a new section And I have added a new section
When I press the "section" delete icon When I will confirm all alerts
And I confirm the alert And I press the "section" delete icon
Then the section does not exist Then the section does not exist
...@@ -69,8 +69,8 @@ def i_see_complete_section_name_with_quote_in_editor(step): ...@@ -69,8 +69,8 @@ def i_see_complete_section_name_with_quote_in_editor(step):
@step('the section does not exist$') @step('the section does not exist$')
def section_does_not_exist(step): def section_does_not_exist(step):
css = 'span.section-name-span' css = 'h3[data-name="My Section"]'
assert world.browser.is_element_not_present_by_css(css) assert world.is_css_not_present(css)
@step('I see a release date for my section$') @step('I see a release date for my section$')
......
Feature: Overview Toggle Section Feature: Overview Toggle Section
In order to quickly view the details of a course's section or to scan the inventory of sections In order to quickly view the details of a course's section or to scan the inventory of sections
As a course author As a course author
I want to toggle the visibility of each section's subsection details in the overview listing I want to toggle the visibility of each section's subsection details in the overview listing
Scenario: The default layout for the overview page is to show sections in expanded view Scenario: The default layout for the overview page is to show sections in expanded view
Given I have a course with multiple sections Given I have a course with multiple sections
When I navigate to the course overview page When I navigate to the course overview page
Then I see the "Collapse All Sections" link Then I see the "Collapse All Sections" link
And all sections are expanded And all sections are expanded
Scenario: Expand /collapse for a course with no sections Scenario: Expand /collapse for a course with no sections
Given I have a course with no sections Given I have a course with no sections
When I navigate to the course overview page When I navigate to the course overview page
Then I do not see the "Collapse All Sections" link Then I do not see the "Collapse All Sections" link
Scenario: Collapse link appears after creating first section of a course Scenario: Collapse link appears after creating first section of a course
Given I have a course with no sections Given I have a course with no sections
When I navigate to the course overview page When I navigate to the course overview page
And I add a section And I add a section
Then I see the "Collapse All Sections" link Then I see the "Collapse All Sections" link
And all sections are expanded And all sections are expanded
# Skipped because Ubuntu ChromeDriver hangs on alert Scenario: Collapse link is not removed after last section of a course is deleted
@skip Given I have a course with 1 section
Scenario: Collapse link is not removed after last section of a course is deleted And I navigate to the course overview page
Given I have a course with 1 section When I will confirm all alerts
And I navigate to the course overview page And I press the "section" delete icon
When I press the "section" delete icon Then I see the "Collapse All Sections" link
And I confirm the alert
Then I see the "Collapse All Sections" link Scenario: Collapsing all sections when all sections are expanded
Given I navigate to the courseware page of a course with multiple sections
Scenario: Collapsing all sections when all sections are expanded And all sections are expanded
Given I navigate to the courseware page of a course with multiple sections When I click the "Collapse All Sections" link
And all sections are expanded Then I see the "Expand All Sections" link
When I click the "Collapse All Sections" link And all sections are collapsed
Then I see the "Expand All Sections" link
And all sections are collapsed Scenario: Collapsing all sections when 1 or more sections are already collapsed
Given I navigate to the courseware page of a course with multiple sections
Scenario: Collapsing all sections when 1 or more sections are already collapsed And all sections are expanded
Given I navigate to the courseware page of a course with multiple sections When I collapse the first section
And all sections are expanded And I click the "Collapse All Sections" link
When I collapse the first section Then I see the "Expand All Sections" link
And I click the "Collapse All Sections" link And all sections are collapsed
Then I see the "Expand All Sections" link
And all sections are collapsed Scenario: Expanding all sections when all sections are collapsed
Given I navigate to the courseware page of a course with multiple sections
Scenario: Expanding all sections when all sections are collapsed And I click the "Collapse All Sections" link
Given I navigate to the courseware page of a course with multiple sections When I click the "Expand All Sections" link
And I click the "Collapse All Sections" link Then I see the "Collapse All Sections" link
When I click the "Expand All Sections" link And all sections are expanded
Then I see the "Collapse All Sections" link
And all sections are expanded Scenario: Expanding all sections when 1 or more sections are already expanded
Given I navigate to the courseware page of a course with multiple sections
Scenario: Expanding all sections when 1 or more sections are already expanded And I click the "Collapse All Sections" link
Given I navigate to the courseware page of a course with multiple sections When I expand the first section
And I click the "Collapse All Sections" link And I click the "Expand All Sections" link
When I expand the first section Then I see the "Collapse All Sections" link
And I click the "Expand All Sections" link And all sections are expanded
Then I see the "Collapse All Sections" link
And all sections are expanded
...@@ -32,12 +32,10 @@ Feature: Create Subsection ...@@ -32,12 +32,10 @@ Feature: Create Subsection
And I reload the page And I reload the page
Then I see the correct dates Then I see the correct dates
# Skipped because Ubuntu ChromeDriver hangs on alert
@skip
Scenario: Delete a subsection Scenario: Delete a subsection
Given I have opened a new course section in Studio Given I have opened a new course section in Studio
And I have added a new subsection And I have added a new subsection
And I see my subsection on the Courseware page And I see my subsection on the Courseware page
When I press the "subsection" delete icon When I will confirm all alerts
And I confirm the alert And I press the "subsection" delete icon
Then the subsection does not exist Then the subsection does not exist
...@@ -8,3 +8,8 @@ Feature: Video Component ...@@ -8,3 +8,8 @@ Feature: Video Component
Scenario: Creating a video takes a single click Scenario: Creating a video takes a single click
Given I have clicked the new unit button Given I have clicked the new unit button
Then creating a video takes a single click Then creating a video takes a single click
Scenario: Captions are shown correctly
Given I have created a Video component
And I have hidden captions
Then when I view the video it does not show the captions
...@@ -16,3 +16,13 @@ def video_takes_a_single_click(step): ...@@ -16,3 +16,13 @@ def video_takes_a_single_click(step):
assert(not world.is_css_present('.xmodule_VideoModule')) assert(not world.is_css_present('.xmodule_VideoModule'))
world.css_click("a[data-location='i4x://edx/templates/video/default']") world.css_click("a[data-location='i4x://edx/templates/video/default']")
assert(world.is_css_present('.xmodule_VideoModule')) assert(world.is_css_present('.xmodule_VideoModule'))
@step('I have hidden captions')
def set_show_captions_false(step):
world.css_click('a.hide-subtitles')
@step('when I view the video it does not show the captions')
def does_not_show_captions(step):
assert world.css_find('.video')[0].has_class('closed')
...@@ -37,6 +37,9 @@ from xmodule.modulestore.exceptions import ItemNotFoundError ...@@ -37,6 +37,9 @@ from xmodule.modulestore.exceptions import ItemNotFoundError
from contentstore.views.component import ADVANCED_COMPONENT_TYPES from contentstore.views.component import ADVANCED_COMPONENT_TYPES
from django_comment_common.utils import are_permissions_roles_seeded from django_comment_common.utils import are_permissions_roles_seeded
from xmodule.exceptions import InvalidVersionError
import datetime
from pytz import UTC
TEST_DATA_MODULESTORE = copy.deepcopy(settings.MODULESTORE) TEST_DATA_MODULESTORE = copy.deepcopy(settings.MODULESTORE)
TEST_DATA_MODULESTORE['default']['OPTIONS']['fs_root'] = path('common/test/data') TEST_DATA_MODULESTORE['default']['OPTIONS']['fs_root'] = path('common/test/data')
...@@ -120,6 +123,17 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -120,6 +123,17 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
def test_advanced_components_require_two_clicks(self): def test_advanced_components_require_two_clicks(self):
self.check_components_on_page(['videoalpha'], ['Video Alpha']) self.check_components_on_page(['videoalpha'], ['Video Alpha'])
def test_malformed_edit_unit_request(self):
store = modulestore('direct')
import_from_xml(store, 'common/test/data/', ['simple'])
# just pick one vertical
descriptor = store.get_items(Location('i4x', 'edX', 'simple', 'vertical', None, None))[0]
location = descriptor.location._replace(name='.' + descriptor.location.name)
resp = self.client.get(reverse('edit_unit', kwargs={'location': location.url()}))
self.assertEqual(resp.status_code, 400)
def check_edit_unit(self, test_course_name): def check_edit_unit(self, test_course_name):
import_from_xml(modulestore('direct'), 'common/test/data/', [test_course_name]) import_from_xml(modulestore('direct'), 'common/test/data/', [test_course_name])
...@@ -404,6 +418,32 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -404,6 +418,32 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
resp = self.client.get(reverse('edit_unit', kwargs={'location': new_loc.url()})) resp = self.client.get(reverse('edit_unit', kwargs={'location': new_loc.url()}))
self.assertEqual(resp.status_code, 200) self.assertEqual(resp.status_code, 200)
def test_illegal_draft_crud_ops(self):
draft_store = modulestore('draft')
direct_store = modulestore('direct')
CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course')
location = Location('i4x://MITx/999/chapter/neuvo')
self.assertRaises(InvalidVersionError, draft_store.clone_item, 'i4x://edx/templates/chapter/Empty',
location)
direct_store.clone_item('i4x://edx/templates/chapter/Empty', location)
self.assertRaises(InvalidVersionError, draft_store.clone_item, location,
location)
self.assertRaises(InvalidVersionError, draft_store.update_item, location,
'chapter data')
# taking advantage of update_children and other functions never checking that the ids are valid
self.assertRaises(InvalidVersionError, draft_store.update_children, location,
['i4x://MITx/999/problem/doesntexist'])
self.assertRaises(InvalidVersionError, draft_store.update_metadata, location,
{'due': datetime.datetime.now(UTC)})
self.assertRaises(InvalidVersionError, draft_store.unpublish, location)
def test_bad_contentstore_request(self): def test_bad_contentstore_request(self):
resp = self.client.get('http://localhost:8001/c4x/CDX/123123/asset/&images_circuits_Lab7Solution2.png') resp = self.client.get('http://localhost:8001/c4x/CDX/123123/asset/&images_circuits_Lab7Solution2.png')
self.assertEqual(resp.status_code, 400) self.assertEqual(resp.status_code, 400)
...@@ -486,6 +526,9 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -486,6 +526,9 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
# check for custom_tags # check for custom_tags
self.verify_content_existence(module_store, root_dir, location, 'custom_tags', 'custom_tag_template') self.verify_content_existence(module_store, root_dir, location, 'custom_tags', 'custom_tag_template')
# check for about content
self.verify_content_existence(module_store, root_dir, location, 'about', 'about', '.html')
# check for graiding_policy.json # check for graiding_policy.json
filesystem = OSFS(root_dir / 'test_export/policies/6.002_Spring_2012') filesystem = OSFS(root_dir / 'test_export/policies/6.002_Spring_2012')
self.assertTrue(filesystem.exists('grading_policy.json')) self.assertTrue(filesystem.exists('grading_policy.json'))
......
...@@ -24,6 +24,30 @@ class LMSLinksTestCase(TestCase): ...@@ -24,6 +24,30 @@ class LMSLinksTestCase(TestCase):
with mock.patch.dict('django.conf.settings.MITX_FEATURES', {'ENABLE_MKTG_SITE': False}): with mock.patch.dict('django.conf.settings.MITX_FEATURES', {'ENABLE_MKTG_SITE': False}):
self.assertEquals(self.get_about_page_link(), "//localhost:8000/courses/mitX/101/test/about") self.assertEquals(self.get_about_page_link(), "//localhost:8000/courses/mitX/101/test/about")
@override_settings(MKTG_URLS={'ROOT': 'http://www.dummy'})
def about_page_marketing_site_remove_http_test(self):
""" Get URL for about page, marketing root present, remove http://. """
with mock.patch.dict('django.conf.settings.MITX_FEATURES', {'ENABLE_MKTG_SITE': True}):
self.assertEquals(self.get_about_page_link(), "//www.dummy/courses/mitX/101/test/about")
@override_settings(MKTG_URLS={'ROOT': 'https://www.dummy'})
def about_page_marketing_site_remove_https_test(self):
""" Get URL for about page, marketing root present, remove https://. """
with mock.patch.dict('django.conf.settings.MITX_FEATURES', {'ENABLE_MKTG_SITE': True}):
self.assertEquals(self.get_about_page_link(), "//www.dummy/courses/mitX/101/test/about")
@override_settings(MKTG_URLS={'ROOT': 'www.dummyhttps://x'})
def about_page_marketing_site_https__edge_test(self):
""" Get URL for about page, only remove https:// at the beginning of the string. """
with mock.patch.dict('django.conf.settings.MITX_FEATURES', {'ENABLE_MKTG_SITE': True}):
self.assertEquals(self.get_about_page_link(), "//www.dummyhttps://x/courses/mitX/101/test/about")
@override_settings(MKTG_URLS={})
def about_page_marketing_urls_not_set_test(self):
""" Error case. ENABLE_MKTG_SITE is True, but there is either no MKTG_URLS, or no MKTG_URLS Root property. """
with mock.patch.dict('django.conf.settings.MITX_FEATURES', {'ENABLE_MKTG_SITE': True}):
self.assertEquals(self.get_about_page_link(), None)
@override_settings(LMS_BASE=None) @override_settings(LMS_BASE=None)
def about_page_no_lms_base_test(self): def about_page_no_lms_base_test(self):
""" No LMS_BASE, nor is ENABLE_MKTG_SITE True """ """ No LMS_BASE, nor is ENABLE_MKTG_SITE True """
......
...@@ -4,8 +4,11 @@ from xmodule.modulestore.django import modulestore ...@@ -4,8 +4,11 @@ from xmodule.modulestore.django import modulestore
from xmodule.modulestore.exceptions import ItemNotFoundError from xmodule.modulestore.exceptions import ItemNotFoundError
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
import copy import copy
import logging
import re
from xmodule.modulestore.draft import DIRECT_ONLY_CATEGORIES
DIRECT_ONLY_CATEGORIES = ['course', 'chapter', 'sequential', 'about', 'static_tab', 'course_info'] log = logging.getLogger(__name__)
#In order to instantiate an open ended tab automatically, need to have this data #In order to instantiate an open ended tab automatically, need to have this data
OPEN_ENDED_PANEL = {"name": "Open Ended Panel", "type": "open_ended"} OPEN_ENDED_PANEL = {"name": "Open Ended Panel", "type": "open_ended"}
...@@ -108,9 +111,20 @@ def get_lms_link_for_about_page(location): ...@@ -108,9 +111,20 @@ def get_lms_link_for_about_page(location):
Returns the url to the course about page from the location tuple. Returns the url to the course about page from the location tuple.
""" """
if settings.MITX_FEATURES.get('ENABLE_MKTG_SITE', False): if settings.MITX_FEATURES.get('ENABLE_MKTG_SITE', False):
# Root will be "www.edx.org". The complete URL will still not be exactly correct, if not hasattr(settings, 'MKTG_URLS'):
# but redirects exist from www.edx.org to get to the drupal course about page URL. log.exception("ENABLE_MKTG_SITE is True, but MKTG_URLS is not defined.")
about_base = settings.MKTG_URLS.get('ROOT') about_base = None
else:
marketing_urls = settings.MKTG_URLS
if marketing_urls.get('ROOT', None) is None:
log.exception('There is no ROOT defined in MKTG_URLS')
about_base = None
else:
# Root will be "https://www.edx.org". The complete URL will still not be exactly correct,
# but redirects exist from www.edx.org to get to the Drupal course about page URL.
about_base = marketing_urls.get('ROOT')
# Strip off https:// (or http://) to be consistent with the formatting of LMS_BASE.
about_base = re.sub(r"^https?://", "", about_base)
elif settings.LMS_BASE is not None: elif settings.LMS_BASE is not None:
about_base = settings.LMS_BASE about_base = settings.LMS_BASE
else: else:
...@@ -214,7 +228,7 @@ def add_extra_panel_tab(tab_type, course): ...@@ -214,7 +228,7 @@ def add_extra_panel_tab(tab_type, course):
course_tabs = copy.copy(course.tabs) course_tabs = copy.copy(course.tabs)
changed = False changed = False
#Check to see if open ended panel is defined in the course #Check to see if open ended panel is defined in the course
tab_panel = EXTRA_TAB_PANELS.get(tab_type) tab_panel = EXTRA_TAB_PANELS.get(tab_type)
if tab_panel not in course_tabs: if tab_panel not in course_tabs:
#Add panel to the tabs if it is not defined #Add panel to the tabs if it is not defined
......
...@@ -7,7 +7,7 @@ from django.contrib.auth.decorators import login_required ...@@ -7,7 +7,7 @@ from django.contrib.auth.decorators import login_required
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django_future.csrf import ensure_csrf_cookie from django_future.csrf import ensure_csrf_cookie
from django.conf import settings from django.conf import settings
from xmodule.modulestore.exceptions import ItemNotFoundError, InvalidLocationError
from mitxmako.shortcuts import render_to_response from mitxmako.shortcuts import render_to_response
from xmodule.modulestore import Location from xmodule.modulestore import Location
...@@ -50,11 +50,18 @@ ADVANCED_COMPONENT_POLICY_KEY = 'advanced_modules' ...@@ -50,11 +50,18 @@ ADVANCED_COMPONENT_POLICY_KEY = 'advanced_modules'
@login_required @login_required
def edit_subsection(request, location): def edit_subsection(request, location):
# check that we have permissions to edit this item # check that we have permissions to edit this item
course = get_course_for_item(location) try:
course = get_course_for_item(location)
except InvalidLocationError:
return HttpResponseBadRequest()
if not has_access(request.user, course.location): if not has_access(request.user, course.location):
raise PermissionDenied() raise PermissionDenied()
item = modulestore().get_item(location, depth=1) try:
item = modulestore().get_item(location, depth=1)
except ItemNotFoundError:
return HttpResponseBadRequest()
lms_link = get_lms_link_for_item(location, course_id=course.location.course_id) lms_link = get_lms_link_for_item(location, course_id=course.location.course_id)
preview_link = get_lms_link_for_item(location, course_id=course.location.course_id, preview=True) preview_link = get_lms_link_for_item(location, course_id=course.location.course_id, preview=True)
...@@ -113,11 +120,18 @@ def edit_unit(request, location): ...@@ -113,11 +120,18 @@ def edit_unit(request, location):
id: A Location URL id: A Location URL
""" """
course = get_course_for_item(location) try:
course = get_course_for_item(location)
except InvalidLocationError:
return HttpResponseBadRequest()
if not has_access(request.user, course.location): if not has_access(request.user, course.location):
raise PermissionDenied() raise PermissionDenied()
item = modulestore().get_item(location, depth=1) try:
item = modulestore().get_item(location, depth=1)
except ItemNotFoundError:
return HttpResponseBadRequest()
lms_link = get_lms_link_for_item(item.location, course_id=course.location.course_id) lms_link = get_lms_link_for_item(item.location, course_id=course.location.course_id)
......
...@@ -95,13 +95,15 @@ SESSION_COOKIE_DOMAIN = ENV_TOKENS.get('SESSION_COOKIE_DOMAIN') ...@@ -95,13 +95,15 @@ SESSION_COOKIE_DOMAIN = ENV_TOKENS.get('SESSION_COOKIE_DOMAIN')
# this is to fix a bug regarding simultaneous logins between edx.org and edge.edx.org which can # this is to fix a bug regarding simultaneous logins between edx.org and edge.edx.org which can
# happen with some browsers (e.g. Firefox) # happen with some browsers (e.g. Firefox)
if ENV_TOKENS.get('SESSION_COOKIE_NAME', None): if ENV_TOKENS.get('SESSION_COOKIE_NAME', None):
SESSION_COOKIE_NAME = ENV_TOKENS.get('SESSION_COOKIE_NAME') # NOTE, there's a bug in Django (http://bugs.python.org/issue18012) which necessitates this being a str()
SESSION_COOKIE_NAME = str(ENV_TOKENS.get('SESSION_COOKIE_NAME'))
#Email overrides #Email overrides
DEFAULT_FROM_EMAIL = ENV_TOKENS.get('DEFAULT_FROM_EMAIL', DEFAULT_FROM_EMAIL) DEFAULT_FROM_EMAIL = ENV_TOKENS.get('DEFAULT_FROM_EMAIL', DEFAULT_FROM_EMAIL)
DEFAULT_FEEDBACK_EMAIL = ENV_TOKENS.get('DEFAULT_FEEDBACK_EMAIL', DEFAULT_FEEDBACK_EMAIL) DEFAULT_FEEDBACK_EMAIL = ENV_TOKENS.get('DEFAULT_FEEDBACK_EMAIL', DEFAULT_FEEDBACK_EMAIL)
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)
#Timezone overrides #Timezone overrides
TIME_ZONE = ENV_TOKENS.get('TIME_ZONE', TIME_ZONE) TIME_ZONE = ENV_TOKENS.get('TIME_ZONE', TIME_ZONE)
......
...@@ -335,3 +335,14 @@ INSTALLED_APPS = ( ...@@ -335,3 +335,14 @@ INSTALLED_APPS = (
################# EDX MARKETING SITE ################################## ################# EDX MARKETING SITE ##################################
EDXMKTG_COOKIE_NAME = 'edxloggedin' EDXMKTG_COOKIE_NAME = 'edxloggedin'
MKTG_URLS = {}
MKTG_URL_LINK_MAP = {
'ABOUT': 'about_edx',
'CONTACT': 'contact',
'FAQ': 'help_edx',
'COURSES': 'courses',
'ROOT': 'root',
'TOS': 'tos',
'HONOR': 'honor',
'PRIVACY': 'privacy_edx',
}
...@@ -165,3 +165,11 @@ MITX_FEATURES['ENABLE_SERVICE_STATUS'] = True ...@@ -165,3 +165,11 @@ MITX_FEATURES['ENABLE_SERVICE_STATUS'] = True
# segment-io key for dev # segment-io key for dev
SEGMENT_IO_KEY = 'mty8edrrsg' SEGMENT_IO_KEY = 'mty8edrrsg'
#####################################################################
# Lastly, see if the developer has any local overrides.
try:
from .private import *
except ImportError:
pass
...@@ -41,7 +41,9 @@ def marketing_link(name): ...@@ -41,7 +41,9 @@ def marketing_link(name):
return settings.MKTG_URLS.get('ROOT') + settings.MKTG_URLS.get(name) return settings.MKTG_URLS.get('ROOT') + settings.MKTG_URLS.get(name)
# only link to the old pages when the marketing site isn't on # only link to the old pages when the marketing site isn't on
elif not settings.MITX_FEATURES.get('ENABLE_MKTG_SITE') and name in link_map: elif not settings.MITX_FEATURES.get('ENABLE_MKTG_SITE') and name in link_map:
return reverse(link_map[name]) # don't try to reverse disabled marketing links
if link_map[name] is not None:
return reverse(link_map[name])
else: else:
log.warning("Cannot find corresponding link for name: {name}".format(name=name)) log.warning("Cannot find corresponding link for name: {name}".format(name=name))
return '#' return '#'
......
...@@ -4,13 +4,15 @@ from django.core.urlresolvers import reverse ...@@ -4,13 +4,15 @@ from django.core.urlresolvers import reverse
from django.conf import settings from django.conf import settings
from mitxmako.shortcuts import marketing_link from mitxmako.shortcuts import marketing_link
from mock import patch from mock import patch
from nose.plugins.skip import SkipTest
class ShortcutsTests(TestCase): class ShortcutsTests(TestCase):
""" """
Test the mitxmako shortcuts file Test the mitxmako shortcuts file
""" """
# TODO: fix this test. It is causing intermittent test failures on
# subsequent tests due to the way urls are loaded
raise SkipTest()
@override_settings(MKTG_URLS={'ROOT': 'dummy-root', 'ABOUT': '/about-us'}) @override_settings(MKTG_URLS={'ROOT': 'dummy-root', 'ABOUT': '/about-us'})
@override_settings(MKTG_URL_LINK_MAP={'ABOUT': 'login'}) @override_settings(MKTG_URL_LINK_MAP={'ABOUT': 'login'})
def test_marketing_link(self): def test_marketing_link(self):
......
...@@ -662,6 +662,7 @@ class CourseEnrollmentAllowed(models.Model): ...@@ -662,6 +662,7 @@ class CourseEnrollmentAllowed(models.Model):
""" """
email = models.CharField(max_length=255, db_index=True) email = models.CharField(max_length=255, db_index=True)
course_id = models.CharField(max_length=255, db_index=True) course_id = models.CharField(max_length=255, db_index=True)
auto_enroll = models.BooleanField(default=0)
created = models.DateTimeField(auto_now_add=True, null=True, db_index=True) created = models.DateTimeField(auto_now_add=True, null=True, db_index=True)
......
...@@ -32,7 +32,7 @@ from student.models import (Registration, UserProfile, TestCenterUser, TestCente ...@@ -32,7 +32,7 @@ from student.models import (Registration, UserProfile, TestCenterUser, TestCente
TestCenterRegistration, TestCenterRegistrationForm, TestCenterRegistration, TestCenterRegistrationForm,
PendingNameChange, PendingEmailChange, PendingNameChange, PendingEmailChange,
CourseEnrollment, unique_id_for_user, CourseEnrollment, unique_id_for_user,
get_testcenter_registration) get_testcenter_registration, CourseEnrollmentAllowed)
from certificates.models import CertificateStatuses, certificate_status_for_student from certificates.models import CertificateStatuses, certificate_status_for_student
...@@ -264,7 +264,6 @@ def dashboard(request): ...@@ -264,7 +264,6 @@ def dashboard(request):
if not user.is_active: if not user.is_active:
message = render_to_string('registration/activate_account_notice.html', {'email': user.email}) message = render_to_string('registration/activate_account_notice.html', {'email': user.email})
# Global staff can see what courses errored on their dashboard # Global staff can see what courses errored on their dashboard
staff_access = False staff_access = False
errored_courses = {} errored_courses = {}
...@@ -355,7 +354,7 @@ def change_enrollment(request): ...@@ -355,7 +354,7 @@ def change_enrollment(request):
course = course_from_id(course_id) course = course_from_id(course_id)
except ItemNotFoundError: except ItemNotFoundError:
log.warning("User {0} tried to enroll in non-existent course {1}" log.warning("User {0} tried to enroll in non-existent course {1}"
.format(user.username, course_id)) .format(user.username, course_id))
return HttpResponseBadRequest("Course id is invalid") return HttpResponseBadRequest("Course id is invalid")
if not has_access(user, course, 'enroll'): if not has_access(user, course, 'enroll'):
...@@ -363,9 +362,9 @@ def change_enrollment(request): ...@@ -363,9 +362,9 @@ def change_enrollment(request):
org, course_num, run = course_id.split("/") org, course_num, run = course_id.split("/")
statsd.increment("common.student.enrollment", statsd.increment("common.student.enrollment",
tags=["org:{0}".format(org), tags=["org:{0}".format(org),
"course:{0}".format(course_num), "course:{0}".format(course_num),
"run:{0}".format(run)]) "run:{0}".format(run)])
try: try:
enrollment, created = CourseEnrollment.objects.get_or_create(user=user, course_id=course.id) enrollment, created = CourseEnrollment.objects.get_or_create(user=user, course_id=course.id)
...@@ -382,9 +381,9 @@ def change_enrollment(request): ...@@ -382,9 +381,9 @@ def change_enrollment(request):
org, course_num, run = course_id.split("/") org, course_num, run = course_id.split("/")
statsd.increment("common.student.unenrollment", statsd.increment("common.student.unenrollment",
tags=["org:{0}".format(org), tags=["org:{0}".format(org),
"course:{0}".format(course_num), "course:{0}".format(course_num),
"run:{0}".format(run)]) "run:{0}".format(run)])
return HttpResponse() return HttpResponse()
except CourseEnrollment.DoesNotExist: except CourseEnrollment.DoesNotExist:
...@@ -454,7 +453,6 @@ def login_user(request, error=""): ...@@ -454,7 +453,6 @@ def login_user(request, error=""):
expires_time = time.time() + max_age expires_time = time.time() + max_age
expires = cookie_date(expires_time) expires = cookie_date(expires_time)
response.set_cookie(settings.EDXMKTG_COOKIE_NAME, response.set_cookie(settings.EDXMKTG_COOKIE_NAME,
'true', max_age=max_age, 'true', max_age=max_age,
expires=expires, domain=settings.SESSION_COOKIE_DOMAIN, expires=expires, domain=settings.SESSION_COOKIE_DOMAIN,
...@@ -515,8 +513,8 @@ def _do_create_account(post_vars): ...@@ -515,8 +513,8 @@ def _do_create_account(post_vars):
Note: this function is also used for creating test users. Note: this function is also used for creating test users.
""" """
user = User(username=post_vars['username'], user = User(username=post_vars['username'],
email=post_vars['email'], email=post_vars['email'],
is_active=False) is_active=False)
user.set_password(post_vars['password']) user.set_password(post_vars['password'])
registration = Registration() registration = Registration()
# TODO: Rearrange so that if part of the process fails, the whole process fails. # TODO: Rearrange so that if part of the process fails, the whole process fails.
...@@ -698,7 +696,6 @@ def create_account(request, post_override=None): ...@@ -698,7 +696,6 @@ def create_account(request, post_override=None):
expires_time = time.time() + max_age expires_time = time.time() + max_age
expires = cookie_date(expires_time) expires = cookie_date(expires_time)
response.set_cookie(settings.EDXMKTG_COOKIE_NAME, response.set_cookie(settings.EDXMKTG_COOKIE_NAME,
'true', max_age=max_age, 'true', max_age=max_age,
expires=expires, domain=settings.SESSION_COOKIE_DOMAIN, expires=expires, domain=settings.SESSION_COOKIE_DOMAIN,
...@@ -708,7 +705,6 @@ def create_account(request, post_override=None): ...@@ -708,7 +705,6 @@ def create_account(request, post_override=None):
return response return response
def exam_registration_info(user, course): def exam_registration_info(user, course):
""" Returns a Registration object if the user is currently registered for a current """ Returns a Registration object if the user is currently registered for a current
exam of the course. Returns None if the user is not registered, or if there is no exam of the course. Returns None if the user is not registered, or if there is no
...@@ -849,7 +845,6 @@ def create_exam_registration(request, post_override=None): ...@@ -849,7 +845,6 @@ def create_exam_registration(request, post_override=None):
response_data['non_field_errors'] = form.non_field_errors() response_data['non_field_errors'] = form.non_field_errors()
return HttpResponse(json.dumps(response_data), mimetype="application/json") return HttpResponse(json.dumps(response_data), mimetype="application/json")
# only do the following if there is accommodation text to send, # only do the following if there is accommodation text to send,
# and a destination to which to send it. # and a destination to which to send it.
# TODO: still need to create the accommodation email templates # TODO: still need to create the accommodation email templates
...@@ -872,7 +867,6 @@ def create_exam_registration(request, post_override=None): ...@@ -872,7 +867,6 @@ def create_exam_registration(request, post_override=None):
# response_data['non_field_errors'] = [ 'Could not send accommodation e-mail.', ] # response_data['non_field_errors'] = [ 'Could not send accommodation e-mail.', ]
# return HttpResponse(json.dumps(response_data), mimetype="application/json") # return HttpResponse(json.dumps(response_data), mimetype="application/json")
js = {'success': True} js = {'success': True}
return HttpResponse(json.dumps(js), mimetype="application/json") return HttpResponse(json.dumps(js), mimetype="application/json")
...@@ -916,6 +910,16 @@ def activate_account(request, key): ...@@ -916,6 +910,16 @@ def activate_account(request, key):
if not r[0].user.is_active: if not r[0].user.is_active:
r[0].activate() r[0].activate()
already_active = False already_active = False
#Enroll student in any pending courses he/she may have if auto_enroll flag is set
student = User.objects.filter(id=r[0].user_id)
if student:
ceas = CourseEnrollmentAllowed.objects.filter(email=student[0].email)
for cea in ceas:
if cea.auto_enroll:
course_id = cea.course_id
enrollment, created = CourseEnrollment.objects.get_or_create(user_id=student[0].id, course_id=course_id)
resp = render_to_response("registration/activation_complete.html", {'user_logged_in': user_logged_in, 'already_active': already_active}) resp = render_to_response("registration/activation_complete.html", {'user_logged_in': user_logged_in, 'already_active': already_active})
return resp return resp
if len(r) == 0: if len(r) == 0:
...@@ -1194,6 +1198,10 @@ def accept_name_change(request): ...@@ -1194,6 +1198,10 @@ def accept_name_change(request):
def _get_news(top=None): def _get_news(top=None):
"Return the n top news items on settings.RSS_URL" "Return the n top news items on settings.RSS_URL"
# Don't return anything if we're in a themed site
if settings.MITX_FEATURES["USE_CUSTOM_THEME"]:
return None
feed_data = cache.get("students_index_rss_feed_data") feed_data = cache.get("students_index_rss_feed_data")
if feed_data is None: if feed_data is None:
if hasattr(settings, 'RSS_URL'): if hasattr(settings, 'RSS_URL'):
......
...@@ -159,3 +159,33 @@ def registered_edx_user(step, uname): ...@@ -159,3 +159,33 @@ def registered_edx_user(step, uname):
@step(u'All dialogs should be closed$') @step(u'All dialogs should be closed$')
def dialogs_are_closed(step): def dialogs_are_closed(step):
assert world.dialogs_closed() assert world.dialogs_closed()
@step('I will confirm all alerts')
def i_confirm_all_alerts(step):
"""
Please note: This method must be called RIGHT BEFORE an expected alert
Window variables are page local and thus all changes are removed upon navigating to a new page
In addition, this method changes the functionality of ONLY future alerts
"""
world.browser.execute_script('window.confirm = function(){return true;} ; window.alert = function(){return;}')
@step('I will cancel all alerts')
def i_cancel_all_alerts(step):
"""
Please note: This method must be called RIGHT BEFORE an expected alert
Window variables are page local and thus all changes are removed upon navigating to a new page
In addition, this method changes the functionality of ONLY future alerts
"""
world.browser.execute_script('window.confirm = function(){return false;} ; window.alert = function(){return;}')
@step('I will answer all prompts with "([^"]*)"')
def i_answer_prompts_with(step, prompt):
"""
Please note: This method must be called RIGHT BEFORE an expected alert
Window variables are page local and thus all changes are removed upon navigating to a new page
In addition, this method changes the functionality of ONLY future alerts
"""
world.browser.execute_script('window.prompt = function(){return %s;}') % prompt
...@@ -12,6 +12,7 @@ from django.core.validators import ValidationError, validate_email ...@@ -12,6 +12,7 @@ from django.core.validators import ValidationError, validate_email
from django.http import Http404, HttpResponse, HttpResponseBadRequest, HttpResponseNotAllowed, HttpResponseServerError from django.http import Http404, HttpResponse, HttpResponseBadRequest, HttpResponseNotAllowed, HttpResponseServerError
from django.shortcuts import redirect from django.shortcuts import redirect
from django_future.csrf import ensure_csrf_cookie from django_future.csrf import ensure_csrf_cookie
from dogapi import dog_stats_api
from mitxmako.shortcuts import render_to_response, render_to_string from mitxmako.shortcuts import render_to_response, render_to_string
from urllib import urlencode from urllib import urlencode
import zendesk import zendesk
...@@ -73,11 +74,64 @@ class _ZendeskApi(object): ...@@ -73,11 +74,64 @@ class _ZendeskApi(object):
self._zendesk_instance.update_ticket(ticket_id=ticket_id, data=update) self._zendesk_instance.update_ticket(ticket_id=ticket_id, data=update)
def submit_feedback_via_zendesk(request): def _record_feedback_in_zendesk(realname, email, subject, details, tags, additional_info):
""" """
Create a new user-requested Zendesk ticket. Create a new user-requested Zendesk ticket.
If Zendesk submission is not enabled, any request will raise `Http404`. Once created, the ticket will be updated with a private comment containing
additional information from the browser and server, such as HTTP headers
and user state. Returns a boolean value indicating whether ticket creation
was successful, regardless of whether the private comment update succeeded.
"""
zendesk_api = _ZendeskApi()
additional_info_string = (
"Additional information:\n\n" +
"\n".join("%s: %s" % (key, value) for (key, value) in additional_info.items() if value is not None)
)
# Tag all issues with LMS to distinguish channel in Zendesk; requested by student support team
zendesk_tags = list(tags.values()) + ["LMS"]
new_ticket = {
"ticket": {
"requester": {"name": realname, "email": email},
"subject": subject,
"comment": {"body": details},
"tags": zendesk_tags
}
}
try:
ticket_id = zendesk_api.create_ticket(new_ticket)
except zendesk.ZendeskError as err:
log.error("Error creating Zendesk ticket: %s", str(err))
return False
# Additional information is provided as a private update so the information
# is not visible to the user.
ticket_update = {"ticket": {"comment": {"public": False, "body": additional_info_string}}}
try:
zendesk_api.update_ticket(ticket_id, ticket_update)
except zendesk.ZendeskError as err:
log.error("Error updating Zendesk ticket: %s", str(err))
# The update is not strictly necessary, so do not indicate failure to the user
pass
return True
DATADOG_FEEDBACK_METRIC = "lms_feedback_submissions"
def _record_feedback_in_datadog(tags):
datadog_tags = ["{k}:{v}".format(k=k, v=v) for k, v in tags.items()]
dog_stats_api.increment(DATADOG_FEEDBACK_METRIC, tags=datadog_tags)
def submit_feedback(request):
"""
Create a new user-requested ticket, currently implemented with Zendesk.
If feedback submission is not enabled, any request will raise `Http404`.
If any configuration parameter (`ZENDESK_URL`, `ZENDESK_USER`, or If any configuration parameter (`ZENDESK_URL`, `ZENDESK_USER`, or
`ZENDESK_API_KEY`) is missing, any request will raise an `Exception`. `ZENDESK_API_KEY`) is missing, any request will raise an `Exception`.
The request must be a POST request specifying `subject` and `details`. The request must be a POST request specifying `subject` and `details`.
...@@ -85,12 +139,9 @@ def submit_feedback_via_zendesk(request): ...@@ -85,12 +139,9 @@ def submit_feedback_via_zendesk(request):
`email`. If the user is authenticated, the `name` and `email` will be `email`. If the user is authenticated, the `name` and `email` will be
populated from the user's information. If any required parameter is populated from the user's information. If any required parameter is
missing, a 400 error will be returned indicating which field is missing and missing, a 400 error will be returned indicating which field is missing and
providing an error message. If Zendesk returns any error on ticket providing an error message. If Zendesk ticket creation fails, 500 error
creation, a 500 error will be returned with no body. Once created, the will be returned with no body; if ticket creation succeeds, an empty
ticket will be updated with a private comment containing additional successful response (200) will be returned.
information from the browser and server, such as HTTP headers and user
state. Whether or not the update succeeds, if the user's ticket is
successfully created, an empty successful response (200) will be returned.
""" """
if not settings.MITX_FEATURES.get('ENABLE_FEEDBACK_SUBMISSION', False): if not settings.MITX_FEATURES.get('ENABLE_FEEDBACK_SUBMISSION', False):
raise Http404() raise Http404()
...@@ -124,9 +175,9 @@ def submit_feedback_via_zendesk(request): ...@@ -124,9 +175,9 @@ def submit_feedback_via_zendesk(request):
subject = request.POST["subject"] subject = request.POST["subject"]
details = request.POST["details"] details = request.POST["details"]
tags = [] tags = dict(
if "tag" in request.POST: [(tag, request.POST[tag]) for tag in ["issue_type", "course_id"] if tag in request.POST]
tags = [request.POST["tag"]] )
if request.user.is_authenticated(): if request.user.is_authenticated():
realname = request.user.profile.name realname = request.user.profile.name
...@@ -140,41 +191,18 @@ def submit_feedback_via_zendesk(request): ...@@ -140,41 +191,18 @@ def submit_feedback_via_zendesk(request):
except ValidationError: except ValidationError:
return build_error_response(400, "email", required_field_errs["email"]) return build_error_response(400, "email", required_field_errs["email"])
for header in ["HTTP_REFERER", "HTTP_USER_AGENT"]: for header, pretty in [
additional_info[header] = request.META.get(header) ("HTTP_REFERER", "Page"),
("HTTP_USER_AGENT", "Browser"),
("REMOTE_ADDR", "Client IP"),
("SERVER_NAME", "Host")
]:
additional_info[pretty] = request.META.get(header)
zendesk_api = _ZendeskApi() success = _record_feedback_in_zendesk(realname, email, subject, details, tags, additional_info)
_record_feedback_in_datadog(tags)
additional_info_string = (
"Additional information:\n\n" +
"\n".join("%s: %s" % (key, value) for (key, value) in additional_info.items() if value is not None)
)
new_ticket = {
"ticket": {
"requester": {"name": realname, "email": email},
"subject": subject,
"comment": {"body": details},
"tags": tags
}
}
try:
ticket_id = zendesk_api.create_ticket(new_ticket)
except zendesk.ZendeskError as err:
log.error("Error creating Zendesk ticket: %s", str(err))
return HttpResponse(status=500)
# Additional information is provided as a private update so the information
# is not visible to the user.
ticket_update = {"ticket": {"comment": {"public": False, "body": additional_info_string}}}
try:
zendesk_api.update_ticket(ticket_id, ticket_update)
except zendesk.ZendeskError as err:
log.error("Error updating Zendesk ticket: %s", str(err))
# The update is not strictly necessary, so do not indicate failure to the user
pass
return HttpResponse() return HttpResponse(status=(200 if success else 500))
def info(request): def info(request):
......
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
<div class="block block-comment">${comment}</div> <div class="block block-comment">${comment}</div>
<div class="block">${comment_prompt}</div> <div class="block">${comment_prompt}</div>
<textarea class="comment" id="input_${id}_comment" name="input_${id}_comment">${comment_value|h}</textarea> <textarea class="comment" id="input_${id}_comment" name="input_${id}_comment" aria-describedby="answer_${id}">${comment_value|h}</textarea>
<div class="block">${tag_prompt}</div> <div class="block">${tag_prompt}</div>
<ul class="tags"> <ul class="tags">
...@@ -22,11 +22,11 @@ ...@@ -22,11 +22,11 @@
<li> <li>
% if has_options_value: % if has_options_value:
% if all([c == 'correct' for c in option['choice'], status]): % if all([c == 'correct' for c in option['choice'], status]):
<span class="tag-status correct" id="status_${id}"></span> <span class="tag-status correct" id="status_${id}" aria-describedby="input_${id}_comment"><span class="sr">Status: Correct</span></span>
% elif all([c == 'partially-correct' for c in option['choice'], status]): % elif all([c == 'partially-correct' for c in option['choice'], status]):
<span class="tag-status partially-correct" id="status_${id}"></span> <span class="tag-status partially-correct" id="status_${id}" aria-describedby="input_${id}_comment"><span class="sr">Status: Partially Correct</span></span>
% elif all([c == 'incorrect' for c in option['choice'], status]): % elif all([c == 'incorrect' for c in option['choice'], status]):
<span class="tag-status incorrect" id="status_${id}"></span> <span class="tag-status incorrect" id="status_${id}" aria-describedby="input_${id}_comment"><span class="sr">Status: Incorrect</span></span>
% endif % endif
% endif % endif
...@@ -53,11 +53,11 @@ ...@@ -53,11 +53,11 @@
% endif % endif
% if status == 'unsubmitted': % if status == 'unsubmitted':
<span class="unanswered" style="display:inline-block;" id="status_${id}"></span> <span class="unanswered" style="display:inline-block;" id="status_${id}" aria-describedby="input_${id}"><span class="sr">Status: Unanswered</span></span>
% elif status == 'incomplete': % elif status == 'incomplete':
<span class="incorrect" id="status_${id}"></span> <span class="incorrect" id="status_${id}" aria-describedby="input_${id}"><span class="sr">Status: Incorrect</span></span>
% elif status == 'incorrect' and not has_options_value: % elif status == 'incorrect' and not has_options_value:
<span class="incorrect" id="status_${id}"></span> <span class="incorrect" id="status_${id}" aria-describedby="input_${id}"><span class="sr">Status: Incorrect</span></span>
% endif % endif
<p id="answer_${id}" class="answer answer-annotation"></p> <p id="answer_${id}" class="answer answer-annotation"></p>
......
...@@ -11,13 +11,13 @@ ...@@ -11,13 +11,13 @@
<div class="incorrect" id="status_${id}"> <div class="incorrect" id="status_${id}">
% endif % endif
<input type="text" name="input_${id}" id="input_${id}" data-input-id="${id}" value="${value|h}" <input type="text" name="input_${id}" id="input_${id}" aria-describedby="answer_${id}" data-input-id="${id}" value="${value|h}"
% if size: % if size:
size="${size}" size="${size}"
% endif % endif
/> />
<p class="status"> <p class="status" aria-describedby="input_${id}">
% if status == 'unsubmitted': % if status == 'unsubmitted':
unanswered unanswered
% elif status == 'correct': % elif status == 'correct':
......
...@@ -3,12 +3,12 @@ ...@@ -3,12 +3,12 @@
% if input_type == 'checkbox' or not value: % if input_type == 'checkbox' or not value:
% if status == 'unsubmitted' or show_correctness == 'never': % if status == 'unsubmitted' or show_correctness == 'never':
<span class="unanswered" style="display:inline-block;" id="status_${id}"></span> <span class="unanswered" style="display:inline-block;" id="status_${id}"></span>
% elif status == 'correct': % elif status == 'correct':
<span class="correct" id="status_${id}"></span> <span class="correct" id="status_${id}"><span class="sr">Status: correct</span></span>
% elif status == 'incorrect': % elif status == 'incorrect':
<span class="incorrect" id="status_${id}"></span> <span class="incorrect" id="status_${id}"><span class="sr">Status: incorrect</span></span>
% elif status == 'incomplete': % elif status == 'incomplete':
<span class="incorrect" id="status_${id}"></span> <span class="incorrect" id="status_${id}"><span class="sr">Status: incomplete</span></span>
% endif % endif
% endif % endif
</div> </div>
...@@ -18,7 +18,7 @@ ...@@ -18,7 +18,7 @@
% for choice_id, choice_description in choices: % for choice_id, choice_description in choices:
<label for="input_${id}_${choice_id}" <label for="input_${id}_${choice_id}"
% if input_type == 'radio' and ( (isinstance(value, basestring) and (choice_id == value)) or (not isinstance(value, basestring) and choice_id in value) ): % if input_type == 'radio' and ( (isinstance(value, basestring) and (choice_id == value)) or (not isinstance(value, basestring) and choice_id in value) ):
<% <%
if status == 'correct': if status == 'correct':
correctness = 'correct' correctness = 'correct'
elif status == 'incorrect': elif status == 'incorrect':
...@@ -31,14 +31,29 @@ ...@@ -31,14 +31,29 @@
% endif % endif
% endif % endif
> >
<input type="${input_type}" name="input_${id}${name_array_suffix}" id="input_${id}_${choice_id}" value="${choice_id}" <input type="${input_type}" name="input_${id}${name_array_suffix}" id="input_${id}_${choice_id}" aria-describedby="answer_${id}" value="${choice_id}"
% if input_type == 'radio' and ( (isinstance(value, basestring) and (choice_id == value)) or (not isinstance(value, basestring) and choice_id in value) ): % if input_type == 'radio' and ( (isinstance(value, basestring) and (choice_id == value)) or (not isinstance(value, basestring) and choice_id in value) ):
checked="true" checked="true"
% elif input_type != 'radio' and choice_id in value: % elif input_type != 'radio' and choice_id in value:
checked="true" checked="true"
% endif % endif
/> ${choice_description} </label> /> ${choice_description}
% if input_type == 'radio' and ( (isinstance(value, basestring) and (choice_id == value)) or (not isinstance(value, basestring) and choice_id in value) ):
<%
if status == 'correct':
correctness = 'correct'
elif status == 'incorrect':
correctness = 'incorrect'
else:
correctness = None
%>
% if correctness and not show_correctness=='never':
<span class="sr" aria-describedby="input_${id}_${choice_id}">Status: ${correctness}</span>
% endif
% endif
</label>
% endfor % endfor
<span id="answer_${id}"></span> <span id="answer_${id}"></span>
</fieldset> </fieldset>
......
<section id="textbox_${id}" class="textbox"> <section id="textbox_${id}" class="textbox">
<textarea rows="${rows}" cols="${cols}" name="input_${id}" id="input_${id}" <textarea rows="${rows}" cols="${cols}" name="input_${id}" aria-describedby="answer_${id}" id="input_${id}"
% if hidden: % if hidden:
style="display:none;" style="display:none;"
% endif % endif
...@@ -7,13 +7,13 @@ ...@@ -7,13 +7,13 @@
<div class="grader-status"> <div class="grader-status">
% if status == 'unsubmitted': % if status == 'unsubmitted':
<span class="unanswered" style="display:inline-block;" id="status_${id}">Unanswered</span> <span class="unanswered" style="display:inline-block;" id="status_${id}" aria-describedby="input_${id}"><span class="sr">Status: </span>Unanswered</span>
% elif status == 'correct': % elif status == 'correct':
<span class="correct" id="status_${id}">Correct</span> <span class="correct" id="status_${id}" aria-describedby="input_${id}"><span class="sr">Status: </span>Correct</span>
% elif status == 'incorrect': % elif status == 'incorrect':
<span class="incorrect" id="status_${id}">Incorrect</span> <span class="incorrect" id="status_${id}" aria-describedby="input_${id}"><span class="sr">Status: </span>Incorrect</span>
% elif status == 'queued': % elif status == 'queued':
<span class="processing" id="status_${id}">Queued</span> <span class="processing" id="status_${id}" aria-describedby="input_${id}"><span class="sr">Status: </span>Queued</span>
<span style="display:none;" class="xqueue" id="${id}" >${queue_len}</span> <span style="display:none;" class="xqueue" id="${id}" >${queue_len}</span>
% endif % endif
......
...@@ -20,9 +20,9 @@ ...@@ -20,9 +20,9 @@
% endif % endif
<input type="text" name="input_${id}" id="input_${id}" value="${value|h}" style="display:none;"/> <input type="text" name="input_${id}" aria-describedby="answer_${id}" id="input_${id}" value="${value|h}" style="display:none;"/>
<p class="status"> <p class="status" aria-describedby="input_${id}">
% if status == 'unsubmitted': % if status == 'unsubmitted':
unanswered unanswered
% elif status == 'correct': % elif status == 'correct':
......
...@@ -11,12 +11,12 @@ ...@@ -11,12 +11,12 @@
% elif status == 'incomplete': % elif status == 'incomplete':
<div class="incomplete" id="status_${id}"> <div class="incomplete" id="status_${id}">
% endif % endif
<div id="protex_container"></div> <div id="protex_container"></div>
<input type="hidden" name="target_shape" id="target_shape" value ="${target_shape}"></input> <input type="hidden" name="target_shape" id="target_shape" value ="${target_shape}"></input>
<input type="hidden" name="input_${id}" id="input_${id}" value="${value|h}"/> <input type="hidden" name="input_${id}" id="input_${id}" aria-describedby="answer_${id}" value="${value|h}"/>
<p class="status"> <p class="status" aria-describedby="input_${id}">
% if status == 'unsubmitted': % if status == 'unsubmitted':
unanswered unanswered
% elif status == 'correct': % elif status == 'correct':
......
...@@ -19,10 +19,10 @@ ...@@ -19,10 +19,10 @@
% endif % endif
<input type="text" name="input_${id}" id="input_${id}" value="${value|h}" <input type="text" name="input_${id}" id="input_${id}" aria-describedby="answer_${id}" value="${value|h}"
style="display:none;"/> style="display:none;"/>
<p class="status"> <p class="status" aria-describedby="input_${id}">
% if status == 'unsubmitted': % if status == 'unsubmitted':
unanswered unanswered
% elif status == 'correct': % elif status == 'correct':
......
...@@ -11,13 +11,13 @@ ...@@ -11,13 +11,13 @@
% elif status == 'incomplete': % elif status == 'incomplete':
<div class="incomplete" id="status_${id}"> <div class="incomplete" id="status_${id}">
% endif % endif
<div id="genex_container"></div> <div id="genex_container"></div>
<input type="hidden" name="genex_dna_sequence" id="genex_dna_sequence" value ="${genex_dna_sequence}"></input> <input type="hidden" name="genex_dna_sequence" id="genex_dna_sequence" value ="${genex_dna_sequence}"></input>
<input type="hidden" name="genex_problem_number" id="genex_problem_number" value ="${genex_problem_number}"></input> <input type="hidden" name="genex_problem_number" id="genex_problem_number" value ="${genex_problem_number}"></input>
<input type="hidden" name="input_${id}" id="input_${id}" value="${value|h}"/> <input type="hidden" name="input_${id}" aria-describedby="answer_${id}" id="input_${id}" value="${value|h}"/>
<p class="status"> <p class="status" aria-describedby="input_${id}">
% if status == 'unsubmitted': % if status == 'unsubmitted':
unanswered unanswered
% elif status == 'correct': % elif status == 'correct':
......
...@@ -16,13 +16,13 @@ ...@@ -16,13 +16,13 @@
<br/> <br/>
<input type="hidden" name="input_${id}" id="input_${id}" value="${value|h}"/> <input type="hidden" name="input_${id}" id="input_${id}" aria-describedby="answer_${id}" value="${value|h}"/>
<button id="reset_${id}" class="reset">Reset</button> <button id="reset_${id}" class="reset">Reset</button>
<p id="answer_${id}" class="answer"></p> <p id="answer_${id}" class="answer"></p>
<p class="status"> <p class="status" aria-describedby="input_${id}">
% if status == 'unsubmitted': % if status == 'unsubmitted':
unanswered unanswered
% elif status == 'correct': % elif status == 'correct':
......
...@@ -5,12 +5,20 @@ ...@@ -5,12 +5,20 @@
</div> </div>
% if status == 'unsubmitted': % if status == 'unsubmitted':
<span class="unanswered" style="display:inline-block;" id="status_${id}"></span> <span class="unanswered" style="display:inline-block;" id="status_${id}" aria-describedby="input_${id}">
% elif status == 'correct': <span class="sr">Status: unanswered</span>
<span class="correct" id="status_${id}"></span> </span>
% elif status == 'correct':
<span class="correct" id="status_${id}" aria-describedby="input_${id}">
<span class="sr">Status: correct</span>
</span>
% elif status == 'incorrect': % elif status == 'incorrect':
<span class="incorrect" id="status_${id}"></span> <span class="incorrect" id="status_${id}" aria-describedby="input_${id}">
<span class="sr">Status: incorrect</span>
</span>
% elif status == 'incomplete': % elif status == 'incomplete':
<span class="incorrect" id="status_${id}"></span> <span class="incorrect" id="status_${id}" aria-describedby="input_${id}">
<span class="sr">Status: incorrect</span>
</span>
% endif % endif
</span> </span>
...@@ -19,13 +19,21 @@ ...@@ -19,13 +19,21 @@
% endif % endif
% if status == 'unsubmitted': % if status == 'unsubmitted':
<span class="unanswered" style="display:inline-block;" id="status_${id}"></span> <span class="unanswered" style="display:inline-block;" id="status_${id}" aria-describedby="input_${id}">
<span class="sr">Status: unanswered</span>
</span>
% elif status == 'correct': % elif status == 'correct':
<span class="correct" id="status_${id}"></span> <span class="correct" id="status_${id}" aria-describedby="input_${id}">
<span class="sr">Status: correct</span>
</span>
% elif status == 'incorrect': % elif status == 'incorrect':
<span class="incorrect" id="status_${id}"></span> <span class="incorrect" id="status_${id}" aria-describedby="input_${id}">
<span class="sr">Status: incorrect</span>
</span>
% elif status == 'incomplete': % elif status == 'incomplete':
<span class="incorrect" id="status_${id}"></span> <span class="incorrect" id="status_${id}" aria-describedby="input_${id}">
<span class="sr">Status: incorrect</span>
</span>
% endif % endif
% if msg: % if msg:
<br/> <br/>
......
<section id="textbox_${id}" class="textbox"> <section id="textbox_${id}" class="textbox">
<textarea rows="${rows}" cols="${cols}" name="input_${id}" id="input_${id}" <textarea rows="${rows}" cols="${cols}" name="input_${id}" aria-describedby="answer_${id}" id="input_${id}"
% if hidden: % if hidden:
style="display:none;" style="display:none;"
% endif % endif
...@@ -7,13 +7,13 @@ ...@@ -7,13 +7,13 @@
<div class="grader-status"> <div class="grader-status">
% if status == 'unsubmitted': % if status == 'unsubmitted':
<span class="unanswered" style="display:inline-block;" id="status_${id}">Unanswered</span> <span class="unanswered" style="display:inline-block;" id="status_${id}"><span class="sr">Status: </span>Unanswered</span>
% elif status == 'correct': % elif status == 'correct':
<span class="correct" id="status_${id}">Correct</span> <span class="correct" id="status_${id}" aria-describedby="input_${id}"><span class="sr">Status: </span>Correct</span>
% elif status == 'incorrect': % elif status == 'incorrect':
<span class="incorrect" id="status_${id}">Incorrect</span> <span class="incorrect" id="status_${id}" aria-describedby="input_${id}"><span class="sr">Status: </span>Incorrect</span>
% elif status == 'queued': % elif status == 'queued':
<span class="processing" id="status_${id}">Queued</span> <span class="processing" id="status_${id}" aria-describedby="input_${id}"><span class="sr">Status: </span>Queued</span>
<span style="display:none;" class="xqueue" id="${id}" >${queue_len}</span> <span style="display:none;" class="xqueue" id="${id}" >${queue_len}</span>
% endif % endif
...@@ -71,7 +71,7 @@ ...@@ -71,7 +71,7 @@
$(parent_elt).find('.action').after(alert_elem); $(parent_elt).find('.action').after(alert_elem);
$(parent_elt).find('.capa_alert').css({opacity: 0}).animate({opacity: 1}, 700); $(parent_elt).find('.capa_alert').css({opacity: 0}).animate({opacity: 1}, 700);
} }
// hook up the plot button // hook up the plot button
var plot = function(event) { var plot = function(event) {
...@@ -97,10 +97,10 @@ ...@@ -97,10 +97,10 @@
} }
} }
var save_callback = function(response) { var save_callback = function(response) {
if(response.success) { if(response.success) {
// send information to the problem's plot functionality // send information to the problem's plot functionality
Problem.inputAjax(url, input_id, 'plot', Problem.inputAjax(url, input_id, 'plot',
{'submission': submission}, plot_callback); {'submission': submission}, plot_callback);
} }
else { else {
......
<form class="option-input"> <form class="option-input">
<select name="input_${id}" id="input_${id}" > <select name="input_${id}" id="input_${id}" aria-describedby="answer_${id}">
<option value="option_${id}_dummy_default"> </option> <option value="option_${id}_dummy_default"> </option>
% for option_id, option_description in options: % for option_id, option_description in options:
<option value="${option_id}" <option value="${option_id}"
...@@ -13,12 +13,20 @@ ...@@ -13,12 +13,20 @@
<span id="answer_${id}"></span> <span id="answer_${id}"></span>
% if status == 'unsubmitted': % if status == 'unsubmitted':
<span class="unanswered" style="display:inline-block;" id="status_${id}"></span> <span class="unanswered" style="display:inline-block;" id="status_${id}" aria-describedby="input_${id}">
% elif status == 'correct': <span class="sr">Status: unsubmitted</span>
<span class="correct" id="status_${id}"></span> </span>
% elif status == 'correct':
<span class="correct" id="status_${id}" aria-describedby="input_${id}">
<span class="sr">Status: correct</span>
</span>
% elif status == 'incorrect': % elif status == 'incorrect':
<span class="incorrect" id="status_${id}"></span> <span class="incorrect" id="status_${id}" aria-describedby="input_${id}">
<span class="sr">Status: incorrect</span>
</span>
% elif status == 'incomplete': % elif status == 'incomplete':
<span class="incorrect" id="status_${id}"></span> <span class="incorrect" id="status_${id}" aria-describedby="input_${id}">
<span class="sr">Status: incomplete</span>
</span>
% endif % endif
</form> </form>
<span> <span>
<input type="hidden" class="schematic" height="${height}" width="${width}" parts="${parts}" analyses="${analyses}" name="input_${id}" id="input_${id}" value="" initial_value=""/> <input type="hidden" class="schematic" height="${height}" width="${width}" parts="${parts}" analyses="${analyses}" name="input_${id}" id="input_${id}" aria-describedby="answer_${id}" value="" initial_value=""/>
<div id="value_${id}" style="display:none">${value}</div> <div id="value_${id}" style="display:none">${value}</div>
<div id="initial_value_${id}" style="display:none">${initial_value}</div> <div id="initial_value_${id}" style="display:none">${initial_value}</div>
...@@ -13,13 +13,21 @@ ...@@ -13,13 +13,21 @@
<span id="answer_${id}"></span> <span id="answer_${id}"></span>
% if status == 'unsubmitted': % if status == 'unsubmitted':
<span class="ui-icon ui-icon-bullet" style="display:inline-block;" id="status_${id}"></span> <span class="ui-icon ui-icon-bullet" style="display:inline-block;" id="status_${id}" aria-describedby="input_${id}">
% elif status == 'correct': <span class="sr">Status: unsubmitted</span>
<span class="ui-icon ui-icon-check" style="display:inline-block;" id="status_${id}"></span> </span>
% elif status == 'correct':
<span class="ui-icon ui-icon-check" style="display:inline-block;" id="status_${id}" aria-describedby="input_${id}">
<span class="sr">Status: correct</span>
</span>
% elif status == 'incorrect': % elif status == 'incorrect':
<span class="ui-icon ui-icon-close" style="display:inline-block;" id="status_${id}"></span> <span class="ui-icon ui-icon-close" style="display:inline-block;" id="status_${id}" aria-describedby="input_${id}">
<span class="sr">Status: incorrect</span>
</span>
% elif status == 'incomplete': % elif status == 'incomplete':
<span class="ui-icon ui-icon-close" style="display:inline-block;" id="status_${id}"></span> <span class="ui-icon ui-icon-close" style="display:inline-block;" id="status_${id}" aria-describedby="input_${id}">
<span class="sr">Status: incomplete</span>
</span>
% endif % endif
</span> </span>
...@@ -20,7 +20,7 @@ ...@@ -20,7 +20,7 @@
<div style="display:none;" name="${hidden}" inputid="input_${id}" /> <div style="display:none;" name="${hidden}" inputid="input_${id}" />
% endif % endif
<input type="text" name="input_${id}" id="input_${id}" value="${value|h}" <input type="text" name="input_${id}" id="input_${id}" aria-describedby="answer_${id}" value="${value|h}"
% if do_math: % if do_math:
class="math" class="math"
% endif % endif
...@@ -33,7 +33,7 @@ ...@@ -33,7 +33,7 @@
/> />
${trailing_text | h} ${trailing_text | h}
<p class="status"> <p class="status" aria-describedby="input_${id}">
% if status == 'unsubmitted': % if status == 'unsubmitted':
unanswered unanswered
% elif status == 'correct': % elif status == 'correct':
......
...@@ -21,11 +21,11 @@ ...@@ -21,11 +21,11 @@
<div class="incorrect" id="status_${id}"> <div class="incorrect" id="status_${id}">
% endif % endif
<input type="text" name="input_${id}" id="input_${id}" value="${value|h}" <input type="text" name="input_${id}" id="input_${id}" aria-describedby="answer_${id}" value="${value|h}"
style="display:none;" style="display:none;"
/> />
<p class="status"> <p class="status" aria-describedby="input_${id}">
% if status == 'unsubmitted': % if status == 'unsubmitted':
unanswered unanswered
% elif status == 'correct': % elif status == 'correct':
......
...@@ -69,7 +69,7 @@ class CapaFields(object): ...@@ -69,7 +69,7 @@ class CapaFields(object):
max_attempts = StringyInteger( max_attempts = StringyInteger(
display_name="Maximum Attempts", display_name="Maximum Attempts",
help="Defines the number of times a student can try to answer this problem. If the value is not set, infinite attempts are allowed.", help="Defines the number of times a student can try to answer this problem. If the value is not set, infinite attempts are allowed.",
values={"min": 1}, scope=Scope.settings values={"min": 0}, scope=Scope.settings
) )
due = Date(help="Date that this problem is due by", scope=Scope.settings) due = Date(help="Date that this problem is due by", scope=Scope.settings)
graceperiod = Timedelta(help="Amount of time after the due date that submissions will be accepted", scope=Scope.settings) graceperiod = Timedelta(help="Amount of time after the due date that submissions will be accepted", scope=Scope.settings)
......
...@@ -555,6 +555,15 @@ section.problem { ...@@ -555,6 +555,15 @@ section.problem {
@extend .blue-button; @extend .blue-button;
} }
button.show {
height: ($baseline*2);
span {
font-size: 1.0em;
font-weight: 600;
}
}
.submission_feedback { .submission_feedback {
// background: #F3F3F3; // background: #F3F3F3;
// border: 1px solid #ddd; // border: 1px solid #ddd;
...@@ -811,13 +820,13 @@ section.problem { ...@@ -811,13 +820,13 @@ section.problem {
} }
.selected-grade { .selected-grade {
background: #666; background: #666;
color: white; color: white;
} }
input[type=radio]:checked + label { input[type=radio]:checked + label {
background: #666; background: #666;
color: white; } color: white; }
input[class='score-selection'] { input[class='score-selection'] {
display: none; display: none;
} }
} }
...@@ -878,11 +887,11 @@ section.problem { ...@@ -878,11 +887,11 @@ section.problem {
.tag-status, .tag { padding: .25em .5em; } .tag-status, .tag { padding: .25em .5em; }
} }
} }
textarea.comment { textarea.comment {
$num-lines-to-show: 5; $num-lines-to-show: 5;
$line-height: 1.4em; $line-height: 1.4em;
$padding: .2em; $padding: .2em;
width: 100%; width: 100%;
padding: $padding (2 * $padding); padding: $padding (2 * $padding);
line-height: $line-height; line-height: $line-height;
height: ($num-lines-to-show * $line-height) + (2*$padding) - (($line-height - 1)/2); height: ($num-lines-to-show * $line-height) + (2*$padding) - (($line-height - 1)/2);
......
...@@ -87,7 +87,7 @@ class ErrorDescriptor(ErrorFields, JSONEditingDescriptor): ...@@ -87,7 +87,7 @@ class ErrorDescriptor(ErrorFields, JSONEditingDescriptor):
# but url_names aren't guaranteed to be unique between descriptor types, # but url_names aren't guaranteed to be unique between descriptor types,
# and ErrorDescriptor can wrap any type. When the wrapped module is fixed, # and ErrorDescriptor can wrap any type. When the wrapped module is fixed,
# it will be written out with the original url_name. # it will be written out with the original url_name.
name=hashlib.sha1(contents).hexdigest() name=hashlib.sha1(contents.encode('utf8')).hexdigest()
) )
# real metadata stays in the content, but add a display name # real metadata stays in the content, but add a display name
......
...@@ -12,3 +12,12 @@ class ProcessingError(Exception): ...@@ -12,3 +12,12 @@ class ProcessingError(Exception):
For example: if an exception occurs while checking a capa problem. For example: if an exception occurs while checking a capa problem.
''' '''
pass pass
class InvalidVersionError(Exception):
"""
Tried to save an item with a location that a store cannot support (e.g., draft version
for a non-leaf node)
"""
def __init__(self, location):
super(InvalidVersionError, self).__init__()
self.location = location
...@@ -13,7 +13,7 @@ ...@@ -13,7 +13,7 @@
<input class="check" type="button" value="Check"> <input class="check" type="button" value="Check">
<input class="reset" type="button" value="Reset"> <input class="reset" type="button" value="Reset">
<input class="save" type="button" value="Save"> <input class="save" type="button" value="Save">
<input class="show" type="button" value="Show Answer"> <button class="show"><span class="show-label">Show Answer(s)</span> <span class="sr">(for question(s) above - adjacent to each field)</span></button>
<a href="/courseware/6.002_Spring_2012/${ explain }" class="new-page">Explanation</a> <a href="/courseware/6.002_Spring_2012/${ explain }" class="new-page">Explanation</a>
<section class="submission_feedback"></section> <section class="submission_feedback"></section>
</section> </section>
......
<div class="course-content"> <div class="course-content">
<div id="video_example" class="video"> <div id="video_example">
<div class="tc-wrapper"> <div id="example">
<article class="video-wrapper"> <div id="video_id" class="video"
<section class="video-player"> data-streams="0.75:slowerSpeedYoutubeId,1.0:normalSpeedYoutubeId"
<div id="example"></div> data-show-captions="true"
</section> data-start=""
<section class="video-controls"></section> data-end=""
</article> data-caption-asset-path="/static/subs/">
<div class="tc-wrapper">
<article class="video-wrapper">
<section class="video-player">
<div id="id"></div>
</section>
<section class="video-controls"></section>
</article>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
\ No newline at end of file
...@@ -16,7 +16,7 @@ describe 'Problem', -> ...@@ -16,7 +16,7 @@ describe 'Problem', ->
# note that the fixturesPath is set in spec/helper.coffee # note that the fixturesPath is set in spec/helper.coffee
loadFixtures 'problem.html' loadFixtures 'problem.html'
spyOn Logger, 'log' spyOn Logger, 'log'
spyOn($.fn, 'load').andCallFake (url, callback) -> spyOn($.fn, 'load').andCallFake (url, callback) ->
$(@).html readFixtures('problem_content.html') $(@).html readFixtures('problem_content.html')
...@@ -27,13 +27,13 @@ describe 'Problem', -> ...@@ -27,13 +27,13 @@ describe 'Problem', ->
it 'set the element from html', -> it 'set the element from html', ->
@problem999 = new Problem (" @problem999 = new Problem ("
<section class='xmodule_display xmodule_CapaModule' data-type='Problem'> <section class='xmodule_display xmodule_CapaModule' data-type='Problem'>
<section id='problem_999' <section id='problem_999'
class='problems-wrapper' class='problems-wrapper'
data-problem-id='i4x://edX/999/problem/Quiz' data-problem-id='i4x://edX/999/problem/Quiz'
data-url='/problem/quiz/'> data-url='/problem/quiz/'>
</section> </section>
</section> </section>
") ")
expect(@problem999.element_id).toBe 'problem_999' expect(@problem999.element_id).toBe 'problem_999'
it 'set the element from loadFixtures', -> it 'set the element from loadFixtures', ->
...@@ -62,7 +62,7 @@ describe 'Problem', -> ...@@ -62,7 +62,7 @@ describe 'Problem', ->
expect($('section.action input.reset')).toHandleWith 'click', @problem.reset expect($('section.action input.reset')).toHandleWith 'click', @problem.reset
it 'bind the show button', -> it 'bind the show button', ->
expect($('section.action input.show')).toHandleWith 'click', @problem.show expect($('section.action button.show')).toHandleWith 'click', @problem.show
it 'bind the save button', -> it 'bind the save button', ->
expect($('section.action input.save')).toHandleWith 'click', @problem.save expect($('section.action input.save')).toHandleWith 'click', @problem.save
...@@ -126,14 +126,14 @@ describe 'Problem', -> ...@@ -126,14 +126,14 @@ describe 'Problem', ->
describe 'when the response is correct', -> describe 'when the response is correct', ->
it 'call render with returned content', -> it 'call render with returned content', ->
spyOn($, 'postWithPrefix').andCallFake (url, answers, callback) -> spyOn($, 'postWithPrefix').andCallFake (url, answers, callback) ->
callback(success: 'correct', contents: 'Correct!') callback(success: 'correct', contents: 'Correct!')
@problem.check() @problem.check()
expect(@problem.el.html()).toEqual 'Correct!' expect(@problem.el.html()).toEqual 'Correct!'
describe 'when the response is incorrect', -> describe 'when the response is incorrect', ->
it 'call render with returned content', -> it 'call render with returned content', ->
spyOn($, 'postWithPrefix').andCallFake (url, answers, callback) -> spyOn($, 'postWithPrefix').andCallFake (url, answers, callback) ->
callback(success: 'incorrect', contents: 'Incorrect!') callback(success: 'incorrect', contents: 'Incorrect!')
@problem.check() @problem.check()
expect(@problem.el.html()).toEqual 'Incorrect!' expect(@problem.el.html()).toEqual 'Incorrect!'
...@@ -159,7 +159,7 @@ describe 'Problem', -> ...@@ -159,7 +159,7 @@ describe 'Problem', ->
it 'POST to the problem reset page', -> it 'POST to the problem reset page', ->
spyOn $, 'postWithPrefix' spyOn $, 'postWithPrefix'
@problem.reset() @problem.reset()
expect($.postWithPrefix).toHaveBeenCalledWith '/problem/Problem1/problem_reset', expect($.postWithPrefix).toHaveBeenCalledWith '/problem/Problem1/problem_reset',
{ id: 'i4x://edX/101/problem/Problem1' }, jasmine.any(Function) { id: 'i4x://edX/101/problem/Problem1' }, jasmine.any(Function)
it 'render the returned content', -> it 'render the returned content', ->
...@@ -179,7 +179,7 @@ describe 'Problem', -> ...@@ -179,7 +179,7 @@ describe 'Problem', ->
it 'log the problem_show event', -> it 'log the problem_show event', ->
@problem.show() @problem.show()
expect(Logger.log).toHaveBeenCalledWith 'problem_show', expect(Logger.log).toHaveBeenCalledWith 'problem_show',
problem: 'i4x://edX/101/problem/Problem1' problem: 'i4x://edX/101/problem/Problem1'
it 'fetch the answers', -> it 'fetch the answers', ->
...@@ -198,7 +198,7 @@ describe 'Problem', -> ...@@ -198,7 +198,7 @@ describe 'Problem', ->
it 'toggle the show answer button', -> it 'toggle the show answer button', ->
spyOn($, 'postWithPrefix').andCallFake (url, callback) -> callback(answers: {}) spyOn($, 'postWithPrefix').andCallFake (url, callback) -> callback(answers: {})
@problem.show() @problem.show()
expect($('.show')).toHaveValue 'Hide Answer' expect($('.show .show-label')).toHaveText 'Hide Answer(s)'
it 'add the showed class to element', -> it 'add the showed class to element', ->
spyOn($, 'postWithPrefix').andCallFake (url, callback) -> callback(answers: {}) spyOn($, 'postWithPrefix').andCallFake (url, callback) -> callback(answers: {})
...@@ -223,7 +223,7 @@ describe 'Problem', -> ...@@ -223,7 +223,7 @@ describe 'Problem', ->
expect($('label[for="input_1_1_3"]')).toHaveAttr 'correct_answer', 'true' expect($('label[for="input_1_1_3"]')).toHaveAttr 'correct_answer', 'true'
expect($('label[for="input_1_2_1"]')).not.toHaveAttr 'correct_answer', 'true' expect($('label[for="input_1_2_1"]')).not.toHaveAttr 'correct_answer', 'true'
describe 'when the answers are alreay shown', -> describe 'when the answers are already shown', ->
beforeEach -> beforeEach ->
@problem.el.addClass 'showed' @problem.el.addClass 'showed'
@problem.el.prepend ''' @problem.el.prepend '''
...@@ -243,7 +243,7 @@ describe 'Problem', -> ...@@ -243,7 +243,7 @@ describe 'Problem', ->
it 'toggle the show answer button', -> it 'toggle the show answer button', ->
@problem.show() @problem.show()
expect($('.show')).toHaveValue 'Show Answer' expect($('.show .show-label')).toHaveText 'Show Answer(s)'
it 'remove the showed class from element', -> it 'remove the showed class from element', ->
@problem.show() @problem.show()
...@@ -261,7 +261,7 @@ describe 'Problem', -> ...@@ -261,7 +261,7 @@ describe 'Problem', ->
it 'POST to save problem', -> it 'POST to save problem', ->
spyOn $, 'postWithPrefix' spyOn $, 'postWithPrefix'
@problem.save() @problem.save()
expect($.postWithPrefix).toHaveBeenCalledWith '/problem/Problem1/problem_save', expect($.postWithPrefix).toHaveBeenCalledWith '/problem/Problem1/problem_save',
'foo=1&bar=2', jasmine.any(Function) 'foo=1&bar=2', jasmine.any(Function)
# TODO: figure out why failing # TODO: figure out why failing
......
...@@ -28,7 +28,7 @@ jasmine.stubRequests = -> ...@@ -28,7 +28,7 @@ jasmine.stubRequests = ->
spyOn($, 'ajax').andCallFake (settings) -> spyOn($, 'ajax').andCallFake (settings) ->
if match = settings.url.match /youtube\.com\/.+\/videos\/(.+)\?v=2&alt=jsonc/ if match = settings.url.match /youtube\.com\/.+\/videos\/(.+)\?v=2&alt=jsonc/
settings.success data: jasmine.stubbedMetadata[match[1]] settings.success data: jasmine.stubbedMetadata[match[1]]
else if match = settings.url.match /static\/subs\/(.+)\.srt\.sjson/ else if match = settings.url.match /static(\/.*)?\/subs\/(.+)\.srt\.sjson/
settings.success jasmine.stubbedCaption settings.success jasmine.stubbedCaption
else if settings.url.match /.+\/problem_get$/ else if settings.url.match /.+\/problem_get$/
settings.success html: readFixtures('problem_content.html') settings.success html: readFixtures('problem_content.html')
...@@ -47,19 +47,15 @@ jasmine.stubYoutubePlayer = -> ...@@ -47,19 +47,15 @@ jasmine.stubYoutubePlayer = ->
jasmine.stubVideoPlayer = (context, enableParts, createPlayer=true) -> jasmine.stubVideoPlayer = (context, enableParts, createPlayer=true) ->
enableParts = [enableParts] unless $.isArray(enableParts) enableParts = [enableParts] unless $.isArray(enableParts)
suite = context.suite suite = context.suite
currentPartName = suite.description while suite = suite.parentSuite currentPartName = suite.description while suite = suite.parentSuite
enableParts.push currentPartName enableParts.push currentPartName
for part in ['VideoCaption', 'VideoSpeedControl', 'VideoVolumeControl', 'VideoProgressSlider']
unless $.inArray(part, enableParts) >= 0
spyOn window, part
loadFixtures 'video.html' loadFixtures 'video.html'
jasmine.stubRequests() jasmine.stubRequests()
YT.Player = undefined YT.Player = undefined
context.video = new Video 'example', '.75:slowerSpeedYoutubeId,1.0:normalSpeedYoutubeId' videosDefinition = '0.75:slowerSpeedYoutubeId,1.0:normalSpeedYoutubeId'
context.video = new Video '#example', videosDefinition
jasmine.stubYoutubePlayer() jasmine.stubYoutubePlayer()
if createPlayer if createPlayer
return new VideoPlayer(video: context.video) return new VideoPlayer(video: context.video)
......
# TODO: figure out why failing describe 'VideoControl', ->
xdescribe 'VideoControl', ->
beforeEach -> beforeEach ->
jasmine.stubVideoPlayer @ window.onTouchBasedDevice = jasmine.createSpy('onTouchBasedDevice').andReturn false
loadFixtures 'video.html'
$('.video-controls').html '' $('.video-controls').html ''
describe 'constructor', -> describe 'constructor', ->
it 'render the video controls', -> it 'render the video controls', ->
new VideoControl(el: $('.video-controls')) @control = new window.VideoControl(el: $('.video-controls'))
expect($('.video-controls').html()).toContain ''' expect($('.video-controls')).toContain
<div class="slider"></div> ['.slider', 'ul.vcr', 'a.play', '.vidtime', '.add-fullscreen'].join(',')
<div> expect($('.video-controls').find('.vidtime')).toHaveText '0:00 / 0:00'
<ul class="vcr">
<li><a class="video_control play" href="#">Play</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>
'''
it 'bind the playback button', -> it 'bind the playback button', ->
control = new VideoControl(el: $('.video-controls')) @control = new window.VideoControl(el: $('.video-controls'))
expect($('.video_control')).toHandleWith 'click', control.togglePlayback expect($('.video_control')).toHandleWith 'click', @control.togglePlayback
describe 'when on a touch based device', -> describe 'when on a touch based device', ->
beforeEach -> beforeEach ->
spyOn(window, 'onTouchBasedDevice').andReturn true window.onTouchBasedDevice.andReturn true
@control = new window.VideoControl(el: $('.video-controls'))
it 'does not add the play class to video control', -> it 'does not add the play class to video control', ->
new VideoControl(el: $('.video-controls'))
expect($('.video_control')).not.toHaveClass 'play' expect($('.video_control')).not.toHaveClass 'play'
expect($('.video_control')).not.toHaveHtml 'Play' expect($('.video_control')).not.toHaveHtml 'Play'
describe 'when on a non-touch based device', -> describe 'when on a non-touch based device', ->
beforeEach -> beforeEach ->
spyOn(window, 'onTouchBasedDevice').andReturn false @control = new window.VideoControl(el: $('.video-controls'))
it 'add the play class to video control', -> it 'add the play class to video control', ->
new VideoControl(el: $('.video-controls'))
expect($('.video_control')).toHaveClass 'play' expect($('.video_control')).toHaveClass 'play'
expect($('.video_control')).toHaveHtml 'Play' expect($('.video_control')).toHaveHtml 'Play'
describe 'play', -> describe 'play', ->
beforeEach -> beforeEach ->
@control = new VideoControl(el: $('.video-controls')) @control = new window.VideoControl(el: $('.video-controls'))
@control.play() @control.play()
it 'switch playback button to play state', -> it 'switch playback button to play state', ->
...@@ -56,8 +47,9 @@ xdescribe 'VideoControl', -> ...@@ -56,8 +47,9 @@ xdescribe 'VideoControl', ->
expect($('.video_control')).toHaveHtml 'Pause' expect($('.video_control')).toHaveHtml 'Pause'
describe 'pause', -> describe 'pause', ->
beforeEach -> beforeEach ->
@control = new VideoControl(el: $('.video-controls')) @control = new window.VideoControl(el: $('.video-controls'))
@control.pause() @control.pause()
it 'switch playback button to pause state', -> it 'switch playback button to pause state', ->
...@@ -66,8 +58,9 @@ xdescribe 'VideoControl', -> ...@@ -66,8 +58,9 @@ xdescribe 'VideoControl', ->
expect($('.video_control')).toHaveHtml 'Play' expect($('.video_control')).toHaveHtml 'Play'
describe 'togglePlayback', -> describe 'togglePlayback', ->
beforeEach -> beforeEach ->
@control = new VideoControl(el: $('.video-controls')) @control = new window.VideoControl(el: $('.video-controls'))
describe 'when the control does not have play or pause class', -> describe 'when the control does not have play or pause class', ->
beforeEach -> beforeEach ->
......
# TODO: figure out why failing describe 'VideoPlayer', ->
xdescribe 'VideoPlayer', ->
beforeEach -> beforeEach ->
window.onTouchBasedDevice = jasmine.createSpy('onTouchBasedDevice').andReturn false
# It tries to call methods of VideoProgressSlider on Spy
for part in ['VideoCaption', 'VideoSpeedControl', 'VideoVolumeControl', 'VideoProgressSlider', 'VideoControl']
spyOn(window[part].prototype, 'initialize').andCallThrough()
jasmine.stubVideoPlayer @, [], false jasmine.stubVideoPlayer @, [], false
afterEach -> afterEach ->
...@@ -8,7 +11,6 @@ xdescribe 'VideoPlayer', -> ...@@ -8,7 +11,6 @@ xdescribe 'VideoPlayer', ->
describe 'constructor', -> describe 'constructor', ->
beforeEach -> beforeEach ->
spyOn window, 'VideoControl'
spyOn YT, 'Player' spyOn YT, 'Player'
$.fn.qtip.andCallFake -> $.fn.qtip.andCallFake ->
$(this).data('qtip', true) $(this).data('qtip', true)
...@@ -22,32 +24,47 @@ xdescribe 'VideoPlayer', -> ...@@ -22,32 +24,47 @@ xdescribe 'VideoPlayer', ->
expect(@player.currentTime).toEqual 0 expect(@player.currentTime).toEqual 0
it 'set the element', -> it 'set the element', ->
expect(@player.el).toBe '#video_example' expect(@player.el).toHaveId 'video_id'
it 'create video control', -> it 'create video control', ->
expect(window.VideoControl).toHaveBeenCalledWith el: $('.video-controls', @player.el) expect(window.VideoControl.prototype.initialize).toHaveBeenCalled()
expect(@player.control).toBeDefined()
expect(@player.control.el).toBe $('.video-controls', @player.el)
it 'create video caption', -> it 'create video caption', ->
expect(window.VideoCaption).toHaveBeenCalledWith el: @player.el, youtubeId: 'normalSpeedYoutubeId', currentSpeed: '1.0' expect(window.VideoCaption.prototype.initialize).toHaveBeenCalled()
expect(@player.caption).toBeDefined()
expect(@player.caption.el).toBe @player.el
expect(@player.caption.youtubeId).toEqual 'normalSpeedYoutubeId'
expect(@player.caption.currentSpeed).toEqual '1.0'
expect(@player.caption.captionAssetPath).toEqual '/static/subs/'
it 'create video speed control', -> it 'create video speed control', ->
expect(window.VideoSpeedControl).toHaveBeenCalledWith el: $('.secondary-controls', @player.el), speeds: ['0.75', '1.0'], currentSpeed: '1.0' expect(window.VideoSpeedControl.prototype.initialize).toHaveBeenCalled()
expect(@player.speedControl).toBeDefined()
expect(@player.speedControl.el).toBe $('.secondary-controls', @player.el)
expect(@player.speedControl.speeds).toEqual ['0.75', '1.0']
expect(@player.speedControl.currentSpeed).toEqual '1.0'
it 'create video progress slider', -> it 'create video progress slider', ->
expect(window.VideoProgressSlider).toHaveBeenCalledWith el: $('.slider', @player.el) expect(window.VideoSpeedControl.prototype.initialize).toHaveBeenCalled()
expect(@player.progressSlider).toBeDefined()
expect(@player.progressSlider.el).toBe $('.slider', @player.el)
it 'create Youtube player', -> it 'create Youtube player', ->
expect(YT.Player).toHaveBeenCalledWith('example', { expect(YT.Player).toHaveBeenCalledWith('id', {
playerVars: playerVars:
controls: 0 controls: 0
wmode: 'transparent' wmode: 'transparent'
rel: 0 rel: 0
showinfo: 0 showinfo: 0
enablejsapi: 1 enablejsapi: 1
modestbranding: 1
videoId: 'normalSpeedYoutubeId' videoId: 'normalSpeedYoutubeId'
events: events:
onReady: @player.onReady onReady: @player.onReady
onStateChange: @player.onStateChange onStateChange: @player.onStateChange
onPlaybackQualityChange: @player.onPlaybackQualityChange
}) })
it 'bind to video control play event', -> it 'bind to video control play event', ->
...@@ -69,14 +86,13 @@ xdescribe 'VideoPlayer', -> ...@@ -69,14 +86,13 @@ xdescribe 'VideoPlayer', ->
expect($(@player.volumeControl)).toHandleWith 'volumeChange', @player.onVolumeChange expect($(@player.volumeControl)).toHandleWith 'volumeChange', @player.onVolumeChange
it 'bind to key press', -> it 'bind to key press', ->
expect($(document)).toHandleWith 'keyup', @player.bindExitFullScreen expect($(document.documentElement)).toHandleWith 'keyup', @player.bindExitFullScreen
it 'bind to fullscreen switching button', -> it 'bind to fullscreen switching button', ->
expect($('.add-fullscreen')).toHandleWith 'click', @player.toggleFullScreen expect($('.add-fullscreen')).toHandleWith 'click', @player.toggleFullScreen
describe 'when not on a touch based device', -> describe 'when not on a touch based device', ->
beforeEach -> beforeEach ->
spyOn(window, 'onTouchBasedDevice').andReturn false
$('.add-fullscreen, .hide-subtitles').removeData 'qtip' $('.add-fullscreen, .hide-subtitles').removeData 'qtip'
@player = new VideoPlayer video: @video @player = new VideoPlayer video: @video
...@@ -85,11 +101,13 @@ xdescribe 'VideoPlayer', -> ...@@ -85,11 +101,13 @@ xdescribe 'VideoPlayer', ->
expect($('.hide-subtitles')).toHaveData 'qtip' expect($('.hide-subtitles')).toHaveData 'qtip'
it 'create video volume control', -> it 'create video volume control', ->
expect(window.VideoVolumeControl).toHaveBeenCalledWith el: $('.secondary-controls', @player.el) expect(window.VideoVolumeControl.prototype.initialize).toHaveBeenCalled()
expect(@player.volumeControl).toBeDefined()
expect(@player.volumeControl.el).toBe $('.secondary-controls', @player.el)
describe 'when on a touch based device', -> describe 'when on a touch based device', ->
beforeEach -> beforeEach ->
spyOn(window, 'onTouchBasedDevice').andReturn true window.onTouchBasedDevice.andReturn true
$('.add-fullscreen, .hide-subtitles').removeData 'qtip' $('.add-fullscreen, .hide-subtitles').removeData 'qtip'
@player = new VideoPlayer video: @video @player = new VideoPlayer video: @video
...@@ -98,7 +116,8 @@ xdescribe 'VideoPlayer', -> ...@@ -98,7 +116,8 @@ xdescribe 'VideoPlayer', ->
expect($('.hide-subtitles')).not.toHaveData 'qtip' expect($('.hide-subtitles')).not.toHaveData 'qtip'
it 'does not create video volume control', -> it 'does not create video volume control', ->
expect(window.VideoVolumeControl).not.toHaveBeenCalled() expect(window.VideoVolumeControl.prototype.initialize).not.toHaveBeenCalled()
expect(@player.volumeControl).not.toBeDefined()
describe 'onReady', -> describe 'onReady', ->
beforeEach -> beforeEach ->
...@@ -110,7 +129,6 @@ xdescribe 'VideoPlayer', -> ...@@ -110,7 +129,6 @@ xdescribe 'VideoPlayer', ->
describe 'when not on a touch based device', -> describe 'when not on a touch based device', ->
beforeEach -> beforeEach ->
spyOn(window, 'onTouchBasedDevice').andReturn false
spyOn @player, 'play' spyOn @player, 'play'
@player.onReady() @player.onReady()
...@@ -119,7 +137,7 @@ xdescribe 'VideoPlayer', -> ...@@ -119,7 +137,7 @@ xdescribe 'VideoPlayer', ->
describe 'when on a touch based device', -> describe 'when on a touch based device', ->
beforeEach -> beforeEach ->
spyOn(window, 'onTouchBasedDevice').andReturn true window.onTouchBasedDevice.andReturn true
spyOn @player, 'play' spyOn @player, 'play'
@player.onReady() @player.onReady()
...@@ -347,9 +365,6 @@ xdescribe 'VideoPlayer', -> ...@@ -347,9 +365,6 @@ xdescribe 'VideoPlayer', ->
it 'replace the full screen button tooltip', -> it 'replace the full screen button tooltip', ->
expect($('.add-fullscreen')).toHaveAttr 'title', 'Exit fill browser' expect($('.add-fullscreen')).toHaveAttr 'title', 'Exit fill browser'
it 'add a new exit from fullscreen button', ->
expect(@player.el).toContain 'a.exit'
it 'add the fullscreen class', -> it 'add the fullscreen class', ->
expect(@player.el).toHaveClass 'fullscreen' expect(@player.el).toHaveClass 'fullscreen'
...@@ -438,7 +453,7 @@ xdescribe 'VideoPlayer', -> ...@@ -438,7 +453,7 @@ xdescribe 'VideoPlayer', ->
describe 'volume', -> describe 'volume', ->
beforeEach -> beforeEach ->
@player = new VideoPlayer @video @player = new VideoPlayer video: @video
@player.player.getVolume.andReturn 42 @player.player.getVolume.andReturn 42
describe 'without value', -> describe 'without value', ->
......
# TODO: figure out why failing describe 'VideoProgressSlider', ->
xdescribe 'VideoProgressSlider', ->
beforeEach -> beforeEach ->
jasmine.stubVideoPlayer @ window.onTouchBasedDevice = jasmine.createSpy('onTouchBasedDevice').andReturn false
describe 'constructor', -> describe 'constructor', ->
describe 'on a non-touch based device', -> describe 'on a non-touch based device', ->
beforeEach -> beforeEach ->
spyOn($.fn, 'slider').andCallThrough() spyOn($.fn, 'slider').andCallThrough()
spyOn(window, 'onTouchBasedDevice').andReturn false @player = jasmine.stubVideoPlayer @
@slider = new VideoProgressSlider el: $('.slider') @progressSlider = @player.progressSlider
it 'build the slider', -> it 'build the slider', ->
expect(@slider.slider).toBe '.slider' expect(@progressSlider.slider).toBe '.slider'
expect($.fn.slider).toHaveBeenCalledWith expect($.fn.slider).toHaveBeenCalledWith
range: 'min' range: 'min'
change: @slider.onChange change: @progressSlider.onChange
slide: @slider.onSlide slide: @progressSlider.onSlide
stop: @slider.onStop stop: @progressSlider.onStop
it 'build the seek handle', -> it 'build the seek handle', ->
expect(@slider.handle).toBe '.slider .ui-slider-handle' expect(@progressSlider.handle).toBe '.slider .ui-slider-handle'
expect($.fn.qtip).toHaveBeenCalledWith expect($.fn.qtip).toHaveBeenCalledWith
content: "0:00" content: "0:00"
position: position:
my: 'bottom center' my: 'bottom center'
at: 'top center' at: 'top center'
container: @slider.handle container: @progressSlider.handle
hide: hide:
delay: 700 delay: 700
style: style:
...@@ -34,47 +33,51 @@ xdescribe 'VideoProgressSlider', -> ...@@ -34,47 +33,51 @@ xdescribe 'VideoProgressSlider', ->
describe 'on a touch-based device', -> describe 'on a touch-based device', ->
beforeEach -> beforeEach ->
window.onTouchBasedDevice.andReturn true
spyOn($.fn, 'slider').andCallThrough() spyOn($.fn, 'slider').andCallThrough()
spyOn(window, 'onTouchBasedDevice').andReturn true @player = jasmine.stubVideoPlayer @
@slider = new VideoProgressSlider el: $('.slider') @progressSlider = @player.progressSlider
it 'does not build the slider', -> it 'does not build the slider', ->
expect(@slider.slider).toBeUndefined expect(@progressSlider.slider).toBeUndefined
expect($.fn.slider).not.toHaveBeenCalled() expect($.fn.slider).not.toHaveBeenCalled()
describe 'play', -> describe 'play', ->
beforeEach -> beforeEach ->
@slider = new VideoProgressSlider el: $('.slider') spyOn(VideoProgressSlider.prototype, 'buildSlider').andCallThrough()
spyOn($.fn, 'slider').andCallThrough() @player = jasmine.stubVideoPlayer @
@progressSlider = @player.progressSlider
describe 'when the slider was already built', -> describe 'when the slider was already built', ->
beforeEach -> beforeEach ->
@slider.play() @progressSlider.play()
it 'does not build the slider', -> it 'does not build the slider', ->
expect($.fn.slider).not.toHaveBeenCalled expect(@progressSlider.buildSlider.calls.length).toEqual 1
describe 'when the slider was not already built', -> describe 'when the slider was not already built', ->
beforeEach -> beforeEach ->
@slider.slider = null spyOn($.fn, 'slider').andCallThrough()
@slider.play() @progressSlider.slider = null
@progressSlider.play()
it 'build the slider', -> it 'build the slider', ->
expect(@slider.slider).toBe '.slider' expect(@progressSlider.slider).toBe '.slider'
expect($.fn.slider).toHaveBeenCalledWith expect($.fn.slider).toHaveBeenCalledWith
range: 'min' range: 'min'
change: @slider.onChange change: @progressSlider.onChange
slide: @slider.onSlide slide: @progressSlider.onSlide
stop: @slider.onStop stop: @progressSlider.onStop
it 'build the seek handle', -> it 'build the seek handle', ->
expect(@slider.handle).toBe '.ui-slider-handle' expect(@progressSlider.handle).toBe '.ui-slider-handle'
expect($.fn.qtip).toHaveBeenCalledWith expect($.fn.qtip).toHaveBeenCalledWith
content: "0:00" content: "0:00"
position: position:
my: 'bottom center' my: 'bottom center'
at: 'top center' at: 'top center'
container: @slider.handle container: @progressSlider.handle
hide: hide:
delay: 700 delay: 700
style: style:
...@@ -83,21 +86,23 @@ xdescribe 'VideoProgressSlider', -> ...@@ -83,21 +86,23 @@ xdescribe 'VideoProgressSlider', ->
describe 'updatePlayTime', -> describe 'updatePlayTime', ->
beforeEach -> beforeEach ->
@slider = new VideoProgressSlider el: $('.slider') @player = jasmine.stubVideoPlayer @
spyOn($.fn, 'slider').andCallThrough() @progressSlider = @player.progressSlider
describe 'when frozen', -> describe 'when frozen', ->
beforeEach -> beforeEach ->
@slider.frozen = true spyOn($.fn, 'slider').andCallThrough()
@slider.updatePlayTime 20, 120 @progressSlider.frozen = true
@progressSlider.updatePlayTime 20, 120
it 'does not update the slider', -> it 'does not update the slider', ->
expect($.fn.slider).not.toHaveBeenCalled() expect($.fn.slider).not.toHaveBeenCalled()
describe 'when not frozen', -> describe 'when not frozen', ->
beforeEach -> beforeEach ->
@slider.frozen = false spyOn($.fn, 'slider').andCallThrough()
@slider.updatePlayTime 20, 120 @progressSlider.frozen = false
@progressSlider.updatePlayTime 20, 120
it 'update the max value of the slider', -> it 'update the max value of the slider', ->
expect($.fn.slider).toHaveBeenCalledWith 'option', 'max', 120 expect($.fn.slider).toHaveBeenCalledWith 'option', 'max', 120
...@@ -107,55 +112,58 @@ xdescribe 'VideoProgressSlider', -> ...@@ -107,55 +112,58 @@ xdescribe 'VideoProgressSlider', ->
describe 'onSlide', -> describe 'onSlide', ->
beforeEach -> beforeEach ->
@slider = new VideoProgressSlider el: $('.slider') @player = jasmine.stubVideoPlayer @
@progressSlider = @player.progressSlider
@time = null @time = null
$(@slider).bind 'seek', (event, time) => @time = time $(@progressSlider).bind 'seek', (event, time) => @time = time
spyOnEvent @slider, 'seek' spyOnEvent @progressSlider, 'seek'
@slider.onSlide {}, value: 20 @progressSlider.onSlide {}, value: 20
it 'freeze the slider', -> it 'freeze the slider', ->
expect(@slider.frozen).toBeTruthy() expect(@progressSlider.frozen).toBeTruthy()
it 'update the tooltip', -> it 'update the tooltip', ->
expect($.fn.qtip).toHaveBeenCalled() expect($.fn.qtip).toHaveBeenCalled()
it 'trigger seek event', -> it 'trigger seek event', ->
expect('seek').toHaveBeenTriggeredOn @slider expect('seek').toHaveBeenTriggeredOn @progressSlider
expect(@time).toEqual 20 expect(@time).toEqual 20
describe 'onChange', -> describe 'onChange', ->
beforeEach -> beforeEach ->
@slider = new VideoProgressSlider el: $('.slider') @player = jasmine.stubVideoPlayer @
@slider.onChange {}, value: 20 @progressSlider = @player.progressSlider
@progressSlider.onChange {}, value: 20
it 'update the tooltip', -> it 'update the tooltip', ->
expect($.fn.qtip).toHaveBeenCalled() expect($.fn.qtip).toHaveBeenCalled()
describe 'onStop', -> describe 'onStop', ->
beforeEach -> beforeEach ->
@slider = new VideoProgressSlider el: $('.slider') @player = jasmine.stubVideoPlayer @
@progressSlider = @player.progressSlider
@time = null @time = null
$(@slider).bind 'seek', (event, time) => @time = time $(@progressSlider).bind 'seek', (event, time) => @time = time
spyOnEvent @slider, 'seek' spyOnEvent @progressSlider, 'seek'
spyOn(window, 'setTimeout') @progressSlider.onStop {}, value: 20
@slider.onStop {}, value: 20
it 'freeze the slider', -> it 'freeze the slider', ->
expect(@slider.frozen).toBeTruthy() expect(@progressSlider.frozen).toBeTruthy()
it 'trigger seek event', -> it 'trigger seek event', ->
expect('seek').toHaveBeenTriggeredOn @slider expect('seek').toHaveBeenTriggeredOn @progressSlider
expect(@time).toEqual 20 expect(@time).toEqual 20
it 'set timeout to unfreeze the slider', -> it 'set timeout to unfreeze the slider', ->
expect(window.setTimeout).toHaveBeenCalledWith jasmine.any(Function), 200 expect(window.setTimeout).toHaveBeenCalledWith jasmine.any(Function), 200
window.setTimeout.mostRecentCall.args[0]() window.setTimeout.mostRecentCall.args[0]()
expect(@slider.frozen).toBeFalsy() expect(@progressSlider.frozen).toBeFalsy()
describe 'updateTooltip', -> describe 'updateTooltip', ->
beforeEach -> beforeEach ->
@slider = new VideoProgressSlider el: $('.slider') @player = jasmine.stubVideoPlayer @
@slider.updateTooltip 90 @progressSlider = @player.progressSlider
@progressSlider.updateTooltip 90
it 'set the tooltip value', -> it 'set the tooltip value', ->
expect($.fn.qtip).toHaveBeenCalledWith 'option', 'content.text', '1:30' expect($.fn.qtip).toHaveBeenCalledWith 'option', 'content.text', '1:30'
# TODO: figure out why failing describe 'VideoSpeedControl', ->
xdescribe 'VideoSpeedControl', ->
beforeEach -> beforeEach ->
window.onTouchBasedDevice = jasmine.createSpy('onTouchBasedDevice').andReturn false
jasmine.stubVideoPlayer @ jasmine.stubVideoPlayer @
$('.speeds').remove() $('.speeds').remove()
...@@ -10,22 +10,23 @@ xdescribe 'VideoSpeedControl', -> ...@@ -10,22 +10,23 @@ xdescribe 'VideoSpeedControl', ->
@speedControl = new VideoSpeedControl el: $('.secondary-controls'), speeds: @video.speeds, currentSpeed: '1.0' @speedControl = new VideoSpeedControl el: $('.secondary-controls'), speeds: @video.speeds, currentSpeed: '1.0'
it 'add the video speed control to player', -> it 'add the video speed control to player', ->
expect($('.secondary-controls').html()).toContain ''' secondaryControls = $('.secondary-controls')
<div class="speeds"> li = secondaryControls.find('.video_speeds li')
<a href="#"> expect(secondaryControls).toContain '.speeds'
<h3>Speed</h3> expect(secondaryControls).toContain '.video_speeds'
<p class="active">1.0x</p> expect(secondaryControls.find('p.active').text()).toBe '1.0x'
</a> expect(li.filter('.active')).toHaveData 'speed', @speedControl.currentSpeed
<ol class="video_speeds"><li data-speed="1.0" class="active"><a href="#">1.0x</a></li><li data-speed="0.75"><a href="#">0.75x</a></li></ol> expect(li.length).toBe @speedControl.speeds.length
</div> $.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', -> it 'bind to change video speed link', ->
expect($('.video_speeds a')).toHandleWith 'click', @speedControl.changeVideoSpeed expect($('.video_speeds a')).toHandleWith 'click', @speedControl.changeVideoSpeed
describe 'when running on touch based device', -> describe 'when running on touch based device', ->
beforeEach -> beforeEach ->
spyOn(window, 'onTouchBasedDevice').andReturn true window.onTouchBasedDevice.andReturn true
$('.speeds').removeClass 'open' $('.speeds').removeClass 'open'
@speedControl = new VideoSpeedControl el: $('.secondary-controls'), speeds: @video.speeds, currentSpeed: '1.0' @speedControl = new VideoSpeedControl el: $('.secondary-controls'), speeds: @video.speeds, currentSpeed: '1.0'
...@@ -37,7 +38,6 @@ xdescribe 'VideoSpeedControl', -> ...@@ -37,7 +38,6 @@ xdescribe 'VideoSpeedControl', ->
describe 'when running on non-touch based device', -> describe 'when running on non-touch based device', ->
beforeEach -> beforeEach ->
spyOn(window, 'onTouchBasedDevice').andReturn false
$('.speeds').removeClass 'open' $('.speeds').removeClass 'open'
@speedControl = new VideoSpeedControl el: $('.secondary-controls'), speeds: @video.speeds, currentSpeed: '1.0' @speedControl = new VideoSpeedControl el: $('.secondary-controls'), speeds: @video.speeds, currentSpeed: '1.0'
......
# TODO: figure out why failing describe 'VideoVolumeControl', ->
xdescribe 'VideoVolumeControl', ->
beforeEach -> beforeEach ->
jasmine.stubVideoPlayer @ jasmine.stubVideoPlayer @
$('.volume').remove() $('.volume').remove()
......
# TODO: figure out why failing describe 'Video', ->
xdescribe 'Video', -> metadata = undefined
beforeEach -> beforeEach ->
loadFixtures 'video.html' loadFixtures 'video.html'
jasmine.stubRequests() jasmine.stubRequests()
@videosDefinition = '.75:slowerSpeedYoutubeId,1.0:normalSpeedYoutubeId' @videosDefinition = '0.75:slowerSpeedYoutubeId,1.0:normalSpeedYoutubeId'
@slowerSpeedYoutubeId = 'slowerSpeedYoutubeId' @slowerSpeedYoutubeId = 'slowerSpeedYoutubeId'
@normalSpeedYoutubeId = 'normalSpeedYoutubeId' @normalSpeedYoutubeId = 'normalSpeedYoutubeId'
metadata =
slowerSpeedYoutubeId:
id: @slowerSpeedYoutubeId
duration: 300
normalSpeedYoutubeId:
id: @normalSpeedYoutubeId
duration: 200
afterEach -> afterEach ->
window.player = undefined window.player = undefined
...@@ -16,17 +24,18 @@ xdescribe 'Video', -> ...@@ -16,17 +24,18 @@ xdescribe 'Video', ->
beforeEach -> beforeEach ->
@stubVideoPlayer = jasmine.createSpy('VideoPlayer') @stubVideoPlayer = jasmine.createSpy('VideoPlayer')
$.cookie.andReturn '0.75' $.cookie.andReturn '0.75'
window.player = 100 window.player = undefined
describe 'by default', -> describe 'by default', ->
beforeEach -> beforeEach ->
@video = new Video 'example', @videosDefinition spyOn(window.Video.prototype, 'fetchMetadata').andCallFake ->
@metadata = metadata
@video = new Video '#example', @videosDefinition
it 'reset the current video player', -> it 'reset the current video player', ->
expect(window.player).toBeNull() expect(window.player).toBeNull()
it 'set the elements', -> it 'set the elements', ->
expect(@video.el).toBe '#video_example' expect(@video.el).toBe '#video_id'
it 'parse the videos', -> it 'parse the videos', ->
expect(@video.videos).toEqual expect(@video.videos).toEqual
...@@ -34,13 +43,8 @@ xdescribe 'Video', -> ...@@ -34,13 +43,8 @@ xdescribe 'Video', ->
'1.0': @normalSpeedYoutubeId '1.0': @normalSpeedYoutubeId
it 'fetch the video metadata', -> it 'fetch the video metadata', ->
expect(@video.metadata).toEqual expect(@video.fetchMetadata).toHaveBeenCalled
slowerSpeedYoutubeId: expect(@video.metadata).toEqual metadata
id: @slowerSpeedYoutubeId
duration: 300
normalSpeedYoutubeId:
id: @normalSpeedYoutubeId
duration: 200
it 'parse available video speeds', -> it 'parse available video speeds', ->
expect(@video.speeds).toEqual ['0.75', '1.0'] expect(@video.speeds).toEqual ['0.75', '1.0']
...@@ -56,7 +60,7 @@ xdescribe 'Video', -> ...@@ -56,7 +60,7 @@ xdescribe 'Video', ->
@originalYT = window.YT @originalYT = window.YT
window.YT = { Player: true } window.YT = { Player: true }
spyOn(window, 'VideoPlayer').andReturn(@stubVideoPlayer) spyOn(window, 'VideoPlayer').andReturn(@stubVideoPlayer)
@video = new Video 'example', @videosDefinition @video = new Video '#example', @videosDefinition
afterEach -> afterEach ->
window.YT = @originalYT window.YT = @originalYT
...@@ -69,7 +73,7 @@ xdescribe 'Video', -> ...@@ -69,7 +73,7 @@ xdescribe 'Video', ->
beforeEach -> beforeEach ->
@originalYT = window.YT @originalYT = window.YT
window.YT = {} window.YT = {}
@video = new Video 'example', @videosDefinition @video = new Video '#example', @videosDefinition
afterEach -> afterEach ->
window.YT = @originalYT window.YT = @originalYT
...@@ -82,7 +86,7 @@ xdescribe 'Video', -> ...@@ -82,7 +86,7 @@ xdescribe 'Video', ->
@originalYT = window.YT @originalYT = window.YT
window.YT = {} window.YT = {}
spyOn(window, 'VideoPlayer').andReturn(@stubVideoPlayer) spyOn(window, 'VideoPlayer').andReturn(@stubVideoPlayer)
@video = new Video 'example', @videosDefinition @video = new Video '#example', @videosDefinition
window.onYouTubePlayerAPIReady() window.onYouTubePlayerAPIReady()
afterEach -> afterEach ->
...@@ -95,7 +99,7 @@ xdescribe 'Video', -> ...@@ -95,7 +99,7 @@ xdescribe 'Video', ->
describe 'youtubeId', -> describe 'youtubeId', ->
beforeEach -> beforeEach ->
$.cookie.andReturn '1.0' $.cookie.andReturn '1.0'
@video = new Video 'example', @videosDefinition @video = new Video '#example', @videosDefinition
describe 'with speed', -> describe 'with speed', ->
it 'return the video id for given speed', -> it 'return the video id for given speed', ->
...@@ -108,7 +112,7 @@ xdescribe 'Video', -> ...@@ -108,7 +112,7 @@ xdescribe 'Video', ->
describe 'setSpeed', -> describe 'setSpeed', ->
beforeEach -> beforeEach ->
@video = new Video 'example', @videosDefinition @video = new Video '#example', @videosDefinition
describe 'when new speed is available', -> describe 'when new speed is available', ->
beforeEach -> beforeEach ->
...@@ -129,14 +133,14 @@ xdescribe 'Video', -> ...@@ -129,14 +133,14 @@ xdescribe 'Video', ->
describe 'getDuration', -> describe 'getDuration', ->
beforeEach -> beforeEach ->
@video = new Video 'example', @videosDefinition @video = new Video '#example', @videosDefinition
it 'return duration for current video', -> it 'return duration for current video', ->
expect(@video.getDuration()).toEqual 200 expect(@video.getDuration()).toEqual 200
describe 'log', -> describe 'log', ->
beforeEach -> beforeEach ->
@video = new Video 'example', @videosDefinition @video = new Video '#example', @videosDefinition
@video.setSpeed '1.0' @video.setSpeed '1.0'
spyOn Logger, 'log' spyOn Logger, 'log'
@video.player = { currentTime: 25 } @video.player = { currentTime: 25 }
...@@ -144,7 +148,7 @@ xdescribe 'Video', -> ...@@ -144,7 +148,7 @@ xdescribe 'Video', ->
it 'call the logger with valid parameters', -> it 'call the logger with valid parameters', ->
expect(Logger.log).toHaveBeenCalledWith 'someEvent', expect(Logger.log).toHaveBeenCalledWith 'someEvent',
id: 'example' id: 'id'
code: @normalSpeedYoutubeId code: @normalSpeedYoutubeId
currentTime: 25 currentTime: 25
speed: '1.0' speed: '1.0'
...@@ -19,12 +19,12 @@ class @Problem ...@@ -19,12 +19,12 @@ class @Problem
problem_prefix = @element_id.replace(/problem_/,'') problem_prefix = @element_id.replace(/problem_/,'')
@inputs = @$("[id^=input_#{problem_prefix}_]") @inputs = @$("[id^=input_#{problem_prefix}_]")
@$('section.action input:button').click @refreshAnswers @$('section.action input:button').click @refreshAnswers
@$('section.action input.check').click @check_fd @$('section.action input.check').click @check_fd
#@$('section.action input.check').click @check #@$('section.action input.check').click @check
@$('section.action input.reset').click @reset @$('section.action input.reset').click @reset
@$('section.action input.show').click @show @$('section.action button.show').click @show
@$('section.action input.save').click @save @$('section.action input.save').click @save
# Collapsibles # Collapsibles
...@@ -44,7 +44,7 @@ class @Problem ...@@ -44,7 +44,7 @@ class @Problem
forceUpdate: (response) => forceUpdate: (response) =>
@el.attr progress: response.progress_status @el.attr progress: response.progress_status
@el.trigger('progressChanged') @el.trigger('progressChanged')
queueing: => queueing: =>
@queued_items = @$(".xqueue") @queued_items = @$(".xqueue")
...@@ -59,11 +59,11 @@ class @Problem ...@@ -59,11 +59,11 @@ class @Problem
get_queuelen: => get_queuelen: =>
minlen = Infinity minlen = Infinity
@queued_items.each (index, qitem) -> @queued_items.each (index, qitem) ->
len = parseInt($.text(qitem)) len = parseInt($.text(qitem))
if len < minlen if len < minlen
minlen = len minlen = len
return minlen return minlen
poll: => poll: =>
$.postWithPrefix "#{@url}/problem_get", (response) => $.postWithPrefix "#{@url}/problem_get", (response) =>
# If queueing status changed, then render # If queueing status changed, then render
...@@ -73,9 +73,9 @@ class @Problem ...@@ -73,9 +73,9 @@ class @Problem
JavascriptLoader.executeModuleScripts @el, () => JavascriptLoader.executeModuleScripts @el, () =>
@setupInputTypes() @setupInputTypes()
@bind() @bind()
@num_queued_items = @new_queued_items.length @num_queued_items = @new_queued_items.length
if @num_queued_items == 0 if @num_queued_items == 0
@forceUpdate response @forceUpdate response
delete window.queuePollerID delete window.queuePollerID
else else
...@@ -83,12 +83,12 @@ class @Problem ...@@ -83,12 +83,12 @@ class @Problem
window.queuePollerID = window.setTimeout(@poll, 1000) window.queuePollerID = window.setTimeout(@poll, 1000)
# Use this if you want to make an ajax call on the input type object # Use this if you want to make an ajax call on the input type object
# static method so you don't have to instantiate a Problem in order to use it # static method so you don't have to instantiate a Problem in order to use it
# Input: # Input:
# url: the AJAX url of the problem # url: the AJAX url of the problem
# input_id: the input_id of the input you would like to make the call on # input_id: the input_id of the input you would like to make the call on
# NOTE: the id is the ${id} part of "input_${id}" during rendering # NOTE: the id is the ${id} part of "input_${id}" during rendering
# If this function is passed the entire prefixed id, the backend may have trouble # If this function is passed the entire prefixed id, the backend may have trouble
# finding the correct input # finding the correct input
# dispatch: string that indicates how this data should be handled by the inputtype # dispatch: string that indicates how this data should be handled by the inputtype
...@@ -98,7 +98,7 @@ class @Problem ...@@ -98,7 +98,7 @@ class @Problem
data['dispatch'] = dispatch data['dispatch'] = dispatch
data['input_id'] = input_id data['input_id'] = input_id
$.postWithPrefix "#{url}/input_ajax", data, callback $.postWithPrefix "#{url}/input_ajax", data, callback
render: (content) -> render: (content) ->
if content if content
...@@ -141,7 +141,7 @@ class @Problem ...@@ -141,7 +141,7 @@ class @Problem
Logger.log 'problem_check', @answers Logger.log 'problem_check', @answers
# If there are no file inputs in the problem, we can fall back on @check # If there are no file inputs in the problem, we can fall back on @check
if $('input:file').length == 0 if $('input:file').length == 0
@check() @check()
return return
...@@ -150,7 +150,7 @@ class @Problem ...@@ -150,7 +150,7 @@ class @Problem
return return
fd = new FormData() fd = new FormData()
# Sanity checks on submission # Sanity checks on submission
max_filesize = 4*1000*1000 # 4 MB max_filesize = 4*1000*1000 # 4 MB
file_too_large = false file_too_large = false
...@@ -195,19 +195,19 @@ class @Problem ...@@ -195,19 +195,19 @@ class @Problem
abort_submission = file_too_large or file_not_selected or unallowed_file_submitted or required_files_not_submitted abort_submission = file_too_large or file_not_selected or unallowed_file_submitted or required_files_not_submitted
settings = settings =
type: "POST" type: "POST"
data: fd data: fd
processData: false processData: false
contentType: false contentType: false
success: (response) => success: (response) =>
switch response.success switch response.success
when 'incorrect', 'correct' when 'incorrect', 'correct'
@render(response.contents) @render(response.contents)
@updateProgress response @updateProgress response
else else
@gentle_alert response.success @gentle_alert response.success
if not abort_submission if not abort_submission
$.ajaxWithPrefix("#{@url}/problem_check", settings) $.ajaxWithPrefix("#{@url}/problem_check", settings)
...@@ -260,14 +260,14 @@ class @Problem ...@@ -260,14 +260,14 @@ class @Problem
@el.find('.problem > div').each (index, element) => @el.find('.problem > div').each (index, element) =>
MathJax.Hub.Queue ["Typeset", MathJax.Hub, element] MathJax.Hub.Queue ["Typeset", MathJax.Hub, element]
@$('.show').val 'Hide Answer' @$('.show-label').text 'Hide Answer(s)'
@el.addClass 'showed' @el.addClass 'showed'
@updateProgress response @updateProgress response
else else
@$('[id^=answer_], [id^=solution_]').text '' @$('[id^=answer_], [id^=solution_]').text ''
@$('[correct_answer]').attr correct_answer: null @$('[correct_answer]').attr correct_answer: null
@el.removeClass 'showed' @el.removeClass 'showed'
@$('.show').val 'Show Answer' @$('.show-label').text 'Show Answer(s)'
@el.find(".capa_inputtype").each (index, inputtype) => @el.find(".capa_inputtype").each (index, inputtype) =>
display = @inputtypeDisplays[$(inputtype).attr('id')] display = @inputtypeDisplays[$(inputtype).attr('id')]
...@@ -306,7 +306,7 @@ class @Problem ...@@ -306,7 +306,7 @@ class @Problem
MathJax.Hub.Queue(['Text', jax, eqn], [@updateMathML, jax, element]) MathJax.Hub.Queue(['Text', jax, eqn], [@updateMathML, jax, element])
return # Explicit return for CoffeeScript return # Explicit return for CoffeeScript
updateMathML: (jax, element) => updateMathML: (jax, element) =>
try try
$("##{element.id}_dynamath").val(jax.root.toMathML '') $("##{element.id}_dynamath").val(jax.root.toMathML '')
......
...@@ -98,8 +98,10 @@ define('ElOutput', ['logme'], function (logme) { ...@@ -98,8 +98,10 @@ define('ElOutput', ['logme'], function (logme) {
); );
logme('Error message: "' + err.message + '".'); logme('Error message: "' + err.message + '".');
$('#' + gstId).html('<div style="color: red;">' + 'ERROR IN XML: Could not create a function from string "' + funcString + '".' + '</div>'); if (state.showDebugInfo) {
$('#' + gstId).append('<div style="color: red;">' + 'Error message: "' + err.message + '".' + '</div>'); $('#' + gstId).html('<div style="color: red;">' + 'ERROR IN XML: Could not create a function from string "' + funcString + '".' + '</div>');
$('#' + gstId).append('<div style="color: red;">' + 'Error message: "' + err.message + '".' + '</div>');
}
paramNames.pop(); paramNames.pop();
......
...@@ -87,8 +87,10 @@ define('GLabelElOutput', ['logme'], function (logme) { ...@@ -87,8 +87,10 @@ define('GLabelElOutput', ['logme'], function (logme) {
); );
logme('Error message: "' + err.message + '".'); logme('Error message: "' + err.message + '".');
$('#' + gstId).html('<div style="color: red;">' + 'ERROR IN XML: Could not create a function from string "' + funcString + '".' + '</div>'); if (state.showDebugInfo) {
$('#' + gstId).append('<div style="color: red;">' + 'Error message: "' + err.message + '".' + '</div>'); $('#' + gstId).html('<div style="color: red;">' + 'ERROR IN XML: Could not create a function from string "' + funcString + '".' + '</div>');
$('#' + gstId).append('<div style="color: red;">' + 'Error message: "' + err.message + '".' + '</div>');
}
paramNames.pop(); paramNames.pop();
......
...@@ -242,8 +242,10 @@ define('Graph', ['logme'], function (logme) { ...@@ -242,8 +242,10 @@ define('Graph', ['logme'], function (logme) {
); );
logme('Error message: "' + err.message + '"'); logme('Error message: "' + err.message + '"');
$('#' + gstId).html('<div style="color: red;">' + 'ERROR IN XML: Could not create a function from the string "' + funcString + '".' + '</div>'); if (state.showDebugInfo) {
$('#' + gstId).append('<div style="color: red;">' + 'Error message: "' + err.message + '".' + '</div>'); $('#' + gstId).html('<div style="color: red;">' + 'ERROR IN XML: Could not create a function from the string "' + funcString + '".' + '</div>');
$('#' + gstId).append('<div style="color: red;">' + 'Error message: "' + err.message + '".' + '</div>');
}
paramNames.pop(); paramNames.pop();
...@@ -709,15 +711,17 @@ define('Graph', ['logme'], function (logme) { ...@@ -709,15 +711,17 @@ define('Graph', ['logme'], function (logme) {
); );
logme('Error message: "' + err.message + '"'); logme('Error message: "' + err.message + '"');
$('#' + gstId).html( if (state.showDebugInfo) {
'<div style="color: red;">' + 'ERROR IN ' + $('#' + gstId).html(
'XML: Could not create a function from the string "' + '<div style="color: red;">' + 'ERROR IN ' +
funcString + '" for xrange.min.' + '</div>' 'XML: Could not create a function from the string "' +
); funcString + '" for xrange.min.' + '</div>'
$('#' + gstId).append( );
'<div style="color: red;">' + 'Error ' + $('#' + gstId).append(
'message: "' + err.message + '".' + '</div>' '<div style="color: red;">' + 'Error ' +
); 'message: "' + err.message + '".' + '</div>'
);
}
return false; return false;
} }
...@@ -790,15 +794,17 @@ define('Graph', ['logme'], function (logme) { ...@@ -790,15 +794,17 @@ define('Graph', ['logme'], function (logme) {
); );
logme('Error message: "' + err.message + '"'); logme('Error message: "' + err.message + '"');
$('#' + gstId).html( if (state.showDebugInfo) {
'<div style="color: red;">' + 'ERROR IN ' + $('#' + gstId).html(
'XML: Could not create a function from the string "' + '<div style="color: red;">' + 'ERROR IN ' +
funcString + '" for xrange.max.' + '</div>' 'XML: Could not create a function from the string "' +
); funcString + '" for xrange.max.' + '</div>'
$('#' + gstId).append( );
'<div style="color: red;">' + 'Error message: "' + $('#' + gstId).append(
err.message + '".' + '</div>' '<div style="color: red;">' + 'Error message: "' +
); err.message + '".' + '</div>'
);
}
return false; return false;
} }
...@@ -1006,8 +1012,10 @@ define('Graph', ['logme'], function (logme) { ...@@ -1006,8 +1012,10 @@ define('Graph', ['logme'], function (logme) {
); );
logme('Error message: "' + err.message + '"'); logme('Error message: "' + err.message + '"');
$('#' + gstId).html('<div style="color: red;">' + 'ERROR IN XML: Could not create a function from the string "' + funcString + '".' + '</div>'); if (state.showDebugInfo) {
$('#' + gstId).append('<div style="color: red;">' + 'Error message: "' + err.message + '".' + '</div>'); $('#' + gstId).html('<div style="color: red;">' + 'ERROR IN XML: Could not create a function from the string "' + funcString + '".' + '</div>');
$('#' + gstId).append('<div style="color: red;">' + 'Error message: "' + err.message + '".' + '</div>');
}
paramNames.pop(); paramNames.pop();
paramNames.pop(); paramNames.pop();
...@@ -1133,8 +1141,10 @@ define('Graph', ['logme'], function (logme) { ...@@ -1133,8 +1141,10 @@ define('Graph', ['logme'], function (logme) {
logme('ERROR: Could not determine xrange start.'); logme('ERROR: Could not determine xrange start.');
logme('Error message: "' + err.message + '".'); logme('Error message: "' + err.message + '".');
$('#' + gstId).html('<div style="color: red;">' + 'ERROR IN XML: Could not determine xrange start from defined function.' + '</div>'); if (state.showDebugInfo) {
$('#' + gstId).append('<div style="color: red;">' + 'Error message: "' + err.message + '".' + '</div>'); $('#' + gstId).html('<div style="color: red;">' + 'ERROR IN XML: Could not determine xrange start from defined function.' + '</div>');
$('#' + gstId).append('<div style="color: red;">' + 'Error message: "' + err.message + '".' + '</div>');
}
return false; return false;
} }
...@@ -1144,8 +1154,10 @@ define('Graph', ['logme'], function (logme) { ...@@ -1144,8 +1154,10 @@ define('Graph', ['logme'], function (logme) {
logme('ERROR: Could not determine xrange end.'); logme('ERROR: Could not determine xrange end.');
logme('Error message: "' + err.message + '".'); logme('Error message: "' + err.message + '".');
$('#' + gstId).html('<div style="color: red;">' + 'ERROR IN XML: Could not determine xrange end from defined function.' + '</div>'); if (state.showDebugInfo) {
$('#' + gstId).append('<div style="color: red;">' + 'Error message: "' + err.message + '".' + '</div>'); $('#' + gstId).html('<div style="color: red;">' + 'ERROR IN XML: Could not determine xrange end from defined function.' + '</div>');
$('#' + gstId).append('<div style="color: red;">' + 'Error message: "' + err.message + '".' + '</div>');
}
return false; return false;
} }
...@@ -1175,8 +1187,10 @@ define('Graph', ['logme'], function (logme) { ...@@ -1175,8 +1187,10 @@ define('Graph', ['logme'], function (logme) {
logme('ERROR: Could not generate data.'); logme('ERROR: Could not generate data.');
logme('Error message: "' + err.message + '".'); logme('Error message: "' + err.message + '".');
$('#' + gstId).html('<div style="color: red;">' + 'ERROR IN XML: Could not generate data from defined function.' + '</div>'); if (state.showDebugInfo) {
$('#' + gstId).append('<div style="color: red;">' + 'Error message: "' + err.message + '".' + '</div>'); $('#' + gstId).html('<div style="color: red;">' + 'ERROR IN XML: Could not generate data from defined function.' + '</div>');
$('#' + gstId).append('<div style="color: red;">' + 'Error message: "' + err.message + '".' + '</div>');
}
return false; return false;
} }
...@@ -1204,8 +1218,10 @@ define('Graph', ['logme'], function (logme) { ...@@ -1204,8 +1218,10 @@ define('Graph', ['logme'], function (logme) {
logme('ERROR: Could not generate data.'); logme('ERROR: Could not generate data.');
logme('Error message: "' + err.message + '".'); logme('Error message: "' + err.message + '".');
$('#' + gstId).html('<div style="color: red;">' + 'ERROR IN XML: Could not generate data from function.' + '</div>'); if (state.showDebugInfo) {
$('#' + gstId).append('<div style="color: red;">' + 'Error message: "' + err.message + '".' + '</div>'); $('#' + gstId).html('<div style="color: red;">' + 'ERROR IN XML: Could not generate data from function.' + '</div>');
$('#' + gstId).append('<div style="color: red;">' + 'Error message: "' + err.message + '".' + '</div>');
}
return false; return false;
} }
......
...@@ -51,6 +51,8 @@ define( ...@@ -51,6 +51,8 @@ define(
// state object. // state object.
state = State(gstId, config); state = State(gstId, config);
state.showDebugInfo = false;
// It is possible that something goes wrong while extracting parameters // It is possible that something goes wrong while extracting parameters
// from the JSON config object. In this case, we will not continue. // from the JSON config object. In this case, we will not continue.
if (state === undefined) { if (state === undefined) {
......
...@@ -5,7 +5,7 @@ class @Video ...@@ -5,7 +5,7 @@ class @Video
@start = @el.data('start') @start = @el.data('start')
@end = @el.data('end') @end = @el.data('end')
@caption_asset_path = @el.data('caption-asset-path') @caption_asset_path = @el.data('caption-asset-path')
@show_captions = @el.data('show-captions') == "true" @show_captions = @el.data('show-captions')
window.player = null window.player = null
@el = $("#video_#{@id}") @el = $("#video_#{@id}")
@parseVideos() @parseVideos()
...@@ -13,7 +13,7 @@ class @Video ...@@ -13,7 +13,7 @@ class @Video
@parseSpeed() @parseSpeed()
$("#video_#{@id}").data('video', this).addClass('video-load-complete') $("#video_#{@id}").data('video', this).addClass('video-load-complete')
@hide_captions = $.cookie('hide_captions') == 'true' @hide_captions = $.cookie('hide_captions') == 'true' or (not @show_captions)
if YT.Player if YT.Player
@embed() @embed()
......
...@@ -37,7 +37,7 @@ class @VideoCaption extends Subview ...@@ -37,7 +37,7 @@ class @VideoCaption extends Subview
@loaded = true @loaded = true
if onTouchBasedDevice() if onTouchBasedDevice()
$('.subtitles li').html "Caption will be displayed when you start playing the video." $('.subtitles').html "<li>Caption will be displayed when you start playing the video.</li>"
else else
@renderCaption() @renderCaption()
......
...@@ -15,7 +15,7 @@ class @VideoPlayer extends Subview ...@@ -15,7 +15,7 @@ class @VideoPlayer extends Subview
$(@progressSlider).bind('seek', @onSeek) $(@progressSlider).bind('seek', @onSeek)
if @volumeControl if @volumeControl
$(@volumeControl).bind('volumeChange', @onVolumeChange) $(@volumeControl).bind('volumeChange', @onVolumeChange)
$(document).keyup @bindExitFullScreen $(document.documentElement).keyup @bindExitFullScreen
@$('.add-fullscreen').click @toggleFullScreen @$('.add-fullscreen').click @toggleFullScreen
@addToolTip() unless onTouchBasedDevice() @addToolTip() unless onTouchBasedDevice()
......
...@@ -11,7 +11,7 @@ class @VideoProgressSlider extends Subview ...@@ -11,7 +11,7 @@ class @VideoProgressSlider extends Subview
@buildHandle() @buildHandle()
buildHandle: -> buildHandle: ->
@handle = @$('.slider .ui-slider-handle') @handle = @$('.ui-slider-handle')
@handle.qtip @handle.qtip
content: "#{Time.format(@slider.slider('value'))}" content: "#{Time.format(@slider.slider('value'))}"
position: position:
......
...@@ -91,12 +91,17 @@ class @VideoAlpha ...@@ -91,12 +91,17 @@ class @VideoAlpha
getDuration: -> getDuration: ->
@metadata[@youtubeId()].duration @metadata[@youtubeId()].duration
log: (eventName)-> log: (eventName, data)->
# Default parameters that always get logged.
logInfo = logInfo =
id: @id id: @id
code: @youtubeId() code: @youtubeId()
currentTime: @player.currentTime
speed: @speed # If extra parameters were passed to the log.
if data
$.each data, (paramName, value) ->
logInfo[paramName] = value
if @videoType is "youtube" if @videoType is "youtube"
logInfo.code = @youtubeId() logInfo.code = @youtubeId()
else logInfo.code = "html5" if @videoType is "html5" else logInfo.code = "html5" if @videoType is "html5"
......
...@@ -120,7 +120,7 @@ class @VideoCaptionAlpha extends SubviewAlpha ...@@ -120,7 +120,7 @@ class @VideoCaptionAlpha extends SubviewAlpha
seekPlayer: (event) => seekPlayer: (event) =>
event.preventDefault() event.preventDefault()
time = Math.round(Time.convert($(event.target).data('start'), '1.0', @currentSpeed) / 1000) time = Math.round(Time.convert($(event.target).data('start'), '1.0', @currentSpeed) / 1000)
$(@).trigger('seek', time) $(@).trigger('caption_seek', time)
calculateOffset: (element) -> calculateOffset: (element) ->
@captionHeight() / 2 - element.height() / 2 @captionHeight() / 2 - element.height() / 2
......
...@@ -24,9 +24,9 @@ class @VideoPlayerAlpha extends SubviewAlpha ...@@ -24,9 +24,9 @@ class @VideoPlayerAlpha extends SubviewAlpha
if @video.videoType is 'youtube' if @video.videoType is 'youtube'
$(@qualityControl).bind('changeQuality', @handlePlaybackQualityChange) $(@qualityControl).bind('changeQuality', @handlePlaybackQualityChange)
if @video.show_captions is true if @video.show_captions is true
$(@caption).bind('seek', @onSeek) $(@caption).bind('caption_seek', @onSeek)
$(@speedControl).bind('speedChange', @onSpeedChange) $(@speedControl).bind('speedChange', @onSpeedChange)
$(@progressSlider).bind('seek', @onSeek) $(@progressSlider).bind('slide_seek', @onSeek)
if @volumeControl if @volumeControl
$(@volumeControl).bind('volumeChange', @onVolumeChange) $(@volumeControl).bind('volumeChange', @onVolumeChange)
$(document).keyup @bindExitFullScreen $(document).keyup @bindExitFullScreen
...@@ -96,6 +96,7 @@ class @VideoPlayerAlpha extends SubviewAlpha ...@@ -96,6 +96,7 @@ class @VideoPlayerAlpha extends SubviewAlpha
at: 'top center' at: 'top center'
onReady: (event) => onReady: (event) =>
@video.log 'load_video'
if @video.videoType is 'html5' if @video.videoType is 'html5'
@player.setPlaybackRate @video.speed @player.setPlaybackRate @video.speed
unless onTouchBasedDevice() unless onTouchBasedDevice()
...@@ -184,7 +185,8 @@ class @VideoPlayerAlpha extends SubviewAlpha ...@@ -184,7 +185,8 @@ class @VideoPlayerAlpha extends SubviewAlpha
@caption.pause() @caption.pause()
onPlay: => onPlay: =>
@video.log 'play_video' @video.log 'play_video',
currentTime: @currentTime
unless @player.interval unless @player.interval
@player.interval = setInterval(@update, 200) @player.interval = setInterval(@update, 200)
if @video.show_captions is true if @video.show_captions is true
...@@ -193,7 +195,8 @@ class @VideoPlayerAlpha extends SubviewAlpha ...@@ -193,7 +195,8 @@ class @VideoPlayerAlpha extends SubviewAlpha
@progressSlider.play() @progressSlider.play()
onPause: => onPause: =>
@video.log 'pause_video' @video.log 'pause_video',
currentTime: @currentTime
clearInterval(@player.interval) clearInterval(@player.interval)
@player.interval = null @player.interval = null
if @video.show_captions is true if @video.show_captions is true
...@@ -206,6 +209,10 @@ class @VideoPlayerAlpha extends SubviewAlpha ...@@ -206,6 +209,10 @@ class @VideoPlayerAlpha extends SubviewAlpha
@caption.pause() @caption.pause()
onSeek: (event, time) => onSeek: (event, time) =>
@video.log 'seek_video',
old_time: @currentTime
new_time: time
type: event.type
@player.seekTo(time, true) @player.seekTo(time, true)
if @isPlaying() if @isPlaying()
clearInterval(@player.interval) clearInterval(@player.interval)
...@@ -218,6 +225,12 @@ class @VideoPlayerAlpha extends SubviewAlpha ...@@ -218,6 +225,12 @@ class @VideoPlayerAlpha extends SubviewAlpha
if @video.videoType is 'youtube' if @video.videoType is 'youtube'
@currentTime = Time.convert(@currentTime, parseFloat(@currentSpeed()), newSpeed) @currentTime = Time.convert(@currentTime, parseFloat(@currentSpeed()), newSpeed)
newSpeed = parseFloat(newSpeed).toFixed(2).replace /\.00$/, '.0' newSpeed = parseFloat(newSpeed).toFixed(2).replace /\.00$/, '.0'
@video.log 'speed_change_video',
currentTime: @currentTime
old_speed: @currentSpeed()
new_speed: newSpeed
@video.setSpeed newSpeed, updateCookie @video.setSpeed newSpeed, updateCookie
if @video.videoType is 'youtube' if @video.videoType is 'youtube'
if @video.show_captions is true if @video.show_captions is true
......
...@@ -6,6 +6,7 @@ class @VideoProgressSliderAlpha extends SubviewAlpha ...@@ -6,6 +6,7 @@ class @VideoProgressSliderAlpha extends SubviewAlpha
@slider = @el.slider @slider = @el.slider
range: 'min' range: 'min'
change: @onChange change: @onChange
slide: @onSlide slide: @onSlide
stop: @onStop stop: @onStop
@buildHandle() @buildHandle()
...@@ -35,7 +36,7 @@ class @VideoProgressSliderAlpha extends SubviewAlpha ...@@ -35,7 +36,7 @@ class @VideoProgressSliderAlpha extends SubviewAlpha
onSlide: (event, ui) => onSlide: (event, ui) =>
@frozen = true @frozen = true
@updateTooltip(ui.value) @updateTooltip(ui.value)
$(@).trigger('seek', ui.value) $(@).trigger('slide_seek', ui.value)
onChange: (event, ui) => onChange: (event, ui) =>
@updateTooltip(ui.value) @updateTooltip(ui.value)
......
...@@ -3,8 +3,11 @@ from datetime import datetime ...@@ -3,8 +3,11 @@ from datetime import datetime
from . import ModuleStoreBase, Location, namedtuple_to_son from . import ModuleStoreBase, Location, namedtuple_to_son
from .exceptions import ItemNotFoundError from .exceptions import ItemNotFoundError
from .inheritance import own_metadata from .inheritance import own_metadata
from xmodule.exceptions import InvalidVersionError
DRAFT = 'draft' DRAFT = 'draft'
# Things w/ these categories should never be marked as version='draft'
DIRECT_ONLY_CATEGORIES = ['course', 'chapter', 'sequential', 'about', 'static_tab', 'course_info']
def as_draft(location): def as_draft(location):
...@@ -111,6 +114,8 @@ class DraftModuleStore(ModuleStoreBase): ...@@ -111,6 +114,8 @@ class DraftModuleStore(ModuleStoreBase):
Clone a new item that is a copy of the item at the location `source` Clone a new item that is a copy of the item at the location `source`
and writes it to `location` and writes it to `location`
""" """
if Location(location).category in DIRECT_ONLY_CATEGORIES:
raise InvalidVersionError(location)
return wrap_draft(super(DraftModuleStore, self).clone_item(source, as_draft(location))) return wrap_draft(super(DraftModuleStore, self).clone_item(source, as_draft(location)))
def update_item(self, location, data, allow_not_found=False): def update_item(self, location, data, allow_not_found=False):
...@@ -203,6 +208,8 @@ class DraftModuleStore(ModuleStoreBase): ...@@ -203,6 +208,8 @@ class DraftModuleStore(ModuleStoreBase):
""" """
Turn the published version into a draft, removing the published version Turn the published version into a draft, removing the published version
""" """
if Location(location).category in DIRECT_ONLY_CATEGORIES:
raise InvalidVersionError(location)
super(DraftModuleStore, self).clone_item(location, as_draft(location)) super(DraftModuleStore, self).clone_item(location, as_draft(location))
super(DraftModuleStore, self).delete_item(location) super(DraftModuleStore, self).delete_item(location)
......
from xmodule.modulestore import Location import os.path
from xmodule.course_module import CourseDescriptor
from xmodule.modulestore.xml import XMLModuleStore from xmodule.modulestore.xml import XMLModuleStore
from xmodule.modulestore.xml_importer import import_from_xml
from nose.tools import assert_raises
from .test_modulestore import check_path_to_location from .test_modulestore import check_path_to_location
from . import DATA_DIR from . import DATA_DIR
...@@ -15,3 +18,22 @@ class TestXMLModuleStore(object): ...@@ -15,3 +18,22 @@ class TestXMLModuleStore(object):
print "finished import" print "finished import"
check_path_to_location(modulestore) check_path_to_location(modulestore)
def test_unicode_chars_in_xml_content(self):
# edX/full/6.002_Spring_2012 has non-ASCII chars, and during
# uniquification of names, would raise a UnicodeError. It no longer does.
# Ensure that there really is a non-ASCII character in the course.
with open(os.path.join(DATA_DIR, "full/sequential/Administrivia_and_Circuit_Elements.xml")) as xmlf:
xml = xmlf.read()
with assert_raises(UnicodeDecodeError):
xml.decode('ascii')
# Load the course, but don't make error modules. This will succeed,
# but will record the errors.
modulestore = XMLModuleStore(DATA_DIR, course_dirs=['full'], load_error_modules=False)
# Look up the errors during load. There should be none.
location = CourseDescriptor.id_to_location("edX/full/6.002_Spring_2012")
errors = modulestore.get_item_errors(location)
assert errors == []
...@@ -108,7 +108,8 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem): ...@@ -108,7 +108,8 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem):
orig_name = orig_name[len(tag) + 1:-12] orig_name = orig_name[len(tag) + 1:-12]
# append the hash of the content--the first 12 bytes should be plenty. # append the hash of the content--the first 12 bytes should be plenty.
orig_name = "_" + orig_name if orig_name not in (None, "") else "" orig_name = "_" + orig_name if orig_name not in (None, "") else ""
return tag + orig_name + "_" + hashlib.sha1(xml).hexdigest()[:12] xml_bytes = xml.encode('utf8')
return tag + orig_name + "_" + hashlib.sha1(xml_bytes).hexdigest()[:12]
# Fallback if there was nothing we could use: # Fallback if there was nothing we could use:
if url_name is None or url_name == "": if url_name is None or url_name == "":
...@@ -322,7 +323,7 @@ class XMLModuleStore(ModuleStoreBase): ...@@ -322,7 +323,7 @@ class XMLModuleStore(ModuleStoreBase):
''' '''
String representation - for debugging String representation - for debugging
''' '''
return '<XMLModuleStore>data_dir=%s, %d courses, %d modules' % ( return '<XMLModuleStore data_dir=%r, %d courses, %d modules>' % (
self.data_dir, len(self.courses), len(self.modules)) self.data_dir, len(self.courses), len(self.modules))
def load_policy(self, policy_path, tracker): def load_policy(self, policy_path, tracker):
......
...@@ -28,6 +28,9 @@ def export_to_xml(modulestore, contentstore, course_location, root_dir, course_d ...@@ -28,6 +28,9 @@ def export_to_xml(modulestore, contentstore, course_location, root_dir, course_d
# export the course updates # export the course updates
export_extra_content(export_fs, modulestore, course_location, 'course_info', 'info', '.html') export_extra_content(export_fs, modulestore, course_location, 'course_info', 'info', '.html')
# export the 'about' data (e.g. overview, etc.)
export_extra_content(export_fs, modulestore, course_location, 'about', 'about', '.html')
# export the grading policy # export the grading policy
policies_dir = export_fs.makeopendir('policies') policies_dir = export_fs.makeopendir('policies')
course_run_policy_dir = policies_dir.makeopendir(course.location.name) course_run_policy_dir = policies_dir.makeopendir(course.location.name)
......
...@@ -58,7 +58,12 @@ def _ensure_dir(dir_): ...@@ -58,7 +58,12 @@ def _ensure_dir(dir_):
def _write_styles(selector, output_root, classes): def _write_styles(selector, output_root, classes):
_ensure_dir(output_root) """
Write the css fragments from all XModules in `classes`
into `output_root` as individual files, hashed by the contents to remove
duplicates
"""
contents = {}
css_fragments = defaultdict(set) css_fragments = defaultdict(set)
for class_ in classes: for class_ in classes:
...@@ -73,25 +78,34 @@ def _write_styles(selector, output_root, classes): ...@@ -73,25 +78,34 @@ def _write_styles(selector, output_root, classes):
hash=hashlib.md5(fragment).hexdigest(), hash=hashlib.md5(fragment).hexdigest(),
type=filetype) type=filetype)
# Prepend _ so that sass just includes the files into a single file # Prepend _ so that sass just includes the files into a single file
with open(output_root / '_' + fragment_name, 'w') as css_file: filename = '_' + fragment_name
css_file.write(fragment) contents[filename] = fragment
for class_ in classes: for class_ in classes:
css_imports[class_].add(fragment_name) css_imports[class_].add(fragment_name)
with open(output_root / '_module-styles.scss', 'w') as module_styles: module_styles_lines = []
module_styles_lines.append("@import 'bourbon/bourbon';")
module_styles_lines.append("@import 'bourbon/addons/button';")
for class_, fragment_names in css_imports.items():
module_styles_lines.append("""{selector}.xmodule_{class_} {{""".format(
class_=class_, selector=selector
))
module_styles_lines.extend(' @import "{0}";'.format(name) for name in fragment_names)
module_styles_lines.append('}')
module_styles.write("@import 'bourbon/bourbon';\n") contents['_module-styles.scss'] = '\n'.join(module_styles_lines)
module_styles.write("@import 'bourbon/addons/button';\n")
for class_, fragment_names in css_imports.items(): _write_files(output_root, contents)
imports = "\n".join('@import "{0}";'.format(name) for name in fragment_names)
module_styles.write("""{selector}.xmodule_{class_} {{ {imports} }}\n""".format(
class_=class_, imports=imports, selector=selector
))
def _write_js(output_root, classes): def _write_js(output_root, classes):
_ensure_dir(output_root) """
Write the javascript fragments from all XModules in `classes`
into `output_root` as individual files, hashed by the contents to remove
duplicates
"""
contents = {}
js_fragments = set() js_fragments = set()
for class_ in classes: for class_ in classes:
...@@ -100,18 +114,25 @@ def _write_js(output_root, classes): ...@@ -100,18 +114,25 @@ def _write_js(output_root, classes):
for idx, fragment in enumerate(module_js.get(filetype, [])): for idx, fragment in enumerate(module_js.get(filetype, [])):
js_fragments.add((idx, filetype, fragment)) js_fragments.add((idx, filetype, fragment))
module_js = []
for idx, filetype, fragment in sorted(js_fragments): for idx, filetype, fragment in sorted(js_fragments):
path = output_root / "{idx:0=3d}-{hash}.{type}".format( filename = "{idx:0=3d}-{hash}.{type}".format(
idx=idx, idx=idx,
hash=hashlib.md5(fragment).hexdigest(), hash=hashlib.md5(fragment).hexdigest(),
type=filetype) type=filetype)
with open(path, 'w') as js_file: contents[filename] = fragment
js_file.write(fragment)
_write_files(output_root, contents)
return [output_root / filename for filename in contents.keys()]
module_js.append(path)
return module_js def _write_files(output_root, contents):
_ensure_dir(output_root)
for extra_file in set(output_root.files()) - set(contents.keys()):
extra_file.remove()
for filename, file_content in contents.iteritems():
(output_root / filename).write_bytes(file_content)
def main(): def main():
...@@ -122,7 +143,6 @@ def main(): ...@@ -122,7 +143,6 @@ def main():
args = docopt(main.__doc__) args = docopt(main.__doc__)
root = path(args['<output_root>']) root = path(args['<output_root>'])
root.rmtree(ignore_errors=True)
write_descriptor_js(root / 'descriptors/js') write_descriptor_js(root / 'descriptors/js')
write_descriptor_styles(root / 'descriptors/css') write_descriptor_styles(root / 'descriptors/css')
write_module_js(root / 'modules/js') write_module_js(root / 'modules/js')
......
...@@ -33,8 +33,8 @@ def test_system(): ...@@ -33,8 +33,8 @@ def test_system():
""" """
Construct a test ModuleSystem instance. Construct a test ModuleSystem instance.
By default, the render_template() method simply returns the context it is By default, the render_template() method simply returns the repr of the
passed as a string. You can override this behavior by monkey patching:: context it is passed. You can override this behavior by monkey patching::
system = test_system() system = test_system()
system.render_template = my_render_func system.render_template = my_render_func
...@@ -46,7 +46,7 @@ def test_system(): ...@@ -46,7 +46,7 @@ def test_system():
ajax_url='courses/course_id/modx/a_location', ajax_url='courses/course_id/modx/a_location',
track_function=Mock(), track_function=Mock(),
get_module=Mock(), get_module=Mock(),
render_template=lambda template, context: str(context), render_template=lambda template, context: repr(context),
replace_urls=lambda html: str(html), replace_urls=lambda html: str(html),
user=Mock(is_staff=False), user=Mock(is_staff=False),
filestore=Mock(), filestore=Mock(),
......
...@@ -18,8 +18,7 @@ class TestErrorModule(unittest.TestCase): ...@@ -18,8 +18,7 @@ class TestErrorModule(unittest.TestCase):
self.org = "org" self.org = "org"
self.course = "course" self.course = "course"
self.location = Location(['i4x', self.org, self.course, None, None]) self.location = Location(['i4x', self.org, self.course, None, None])
self.valid_xml = "<problem />" self.valid_xml = u"<problem>ABC \N{SNOWMAN}</problem>"
self.broken_xml = "<problem>"
self.error_msg = "Error" self.error_msg = "Error"
def test_error_module_xml_rendering(self): def test_error_module_xml_rendering(self):
...@@ -27,9 +26,9 @@ class TestErrorModule(unittest.TestCase): ...@@ -27,9 +26,9 @@ class TestErrorModule(unittest.TestCase):
self.valid_xml, self.system, self.org, self.course, self.error_msg) self.valid_xml, self.system, self.org, self.course, self.error_msg)
self.assertTrue(isinstance(descriptor, error_module.ErrorDescriptor)) self.assertTrue(isinstance(descriptor, error_module.ErrorDescriptor))
module = descriptor.xmodule(self.system) module = descriptor.xmodule(self.system)
rendered_html = module.get_html() context_repr = module.get_html()
self.assertIn(self.error_msg, rendered_html) self.assertIn(self.error_msg, context_repr)
self.assertIn(self.valid_xml, rendered_html) self.assertIn(repr(self.valid_xml), context_repr)
def test_error_module_from_descriptor(self): def test_error_module_from_descriptor(self):
descriptor = MagicMock([XModuleDescriptor], descriptor = MagicMock([XModuleDescriptor],
...@@ -41,9 +40,9 @@ class TestErrorModule(unittest.TestCase): ...@@ -41,9 +40,9 @@ class TestErrorModule(unittest.TestCase):
descriptor, self.error_msg) descriptor, self.error_msg)
self.assertTrue(isinstance(error_descriptor, error_module.ErrorDescriptor)) self.assertTrue(isinstance(error_descriptor, error_module.ErrorDescriptor))
module = error_descriptor.xmodule(self.system) module = error_descriptor.xmodule(self.system)
rendered_html = module.get_html() context_repr = module.get_html()
self.assertIn(self.error_msg, rendered_html) self.assertIn(self.error_msg, context_repr)
self.assertIn(str(descriptor), rendered_html) self.assertIn(repr(descriptor), context_repr)
class TestNonStaffErrorModule(TestErrorModule): class TestNonStaffErrorModule(TestErrorModule):
...@@ -60,9 +59,9 @@ class TestNonStaffErrorModule(TestErrorModule): ...@@ -60,9 +59,9 @@ class TestNonStaffErrorModule(TestErrorModule):
descriptor = error_module.NonStaffErrorDescriptor.from_xml( descriptor = error_module.NonStaffErrorDescriptor.from_xml(
self.valid_xml, self.system, self.org, self.course) self.valid_xml, self.system, self.org, self.course)
module = descriptor.xmodule(self.system) module = descriptor.xmodule(self.system)
rendered_html = module.get_html() context_repr = module.get_html()
self.assertNotIn(self.error_msg, rendered_html) self.assertNotIn(self.error_msg, context_repr)
self.assertNotIn(self.valid_xml, rendered_html) self.assertNotIn(repr(self.valid_xml), context_repr)
def test_error_module_from_descriptor(self): def test_error_module_from_descriptor(self):
descriptor = MagicMock([XModuleDescriptor], descriptor = MagicMock([XModuleDescriptor],
...@@ -74,6 +73,6 @@ class TestNonStaffErrorModule(TestErrorModule): ...@@ -74,6 +73,6 @@ class TestNonStaffErrorModule(TestErrorModule):
descriptor, self.error_msg) descriptor, self.error_msg)
self.assertTrue(isinstance(error_descriptor, error_module.ErrorDescriptor)) self.assertTrue(isinstance(error_descriptor, error_module.ErrorDescriptor))
module = error_descriptor.xmodule(self.system) module = error_descriptor.xmodule(self.system)
rendered_html = module.get_html() context_repr = module.get_html()
self.assertNotIn(self.error_msg, rendered_html) self.assertNotIn(self.error_msg, context_repr)
self.assertNotIn(str(descriptor), rendered_html) self.assertNotIn(str(descriptor), context_repr)
...@@ -41,7 +41,7 @@ class DummySystem(ImportSystem): ...@@ -41,7 +41,7 @@ class DummySystem(ImportSystem):
) )
def render_template(self, template, context): def render_template(self, template, context):
raise Exception("Shouldn't be called") raise Exception("Shouldn't be called")
class BaseCourseTestCase(unittest.TestCase): class BaseCourseTestCase(unittest.TestCase):
...@@ -66,13 +66,13 @@ class ImportTestCase(BaseCourseTestCase): ...@@ -66,13 +66,13 @@ class ImportTestCase(BaseCourseTestCase):
def test_fallback(self): def test_fallback(self):
'''Check that malformed xml loads as an ErrorDescriptor.''' '''Check that malformed xml loads as an ErrorDescriptor.'''
bad_xml = '''<sequential display_name="oops"><video url="hi"></sequential>''' # Use an exotic character to also flush out Unicode issues.
bad_xml = u'''<sequential display_name="oops\N{SNOWMAN}"><video url="hi"></sequential>'''
system = self.get_system() system = self.get_system()
descriptor = system.process_xml(bad_xml) descriptor = system.process_xml(bad_xml)
self.assertEqual(descriptor.__class__.__name__, self.assertEqual(descriptor.__class__.__name__, 'ErrorDescriptor')
'ErrorDescriptor')
def test_unique_url_names(self): def test_unique_url_names(self):
'''Check that each error gets its very own url_name''' '''Check that each error gets its very own url_name'''
......
# pylint: disable=W0223
"""Video is ungraded Xmodule for support video content."""
import json import json
import logging import logging
...@@ -15,6 +18,7 @@ log = logging.getLogger(__name__) ...@@ -15,6 +18,7 @@ log = logging.getLogger(__name__)
class VideoFields(object): class VideoFields(object):
"""Fields for `VideoModule` and `VideoDescriptor`."""
position = Integer(help="Current position in the video", scope=Scope.user_state, default=0) position = Integer(help="Current position in the video", scope=Scope.user_state, default=0)
show_captions = Boolean(help="Whether or not captions are shown", display_name="Show Captions", scope=Scope.settings, default=True) show_captions = Boolean(help="Whether or not captions are shown", display_name="Show Captions", scope=Scope.settings, default=True)
youtube_id_1_0 = String(help="Youtube ID for normal speed video", display_name="Normal Speed", scope=Scope.settings, default="OEoXaMPEzfM") youtube_id_1_0 = String(help="Youtube ID for normal speed video", display_name="Normal Speed", scope=Scope.settings, default="OEoXaMPEzfM")
...@@ -28,16 +32,20 @@ class VideoFields(object): ...@@ -28,16 +32,20 @@ class VideoFields(object):
class VideoModule(VideoFields, XModule): class VideoModule(VideoFields, XModule):
"""Video Xmodule."""
video_time = 0 video_time = 0
icon_class = 'video' icon_class = 'video'
js = {'coffee': js = {
[resource_string(__name__, 'js/src/time.coffee'), 'coffee': [
resource_string(__name__, 'js/src/video/display.coffee')] + resource_string(__name__, 'js/src/time.coffee'),
resource_string(__name__, 'js/src/video/display.coffee')
] +
[resource_string(__name__, 'js/src/video/display/' + filename) [resource_string(__name__, 'js/src/video/display/' + filename)
for filename for filename
in sorted(resource_listdir(__name__, 'js/src/video/display')) in sorted(resource_listdir(__name__, 'js/src/video/display'))
if filename.endswith('.coffee')]} if filename.endswith('.coffee')]
}
css = {'scss': [resource_string(__name__, 'css/video/display.scss')]} css = {'scss': [resource_string(__name__, 'css/video/display.scss')]}
js_module_name = "Video" js_module_name = "Video"
...@@ -45,31 +53,13 @@ class VideoModule(VideoFields, XModule): ...@@ -45,31 +53,13 @@ class VideoModule(VideoFields, XModule):
XModule.__init__(self, *args, **kwargs) XModule.__init__(self, *args, **kwargs)
def handle_ajax(self, dispatch, get): def handle_ajax(self, dispatch, get):
''' """This is not being called right now and we raise 404 error."""
Handle ajax calls to this video.
TODO (vshnayder): This is not being called right now, so the position
is not being saved.
'''
log.debug(u"GET {0}".format(get)) log.debug(u"GET {0}".format(get))
log.debug(u"DISPATCH {0}".format(dispatch)) log.debug(u"DISPATCH {0}".format(dispatch))
if dispatch == 'goto_position':
self.position = int(float(get['position']))
log.info(u"NEW POSITION {0}".format(self.position))
return json.dumps({'success': True})
raise Http404() raise Http404()
def get_progress(self):
''' TODO (vshnayder): Get and save duration of youtube video, then return
fraction watched.
(Be careful to notice when video link changes and update)
For now, we have no way of knowing if the video has even been watched, so
just return None.
'''
return None
def get_instance_state(self): def get_instance_state(self):
#log.debug(u"STATE POSITION {0}".format(self.position)) """Return information about state (position)."""
return json.dumps({'position': self.position}) return json.dumps({'position': self.position})
def get_html(self): def get_html(self):
......
...@@ -141,21 +141,36 @@ Very handy: if you uncomment the `pdb=1` line in `setup.cfg`, it will drop you i ...@@ -141,21 +141,36 @@ Very handy: if you uncomment the `pdb=1` line in `setup.cfg`, it will drop you i
### Running Javascript Unit Tests ### Running Javascript Unit Tests
These commands start a development server with jasmine testing enabled, and launch your default browser To run all of the javascript unit tests, use
pointing to those tests
rake browse_jasmine_{lms,cms} rake jasmine
To run the tests headless, you must install [phantomjs](http://phantomjs.org/download.html), then run: If the `phantomjs` binary is on the path, or the `PHANTOMJS_PATH` environment variable is
set to point to it, then the tests will be run headless. Otherwise, they will be run in
your default browser
rake phantomjs_jasmine_{lms,cms} export PATH=/path/to/phantomjs:$PATH
rake jasmine # Runs headless
If the `phantomjs` binary is not on the path, set the `PHANTOMJS_PATH` environment variable to point to it or
PHANTOMJS_PATH=/path/to/phantomjs rake jasmine # Runs headless
or
rake jasmine # Runs in browser
You can also force a run using phantomjs or the browser using the commands
rake jasmine:browser # Runs in browser
rake jasmine:phantomjs # Runs headless
You can run tests for a specific subsystems as well
PHANTOMJS_PATH=/path/to/phantomjs rake phantomjs_jasmine_{lms,cms} rake jasmine:lms # Runs all lms javascript unit tests using the default method
rake jasmine:cms:browser # Runs all cms javascript unit tests in the browser
Once you have run the `rake` command, your browser should open to Use `rake -T` to get a list of all available subsystems
to `http://localhost/_jasmine/`, which displays the test results.
**Troubleshooting**: If you get an error message while running the `rake` task, **Troubleshooting**: If you get an error message while running the `rake` task,
try running `bundle install` to install the required ruby gems. try running `bundle install` to install the required ruby gems.
...@@ -202,9 +217,10 @@ To view test coverage: ...@@ -202,9 +217,10 @@ To view test coverage:
2. Generate reports: 2. Generate reports:
rake coverage:html rake coverage
3. HTML reports are located in the `reports` folder. 3. Reports are located in the `reports` folder. The command
generates HTML and XML (Cobertura format) reports.
## Testing using queue servers ## Testing using queue servers
......
...@@ -70,23 +70,12 @@ rake clobber ...@@ -70,23 +70,12 @@ rake clobber
rake pep8 > pep8.log || cat pep8.log rake pep8 > pep8.log || cat pep8.log
rake pylint > pylint.log || cat pylint.log rake pylint > pylint.log || cat pylint.log
TESTS_FAILED=0 # Run the unit tests (use phantomjs for javascript unit tests)
rake test
# Run the python unit tests # Generate coverage reports
rake test_cms || TESTS_FAILED=1 rake coverage
rake test_lms || TESTS_FAILED=1
rake test_common/lib/capa || TESTS_FAILED=1
rake test_common/lib/xmodule || TESTS_FAILED=1
# Run the javascript unit tests
rake phantomjs_jasmine_lms || TESTS_FAILED=1
rake phantomjs_jasmine_cms || TESTS_FAILED=1
rake phantomjs_jasmine_common/lib/xmodule || TESTS_FAILED=1
rake phantomjs_jasmine_common/static/coffee || TESTS_FAILED=1
rake coverage:xml coverage:html
[ $TESTS_FAILED == '0' ]
rake autodeploy_properties rake autodeploy_properties
github_status state:success "passed" github_status state:success "passed"
"""
integration tests for xmodule
Contains:
1. BaseTestXmodule class provides course and users
for testing Xmodules with mongo store.
"""
from django.test.utils import override_settings
from django.core.urlresolvers import reverse
from django.test.client import Client
from student.tests.factories import UserFactory, CourseEnrollmentFactory
from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE
from xmodule.tests import test_system
from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE)
class BaseTestXmodule(ModuleStoreTestCase):
"""Base class for testing Xmodules with mongo store.
This class prepares course and users for tests:
1. create test course
2. create, enrol and login users for this course
Any xmodule should overwrite only next parameters for test:
1. TEMPLATE_NAME
2. DATA
3. MODEL_DATA
4. COURSE_DATA and USER_COUNT if needed
This class should not contain any tests, because TEMPLATE_NAME
should be defined in child class.
"""
USER_COUNT = 2
COURSE_DATA = {}
# Data from YAML common/lib/xmodule/xmodule/templates/NAME/default.yaml
TEMPLATE_NAME = ""
DATA = ''
MODEL_DATA = {'data': '<some_module></some_module>'}
def setUp(self):
self.course = CourseFactory.create(data=self.COURSE_DATA)
# Turn off cache.
modulestore().request_cache = None
modulestore().metadata_inheritance_cache_subsystem = None
chapter = ItemFactory.create(
parent_location=self.course.location,
template="i4x://edx/templates/sequential/Empty",
)
section = ItemFactory.create(
parent_location=chapter.location,
template="i4x://edx/templates/sequential/Empty"
)
# username = robot{0}, password = 'test'
self.users = [
UserFactory.create(username='robot%d' % i, email='robot+test+%d@edx.org' % i)
for i in range(self.USER_COUNT)
]
for user in self.users:
CourseEnrollmentFactory.create(user=user, course_id=self.course.id)
self.item_descriptor = ItemFactory.create(
parent_location=section.location,
template=self.TEMPLATE_NAME,
data=self.DATA
)
location = self.item_descriptor.location
system = test_system()
system.render_template = lambda template, context: context
self.item_module = self.item_descriptor.module_class(
system, location, self.item_descriptor, self.MODEL_DATA
)
self.item_url = Location(location).url()
# login all users for acces to Xmodule
self.clients = {user.username: Client() for user in self.users}
self.login_statuses = [
self.clients[user.username].login(
username=user.username, password='test')
for user in self.users
]
self.assertTrue(all(self.login_statuses))
def get_url(self, dispatch):
"""Return item url with dispatch."""
return reverse(
'modx_dispatch',
args=(self.course.id, self.item_url, dispatch)
)
def tearDown(self):
for user in self.users:
user.delete()
...@@ -64,7 +64,7 @@ class TestStaffMasqueradeAsStudent(LoginEnrollmentTestCase): ...@@ -64,7 +64,7 @@ class TestStaffMasqueradeAsStudent(LoginEnrollmentTestCase):
def test_staff_debug_for_staff(self): def test_staff_debug_for_staff(self):
resp = self.get_cw_section() resp = self.get_cw_section()
sdebug = '<div><a href="#i4x_edX_graded_problem_H1P1_debug" id="i4x_edX_graded_problem_H1P1_trig">Staff Debug Info</a></div>' sdebug = '<div><a href="#i4x_edX_graded_problem_H1P1_debug" id="i4x_edX_graded_problem_H1P1_trig">Staff Debug Info</a></div>'
self.assertTrue(sdebug in resp.content) self.assertTrue(sdebug in resp.content)
...@@ -84,9 +84,9 @@ class TestStaffMasqueradeAsStudent(LoginEnrollmentTestCase): ...@@ -84,9 +84,9 @@ class TestStaffMasqueradeAsStudent(LoginEnrollmentTestCase):
resp = self.get_cw_section() resp = self.get_cw_section()
sdebug = '<div><a href="#i4x_edX_graded_problem_H1P1_debug" id="i4x_edX_graded_problem_H1P1_trig">Staff Debug Info</a></div>' sdebug = '<div><a href="#i4x_edX_graded_problem_H1P1_debug" id="i4x_edX_graded_problem_H1P1_trig">Staff Debug Info</a></div>'
self.assertFalse(sdebug in resp.content) self.assertFalse(sdebug in resp.content)
def get_problem(self): def get_problem(self):
pun = 'H1P1' pun = 'H1P1'
problem_location = "i4x://edX/graded/problem/%s" % pun problem_location = "i4x://edX/graded/problem/%s" % pun
...@@ -105,7 +105,7 @@ class TestStaffMasqueradeAsStudent(LoginEnrollmentTestCase): ...@@ -105,7 +105,7 @@ class TestStaffMasqueradeAsStudent(LoginEnrollmentTestCase):
resp = self.get_problem() resp = self.get_problem()
html = json.loads(resp.content)['html'] html = json.loads(resp.content)['html']
print html print html
sabut = '<input class="show" type="button" value="Show Answer">' sabut = '<button class="show"><span class="show-label">Show Answer(s)</span> <span class="sr">(for question(s) above - adjacent to each field)</span></button>'
self.assertTrue(sabut in html) self.assertTrue(sabut in html)
def test_no_showanswer_for_student(self): def test_no_showanswer_for_student(self):
...@@ -116,5 +116,5 @@ class TestStaffMasqueradeAsStudent(LoginEnrollmentTestCase): ...@@ -116,5 +116,5 @@ class TestStaffMasqueradeAsStudent(LoginEnrollmentTestCase):
resp = self.get_problem() resp = self.get_problem()
html = json.loads(resp.content)['html'] html = json.loads(resp.content)['html']
print html print html
sabut = '<input class="show" type="button" value="Show Answer">' sabut = '<button class="show"><span class="show-label">Show Answer(s)</span> <span class="sr">(for question(s) above - adjacent to each field)</span></button>'
self.assertFalse(sabut in html) self.assertFalse(sabut in html)
# -*- coding: utf-8 -*-
"""Video xmodule tests in mongo."""
from . import BaseTestXmodule
class TestVideo(BaseTestXmodule):
"""Integration tests: web client + mongo."""
TEMPLATE_NAME = "i4x://edx/templates/video/default"
DATA = '<video youtube="0.75:JMD_ifUUfsU,1.0:OEoXaMPEzfM,1.25:AKqURZnYqpk,1.50:DYpADpL7jAY"/>'
def test_handle_ajax_dispatch(self):
responses = {
user.username: self.clients[user.username].post(
self.get_url('whatever'),
{},
HTTP_X_REQUESTED_WITH='XMLHttpRequest')
for user in self.users
}
self.assertEqual(
set([
response.status_code
for _, response in responses.items()
]).pop(),
404)
# -*- 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.
"""
import json
import unittest
from mock import Mock
from lxml import etree
from xmodule.video_module import VideoDescriptor, VideoModule
from xmodule.modulestore import Location
from xmodule.tests import test_system
from xmodule.tests.test_logic 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}
descriptor = Mock(weight="1")
system = test_system()
system.render_template = lambda template, context: context
module = VideoModule(system, location, descriptor, model_data)
return module
class VideoModuleLogicTest(LogicTest):
"""Tests for logic of Video Xmodule."""
descriptor_class = VideoDescriptor
raw_model_data = {
'data': '<video />'
}
def test_get_timeframe_no_parameters(self):
"""Make sure that timeframe() works correctly w/o parameters"""
xmltree = etree.fromstring('<video>test</video>')
output = self.xmodule.get_timeframe(xmltree)
self.assertEqual(output, ('', ''))
def test_get_timeframe_with_one_parameter(self):
"""Make sure that timeframe() works correctly with one parameter"""
xmltree = etree.fromstring(
'<video from="00:04:07">test</video>'
)
output = self.xmodule.get_timeframe(xmltree)
self.assertEqual(output, (247, ''))
def test_get_timeframe_with_two_parameters(self):
"""Make sure that timeframe() works correctly with two parameters"""
xmltree = etree.fromstring(
'''<video
from="00:04:07"
to="13:04:39"
>test</video>'''
)
output = self.xmodule.get_timeframe(xmltree)
self.assertEqual(output, (247, 47079))
class VideoModuleUnitTest(unittest.TestCase):
"""Unit tests for Video Xmodule."""
def test_video_constructor(self):
"""Make sure that all parameters extracted correclty from xml"""
module = VideoFactory.create()
# `get_html` return only context, cause we
# overwrite `system.render_template`
context = module.get_html()
expected_context = {
'track': None,
'show_captions': 'true',
'display_name': 'SampleProblem1',
'id': module.location.html_id(),
'end': 3610.0,
'caption_asset_path': '/static/subs/',
'source': '.../mit-3091x/M-3091X-FA12-L21-3_100.mp4',
'streams': '0.75:jNCf2gIqpeE,1.0:ZwkTiUPN0mg,1.25:rsq9auxASqI,1.50:kMyNdzVHHgg',
'normal_speed_video_id': 'ZwkTiUPN0mg',
'position': 0,
'start': 3603.0
}
self.assertDictEqual(context, expected_context)
self.assertEqual(
module.youtube,
'0.75:jNCf2gIqpeE,1.0:ZwkTiUPN0mg,1.25:rsq9auxASqI,1.50:kMyNdzVHHgg')
self.assertEqual(
module.video_list(),
module.youtube)
self.assertEqual(
module.position,
0)
self.assertDictEqual(
json.loads(module.get_instance_state()),
{'position': 0})
...@@ -620,7 +620,7 @@ def upload(request, course_id): # ajax upload file to a question or answer ...@@ -620,7 +620,7 @@ def upload(request, course_id): # ajax upload file to a question or answer
raise exceptions.PermissionDenied(msg) raise exceptions.PermissionDenied(msg)
except exceptions.PermissionDenied, err: except exceptions.PermissionDenied, err:
error = unicode(e) error = unicode(err)
except Exception, err: except Exception, err:
print err print err
logging.critical(unicode(err)) logging.critical(unicode(err))
......
'''
Unit tests for enrollment methods in views.py
'''
from django.test.utils import override_settings
from django.contrib.auth.models import Group, User
from django.core.urlresolvers import reverse
from courseware.access import _course_staff_group_name
from courseware.tests.tests import LoginEnrollmentTestCase, TEST_DATA_XML_MODULESTORE, get_user
from xmodule.modulestore.django import modulestore
import xmodule.modulestore.django
from student.models import CourseEnrollment, CourseEnrollmentAllowed
from instructor.views import get_and_clean_student_list
@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE)
class TestInstructorEnrollsStudent(LoginEnrollmentTestCase):
'''
Check Enrollment/Unenrollment with/without auto-enrollment on activation
'''
def setUp(self):
self.full = modulestore().get_course("edX/full/6.002_Spring_2012")
self.toy = modulestore().get_course("edX/toy/2012_Fall")
#Create instructor and student accounts
self.instructor = 'instructor1@test.com'
self.student1 = 'student1@test.com'
self.student2 = 'student2@test.com'
self.password = 'foo'
self.create_account('it1', self.instructor, self.password)
self.create_account('st1', self.student1, self.password)
self.create_account('st2', self.student2, self.password)
self.activate_user(self.instructor)
self.activate_user(self.student1)
self.activate_user(self.student2)
def make_instructor(course):
group_name = _course_staff_group_name(course.location)
g = Group.objects.create(name=group_name)
g.user_set.add(get_user(self.instructor))
make_instructor(self.toy)
#Enroll Students
self.logout()
self.login(self.student1, self.password)
self.enroll(self.toy)
self.logout()
self.login(self.student2, self.password)
self.enroll(self.toy)
#Enroll Instructor
self.logout()
self.login(self.instructor, self.password)
self.enroll(self.toy)
def test_unenrollment(self):
'''
Do un-enrollment test
'''
course = self.toy
url = reverse('instructor_dashboard', kwargs={'course_id': course.id})
response = self.client.post(url, {'action': 'Unenroll multiple students', 'multiple_students': 'student1@test.com, student2@test.com'})
#Check the page output
self.assertContains(response, '<td>student1@test.com</td>')
self.assertContains(response, '<td>student2@test.com</td>')
self.assertContains(response, '<td>un-enrolled</td>')
#Check the enrollment table
user = User.objects.get(email='student1@test.com')
ce = CourseEnrollment.objects.filter(course_id=course.id, user=user)
self.assertEqual(0, len(ce))
user = User.objects.get(email='student2@test.com')
ce = CourseEnrollment.objects.filter(course_id=course.id, user=user)
self.assertEqual(0, len(ce))
def test_enrollment_new_student_autoenroll_on(self):
'''
Do auto-enroll on test
'''
#Run the Enroll students command
course = self.toy
url = reverse('instructor_dashboard', kwargs={'course_id': course.id})
response = self.client.post(url, {'action': 'Enroll multiple students', 'multiple_students': 'test1_1@student.com, test1_2@student.com', 'auto_enroll': 'on'})
#Check the page output
self.assertContains(response, '<td>test1_1@student.com</td>')
self.assertContains(response, '<td>test1_2@student.com</td>')
self.assertContains(response, '<td>user does not exist, enrollment allowed, pending with auto enrollment on</td>')
#Check the enrollmentallowed db entries
cea = CourseEnrollmentAllowed.objects.filter(email='test1_1@student.com', course_id=course.id)
self.assertEqual(1, cea[0].auto_enroll)
cea = CourseEnrollmentAllowed.objects.filter(email='test1_2@student.com', course_id=course.id)
self.assertEqual(1, cea[0].auto_enroll)
#Check there is no enrollment db entry other than for the setup instructor and students
ce = CourseEnrollment.objects.filter(course_id=course.id)
self.assertEqual(3, len(ce))
#Create and activate student accounts with same email
self.student1 = 'test1_1@student.com'
self.password = 'bar'
self.create_account('s1_1', self.student1, self.password)
self.activate_user(self.student1)
self.student2 = 'test1_2@student.com'
self.create_account('s1_2', self.student2, self.password)
self.activate_user(self.student2)
#Check students are enrolled
user = User.objects.get(email='test1_1@student.com')
ce = CourseEnrollment.objects.filter(course_id=course.id, user=user)
self.assertEqual(1, len(ce))
user = User.objects.get(email='test1_2@student.com')
ce = CourseEnrollment.objects.filter(course_id=course.id, user=user)
self.assertEqual(1, len(ce))
def test_enrollmemt_new_student_autoenroll_off(self):
'''
Do auto-enroll off test
'''
#Run the Enroll students command
course = self.toy
url = reverse('instructor_dashboard', kwargs={'course_id': course.id})
response = self.client.post(url, {'action': 'Enroll multiple students', 'multiple_students': 'test2_1@student.com, test2_2@student.com'})
#Check the page output
self.assertContains(response, '<td>test2_1@student.com</td>')
self.assertContains(response, '<td>test2_2@student.com</td>')
self.assertContains(response, '<td>user does not exist, enrollment allowed, pending with auto enrollment off</td>')
#Check the enrollmentallowed db entries
cea = CourseEnrollmentAllowed.objects.filter(email='test2_1@student.com', course_id=course.id)
self.assertEqual(0, cea[0].auto_enroll)
cea = CourseEnrollmentAllowed.objects.filter(email='test2_2@student.com', course_id=course.id)
self.assertEqual(0, cea[0].auto_enroll)
#Check there is no enrollment db entry other than for the setup instructor and students
ce = CourseEnrollment.objects.filter(course_id=course.id)
self.assertEqual(3, len(ce))
#Create and activate student accounts with same email
self.student = 'test2_1@student.com'
self.password = 'bar'
self.create_account('s2_1', self.student, self.password)
self.activate_user(self.student)
self.student = 'test2_2@student.com'
self.create_account('s2_2', self.student, self.password)
self.activate_user(self.student)
#Check students are not enrolled
user = User.objects.get(email='test2_1@student.com')
ce = CourseEnrollment.objects.filter(course_id=course.id, user=user)
self.assertEqual(0, len(ce))
user = User.objects.get(email='test2_2@student.com')
ce = CourseEnrollment.objects.filter(course_id=course.id, user=user)
self.assertEqual(0, len(ce))
def test_get_and_clean_student_list(self):
'''
Clean user input test
'''
string = "abc@test.com, def@test.com ghi@test.com \n \n jkl@test.com "
cleaned_string, cleaned_string_lc = get_and_clean_student_list(string)
self.assertEqual(cleaned_string, ['abc@test.com', 'def@test.com', 'ghi@test.com', 'jkl@test.com'])
"""
Tests of various instructor dashboard features that include lists of students
"""
from django.conf import settings
from django.test import TestCase
from django.test.client import RequestFactory
from django.test.utils import override_settings
from markupsafe import escape
from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE
from student.tests.factories import UserFactory, CourseEnrollmentFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
from instructor import views
@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE)
class TestXss(ModuleStoreTestCase):
def setUp(self):
self._request_factory = RequestFactory()
self._course = CourseFactory.create()
self._evil_student = UserFactory.create(
email="robot+evil@edx.org",
username="evil-robot",
profile__name='<span id="evil">Evil Robot</span>',
)
self._instructor = UserFactory.create(
email="robot+instructor@edx.org",
username="instructor",
is_staff=True
)
CourseEnrollmentFactory.create(
user=self._evil_student,
course_id=self._course.id
)
def _test_action(self, action):
"""
Test for XSS vulnerability in the given action
Build a request with the given action, call the instructor dashboard
view, and check that HTML code in a user's name is properly escaped.
"""
req = self._request_factory.post(
"dummy_url",
data={"action": action}
)
req.user = self._instructor
req.session = {}
resp = views.instructor_dashboard(req, self._course.id)
respUnicode = resp.content.decode(settings.DEFAULT_CHARSET)
self.assertNotIn(self._evil_student.profile.name, respUnicode)
self.assertIn(escape(self._evil_student.profile.name), respUnicode)
def test_list_enrolled(self):
self._test_action("List enrolled students")
def test_dump_list_of_enrolled(self):
self._test_action("Dump list of enrolled students")
def test_dump_grades(self):
self._test_action("Dump Grades for all students in this course")
...@@ -99,6 +99,8 @@ CELERY_QUEUES = { ...@@ -99,6 +99,8 @@ CELERY_QUEUES = {
with open(ENV_ROOT / CONFIG_PREFIX + "env.json") as env_file: with open(ENV_ROOT / CONFIG_PREFIX + "env.json") as env_file:
ENV_TOKENS = json.load(env_file) ENV_TOKENS = json.load(env_file)
PLATFORM_NAME = ENV_TOKENS.get('PLATFORM_NAME', PLATFORM_NAME)
SITE_NAME = ENV_TOKENS['SITE_NAME'] SITE_NAME = ENV_TOKENS['SITE_NAME']
SESSION_COOKIE_DOMAIN = ENV_TOKENS.get('SESSION_COOKIE_DOMAIN') SESSION_COOKIE_DOMAIN = ENV_TOKENS.get('SESSION_COOKIE_DOMAIN')
...@@ -106,7 +108,8 @@ SESSION_COOKIE_DOMAIN = ENV_TOKENS.get('SESSION_COOKIE_DOMAIN') ...@@ -106,7 +108,8 @@ SESSION_COOKIE_DOMAIN = ENV_TOKENS.get('SESSION_COOKIE_DOMAIN')
# this is to fix a bug regarding simultaneous logins between edx.org and edge.edx.org which can # this is to fix a bug regarding simultaneous logins between edx.org and edge.edx.org which can
# happen with some browsers (e.g. Firefox) # happen with some browsers (e.g. Firefox)
if ENV_TOKENS.get('SESSION_COOKIE_NAME', None): if ENV_TOKENS.get('SESSION_COOKIE_NAME', None):
SESSION_COOKIE_NAME = ENV_TOKENS.get('SESSION_COOKIE_NAME') # NOTE, there's a bug in Django (http://bugs.python.org/issue18012) which necessitates this being a str()
SESSION_COOKIE_NAME = str(ENV_TOKENS.get('SESSION_COOKIE_NAME'))
BOOK_URL = ENV_TOKENS['BOOK_URL'] BOOK_URL = ENV_TOKENS['BOOK_URL']
MEDIA_URL = ENV_TOKENS['MEDIA_URL'] MEDIA_URL = ENV_TOKENS['MEDIA_URL']
...@@ -119,11 +122,18 @@ DEFAULT_FROM_EMAIL = ENV_TOKENS.get('DEFAULT_FROM_EMAIL', DEFAULT_FROM_EMAIL) ...@@ -119,11 +122,18 @@ DEFAULT_FROM_EMAIL = ENV_TOKENS.get('DEFAULT_FROM_EMAIL', DEFAULT_FROM_EMAIL)
DEFAULT_FEEDBACK_EMAIL = ENV_TOKENS.get('DEFAULT_FEEDBACK_EMAIL', DEFAULT_FEEDBACK_EMAIL) DEFAULT_FEEDBACK_EMAIL = ENV_TOKENS.get('DEFAULT_FEEDBACK_EMAIL', DEFAULT_FEEDBACK_EMAIL)
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)
TECH_SUPPORT_EMAIL = ENV_TOKENS.get('TECH_SUPPORT_EMAIL', TECH_SUPPORT_EMAIL)
CONTACT_EMAIL = ENV_TOKENS.get('CONTACT_EMAIL', CONTACT_EMAIL)
BUGS_EMAIL = ENV_TOKENS.get('BUGS_EMAIL', BUGS_EMAIL)
#Theme overrides #Theme overrides
THEME_NAME = ENV_TOKENS.get('THEME_NAME', None) THEME_NAME = ENV_TOKENS.get('THEME_NAME', None)
if not THEME_NAME is None: if not THEME_NAME is None:
enable_theme(THEME_NAME) enable_theme(THEME_NAME)
FAVICON_PATH = 'themes/%s/images/favicon.ico' % THEME_NAME
# Marketing link overrides
MKTG_URL_LINK_MAP.update(ENV_TOKENS.get('MKTG_URL_LINK_MAP', {}))
#Timezone overrides #Timezone overrides
TIME_ZONE = ENV_TOKENS.get('TIME_ZONE', TIME_ZONE) TIME_ZONE = ENV_TOKENS.get('TIME_ZONE', TIME_ZONE)
...@@ -162,6 +172,11 @@ for name, value in ENV_TOKENS.get("CODE_JAIL", {}).items(): ...@@ -162,6 +172,11 @@ for name, value in ENV_TOKENS.get("CODE_JAIL", {}).items():
COURSES_WITH_UNSAFE_CODE = ENV_TOKENS.get("COURSES_WITH_UNSAFE_CODE", []) COURSES_WITH_UNSAFE_CODE = ENV_TOKENS.get("COURSES_WITH_UNSAFE_CODE", [])
# If segment.io key specified, load it and turn on segment IO if the feature flag is set
SEGMENT_IO_LMS_KEY = ENV_TOKENS.get('SEGMENT_IO_LMS_KEY')
if SEGMENT_IO_LMS_KEY:
MITX_FEATURES['SEGMENT_IO_LMS'] = ENV_TOKENS.get('SEGMENT_IO_LMS', False)
############################## SECURE AUTH ITEMS ############### ############################## SECURE AUTH ITEMS ###############
# Secret things: passwords, access keys, etc. # Secret things: passwords, access keys, etc.
......
...@@ -31,6 +31,9 @@ from path import path ...@@ -31,6 +31,9 @@ from path import path
from .discussionsettings import * from .discussionsettings import *
################################### FEATURES ################################### ################################### FEATURES ###################################
# The display name of the platform to be used in templates/emails/etc.
PLATFORM_NAME = "edX"
COURSEWARE_ENABLED = True COURSEWARE_ENABLED = True
ENABLE_JASMINE = False ENABLE_JASMINE = False
...@@ -99,6 +102,9 @@ MITX_FEATURES = { ...@@ -99,6 +102,9 @@ MITX_FEATURES = {
# Staff Debug tool. # Staff Debug tool.
'ENABLE_STUDENT_HISTORY_VIEW': True, 'ENABLE_STUDENT_HISTORY_VIEW': True,
# segment.io for LMS--need to explicitly turn it on on production.
'SEGMENT_IO_LMS': False,
# Enables the student notes API and UI. # Enables the student notes API and UI.
'ENABLE_STUDENT_NOTES': True, 'ENABLE_STUDENT_NOTES': True,
...@@ -315,6 +321,9 @@ EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' ...@@ -315,6 +321,9 @@ EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
DEFAULT_FROM_EMAIL = 'registration@edx.org' DEFAULT_FROM_EMAIL = 'registration@edx.org'
DEFAULT_FEEDBACK_EMAIL = 'feedback@edx.org' DEFAULT_FEEDBACK_EMAIL = 'feedback@edx.org'
SERVER_EMAIL = 'devops@edx.org' SERVER_EMAIL = 'devops@edx.org'
TECH_SUPPORT_EMAIL = 'technical@edx.org'
CONTACT_EMAIL = 'info@edx.org'
BUGS_EMAIL = 'bugs@edx.org'
ADMINS = ( ADMINS = (
('edX Admins', 'admin@edx.org'), ('edX Admins', 'admin@edx.org'),
) )
...@@ -330,6 +339,8 @@ STATICFILES_DIRS = [ ...@@ -330,6 +339,8 @@ STATICFILES_DIRS = [
PROJECT_ROOT / "static", PROJECT_ROOT / "static",
] ]
FAVICON_PATH = 'images/favicon.ico'
# Locale/Internationalization # Locale/Internationalization
TIME_ZONE = 'America/New_York' # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name TIME_ZONE = 'America/New_York' # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
LANGUAGE_CODE = 'en' # http://www.i18nguy.com/unicode/language-identifiers.html LANGUAGE_CODE = 'en' # http://www.i18nguy.com/unicode/language-identifiers.html
......
...@@ -243,3 +243,18 @@ MITX_FEATURES['ENABLE_PEARSON_LOGIN'] = False ...@@ -243,3 +243,18 @@ MITX_FEATURES['ENABLE_PEARSON_LOGIN'] = False
ANALYTICS_SERVER_URL = "http://127.0.0.1:9000/" ANALYTICS_SERVER_URL = "http://127.0.0.1:9000/"
ANALYTICS_API_KEY = "" ANALYTICS_API_KEY = ""
##### segment-io ######
# If there's an environment variable set, grab it and turn on segment io
SEGMENT_IO_LMS_KEY = os.environ.get('SEGMENT_IO_LMS_KEY')
if SEGMENT_IO_LMS_KEY:
MITX_FEATURES['SEGMENT_IO_LMS'] = True
#####################################################################
# Lastly, see if the developer has any local overrides.
try:
from .private import *
except ImportError:
pass
...@@ -98,3 +98,16 @@ ...@@ -98,3 +98,16 @@
} }
} }
} }
//--------------------------------------
// The Following is to enable themes to
// display H1s on login and register pages
//--------------------------------------
.view-login .introduction header h1,
.view-register .introduction header h1 {
@include login_register_h1_style;
}
footer .references {
@include footer_references_style;
}
\ No newline at end of file
...@@ -4,6 +4,20 @@ ...@@ -4,6 +4,20 @@
@import 'base/font_face'; @import 'base/font_face';
@import 'base/mixins'; @import 'base/mixins';
@import 'base/variables'; @import 'base/variables';
## THEMING
## -------
## Set up this file to import an edX theme library if the environment
## indicates that a theme should be used. The assumption is that the
## theme resides outside of this main edX repository, in a directory
## called themes/<theme-name>/, with its base Sass file in
## themes/<theme-name>/static/sass/_<theme-name>.scss. That one entry
## point can be used to @import in as many other things as needed.
% if env.get('THEME_NAME') is not None:
// import theme's Sass overrides
@import '${env.get('THEME_NAME')}';
% endif
@import 'base/base'; @import 'base/base';
@import 'base/extends'; @import 'base/extends';
@import 'base/animations'; @import 'base/animations';
...@@ -36,16 +50,3 @@ ...@@ -36,16 +50,3 @@
@import 'news'; @import 'news';
@import 'shame'; @import 'shame';
## THEMING
## -------
## Set up this file to import an edX theme library if the environment
## indicates that a theme should be used. The assumption is that the
## theme resides outside of this main edX repository, in a directory
## called themes/<theme-name>/, with its base Sass file in
## themes/<theme-name>/static/sass/_<theme-name>.scss. That one entry
## point can be used to @import in as many other things as needed.
% if env.get('THEME_NAME') is not None:
// import theme's Sass overrides
@import '${env.get('THEME_NAME')}'
% endif
...@@ -209,7 +209,7 @@ mark { ...@@ -209,7 +209,7 @@ mark {
} }
.sr { .sr {
@include text-sr(); @extend .text-sr;
} }
.help-tab { .help-tab {
......
...@@ -14,18 +14,6 @@ ...@@ -14,18 +14,6 @@
overflow: hidden; overflow: hidden;
} }
// hidden elems - screenreaders
@mixin text-sr() {
border: 0;
clip: rect(0 0 0 0);
height: 1px;
margin: -1px;
overflow: hidden;
padding: 0;
position: absolute;
width: 1px;
}
@mixin vertically-and-horizontally-centered ( $height, $width ) { @mixin vertically-and-horizontally-centered ( $height, $width ) {
left: 50%; left: 50%;
margin-left: -$width / 2; margin-left: -$width / 2;
...@@ -42,3 +30,25 @@ ...@@ -42,3 +30,25 @@
overflow: hidden; overflow: hidden;
display: block; display: block;
} }
//-----------------
// Theme Mixin Styles
//-----------------
@mixin login_register_h1_style {}
@mixin footer_references_style {}
// ====================
// extends -hidden elems - screenreaders
.text-sr {
border: 0;
clip: rect(1px 1px 1px 1px);
height: 1px;
margin: -1px;
overflow: hidden;
padding: 0;
position: absolute;
width: 1px;
}
...@@ -62,11 +62,12 @@ $lighter-base-font-color: rgb(100,100,100); ...@@ -62,11 +62,12 @@ $lighter-base-font-color: rgb(100,100,100);
$text-color: $dark-gray; $text-color: $dark-gray;
$body-bg: rgb(250,250,250); $body-bg: rgb(250,250,250);
$container-bg: $white;
$header-image: linear-gradient(-90deg, rgba(255,255,255, 1), rgba(230,230,230, 0.9)); $header-image: linear-gradient(-90deg, rgba(255,255,255, 1), rgba(230,230,230, 0.9));
$header-bg: transparent; $header-bg: $white;
$courseware-header-image: linear-gradient(top, #fff, #eee); $courseware-header-image: linear-gradient(top, #fff, #eee);
$courseware-header-bg: transparent; $courseware-header-bg: transparent;
$footer-bg: transparent; $footer-bg: $white;
$courseware-footer-border: none; $courseware-footer-border: none;
$courseware-footer-shadow: none; $courseware-footer-shadow: none;
$courseware-footer-margin: 0px; $courseware-footer-margin: 0px;
...@@ -87,9 +88,10 @@ $dashboard-profile-header-color: transparent; ...@@ -87,9 +88,10 @@ $dashboard-profile-header-color: transparent;
$dashboard-profile-color: rgb(252,252,252); $dashboard-profile-color: rgb(252,252,252);
$dot-color: $light-gray; $dot-color: $light-gray;
$content-wrapper-bg: rgb(255,255,255); $content-wrapper-bg: $white;
$course-bg-color: #d6d6d6; $course-bg-color: #d6d6d6;
$course-bg-image: url(../images/bg-texture.png); $course-bg-image: url(../images/bg-texture.png);
$account-content-wrapper-bg: shade($body-bg, 2%);
$course-profile-bg: rgb(245,245,245); $course-profile-bg: rgb(245,245,245);
$course-header-bg: rgba(255,255,255, 0.93); $course-header-bg: rgba(255,255,255, 0.93);
...@@ -100,6 +102,7 @@ $border-color-3: rgb(100,100,100); ...@@ -100,6 +102,7 @@ $border-color-3: rgb(100,100,100);
$border-color-4: rgb(252,252,252); $border-color-4: rgb(252,252,252);
$link-color: $blue; $link-color: $blue;
$link-color-d1: $m-blue;
$link-hover: $pink; $link-hover: $pink;
$selection-color-1: $pink; $selection-color-1: $pink;
$selection-color-2: #444; $selection-color-2: #444;
...@@ -118,9 +121,18 @@ $sidebar-active-image: linear-gradient(top, #e6e6e6, #d6d6d6); ...@@ -118,9 +121,18 @@ $sidebar-active-image: linear-gradient(top, #e6e6e6, #d6d6d6);
$form-bg-color: #fff; $form-bg-color: #fff;
$modal-bg-color: rgb(245,245,245); $modal-bg-color: rgb(245,245,245);
//TOP HEADER IMAGE MARGIN
$header_image_margin: -69px;
//FOOTER MARGIN
$footer_margin: ($baseline/4) 0 ($baseline*1.5) 0;
//----------------- //-----------------
// CSS BG Images // CSS BG Images
//----------------- //-----------------
$homepage-bg-image: '../images/homepage-bg.jpg'; $homepage-bg-image: '../images/homepage-bg.jpg';
$login-banner-image: url(../images/bg-banner-login.png);
$register-banner-image: url(../images/bg-banner-register.png);
$video-thumb-url: '../images/courses/video-thumb.jpg'; $video-thumb-url: '../images/courses/video-thumb.jpg';
...@@ -4,6 +4,20 @@ ...@@ -4,6 +4,20 @@
@import 'base/font_face'; @import 'base/font_face';
@import 'base/mixins'; @import 'base/mixins';
@import 'base/variables'; @import 'base/variables';
## THEMING
## -------
## Set up this file to import an edX theme library if the environment
## indicates that a theme should be used. The assumption is that the
## theme resides outside of this main edX repository, in a directory
## called themes/<theme-name>/, with its base Sass file in
## themes/<theme-name>/static/sass/_<theme-name>.scss. That one entry
## point can be used to @import in as many other things as needed.
% if env.get('THEME_NAME') is not None:
// import theme's Sass overrides
@import '${env.get('THEME_NAME')}';
% endif
@import 'base/base'; @import 'base/base';
@import 'base/extends'; @import 'base/extends';
@import 'base/animations'; @import 'base/animations';
......
...@@ -35,7 +35,7 @@ a { ...@@ -35,7 +35,7 @@ a {
width: 100%; width: 100%;
border-radius: 3px; border-radius: 3px;
border: 1px solid $outer-border-color; border: 1px solid $outer-border-color;
background: $body-bg; background: $container-bg;
@include box-shadow(0 1px 2px rgba(0, 0, 0, 0.05)); @include box-shadow(0 1px 2px rgba(0, 0, 0, 0.05));
} }
} }
...@@ -50,7 +50,7 @@ textarea, ...@@ -50,7 +50,7 @@ textarea,
input[type="text"], input[type="text"],
input[type="email"], input[type="email"],
input[type="password"] { input[type="password"] {
background: $body-bg; background: $white;
border: 1px solid $border-color-2; border: 1px solid $border-color-2;
@include border-radius(0); @include border-radius(0);
@include box-shadow(0 1px 0 0 rgba(255,255,255, 0.6), inset 0 0 3px 0 rgba(0,0,0, 0.1)); @include box-shadow(0 1px 0 0 rgba(255,255,255, 0.6), inset 0 0 3px 0 rgba(0,0,0, 0.1));
......
...@@ -65,7 +65,7 @@ header.global.slim { ...@@ -65,7 +65,7 @@ header.global.slim {
height: auto; height: auto;
padding: 5px 0 10px 0; padding: 5px 0 10px 0;
border-bottom: 1px solid $outer-border-color; border-bottom: 1px solid $outer-border-color;
background: $white; background: $header-bg;
.guest .secondary { .guest .secondary {
margin-right: 0; margin-right: 0;
......
footer { footer {
border: $courseware-footer-border;
box-shadow: $courseware-footer-shadow; box-shadow: $courseware-footer-shadow;
margin-top: $courseware-footer-margin; margin-top: $courseware-footer-margin;
} }
\ No newline at end of file
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
// page-level // page-level
.view-register, .view-login, .view-passwordreset { .view-register, .view-login, .view-passwordreset {
background: $white; background: $container-bg;
...@@ -22,14 +22,14 @@ ...@@ -22,14 +22,14 @@
margin: 0 0 $baseline 0; margin: 0 0 $baseline 0;
font-weight: 300; font-weight: 300;
text-transform: uppercase; text-transform: uppercase;
color: $m-blue; color: $link-color-d1;
} }
.heading-3 { .heading-3 {
font-size: 21px; font-size: 21px;
margin: 0 0 $baseline 0; margin: 0 0 $baseline 0;
font-weight: 300; font-weight: 300;
color: $m-gray-d2; color: $base-font-color;
} }
.heading-4 { .heading-4 {
...@@ -37,7 +37,7 @@ ...@@ -37,7 +37,7 @@
font-weight: 600; font-weight: 600;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0 !important; letter-spacing: 0 !important;
color: $m-blue-s1; color: saturate($link-color-d1,15%);
} }
.heading-5 { .heading-5 {
...@@ -48,7 +48,7 @@ ...@@ -48,7 +48,7 @@
font-size: 18px; font-size: 18px;
margin: 0 0 $baseline 0; margin: 0 0 $baseline 0;
font-weight: 300; font-weight: 300;
color: $m-gray-a1; color: $base-font-color;
font-family: 'Open Sans', sans-serif; font-family: 'Open Sans', sans-serif;
line-height: lh(1.1); line-height: lh(1.1);
} }
...@@ -56,18 +56,18 @@ ...@@ -56,18 +56,18 @@
.body-text { .body-text {
font-size: 15px; font-size: 15px;
margin: 0 0 $baseline 0; margin: 0 0 $baseline 0;
color: $m-gray-a1; color: $base-font-color;
line-height: lh(1); line-height: lh(1);
} }
// specific examples - buttons // specific examples - buttons
.button-primary { .button-primary {
@include border-radius(0); @include border-radius(0);
@include linear-gradient($m-blue-s1 5%, $m-blue-d1 95%); @include linear-gradient(saturate($link-color-d1,15%) 5%, shade($link-color-d1,15%) 95%);
display: inline-block; display: inline-block;
padding: $baseline/2 $baseline*2.5; padding: $baseline/2 $baseline*2.5;
text-transform: lowercase; text-transform: lowercase;
color: $white; color: $very-light-text;
letter-spacing: 0.1rem; letter-spacing: 0.1rem;
font-weight: 500; font-weight: 500;
cursor: pointer; cursor: pointer;
...@@ -80,11 +80,11 @@ ...@@ -80,11 +80,11 @@
} }
.button-secondary { .button-secondary {
@include linear-gradient($m-gray 5%, $m-gray-d1 95%); @include linear-gradient($outer-border-color 5%, $lighter-base-font-color 95%);
display: inline-block; display: inline-block;
padding: $baseline/2 $baseline*2.5; padding: $baseline/2 $baseline*2.5;
text-transform: lowercase; text-transform: lowercase;
color: $white; color: $very-light-text;
letter-spacing: 0.1rem; letter-spacing: 0.1rem;
font-weight: 600; font-weight: 600;
cursor: pointer; cursor: pointer;
...@@ -98,7 +98,7 @@ ...@@ -98,7 +98,7 @@
// layout // layout
.content-wrapper { .content-wrapper {
background: $m-gray-l2; background: $account-content-wrapper-bg;
padding-bottom: 0; padding-bottom: 0;
} }
...@@ -107,7 +107,7 @@ ...@@ -107,7 +107,7 @@
@include clearfix; @include clearfix;
margin: 0 auto; margin: 0 auto;
width: 960px; width: 960px;
background: $white; background: $container-bg;
} }
.container { .container {
...@@ -142,14 +142,15 @@ ...@@ -142,14 +142,15 @@
@include transition(color 0.15s ease-in-out, border 0.15s ease-in-out); @include transition(color 0.15s ease-in-out, border 0.15s ease-in-out);
&:link, &:visited, &:hover, &:active { &:link, &:visited, &:hover, &:active {
color: $m-blue; color: $link-color-d1;
text-decoration: none !important; font-weight: 400;
text-decoration: none !important; // needed but nasty
font-family: $sans-serif; font-family: $sans-serif;
} }
&:hover, &:active { &:hover, &:active {
border-bottom: 1px dotted $m-blue-l1; text-decoration: none !important; // needed but nasty
color: $m-blue-l1; border-bottom: 1px dotted $link-color-d1;
} }
} }
...@@ -254,7 +255,7 @@ ...@@ -254,7 +255,7 @@
font-family: $sans-serif; font-family: $sans-serif;
font-style: normal; font-style: normal;
font-weight: 500; font-weight: 500;
color: $m-gray-d2; color: $base-font-color;
} }
label { label {
...@@ -267,7 +268,7 @@ ...@@ -267,7 +268,7 @@
@include transition(color 0.15s ease-in-out); @include transition(color 0.15s ease-in-out);
display: block; display: block;
margin-top: ($baseline/4); margin-top: ($baseline/4);
color: tint($m-gray, 50%); color: tint($outer-border-color, 50%);
font-size: em(13); font-size: em(13);
} }
...@@ -330,7 +331,7 @@ ...@@ -330,7 +331,7 @@
} }
textarea, input { textarea, input {
background: $white; background: $container-bg;
color: rgba(0,0,0,.25); color: rgba(0,0,0,.25);
} }
} }
...@@ -339,11 +340,11 @@ ...@@ -339,11 +340,11 @@
&.is-focused { &.is-focused {
label { label {
color: $m-blue-l1; color: saturate($link-color-d1,15%);
} }
.tip { .tip {
color: $m-blue-l1; color: saturate($link-color-d1,15%);
} }
} }
...@@ -461,7 +462,7 @@ ...@@ -461,7 +462,7 @@
// misc // misc
.orn-plus { .orn-plus {
color: $white; color: $very-light-text;
padding: 0 $baseline/4; padding: 0 $baseline/4;
} }
...@@ -492,7 +493,7 @@ ...@@ -492,7 +493,7 @@
header { header {
height: 120px; height: 120px;
border-bottom: 1px solid $m-gray; border-bottom: 1px solid $m-gray;
background: transparent url("../images/bg-banner-login.png") 0 0 no-repeat; background: transparent $login-banner-image 0 0 no-repeat;
} }
} }
} }
...@@ -506,14 +507,14 @@ ...@@ -506,14 +507,14 @@
header { header {
height: 120px; height: 120px;
border-bottom: 1px solid $m-gray; border-bottom: 1px solid $m-gray;
background: transparent url("../images/bg-banner-register.png") 0 0 no-repeat; background: transparent $register-banner-image 0 0 no-repeat;
} }
} }
} }
// password reset // password reset
.view-passwordreset { .view-passwordreset {
background: $m-gray-l2; background: $sidebar-color;
header.global { header.global {
...@@ -543,7 +544,7 @@ ...@@ -543,7 +544,7 @@
.inner-wrapper { .inner-wrapper {
@include border-radius(2px); @include border-radius(2px);
background: $white; background: $body-bg;
padding-bottom: 0 !important; padding-bottom: 0 !important;
} }
......
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
border-bottom: 1px solid $border-color-3; border-bottom: 1px solid $border-color-3;
@include box-shadow(inset 0 1px 5px 0 rgba(0,0,0, 0.1)); @include box-shadow(inset 0 1px 5px 0 rgba(0,0,0, 0.1));
height: 280px; height: 280px;
margin-top: -69px; margin-top: $header_image_margin;
padding-top: 150px; padding-top: 150px;
overflow: hidden; overflow: hidden;
position: relative; position: relative;
......
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