Commit 6362ef4c by Joe Blaylock

Merge remote-tracking branch 'origin/release' into edx-west/release-candidate-20130927

Conflicts:
	common/lib/xmodule/xmodule/capa_module.py
parents d7912aeb 908371ea
......@@ -86,5 +86,6 @@ Yarko Tymciurak <yarkot1@gmail.com>
Miles Steele <miles@milessteele.com>
Kevin Luo <kevluo@edx.org>
Akshay Jagadeesh <akjags@gmail.com>
Nick Parlante <nick.parlante@cs.stanford.edu>
Marko Seric <marko.seric@math.uzh.ch>
Felipe Montoya <felipe.montoya@edunext.co>
......@@ -5,6 +5,19 @@ These are notable changes in edx-platform. This is a rolling list of changes,
in roughly chronological order, most recent first. Add your entries at or near
the top. Include a label indicating the component affected.
LMS: Improved accessibility of parts of forum navigation sidebar.
LMS: enhanced accessibility labeling and aria support for the discussion forum new post dropdown as well as response and comment area labeling.
LMS: enhanced shib support, including detection of linked shib account
at login page and support for the ?next= GET parameter.
LMS: Experimental feature using the ICE change tracker JS pkg to allow peer
assessors to edit the original submitter's work.
LMS: Fixed a bug that caused links from forum user profile pages to
threads to lead to 404s if the course id contained a '-' character.
Studio/LMS: Added ability to set due date formatting through Studio's Advanced Settings.
The key is due_date_display_format, and the value should be a format supported by Python's
strftime function.
......
......@@ -6,3 +6,8 @@ gem 'neat', '~> 1.3.0'
gem 'colorize', '~> 0.5.8'
gem 'launchy', '~> 2.1.2'
gem 'sys-proctable', '~> 0.9.3'
# These gems aren't actually required; they are used by Linux and Mac to
# detect when files change. If these gems are not installed, the system
# will fall back to polling files.
gem 'rb-inotify', '~> 0.9'
gem 'rb-fsevent', '~> 0.9.3'
......@@ -22,12 +22,13 @@ Vagrant.configure("2") do |config|
config.vm.provider :virtualbox do |vb|
# Use VBoxManage to customize the VM. For example to change memory:
vb.customize ["modifyvm", :id, "--memory", "1024"]
vb.customize ["modifyvm", :id, "--memory", "2048"]
# This setting makes it so that network access from inside the vagrant guest
# is able to resolve DNS using the hosts VPN connection.
vb.customize ["modifyvm", :id, "--natdnshostresolver1", "on"]
end
config.vm.provision :shell, :path => "scripts/install-acceptance-req.sh"
config.vm.provision :shell, :path => "scripts/vagrant-provisioning.sh"
end
......@@ -5,16 +5,16 @@ Feature: Static Pages
Given I have opened a new course in Studio
And I go to the static pages page
When I add a new page
Then I should see a "Empty" static page
Then I should see a static page named "Empty"
Scenario: Users can delete static pages
Given I have opened a new course in Studio
And I go to the static pages page
And I add a new page
And I "delete" the "Empty" page
And I "delete" the static page
Then I am shown a prompt
When I confirm the prompt
Then I should not see a "Empty" static page
Then I should not see any static pages
# Safari won't update the name properly
@skip_safari
......@@ -22,6 +22,6 @@ Feature: Static Pages
Given I have opened a new course in Studio
And I go to the static pages page
And I add a new page
When I "edit" the "Empty" page
When I "edit" the static page
And I change the name to "New"
Then I should see a "New" static page
Then I should see a static page named "New"
......@@ -2,10 +2,10 @@
#pylint: disable=W0621
from lettuce import world, step
from nose.tools import assert_true # pylint: disable=E0611
from nose.tools import assert_equal # pylint: disable=E0611
@step(u'I go to the static pages page')
@step(u'I go to the static pages page$')
def go_to_static(step):
menu_css = 'li.nav-course-courseware'
static_css = 'li.nav-course-courseware-pages a'
......@@ -13,42 +13,29 @@ def go_to_static(step):
world.css_click(static_css)
@step(u'I add a new page')
@step(u'I add a new page$')
def add_page(step):
button_css = 'a.new-button'
world.css_click(button_css)
@step(u'I should not see a "([^"]*)" static page$')
def not_see_page(step, page):
# Either there are no pages, or there are pages but
# not the one I expect not to exist.
@step(u'I should see a static page named "([^"]*)"$')
def see_a_static_page_named_foo(step, name):
pages_css = 'section.xmodule_StaticTabModule'
page_name_html = world.css_html(pages_css)
assert_equal(page_name_html, '\n {name}\n'.format(name=name))
# Since our only test for deletion right now deletes
# the only static page that existed, our success criteria
# will be that there are no static pages.
# In the future we can refactor if necessary.
tabs_css = 'li.component'
assert (world.is_css_not_present(tabs_css, wait_time=30))
@step(u'I should not see any static pages$')
def not_see_any_static_pages(step):
pages_css = 'section.xmodule_StaticTabModule'
assert (world.is_css_not_present(pages_css, wait_time=30))
@step(u'I should see a "([^"]*)" static page$')
def see_page(step, page):
# Need to retry here because the element
# will sometimes exist before the HTML content is loaded
exists_func = lambda(driver): page_exists(page)
world.wait_for(exists_func)
assert_true(exists_func(None))
@step(u'I "([^"]*)" the "([^"]*)" page$')
def click_edit_delete(step, edit_delete, page):
button_css = 'a.%s-button' % edit_delete
index = get_index(page)
assert index is not None
world.css_click(button_css, index=index)
@step(u'I "(edit|delete)" the static page$')
def click_edit_or_delete(step, edit_or_delete):
button_css = 'div.component-actions a.%s-button' % edit_or_delete
world.css_click(button_css)
@step(u'I change the name to "([^"]*)"$')
......@@ -61,16 +48,3 @@ def change_name(step, new_name):
world.trigger_event(input_css)
save_button = 'a.save-button'
world.css_click(save_button)
def get_index(name):
page_name_css = 'section[data-type="HTMLModule"]'
all_pages = world.css_find(page_name_css)
for i in range(len(all_pages)):
if world.retry_on_exception(lambda: all_pages[i].html) == '\n {name}\n'.format(name=name):
return i
return None
def page_exists(page):
return get_index(page) is not None
......@@ -2,7 +2,10 @@
Unit tests for the asset upload endpoint.
"""
import json
#pylint: disable=C0111
#pylint: disable=W0621
#pylint: disable=W0212
from datetime import datetime
from io import BytesIO
from pytz import UTC
......@@ -12,7 +15,9 @@ from django.core.urlresolvers import reverse
from contentstore.views import assets
from xmodule.contentstore.content import StaticContent
from xmodule.modulestore import Location
from xmodule.contentstore.django import contentstore
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.xml_importer import import_from_xml
class AssetsTestCase(CourseTestCase):
def setUp(self):
......@@ -27,22 +32,27 @@ class AssetsTestCase(CourseTestCase):
resp = self.client.get(self.url)
self.assertEquals(resp.status_code, 200)
def test_json(self):
resp = self.client.get(
self.url,
HTTP_ACCEPT="application/json",
HTTP_X_REQUESTED_WITH="XMLHttpRequest",
)
self.assertEquals(resp.status_code, 200)
content = json.loads(resp.content)
self.assertIsInstance(content, list)
def test_static_url_generation(self):
location = Location(['i4x', 'foo', 'bar', 'asset', 'my_file_name.jpg'])
path = StaticContent.get_static_path_from_location(location)
self.assertEquals(path, '/static/my_file_name.jpg')
class AssetsToyCourseTestCase(CourseTestCase):
"""
Tests the assets returned from asset_index for the toy test course.
"""
def test_toy_assets(self):
module_store = modulestore('direct')
import_from_xml(module_store, 'common/test/data/', ['toy'], static_content_store=contentstore(), verbose=True)
url = reverse("asset_index", kwargs={'org': 'edX', 'course': 'toy', 'name': '2012_Fall'})
resp = self.client.get(url)
# Test a small portion of the asset data passed to the client.
self.assertContains(resp, "new CMS.Models.AssetCollection([{")
self.assertContains(resp, "/c4x/edX/toy/asset/handouts_sample_handout.txt")
class UploadTestCase(CourseTestCase):
"""
Unit tests for uploading a file
......@@ -71,32 +81,25 @@ class UploadTestCase(CourseTestCase):
self.assertEquals(resp.status_code, 405)
class AssetsToJsonTestCase(TestCase):
class AssetToJsonTestCase(TestCase):
"""
Unit tests for transforming the results of a database call into something
Unit test for transforming asset information into something
we can send out to the client via JSON.
"""
def test_basic(self):
upload_date = datetime(2013, 6, 1, 10, 30, tzinfo=UTC)
asset = {
"displayname": "foo",
"chunkSize": 512,
"filename": "foo.png",
"length": 100,
"uploadDate": upload_date,
"_id": {
"course": "course",
"org": "org",
"revision": 12,
"category": "category",
"name": "name",
"tag": "tag",
}
}
output = assets.assets_to_json_dict([asset])
self.assertEquals(len(output), 1)
compare = output[0]
self.assertEquals(compare["name"], "foo")
self.assertEquals(compare["path"], "foo.png")
self.assertEquals(compare["uploaded"], upload_date.isoformat())
self.assertEquals(compare["id"], "/tag/org/course/12/category/name")
location = Location(['i4x', 'foo', 'bar', 'asset', 'my_file_name.jpg'])
thumbnail_location = Location(['i4x', 'foo', 'bar', 'asset', 'my_file_name_thumb.jpg'])
output = assets._get_asset_json("my_file", upload_date, location, thumbnail_location)
self.assertEquals(output["display_name"], "my_file")
self.assertEquals(output["date_added"], "Jun 01, 2013 at 10:30 UTC")
self.assertEquals(output["url"], "/i4x/foo/bar/asset/my_file_name.jpg")
self.assertEquals(output["portable_url"], "/static/my_file_name.jpg")
self.assertEquals(output["thumbnail"], "/i4x/foo/bar/asset/my_file_name_thumb.jpg")
self.assertEquals(output["id"], output["url"])
output = assets._get_asset_json("name", upload_date, location, None)
self.assertIsNone(output["thumbnail"])
......@@ -593,9 +593,9 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
# go through the website to do the delete, since the soft-delete logic is in the view
url = reverse('remove_asset', kwargs={'org': 'edX', 'course': 'toy', 'name': '2012_Fall'})
resp = self.client.post(url, {'location': '/c4x/edX/toy/asset/sample_static.txt'})
self.assertEqual(resp.status_code, 200)
url = reverse('update_asset', kwargs={'org': 'edX', 'course': 'toy', 'name': '2012_Fall', 'asset_id': '/c4x/edX/toy/asset/sample_static.txt'})
resp = self.client.delete(url)
self.assertEqual(resp.status_code, 204)
asset_location = StaticContent.get_location_from_path('/c4x/edX/toy/asset/sample_static.txt')
......@@ -628,7 +628,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
def test_empty_trashcan(self):
'''
This test will exercise the empting of the asset trashcan
This test will exercise the emptying of the asset trashcan
'''
content_store = contentstore()
trash_store = contentstore('trashcan')
......@@ -644,9 +644,9 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
# go through the website to do the delete, since the soft-delete logic is in the view
url = reverse('remove_asset', kwargs={'org': 'edX', 'course': 'toy', 'name': '2012_Fall'})
resp = self.client.post(url, {'location': '/c4x/edX/toy/asset/sample_static.txt'})
self.assertEqual(resp.status_code, 200)
url = reverse('update_asset', kwargs={'org': 'edX', 'course': 'toy', 'name': '2012_Fall', 'asset_id': '/c4x/edX/toy/asset/sample_static.txt'})
resp = self.client.delete(url)
self.assertEqual(resp.status_code, 204)
# make sure there's something in the trashcan
all_assets = trash_store.get_all_content_for_course(course_location)
......@@ -907,7 +907,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
draft_store = modulestore('draft')
content_store = contentstore()
import_from_xml(module_store, 'common/test/data/', ['toy'])
import_from_xml(module_store, 'common/test/data/', ['toy'], static_content_store=content_store)
location = CourseDescriptor.id_to_location('edX/toy/2012_Fall')
# get a vertical (and components in it) to copy into an orphan sub dag
......@@ -986,7 +986,9 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
delete_course(module_store, content_store, location, commit=True)
# reimport
import_from_xml(module_store, root_dir, ['test_export'], draft_store=draft_store)
import_from_xml(
module_store, root_dir, ['test_export'], draft_store=draft_store, static_content_store=content_store
)
items = module_store.get_items(Location(['i4x', 'edX', 'toy', 'vertical', None]))
self.assertGreater(len(items), 0)
......
import logging
import json
import os
import tarfile
import shutil
import cgi
import re
from functools import partial
from tempfile import mkdtemp
from path import path
from django.conf import settings
from django.http import HttpResponse, HttpResponseBadRequest
from django.http import HttpResponseBadRequest
from django.contrib.auth.decorators import login_required
from django.views.decorators.http import require_http_methods
from django_future.csrf import ensure_csrf_cookie
from django.core.urlresolvers import reverse
from django.core.servers.basehttp import FileWrapper
from django.core.files.temp import NamedTemporaryFile
from django.views.decorators.http import require_POST, require_http_methods
from django.views.decorators.http import require_POST
from mitxmako.shortcuts import render_to_response
from cache_toolbox.core import del_cached_content
from auth.authz import create_all_course_groups
from xmodule.modulestore.xml_importer import import_from_xml
from xmodule.contentstore.django import contentstore
from xmodule.modulestore.xml_exporter import export_to_xml
from xmodule.modulestore.django import modulestore
from xmodule.modulestore import Location
from xmodule.contentstore.content import StaticContent
from xmodule.util.date_utils import get_default_time_display
from xmodule.modulestore import InvalidLocationError
from xmodule.exceptions import NotFoundError, SerializationError
from xmodule.exceptions import NotFoundError
from .access import get_location_and_verify_access
from util.json_request import JsonResponse
import json
from django.utils.translation import ugettext as _
__all__ = ['asset_index', 'upload_asset']
def assets_to_json_dict(assets):
"""
Transform the results of a contentstore query into something appropriate
for output via JSON.
"""
ret = []
for asset in assets:
obj = {
"name": asset.get("displayname", ""),
"chunkSize": asset.get("chunkSize", 0),
"path": asset.get("filename", ""),
"length": asset.get("length", 0),
}
uploaded = asset.get("uploadDate")
if uploaded:
obj["uploaded"] = uploaded.isoformat()
thumbnail = asset.get("thumbnail_location")
if thumbnail:
obj["thumbnail"] = thumbnail
id_info = asset.get("_id")
if id_info:
obj["id"] = "/{tag}/{org}/{course}/{revision}/{category}/{name}" \
.format(
org=id_info.get("org", ""),
course=id_info.get("course", ""),
revision=id_info.get("revision", ""),
tag=id_info.get("tag", ""),
category=id_info.get("category", ""),
name=id_info.get("name", ""),
)
ret.append(obj)
return ret
@login_required
@ensure_csrf_cookie
......@@ -96,32 +52,21 @@ def asset_index(request, org, course, name):
# sort in reverse upload date order
assets = sorted(assets, key=lambda asset: asset['uploadDate'], reverse=True)
if request.META.get('HTTP_ACCEPT', "").startswith("application/json"):
return JsonResponse(assets_to_json_dict(assets))
asset_display = []
asset_json = []
for asset in assets:
asset_id = asset['_id']
display_info = {}
display_info['displayname'] = asset['displayname']
display_info['uploadDate'] = get_default_time_display(asset['uploadDate'])
asset_location = StaticContent.compute_location(asset_id['org'], asset_id['course'], asset_id['name'])
display_info['url'] = StaticContent.get_url_path_from_location(asset_location)
display_info['portable_url'] = StaticContent.get_static_path_from_location(asset_location)
# note, due to the schema change we may not have a 'thumbnail_location' in the result set
_thumbnail_location = asset.get('thumbnail_location', None)
thumbnail_location = Location(_thumbnail_location) if _thumbnail_location is not None else None
display_info['thumb_url'] = StaticContent.get_url_path_from_location(thumbnail_location) if thumbnail_location is not None else None
asset_display.append(display_info)
asset_json.append(_get_asset_json(asset['displayname'], asset['uploadDate'], asset_location, thumbnail_location))
return render_to_response('asset_index.html', {
'context_course': course_module,
'assets': asset_display,
'asset_list': json.dumps(asset_json),
'upload_asset_callback_url': upload_asset_callback_url,
'remove_asset_callback_url': reverse('remove_asset', kwargs={
'update_asset_callback_url': reverse('update_asset', kwargs={
'org': org,
'course': course,
'name': name
......@@ -171,9 +116,6 @@ def upload_asset(request, org, course, coursename):
content = sc_partial(upload_file.read())
tempfile_path = None
thumbnail_content = None
thumbnail_location = None
# first let's see if a thumbnail can be created
(thumbnail_content, thumbnail_location) = contentstore().generate_thumbnail(
content,
......@@ -195,46 +137,38 @@ def upload_asset(request, org, course, coursename):
readback = contentstore().find(content.location)
response_payload = {
'displayname': content.name,
'uploadDate': get_default_time_display(readback.last_modified_at),
'url': StaticContent.get_url_path_from_location(content.location),
'portable_url': StaticContent.get_static_path_from_location(content.location),
'thumb_url': StaticContent.get_url_path_from_location(thumbnail_location)
if thumbnail_content is not None else None,
'msg': 'Upload completed'
'asset': _get_asset_json(content.name, readback.last_modified_at, content.location, content.thumbnail_location),
'msg': _('Upload completed')
}
response = JsonResponse(response_payload)
return response
return JsonResponse(response_payload)
@ensure_csrf_cookie
@require_http_methods(("DELETE",))
@login_required
def remove_asset(request, org, course, name):
'''
This method will perform a 'soft-delete' of an asset, which is basically to
copy the asset from the main GridFS collection and into a Trashcan
'''
get_location_and_verify_access(request, org, course, name)
@ensure_csrf_cookie
def update_asset(request, org, course, name, asset_id):
"""
restful CRUD operations for a course asset.
Currently only the DELETE method is implemented.
location = request.POST['location']
org, course, name: Attributes of the Location for the item to edit
asset_id: the URL of the asset (used by Backbone as the id)
"""
get_location_and_verify_access(request, org, course, name)
# make sure the location is valid
try:
loc = StaticContent.get_location_from_path(location)
except InvalidLocationError:
loc = StaticContent.get_location_from_path(asset_id)
except InvalidLocationError as err:
# return a 'Bad Request' to browser as we have a malformed Location
response = HttpResponse()
response.status_code = 400
return response
return JsonResponse({"error": err.message}, status=400)
# also make sure the item to delete actually exists
try:
content = contentstore().find(loc)
except NotFoundError:
response = HttpResponse()
response.status_code = 404
return response
return JsonResponse(status=404)
# ok, save the content into the trashcan
contentstore('trashcan').save(content)
......@@ -249,13 +183,26 @@ def remove_asset(request, org, course, name):
# remove from any caching
del_cached_content(thumbnail_content.location)
except:
pass # OK if this is left dangling
logging.warning('Could not delete thumbnail: ' + content.thumbnail_location)
# delete the original
contentstore().delete(content.get_id())
# remove from cache
del_cached_content(content.location)
return HttpResponse()
return JsonResponse()
def _get_asset_json(display_name, date, location, thumbnail_location):
"""
Helper method for formatting the asset information to send to client.
"""
asset_url = StaticContent.get_url_path_from_location(location)
return {
'display_name': display_name,
'date_added': get_default_time_display(date),
'url': asset_url,
'portable_url': StaticContent.get_static_path_from_location(location),
'thumbnail': StaticContent.get_url_path_from_location(thumbnail_location) if thumbnail_location is not None else None,
# Needed for Backbone delete/update.
'id': asset_url
}
......@@ -107,13 +107,13 @@ def preview_module_system(request, preview_id, descriptor):
ajax_url=reverse('preview_dispatch', args=[preview_id, descriptor.location.url(), '']).rstrip('/'),
# TODO (cpennington): Do we want to track how instructors are using the preview problems?
track_function=lambda event_type, event: None,
filestore=descriptor.system.resources_fs,
filestore=descriptor.runtime.resources_fs,
get_module=partial(load_preview_module, request, preview_id),
render_template=render_from_lms,
debug=True,
replace_urls=partial(static_replace.replace_static_urls, data_directory=None, course_id=course_id),
user=request.user,
xblock_field_data=preview_field_data,
xmodule_field_data=preview_field_data,
can_execute_unsafe_code=(lambda: can_execute_unsafe_code(course_id)),
mixins=settings.XBLOCK_MIXINS,
course_id=course_id,
......
......@@ -84,12 +84,26 @@ USE_I18N = True
# Include the lettuce app for acceptance testing, including the 'harvest' django-admin command
INSTALLED_APPS += ('lettuce.django',)
LETTUCE_APPS = ('contentstore',)
LETTUCE_SERVER_PORT = choice(PORTS) if SAUCE.get('SAUCE_ENABLED') else randint(1024, 65535)
LETTUCE_BROWSER = os.environ.get('LETTUCE_BROWSER', 'chrome')
# Where to run: local, saucelabs, or grid
LETTUCE_SELENIUM_CLIENT = os.environ.get('LETTUCE_SELENIUM_CLIENT', 'local')
SELENIUM_GRID = {
'URL': 'http://127.0.0.1:4444/wd/hub',
'BROWSER': LETTUCE_BROWSER,
}
#####################################################################
# Lastly, see if the developer has any local overrides.
try:
from .private import * # pylint: disable=F0401
except ImportError:
pass
# Because an override for where to run will affect which ports to use,
# set this up after the local overrides.
if LETTUCE_SELENIUM_CLIENT == 'saucelabs':
LETTUCE_SERVER_PORT = choice(PORTS)
else:
LETTUCE_SERVER_PORT = randint(1024, 65535)
......@@ -254,8 +254,11 @@ PIPELINE_JS = {
'js/models/metadata_model.js', 'js/views/metadata_editor_view.js',
'js/models/uploads.js', 'js/views/uploads.js',
'js/models/textbook.js', 'js/views/textbook.js',
'js/views/assets.js', 'js/src/utility.js',
'js/models/settings/course_grading_policy.js'],
'js/src/utility.js',
'js/models/settings/course_grading_policy.js',
'js/models/asset.js', 'js/models/assets.js',
'js/views/assets.js',
'js/views/assets_view.js', 'js/views/asset_view.js'],
'output_filename': 'js/cms-application.js',
'test_order': 0
},
......
......@@ -25,7 +25,7 @@ describe "CMS.Models.Textbook", ->
expect(@model.isEmpty()).toBeTruthy()
it "should have a URL set", ->
expect(_.result(@model, "url")).toBeTruthy()
expect(@model.url()).toBeTruthy()
it "should be able to reset itself", ->
@model.set("name", "foobar")
......@@ -124,14 +124,14 @@ describe "CMS.Models.Textbook", ->
describe "CMS.Collections.TextbookSet", ->
beforeEach ->
CMS.URL.TEXTBOOK = "/textbooks"
CMS.URL.TEXTBOOKS = "/textbooks"
@collection = new CMS.Collections.TextbookSet()
afterEach ->
delete CMS.URL.TEXTBOOK
delete CMS.URL.TEXTBOOKS
it "should have a url set", ->
expect(_.result(@collection, "url"), "/textbooks")
expect(@collection.url()).toEqual("/textbooks")
it "can call save", ->
spyOn(@collection, "sync")
......
feedbackTpl = readFixtures('system-feedback.underscore')
assetTpl = readFixtures('asset.underscore')
describe "CMS.Views.Asset", ->
beforeEach ->
setFixtures($("<script>", {id: "asset-tpl", type: "text/template"}).text(assetTpl))
appendSetFixtures($("<script>", {id: "system-feedback-tpl", type: "text/template"}).text(feedbackTpl))
appendSetFixtures(sandbox({id: "page-prompt"}))
@model = new CMS.Models.Asset({display_name: "test asset", url: 'actual_asset_url', portable_url: 'portable_url', date_added: 'date', thumbnail: null, id: 'id'})
spyOn(@model, "destroy").andCallThrough()
@collection = new CMS.Models.AssetCollection([@model])
@collection.url = "update-asset-url"
@view = new CMS.Views.Asset({model: @model})
@promptSpies = spyOnConstructor(CMS.Views.Prompt, "Warning", ["show", "hide"])
@promptSpies.show.andReturn(@promptSpies)
describe "Basic", ->
it "should render properly", ->
@view.render()
expect(@view.$el).toContainText("test asset")
it "should pop a delete confirmation when the delete button is clicked", ->
@view.render().$(".remove-asset-button").click()
expect(@promptSpies.constructor).toHaveBeenCalled()
ctorOptions = @promptSpies.constructor.mostRecentCall.args[0]
expect(ctorOptions.title).toMatch('Delete File Confirmation')
# hasn't actually been removed
expect(@model.destroy).not.toHaveBeenCalled()
expect(@collection).toContain(@model)
describe "AJAX", ->
beforeEach ->
@requests = requests = []
@xhr = sinon.useFakeXMLHttpRequest()
@xhr.onCreate = (xhr) -> requests.push(xhr)
@savingSpies = spyOnConstructor(CMS.Views.Notification, "Confirmation", ["show"])
@savingSpies.show.andReturn(@savingSpies)
afterEach ->
@xhr.restore()
it "should destroy itself on confirmation", ->
@view.render().$(".remove-asset-button").click()
ctorOptions = @promptSpies.constructor.mostRecentCall.args[0]
# run the primary function to indicate confirmation
ctorOptions.actions.primary.click(@promptSpies)
# AJAX request has been sent, but not yet returned
expect(@model.destroy).toHaveBeenCalled()
expect(@requests.length).toEqual(1)
expect(@savingSpies.constructor).not.toHaveBeenCalled()
expect(@collection.contains(@model)).toBeTruthy()
# return a success response
@requests[0].respond(200)
expect(@savingSpies.constructor).toHaveBeenCalled()
expect(@savingSpies.show).toHaveBeenCalled()
savingOptions = @savingSpies.constructor.mostRecentCall.args[0]
expect(savingOptions.title).toMatch("Your file has been deleted.")
expect(@collection.contains(@model)).toBeFalsy()
it "should not destroy itself if server errors", ->
@view.render().$(".remove-asset-button").click()
ctorOptions = @promptSpies.constructor.mostRecentCall.args[0]
# run the primary function to indicate confirmation
ctorOptions.actions.primary.click(@promptSpies)
# AJAX request has been sent, but not yet returned
expect(@model.destroy).toHaveBeenCalled()
# return an error response
@requests[0].respond(404)
expect(@savingSpies.constructor).not.toHaveBeenCalled()
expect(@collection.contains(@model)).toBeTruthy()
describe "CMS.Views.Assets", ->
beforeEach ->
setFixtures($("<script>", {id: "asset-tpl", type: "text/template"}).text(assetTpl))
window.analytics = jasmine.createSpyObj('analytics', ['track'])
window.course_location_analytics = jasmine.createSpy()
appendSetFixtures(sandbox({id: "asset_table_body"}))
@collection = new CMS.Models.AssetCollection(
[
{display_name: "test asset 1", url: 'actual_asset_url_1', portable_url: 'portable_url_1', date_added: 'date_1', thumbnail: null, id: 'id_1'},
{display_name: "test asset 2", url: 'actual_asset_url_2', portable_url: 'portable_url_2', date_added: 'date_2', thumbnail: null, id: 'id_2'}
])
@collection.url = "update-asset-url"
@view = new CMS.Views.Assets({collection: @collection, el: $('#asset_table_body')})
@promptSpies = spyOnConstructor(CMS.Views.Prompt, "Warning", ["show", "hide"])
@promptSpies.show.andReturn(@promptSpies)
@requests = requests = []
@xhr = sinon.useFakeXMLHttpRequest()
@xhr.onCreate = (xhr) -> requests.push(xhr)
afterEach ->
delete window.analytics
delete window.course_location_analytics
describe "Basic", ->
it "should render both assets", ->
@view.render()
expect(@view.$el).toContainText("test asset 1")
expect(@view.$el).toContainText("test asset 2")
it "should remove the deleted asset from the view", ->
# Delete the 2nd asset with success from server.
@view.render().$(".remove-asset-button")[1].click()
@promptSpies.constructor.mostRecentCall.args[0].actions.primary.click(@promptSpies)
@requests[0].respond(200)
expect(@view.$el).toContainText("test asset 1")
expect(@view.$el).not.toContainText("test asset 2")
it "does not remove asset if deletion failed", ->
# Delete the 2nd asset, but mimic a failure from the server.
@view.render().$(".remove-asset-button")[1].click()
@promptSpies.constructor.mostRecentCall.args[0].actions.primary.click(@promptSpies)
@requests[0].respond(404)
expect(@view.$el).toContainText("test asset 1")
expect(@view.$el).toContainText("test asset 2")
it "adds an asset if asset does not already exist", ->
@view.render()
model = new CMS.Models.Asset({display_name: "new asset", url: 'new_actual_asset_url', portable_url: 'portable_url', date_added: 'date', thumbnail: null, id: 'idx'})
@view.addAsset(model)
expect(@view.$el).toContainText("new asset")
expect(@collection.models.indexOf(model)).toBe(0)
expect(@collection.models.length).toBe(3)
it "does not add an asset if asset already exists", ->
@view.render()
spyOn(@collection, "add").andCallThrough()
model = @collection.models[1]
@view.addAsset(model)
expect(@collection.add).not.toHaveBeenCalled()
......@@ -11,15 +11,11 @@ describe "CMS.Views.UploadDialog", ->
@model = new CMS.Models.FileUpload(
mimeTypes: ['application/pdf']
)
@chapter = new CMS.Models.Chapter()
@dialogResponse = dialogResponse = []
@view = new CMS.Views.UploadDialog(
model: @model,
onSuccess: (response) =>
options = {}
if !@chapter.get('name')
options.name = response.displayname
options.asset_path = response.url
@chapter.set(options)
dialogResponse.push(response.response)
)
spyOn(@view, 'remove').andCallThrough()
......@@ -66,7 +62,6 @@ describe "CMS.Views.UploadDialog", ->
expect(@view.$el).toContain("#upload_error")
expect(@view.$(".action-upload")).toHaveClass("disabled")
it "adds body class on show()", ->
@view.show()
expect(@view.options.shown).toBeTruthy()
......@@ -99,11 +94,10 @@ describe "CMS.Views.UploadDialog", ->
expect(request.method).toEqual("POST")
request.respond(200, {"Content-Type": "application/json"},
'{"displayname": "starfish", "url": "/uploaded/starfish.pdf"}')
'{"response": "dummy_response"}')
expect(@model.get("uploading")).toBeFalsy()
expect(@model.get("finished")).toBeTruthy()
expect(@chapter.get("name")).toEqual("starfish")
expect(@chapter.get("asset_path")).toEqual("/uploaded/starfish.pdf")
expect(@dialogResponse.pop()).toEqual("dummy_response")
it "can handle upload errors", ->
@view.upload()
......@@ -114,7 +108,7 @@ describe "CMS.Views.UploadDialog", ->
it "removes itself after two seconds on successful upload", ->
@view.upload()
@requests[0].respond(200, {"Content-Type": "application/json"},
'{"displayname": "starfish", "url": "/uploaded/starfish.pdf"}')
'{"response": "dummy_response"}')
expect(@view.remove).not.toHaveBeenCalled()
@clock.tick(2001)
expect(@view.remove).toHaveBeenCalled()
......@@ -421,11 +421,6 @@ function _deleteItem($el, type) {
confirm.show();
}
function markAsLoaded() {
$('.upload-modal .copy-button').css('display', 'inline-block');
$('.upload-modal .progress-bar').addClass('loaded');
}
function hideModal(e) {
if (e) {
e.preventDefault();
......@@ -434,7 +429,6 @@ function hideModal(e) {
// of the editor. Users must press Cancel or Save to exit the editor.
// module_edit adds and removes the "is-fixed" class.
if (!$modalCover.hasClass("is-fixed")) {
$('.file-input').unbind('change', startUpload);
$modal.hide();
$modalCover.hide();
}
......
/**
* Simple model for an asset.
*/
CMS.Models.Asset = Backbone.Model.extend({
defaults: {
display_name: "",
thumbnail: "",
date_added: "",
url: "",
portable_url: "",
is_locked: false
}
});
CMS.Models.AssetCollection = Backbone.Collection.extend({
model : CMS.Models.Asset
});
CMS.Views.Asset = Backbone.View.extend({
initialize: function() {
this.template = _.template($("#asset-tpl").text());
},
tagName: "tr",
events: {
"click .remove-asset-button": "confirmDelete"
},
render: function() {
this.$el.html(this.template({
display_name: this.model.get('display_name'),
thumbnail: this.model.get('thumbnail'),
date_added: this.model.get('date_added'),
url: this.model.get('url'),
portable_url: this.model.get('portable_url')}));
return this;
},
confirmDelete: function(e) {
if(e && e.preventDefault) { e.preventDefault(); }
var asset = this.model, collection = this.model.collection;
new CMS.Views.Prompt.Warning({
title: gettext("Delete File Confirmation"),
message: gettext("Are you sure you wish to delete this item. It cannot be reversed!\n\nAlso any content that links/refers to this item will no longer work (e.g. broken images and/or links)"),
actions: {
primary: {
text: gettext("Delete"),
click: function (view) {
view.hide();
asset.destroy({
wait: true, // Don't remove the asset from the collection until successful.
success: function () {
new CMS.Views.Notification.Confirmation({
title: gettext("Your file has been deleted."),
closeIcon: false,
maxShown: 2000
}).show()
}
}
);
}
},
secondary: [
{
text: gettext("Cancel"),
click: function (view) {
view.hide();
}
}
]
}
}).show();
}
});
// This code is temporarily moved out of asset_index.html
// to fix AWS pipelining issues. We can move it back after RequireJS is integrated.
$(document).ready(function() {
$('.uploads .upload-button').bind('click', showUploadModal);
$('.upload-modal .close-button').bind('click', hideModal);
$('.upload-modal .choose-file-button').bind('click', showFileSelectionMenu);
$('.remove-asset-button').bind('click', removeAsset);
});
function removeAsset(e){
e.preventDefault();
var that = this;
var msg = new CMS.Views.Prompt.Warning({
title: gettext("Delete File Confirmation"),
message: gettext("Are you sure you wish to delete this item. It cannot be reversed!\n\nAlso any content that links/refers to this item will no longer work (e.g. broken images and/or links)"),
actions: {
primary: {
text: gettext("OK"),
click: function(view) {
// call the back-end to actually remove the asset
var url = $('.asset-library').data('remove-asset-callback-url');
var row = $(that).closest('tr');
$.post(url,
{ 'location': row.data('id') },
function() {
// show the post-commit confirmation
var deleted = new CMS.Views.Notification.Confirmation({
title: gettext("Your file has been deleted."),
closeIcon: false,
maxShown: 2000
});
deleted.show();
row.remove();
analytics.track('Deleted Asset', {
'course': course_location_analytics,
'id': row.data('id')
});
}
);
view.hide();
}
},
secondary: [{
text: gettext("Cancel"),
click: function(view) {
view.hide();
}
}]
}
});
return msg.show();
}
function showUploadModal(e) {
var showUploadModal = function (e) {
e.preventDefault();
resetUploadModal();
// $modal has to be global for hideModal to work.
$modal = $('.upload-modal').show();
$('.file-input').bind('change', startUpload);
$('.upload-modal .file-chooser').fileupload({
dataType: 'json',
type: 'POST',
......@@ -75,73 +33,58 @@ function showUploadModal(e) {
}
});
$('.file-input').bind('change', startUpload);
$modalCover.show();
}
};
function showFileSelectionMenu(e) {
var showFileSelectionMenu = function(e) {
e.preventDefault();
$('.file-input').click();
}
};
function startUpload(e) {
var files = $('.file-input').get(0).files;
if (files.length === 0)
return;
var startUpload = function (e) {
var file = e.target.value;
$('.upload-modal h1').html(gettext('Uploading…'));
$('.upload-modal .file-name').html(files[0].name);
$('.upload-modal .file-name').html(file.substring(file.lastIndexOf("\\") + 1));
$('.upload-modal .choose-file-button').hide();
$('.upload-modal .progress-bar').removeClass('loaded').show();
}
};
function resetUploadBar() {
var resetUploadModal = function () {
$('.file-input').unbind('change', startUpload);
// Reset modal so it no longer displays information about previously
// completed uploads.
var percentVal = '0%';
$('.upload-modal .progress-fill').width(percentVal);
$('.upload-modal .progress-fill').html(percentVal);
}
$('.upload-modal .progress-bar').hide();
function resetUploadModal() {
// Reset modal so it no longer displays information about previously
// completed uploads.
resetUploadBar();
$('.upload-modal .file-name').show();
$('.upload-modal .file-name').html('');
$('.upload-modal h1').html(gettext('Upload New File'));
$('.upload-modal .choose-file-button').html(gettext('Choose File'));
$('.upload-modal .embeddable-xml-input').val('');
$('.upload-modal .embeddable').hide();
}
};
function showUploadFeedback(event, percentComplete) {
var showUploadFeedback = function (event, percentComplete) {
var percentVal = percentComplete + '%';
$('.upload-modal .progress-fill').width(percentVal);
$('.upload-modal .progress-fill').html(percentVal);
}
};
function displayFinishedUpload(resp) {
if (resp.status == 200) {
markAsLoaded();
}
var displayFinishedUpload = function (resp) {
var asset = resp.asset;
$('.upload-modal .embeddable-xml-input').val(resp.portable_url);
$('.upload-modal h1').html(gettext('Upload New File'));
$('.upload-modal .embeddable-xml-input').val(asset.portable_url);
$('.upload-modal .embeddable').show();
$('.upload-modal .file-name').hide();
$('.upload-modal .progress-fill').html(resp.msg);
$('.upload-modal .choose-file-button').html(gettext('Load Another File')).show();
$('.upload-modal .progress-fill').width('100%');
// see if this id already exists, if so, then user must have updated an existing piece of content
$("tr[data-id='" + resp.url + "']").remove();
var template = $('#new-asset-element').html();
var html = Mustache.to_html(template, resp);
$('table > tbody').prepend(html);
// re-bind the listeners to delete it
$('.remove-asset-button').bind('click', removeAsset);
analytics.track('Uploaded a File', {
'course': course_location_analytics,
'asset_url': resp.url
});
}
// TODO remove setting on window object after RequireJS.
window.assetsView.addAsset(new CMS.Models.Asset(asset));
};
CMS.Views.Assets = Backbone.View.extend({
// takes CMS.Models.AssetCollection as model
initialize : function() {
this.listenTo(this.collection, 'destroy', this.handleDestroy);
this.render();
},
render: function() {
this.$el.empty();
var self = this;
this.collection.each(
function(asset) {
var view = new CMS.Views.Asset({model: asset});
self.$el.append(view.render().el);
});
return this;
},
handleDestroy: function(model, collection, options) {
var index = options.index;
this.$el.children().eq(index).remove();
analytics.track('Deleted Asset', {
'course': course_location_analytics,
'id': model.get('url')
});
},
addAsset: function (model) {
// If asset is not already being shown, add it.
if (this.collection.findWhere({'url': model.get('url')}) === undefined) {
this.collection.add(model, {at: 0});
var view = new CMS.Views.Asset({model: model});
this.$el.prepend(view.render().el);
analytics.track('Uploaded a File', {
'course': course_location_analytics,
'asset_url': model.get('url')
});
}
}
});
......@@ -60,7 +60,7 @@ CMS.Views.Settings.Details = CMS.Views.ValidatingView.extend({
this.$el.find('#' + this.fieldToSelectorMap['effort']).val(this.model.get('effort'));
var imageURL = this.model.get('course_image_asset_path');
this.$el.find('#course-image-url').val(imageURL)
this.$el.find('#course-image-url').val(imageURL);
this.$el.find('#course-image').attr('src', imageURL);
return this;
......@@ -262,9 +262,9 @@ CMS.Views.Settings.Details = CMS.Views.ValidatingView.extend({
model: upload,
onSuccess: function(response) {
var options = {
'course_image_name': response.displayname,
'course_image_asset_path': response.url
}
'course_image_name': response.asset.display_name,
'course_image_asset_path': response.asset.url
};
self.model.set(options);
self.render();
$('#course-image').attr('src', self.model.get('course_image_asset_path'))
......
......@@ -156,7 +156,6 @@ CMS.Views.ListTextbooks = Backbone.View.extend({
initialize: function() {
this.emptyTemplate = _.template($("#no-textbooks-tpl").text());
this.listenTo(this.collection, 'all', this.render);
this.listenTo(this.collection, 'destroy', this.handleDestroy);
},
tagName: "div",
className: "textbooks-list",
......@@ -185,9 +184,6 @@ CMS.Views.ListTextbooks = Backbone.View.extend({
addOne: function(e) {
if(e && e.preventDefault) { e.preventDefault(); }
this.collection.add([{editing: true}]);
},
handleDestroy: function(model, collection, options) {
collection.remove(model);
}
});
CMS.Views.EditChapter = Backbone.View.extend({
......@@ -252,11 +248,11 @@ CMS.Views.EditChapter = Backbone.View.extend({
onSuccess: function(response) {
var options = {};
if(!that.model.get('name')) {
options.name = response.displayname;
options.name = response.asset.display_name;
}
options.asset_path = response.url;
options.asset_path = response.asset.url;
that.model.set(options);
},
}
});
$(".wrapper-view").after(view.show().el);
}
......
......@@ -8,7 +8,7 @@ html {
}
body {
@extend .t-copy-base;
@extend %t-copy-base;
min-width: $fg-min-width;
background: $gray-l5;
color: $gray-d2;
......@@ -29,7 +29,7 @@ a {
}
h1 {
@extend .t-title4;
@extend %t-title4;
font-weight: 300;
}
......@@ -51,13 +51,13 @@ h1 {
// typography - basic
.page-header {
@extend .t-title3;
@extend %t-title3;
display: block;
font-weight: 600;
color: $gray-d3;
.subtitle {
@extend .t-title7;
@extend %t-title7;
position: relative;
top: ($baseline/4);
display: block;
......@@ -67,29 +67,29 @@ h1 {
}
.section-header {
@extend .t-title4;
@extend %t-title4;
font-weight: 600;
.subtitle {
@extend .t-title7;
@extend %t-title7;
}
}
.area-header {
@extend .t-title6;
@extend %t-title6;
font-weight: 600;
.subtitle {
@extend .t-title8;
@extend %t-title8;
}
}
.area-subheader {
@extend .t-title7;
@extend %t-title7;
font-weight: 600;
.subtitle {
@extend .t-title9;
@extend %t-title9;
}
}
......@@ -198,35 +198,35 @@ h1 {
// typography - loose headings (BT: needs to be removed once html is clean)
.title-1 {
@extend .t-title3;
@extend %t-title3;
margin-bottom: ($baseline*1.5);
}
.title-2 {
@extend .t-title4;
@extend %t-title4;
margin-bottom: $baseline;
}
.title-3 {
@extend .t-title5;
@extend %t-title5;
margin-bottom: ($baseline/2);
}
.title-4 {
@extend .t-title7;
@extend %t-title7;
margin-bottom: $baseline;
font-weight: 500
}
.title-5 {
@extend .t-title7;
@extend %t-title7;
color: $gray-l1;
margin-bottom: $baseline;
font-weight: 500
}
.title-6 {
@extend .t-title7;
@extend %t-title7;
color: $gray-l2;
margin-bottom: $baseline;
font-weight: 500
......@@ -340,7 +340,7 @@ p, ul, ol, dl {
.content {
@include clearfix();
@extend .t-copy-base;
@extend %t-copy-base;
max-width: $fg-max-width;
min-width: $fg-min-width;
width: flex-grid(12);
......@@ -354,14 +354,14 @@ p, ul, ol, dl {
padding-bottom: ($baseline/2);
.title-sub {
@extend .t-copy-sub1;
@extend %t-copy-sub1;
display: block;
margin: 0;
color: $gray-l2;
}
.title-1 {
@extend .t-title3;
@extend %t-title3;
margin: 0;
padding: 0;
font-weight: 600;
......@@ -378,16 +378,16 @@ p, ul, ol, dl {
.content-primary {
.title-1 {
@extend .t-title3;
@extend %t-title3;
}
.title-2 {
@extend .t-title4;
@extend %t-title4;
margin: 0 0 ($baseline/2) 0;
}
.title-3 {
@extend .t-title6;
@extend %t-title6;
margin: 0 0 ($baseline/2) 0;
}
......@@ -401,7 +401,7 @@ p, ul, ol, dl {
}
.tip {
@extend .t-copy-sub2;
@extend %t-copy-sub2;
width: flex-grid(7, 12);
float: right;
margin-top: ($baseline/2);
......@@ -419,7 +419,7 @@ p, ul, ol, dl {
}
.bit {
@extend .t-copy-sub1;
@extend %t-copy-sub1;
margin: 0 0 $baseline 0;
border-bottom: 1px solid $gray-l4;
padding: 0 0 $baseline 0;
......@@ -432,7 +432,7 @@ p, ul, ol, dl {
}
h3 {
@extend .t-title7;
@extend %t-title7;
margin: 0 0 ($baseline/4) 0;
color: $gray-d2;
font-weight: 600;
......@@ -448,7 +448,7 @@ p, ul, ol, dl {
// actions
.list-actions {
@extend .cont-no-list;
@extend %cont-no-list;
.action-item {
margin-bottom: ($baseline/4);
......@@ -558,7 +558,7 @@ p, ul, ol, dl {
// misc
hr.divide {
@extend .cont-text-sr;
@extend %cont-text-sr;
}
.item-details {
......@@ -719,7 +719,7 @@ hr.divide {
.new-button {
@include green-button;
@extend .t-action4;
@extend %t-action4;
padding: 8px 20px 10px;
text-align: center;
......@@ -735,7 +735,7 @@ hr.divide {
.view-button {
@include blue-button;
@extend .t-copy-base;
@extend %t-action4;
text-align: center;
&.big {
......@@ -754,7 +754,7 @@ hr.divide {
.edit-button.standard,
.delete-button.standard {
@extend .t-action4;
@extend %t-action4;
@include white-button;
float: left;
padding: 3px 10px 4px;
......@@ -806,7 +806,7 @@ hr.divide {
// basic utility
.sr {
@extend .cont-text-sr;
@extend %cont-text-sr;
}
.fake-link {
......@@ -827,7 +827,7 @@ hr.divide {
}
hr.divider {
@extend .sr;
@extend %cont-text-sr;
}
// ====================
......@@ -859,7 +859,7 @@ body.js {
text-align: center;
.label {
@extend .cont-text-sr;
@extend %cont-text-sr;
}
[class^="icon-"] {
......@@ -882,14 +882,14 @@ body.js {
}
.title {
@extend .t-title5;
@extend %t-title5;
margin: 0 0 ($baseline/2) 0;
font-weight: 600;
color: $gray-d3;
}
.description {
@extend .t-copy-sub2;
@extend %t-copy-sub2;
margin-top: ($baseline/2);
color: $gray-l1;
}
......
......@@ -17,7 +17,7 @@
}
// canned animation - use if you want out of the box/non-customized anim
.anim-fadeIn {
%anim-fadeIn {
@include animation(fadeIn $tmg-f2 linear 1);
}
......@@ -38,7 +38,7 @@
}
// canned animation - use if you want out of the box/non-customized anim
.anim-fadeOut {
%anim-fadeOut {
@include animation(fadeOut $tmg-f2 linear 1);
}
......@@ -62,7 +62,7 @@
}
// canned animation - use if you want out of the box/non-customized anim
.anim-rotateUp {
%anim-rotateUp {
@include animation(rotateUp $tmg-f2 ease-in-out 1);
}
......@@ -83,7 +83,7 @@
}
// canned animation - use if you want out of the box/non-customized anim
.anim-rotateDown {
%anim-rotateDown {
@include animation(rotateDown $tmg-f2 ease-in-out 1);
}
......@@ -104,7 +104,7 @@
}
// canned animation - use if you want out of the box/non-customized anim
.anim-rotateCW {
%anim-rotateCW {
@include animation(rotateCW $tmg-s1 linear infinite);
}
......@@ -125,7 +125,7 @@
}
// canned animation - use if you want out of the box/non-customized anim
.anim-rotateCCW {
%anim-rotateCCW {
@include animation(rotateCCW $tmg-s1 linear infinite);
}
......@@ -185,7 +185,7 @@
}
// canned animation - use if you want out of the box/non-customized anim
.anim-bounceIn {
%anim-bounceIn {
@include animation(bounceIn $tmg-f1 ease-in-out 1);
}
......@@ -208,6 +208,6 @@
}
// canned animation - use if you want out of the box/non-customized anim
.anim-bounceOut {
%anim-bounceOut {
@include animation(bounceOut $tmg-f1 ease-in-out 1);
}
.content-type {
%content-type {
display: inline-block;
width: 14px;
height: 16px;
......@@ -9,61 +9,61 @@
}
.videosequence-icon {
@extend .content-type;
@extend %content-type;
background-image: url('../img/content-types/videosequence.png');
}
.video-icon {
@extend .content-type;
@extend %content-type;
background-image: url('../img/content-types/video.png');
}
.problemset-icon {
@extend .content-type;
@extend %content-type;
background-image: url('../img/content-types/problemset.png');
}
.problem-icon {
@extend .content-type;
@extend %content-type;
background-image: url('../img/content-types/problem.png');
}
.lab-icon {
@extend .content-type;
@extend %content-type;
background-image: url('../img/content-types/lab.png');
}
.tab-icon {
@extend .content-type;
@extend %content-type;
background-image: url('../img/content-types/lab.png');
}
.html-icon {
@extend .content-type;
@extend %content-type;
background-image: url('../img/content-types/html.png');
}
.vertical-icon {
@extend .content-type;
@extend %content-type;
background-image: url('../img/content-types/vertical.png');
}
.sequential-icon {
@extend .content-type;
@extend %content-type;
background-image: url('../img/content-types/sequential.png');
}
.chapter-icon {
@extend .content-type;
@extend %content-type;
background-image: url('../img/content-types/chapter.png');
}
.module-icon {
@extend .content-type;
@extend %content-type;
background-image: url('../img/content-types/module.png');
}
.module-icon {
@extend .content-type;
@extend %content-type;
background-image: url('../img/content-types/module.png');
}
......@@ -2,8 +2,8 @@
// ====================
// gray primary button
.btn-primary-gray {
@extend .ui-btn-primary;
%btn-primary-gray {
@extend %ui-btn-primary;
background: $gray-l1;
border-color: $gray-l2;
color: $white;
......@@ -24,8 +24,8 @@
}
// blue primary button
.btn-primary-blue {
@extend .ui-btn-primary;
%btn-primary-blue {
@extend %ui-btn-primary;
background: $blue;
border-color: $blue-s1;
color: $white;
......@@ -47,8 +47,8 @@
}
// green primary button
.btn-primary-green {
@extend .ui-btn-primary;
%btn-primary-green {
@extend %ui-btn-primary;
background: $green;
border-color: $green;
color: $white;
......@@ -70,8 +70,8 @@
}
// gray secondary button
.btn-secondary-gray {
@extend .ui-btn-secondary;
%btn-secondary-gray {
@extend %ui-btn-secondary;
border-color: $gray-l3;
color: $gray-l1;
......@@ -91,8 +91,8 @@
}
// blue secondary button
.btn-secondary-blue {
@extend .ui-btn-secondary;
%btn-secondary-blue {
@extend %ui-btn-secondary;
border-color: $blue-l3;
color: $blue;
......@@ -113,8 +113,8 @@
}
// green secondary button
.btn-secondary-green {
@extend .ui-btn-secondary;
%btn-secondary-green {
@extend %ui-btn-secondary;
border-color: $green-l4;
color: $green-l2;
......@@ -148,9 +148,9 @@
// ====================
// simple dropdown button styling - should we move this elsewhere?
.ui-btn-dd {
@extend .ui-btn;
@extend .ui-btn-pill;
%ui-btn-dd {
@extend %ui-btn;
@extend %ui-btn-pill;
padding:($baseline/4) ($baseline/2);
border-width: 1px;
border-style: solid;
......@@ -158,7 +158,7 @@
text-align: center;
&:hover, &:active {
@extend .ui-fake-link;
@extend %ui-fake-link;
border-color: $gray-l3;
}
......@@ -169,8 +169,8 @@
}
// layout-based buttons - nav dd
.ui-btn-dd-nav-primary {
@extend .ui-btn-dd;
%ui-btn-dd-nav-primary {
@extend %ui-btn-dd;
background: $white;
border-color: $white;
color: $gray-d1;
......@@ -197,6 +197,6 @@
// ====================
// specific buttons - view live
.view-live-button {
@extend .t-action4;
%view-live-button {
@extend %t-action4;
}
......@@ -8,7 +8,7 @@
padding: $baseline;
footer.primary {
@extend .t-copy-sub2;
@extend %t-copy-sub2;
@include clearfix();
max-width: $fg-max-width;
min-width: $fg-min-width;
......
......@@ -115,10 +115,10 @@ form {
// ELEM: form
// form styling for creating a new content item (course, user, textbook)
form[class^="create-"] {
@extend .ui-window;
@extend %ui-window;
.title {
@extend .t-title4;
@extend %t-title4;
font-weight: 600;
padding: $baseline ($baseline*1.5) 0 ($baseline*1.5);
}
......@@ -129,7 +129,7 @@ form[class^="create-"] {
.list-input {
@extend .cont-no-list;
@extend %cont-no-list;
.field {
margin: 0 0 ($baseline*0.75) 0;
......@@ -155,7 +155,7 @@ form[class^="create-"] {
}
label {
@extend .t-copy-sub1;
@extend %t-copy-sub1;
@include transition(color $tmg-f3 ease-in-out 0s);
margin: 0 0 ($baseline/4) 0;
......@@ -166,7 +166,7 @@ form[class^="create-"] {
input, textarea {
@extend .t-copy-base;
@extend %t-copy-base;
@include transition(all $tmg-f2 ease-in-out 0s);
height: 100%;
width: 100%;
......@@ -208,7 +208,7 @@ form[class^="create-"] {
}
.tip {
@extend .t-copy-sub2;
@extend %t-copy-sub2;
@include transition(color, 0.15s, ease-in-out);
display: block;
margin-top: ($baseline/4);
......@@ -226,11 +226,11 @@ form[class^="create-"] {
}
.is-showing {
@extend .anim-fadeIn;
@extend %anim-fadeIn;
}
.is-hiding {
@extend .anim-fadeOut;
@extend %anim-fadeOut;
}
.tip-error {
......@@ -311,12 +311,12 @@ form[class^="create-"] {
.action-primary {
@include blue-button;
@extend .t-action2;
@extend %t-action2;
}
.action-secondary {
@include grey-button;
@extend .t-action2;
@extend %t-action2;
}
}
}
......
......@@ -2,7 +2,7 @@
// ====================
.wrapper-header {
@extend .ui-depth3;
@extend %ui-depth3;
position: relative;
width: 100%;
box-shadow: 0 1px 2px 0 $shadow-l1;
......@@ -51,7 +51,7 @@
nav {
> ol > .nav-item {
@extend .t-action3;
@extend %t-action3;
display: inline-block;
vertical-align: middle;
font-weight: 600;
......@@ -74,8 +74,8 @@
.nav-dd {
.title {
@extend .t-action2;
@extend .ui-btn-dd-nav-primary;
@extend %t-action2;
@extend %ui-btn-dd-nav-primary;
@include transition(all $tmg-f2 ease-in-out 0s);
.label, .icon-caret-down {
......@@ -133,7 +133,7 @@
padding: ($baseline*0.75) flex-gutter() ($baseline*0.75) 0;
.course-org, .course-number {
@extend .t-action4;
@extend %t-action4;
display: inline-block;
vertical-align: middle;
max-width: 45%;
......@@ -148,7 +148,7 @@
}
.course-title {
@extend .t-action2;
@extend %t-action2;
display: block;
width: 100%;
overflow: hidden;
......
......@@ -23,8 +23,8 @@
width: 100%;
}
.ui-badge {
@extend .t-title9;
%ui-badge {
@extend %t-title9;
position: relative;
border-bottom-right-radius: ($baseline/10);
border-bottom-left-radius: ($baseline/10);
......@@ -39,7 +39,7 @@
// OPTION: add this class for a visual hanging display
&.is-hanging {
@include box-sizing(border-box);
@extend .ui-depth2;
@extend %ui-depth2;
top: -($baseline/4);
&:after {
......
......@@ -2,7 +2,7 @@
// ====================
.modal-cover {
@extend .ui-depth3;
@extend %ui-depth3;
display: none;
position: fixed;
top: 0;
......@@ -13,7 +13,7 @@
}
.modal {
@extend .ui-depth4;
@extend %ui-depth4;
display: none;
position: fixed;
top: 60px;
......@@ -61,7 +61,7 @@
// lean modal alternative
#lean_overlay {
@extend .ui-depth4;
@extend %ui-depth4;
position: fixed;
top: 0px;
left: 0px;
......
......@@ -5,7 +5,7 @@
nav {
ol, ul {
@extend .cont-no-list;
@extend %cont-no-list;
}
.nav-item {
......@@ -111,7 +111,7 @@ nav {
}
.nav-item {
@extend .t-action3;
@extend %t-action3;
display: block;
margin: 0 0 ($baseline/4) 0;
border-bottom: 1px solid $gray-l5;
......
......@@ -10,7 +10,7 @@
.wrapper-inner {
@include linear-gradient($gray-d3 0%, $gray-d3 98%, $black 100%);
@extend .ui-depth0;
@extend %ui-depth0;
display: none;
width: 100% !important;
border-bottom: 1px solid $white;
......@@ -19,7 +19,7 @@
// sock - actions
.list-cta {
@extend .ui-depth1;
@extend %ui-depth1;
position: absolute;
top: -($baseline*0.75);
width: 100%;
......@@ -27,8 +27,8 @@
text-align: center;
.cta-show-sock {
@extend .ui-btn-pill;
@extend .t-action4;
@extend %ui-btn-pill;
@extend %t-action4;
background: $gray-l5;
padding: ($baseline/2) $baseline;
color: $gray;
......@@ -48,7 +48,7 @@
// sock - additional help
.sock {
@include clearfix();
@extend .t-copy-sub2;
@extend %t-copy-sub2;
max-width: $fg-max-width;
min-width: $fg-min-width;
width: flex-grid(12);
......@@ -60,7 +60,7 @@
header {
.title {
@extend .t-title4;
@extend %t-title4;
}
}
......@@ -70,13 +70,13 @@
@include box-sizing(border-box);
.title {
@extend .t-title6;
@extend %t-title6;
color: $white;
margin-bottom: ($baseline/2);
}
.copy {
@extend .t-copy-sub2;
@extend %t-copy-sub2;
margin: 0 0 $baseline 0;
}
......@@ -92,7 +92,7 @@
}
.action {
@extend .t-action4;
@extend %t-action4;
display: block;
[class^="icon-"] {
......@@ -108,13 +108,13 @@
}
.tip {
@extend .sr;
@extend %cont-text-sr;
}
}
.action-primary {
@extend .btn-primary-blue;
@extend .t-action3;
@extend %btn-primary-blue;
@extend %t-action3;
}
}
}
......
......@@ -3,7 +3,7 @@
// messages
.message {
@extend .t-copy-sub1;
@extend %t-copy-sub1;
display: block;
}
......@@ -49,7 +49,7 @@
@include box-sizing(border-box);
.copy {
@extend .t-copy-sub1;
@extend %t-copy-sub1;
}
}
......@@ -186,7 +186,7 @@
// prompts
.wrapper-prompt {
@extend .ui-depth5;
@extend %ui-depth5;
@include transition(all $tmg-f3 ease-in-out 0s);
position: fixed;
top: 0;
......@@ -233,12 +233,12 @@
}
.action-primary {
@extend .t-action4;
@extend %t-action4;
font-weight: 600;
}
.action-secondary {
@extend .t-action4;
@extend %t-action4;
}
}
}
......@@ -284,7 +284,7 @@
// notifications
.wrapper-notification {
@extend .ui-depth5;
@extend %ui-depth5;
@include clearfix();
box-shadow: 0 -1px 3px $shadow, inset 0 3px 1px $blue;
position: fixed;
......@@ -417,12 +417,12 @@
}
.copy {
@extend .t-copy-sub1;
@extend %t-copy-sub1;
width: flex-grid(10, 12);
color: $gray-l2;
.title {
@extend .t-title7;
@extend %t-title7;
margin-bottom: 0;
color: $white;
}
......@@ -465,7 +465,7 @@
.action-secondary {
@extend .t-action4;
@extend %t-action4;
}
}
......@@ -486,7 +486,7 @@
}
.copy p {
@extend .cont-text-sr;
@extend %cont-text-sr;
}
}
}
......@@ -495,7 +495,7 @@
// alerts
.wrapper-alert {
@extend .ui-depth2;
@extend %ui-depth2;
@include box-sizing(border-box);
box-shadow: 0 1px 1px $white, inset 0 2px 2px $shadow-d1, inset 0 -4px 1px $blue;
position: relative;
......@@ -585,7 +585,7 @@
color: $gray-l2;
.title {
@extend .t-title7;
@extend %t-title7;
margin-bottom: 0;
color: $white;
}
......@@ -619,12 +619,12 @@
}
.action-primary {
@extend .t-action4;
@extend %t-action4;
font-weight: 600;
}
.action-secondary {
@extend .t-action4;
@extend %t-action4;
}
}
}
......@@ -641,7 +641,7 @@
text-align: center;
.label {
@extend .cont-text-sr;
@extend %cont-text-sr;
}
[class^="icon"] {
......@@ -738,7 +738,7 @@ body.uxdesign.alerts {
}
.content-primary {
@extend .ui-window;
@extend %ui-window;
width: flex-grid(12, 12);
margin-right: flex-gutter();
padding: $baseline ($baseline*1.5);
......
......@@ -7,12 +7,12 @@
margin-bottom: $baseline;
.title {
@extend .t-title4;
@extend %t-title4;
font-weight: 600;
}
.copy {
@extend .t-copy-sub1;
@extend %t-copy-sub1;
}
strong {
......@@ -30,14 +30,14 @@
}
.nav-introduction-supplementary {
@extend .t-copy-sub2;
@extend %t-copy-sub2;
float: right;
width: flex-grid(4,12);
display: block;
text-align: right;
.icon {
@extend .t-action3;
@extend %t-action3;
display: inline-block;
vertical-align: middle;
margin-right: ($baseline/4);
......@@ -48,20 +48,20 @@
// notices - in-context: to be used as notices to users within the context of a form/action
.notice-incontext {
@extend .ui-well;
@extend %ui-well;
border-radius: ($baseline/10);
position: relative;
overflow: hidden;
margin-bottom: $baseline;
.title {
@extend .t-title6;
@extend %t-title6;
margin-bottom: ($baseline/2);
font-weight: 700;
}
.copy {
@extend .t-copy-sub1;
@extend %t-copy-sub1;
@include transition(opacity $tmg-f2 ease-in-out 0s);
opacity: 0.75;
margin-bottom: $baseline;
......@@ -99,8 +99,8 @@
}
.action-primary {
@extend .btn-primary-blue;
@extend .t-action3;
@extend %btn-primary-blue;
@extend %t-action3;
}
}
}
......@@ -160,8 +160,8 @@
}
.action-primary {
@extend .btn-primary-blue;
@extend .t-action3;
@extend %btn-primary-blue;
@extend %t-action3;
}
}
}
......@@ -188,8 +188,8 @@
}
.action-primary {
@extend .btn-primary-green;
@extend .t-action3;
@extend %btn-primary-green;
@extend %t-action3;
}
}
}
......
......@@ -4,59 +4,59 @@
// Scale - (6, 7, 8, 9, 10, 11, 12, 14, 16, 18, 21, 24, 36, 48, 60, 72)
// headings/titles
.t-title {
%t-title {
font-family: $f-sans-serif;
}
.t-title1 {
@extend .t-title;
%t-title1 {
@extend %t-title;
@include font-size(60);
@include line-height(60);
}
.t-title2 {
@extend .t-title;
%t-title2 {
@extend %t-title;
@include font-size(48);
@include line-height(48);
}
.t-title3 {
%t-title3 {
@include font-size(36);
@include line-height(36);
}
.t-title4 {
@extend .t-title;
%t-title4 {
@extend %t-title;
@include font-size(24);
@include line-height(24);
}
.t-title5 {
@extend .t-title;
%t-title5 {
@extend %t-title;
@include font-size(18);
@include line-height(18);
}
.t-title6 {
@extend .t-title;
%t-title6 {
@extend %t-title;
@include font-size(16);
@include line-height(16);
}
.t-title7 {
@extend .t-title;
%t-title7 {
@extend %t-title;
@include font-size(14);
@include line-height(14);
}
.t-title8 {
@extend .t-title;
%t-title8 {
@extend %t-title;
@include font-size(12);
@include line-height(12);
}
.t-title9 {
@extend .t-title;
%t-title9 {
@extend %t-title;
@include font-size(11);
@include line-height(11);
}
......@@ -64,36 +64,36 @@
// ====================
// copy
.t-copy {
%t-copy {
font-family: $f-sans-serif;
}
.t-copy-base {
@extend .t-copy;
%t-copy-base {
@extend %t-copy;
@include font-size(16);
@include line-height(16);
}
.t-copy-lead1 {
@extend .t-copy;
%t-copy-lead1 {
@extend %t-copy;
@include font-size(18);
@include line-height(18);
}
.t-copy-lead2 {
@extend .t-copy;
%t-copy-lead2 {
@extend %t-copy;
@include font-size(24);
@include line-height(24);
}
.t-copy-sub1 {
@extend .t-copy;
%t-copy-sub1 {
@extend %t-copy;
@include font-size(14);
@include line-height(14);
}
.t-copy-sub2 {
@extend .t-copy;
%t-copy-sub2 {
@extend %t-copy;
@include font-size(12);
@include line-height(12);
}
......@@ -101,22 +101,22 @@
// ====================
// actions/labels
.t-action1 {
%t-action1 {
@include font-size(18);
@include line-height(18);
}
.t-action2 {
%t-action2 {
@include font-size(16);
@include line-height(16);
}
.t-action3 {
%t-action3 {
@include font-size(14);
@include line-height(14);
}
.t-action4 {
%t-action4 {
@include font-size(12);
@include line-height(12);
}
......@@ -125,54 +125,54 @@
// ====================
// code
.t-code {
%t-code {
font-family: $f-monospace;
}
// ====================
// icons
.t-icon1 {
%t-icon1 {
@include font-size(48);
@include line-height(48);
}
.t-icon2 {
%t-icon2 {
@include font-size(36);
@include line-height(36);
}
.t-icon3 {
%t-icon3 {
@include font-size(24);
@include line-height(24);
}
.t-icon4 {
%t-icon4 {
@include font-size(18);
@include line-height(18);
}
.t-icon5 {
%t-icon5 {
@include font-size(16);
@include line-height(16);
}
.t-icon6 {
%t-icon6 {
@include font-size(14);
@include line-height(14);
}
.t-icon7 {
%t-icon7 {
@include font-size(12);
@include line-height(12);
}
.t-icon8 {
%t-icon8 {
@include font-size(11);
@include line-height(11);
}
.t-icon9 {
%t-icon9 {
@include font-size(10);
@include line-height(10);
}
......@@ -5,7 +5,7 @@ body.course.feature-upload {
// dialog
.wrapper-dialog {
@extend .ui-depth5;
@extend %ui-depth5;
@include transition(all 0.05s ease-in-out);
position: fixed;
top: 0;
......@@ -34,14 +34,14 @@ body.course.feature-upload {
text-align: left;
.title {
@extend .t-title5;
@extend %t-title5;
margin-bottom: ($baseline/2);
font-weight: 600;
color: $black;
}
.message {
@extend .t-copy-sub2;
@extend %t-copy-sub2;
color: $gray;
}
......@@ -59,7 +59,7 @@ body.course.feature-upload {
}
input[type="file"] {
@extend .t-copy-sub2;
@extend %t-copy-sub2;
}
.status-upload {
......@@ -140,7 +140,7 @@ body.course.feature-upload {
.action-item {
@extend .t-action4;
@extend %t-action4;
display: inline-block;
margin-right: ($baseline*0.75);
......
......@@ -12,7 +12,7 @@ body.signup, body.signin {
.content {
@include clearfix();
@extend .t-copy-base;
@extend %t-copy-base;
max-width: $fg-max-width;
min-width: $fg-min-width;
width: flex-grid(12);
......@@ -26,14 +26,14 @@ body.signup, body.signin {
padding-bottom: ($baseline/2);
h1 {
@extend .t-title3;
@extend %t-title3;
margin: 0;
padding: 0;
font-weight: 600;
}
.action {
@extend .t-action3;
@extend %t-action3;
position: absolute;
right: 0;
top: 40%;
......@@ -41,7 +41,7 @@ body.signup, body.signin {
}
.introduction {
@extend .t-copy-sub1;
@extend %t-copy-sub1;
margin: 0 0 $baseline 0;
}
}
......@@ -69,7 +69,7 @@ body.signup, body.signin {
.action-primary {
@include blue-button;
@extend .t-action2;
@extend %t-action2;
@include transition(all $tmg-f3 linear 0s);
display: block;
width: 100%;
......@@ -108,7 +108,7 @@ body.signup, body.signin {
}
label {
@extend .t-copy-sub1;
@extend %t-copy-sub1;
@include transition(color $tmg-f3 ease-in-out 0s);
margin: 0 0 ($baseline/4) 0;
......@@ -118,7 +118,7 @@ body.signup, body.signin {
}
input, textarea {
@extend .t-copy-base;
@extend %t-copy-base;
height: 100%;
width: 100%;
padding: ($baseline/2);
......@@ -171,7 +171,7 @@ body.signup, body.signin {
}
.tip {
@extend .t-copy-sub2;
@extend %t-copy-sub2;
@include transition(color $tmg-f3 ease-in-out 0s);
display: block;
margin-top: ($baseline/4);
......@@ -212,7 +212,7 @@ body.signup, body.signin {
width: flex-grid(4, 12);
.bit {
@extend .t-copy-sub1;
@extend %t-copy-sub1;
margin: 0 0 $baseline 0;
border-bottom: 1px solid $gray-l4;
padding: 0 0 $baseline 0;
......@@ -225,7 +225,7 @@ body.signup, body.signin {
}
h3 {
@extend .t-title7;
@extend %t-title7;
margin: 0 0 ($baseline/4) 0;
color: $gray-d2;
font-weight: 600;
......@@ -245,7 +245,7 @@ body.signup, body.signin {
position: relative;
.action-forgotpassword {
@extend .t-action3;
@extend %t-action3;
position: absolute;
top: 0;
right: 0;
......
......@@ -14,7 +14,7 @@ body.course.checklists {
// checklists - general
.course-checklist {
@extend .ui-window;
@extend %ui-window;
margin: 0 0 ($baseline*2) 0;
&:last-child {
......@@ -23,7 +23,7 @@ body.course.checklists {
// visual status
.viz-checklist-status {
@extend .cont-text-hide;
@extend %cont-text-hide;
@include size(100%,($baseline/4));
position: relative;
display: block;
......@@ -40,7 +40,7 @@ body.course.checklists {
background: $green;
.int {
@extend .cont-text-sr;
@extend %cont-text-sr;
}
}
}
......@@ -83,7 +83,7 @@ body.course.checklists {
}
.checklist-status {
@extend .t-copy-sub1;
@extend %t-copy-sub1;
width: flex-grid(3, 9);
float: right;
margin-top: ($baseline/2);
......@@ -99,7 +99,7 @@ body.course.checklists {
}
.status-count {
@extend .t-copy-base;
@extend %t-copy-base;
margin-left: ($baseline/4);
margin-right: ($baseline/4);
color: $gray-d3;
......@@ -107,7 +107,7 @@ body.course.checklists {
}
.status-amount {
@extend .t-copy-base;
@extend %t-copy-base;
margin-left: ($baseline/4);
color: $gray-d3;
font-weight: 600;
......@@ -138,7 +138,7 @@ body.course.checklists {
.action-secondary {
@include grey-button();
@extend .t-action3;
@extend %t-action3;
font-weight: 400;
float: right;
......@@ -245,13 +245,13 @@ body.course.checklists {
}
.task-description {
@extend .t-copy-sub1;
@extend %t-copy-sub1;
@include transition(color $tmg-f2 ease-in-out 0s);
color: $gray-l2;
}
.task-support {
@extend .t-copy-sub2;
@extend %t-copy-sub2;
@include transition(opacity $tmg-f2 ease-in-out 0s);
opacity: 0.0;
pointer-events: none;
......@@ -272,13 +272,13 @@ body.course.checklists {
.action-primary {
@include blue-button;
@extend .t-action4;
@extend %t-action4;
font-weight: 600;
text-align: center;
}
.action-secondary {
@extend .t-action4;
@extend %t-action4;
margin-top: ($baseline/2);
}
}
......@@ -319,7 +319,7 @@ body.course.checklists {
.action-primary {
@include grey-button;
@extend .t-action4;
@extend %t-action4;
font-weight: 600;
text-align: center;
}
......
......@@ -58,8 +58,8 @@ body.dashboard {
}
.action-create-course {
@extend .btn-primary-green;
@extend .t-action3;
@extend %btn-primary-green;
@extend %t-action3;
}
}
}
......@@ -72,8 +72,8 @@ body.dashboard {
overflow: hidden;
.ui-toggle-control {
@extend .ui-depth2;
@extend .btn-secondary-gray;
@extend %ui-depth2;
@extend %btn-secondary-gray;
@include clearfix();
display: block;
text-align: left;
......@@ -85,14 +85,14 @@ body.dashboard {
}
.label {
@extend .t-action3;
@extend %t-action3;
float: left;
width: flex-grid(8, 9);
margin: 3px flex-gutter() 0 0;
}
.icon-remove-sign {
@extend .t-action1;
@extend %t-action1;
@include transform(rotate(45deg));
@include transform-origin(center center);
@include transition(all $tmg-f1 linear 0s);
......@@ -102,7 +102,7 @@ body.dashboard {
}
.ui-toggle-target {
@extend .ui-depth1;
@extend %ui-depth1;
@include transition(opacity $tmg-f1 ease-in-out 0s);
position: relative;
top: -2px;
......@@ -136,7 +136,7 @@ body.dashboard {
margin-top: $baseline;
.title {
@extend .t-title7;
@extend %t-title7;
margin-bottom: ($baseline/4);
font-weight: 700;
color: $gray-d1;
......@@ -154,8 +154,8 @@ body.dashboard {
}
.action-primary {
@extend .btn-primary-blue;
@extend .t-action3;
@extend %btn-primary-blue;
@extend %t-action3;
}
// specific - request button
......@@ -203,7 +203,7 @@ body.dashboard {
.status-update {
.label {
@extend .cont-text-sr;
@extend %cont-text-sr;
}
.value {
......@@ -232,7 +232,7 @@ body.dashboard {
}
.value-formal {
@extend .t-title5;
@extend %t-title5;
margin: ($baseline/2);
font-weight: 700;
......@@ -242,7 +242,7 @@ body.dashboard {
}
.value-description {
@extend .t-copy-sub1;
@extend %t-copy-sub1;
position: relative;
color: $white;
opacity: 0.85;
......@@ -253,7 +253,7 @@ body.dashboard {
&.is-unrequested {
.title {
@extend .cont-text-sr;
@extend %cont-text-sr;
}
}
......@@ -336,21 +336,21 @@ body.dashboard {
// encompassing course link
.course-link {
@extend .ui-depth2;
@extend %ui-depth2;
width: flex-grid(7, 9);
margin-right: flex-gutter();
}
// course title
.course-title {
@extend .t-title4;
@extend %t-title4;
margin: 0 ($baseline*2) ($baseline/4) 0;
font-weight: 300;
}
// course metadata
.course-metadata {
@extend .t-copy-sub1;
@extend %t-copy-sub1;
@include transition(opacity $tmg-f1 ease-in-out 0);
color: $gray;
opacity: 0.75;
......@@ -375,20 +375,20 @@ body.dashboard {
}
.label {
@extend .cont-text-sr;
@extend %cont-text-sr;
}
}
}
.course-actions {
@extend .ui-depth3;
@extend %ui-depth3;
position: static;
width: flex-grid(2, 9);
text-align: right;
// view live button
.view-live-button {
@extend .ui-depth3;
@extend %ui-depth3;
@include transition(opacity $tmg-f2 ease-in-out 0);
@include box-sizing(border-box);
padding: ($baseline/2);
......@@ -447,7 +447,7 @@ body.dashboard {
}
label {
@extend .t-title7;
@extend %t-title7;
display: block;
font-weight: 700;
}
......@@ -460,7 +460,7 @@ body.dashboard {
}
.new-course-name {
@extend .t-title5;
@extend %t-title5;
font-weight: 300;
}
......
......@@ -4,7 +4,7 @@
body.course.export {
.export-overview {
@extend .ui-window;
@extend %ui-window;
@include clearfix;
padding: 30px 40px;
}
......
......@@ -4,7 +4,7 @@
body.course.import {
.import-overview {
@extend .ui-window;
@extend %ui-window;
@include clearfix;
padding: 30px 40px;
}
......
......@@ -18,7 +18,7 @@ body.index {
}
.content {
@extend .t-copy-base;
@extend %t-copy-base;
@include clearfix();
max-width: $fg-max-width;
min-width: $fg-min-width;
......@@ -62,7 +62,7 @@ body.index {
color: $white;
h1 {
@extend .t-title2;
@extend %t-title2;
float: none;
margin: 0 0 ($baseline/2) 0;
border-bottom: 1px solid $blue-l1;
......@@ -81,7 +81,7 @@ body.index {
}
.tagline {
@extend .t-title4;
@extend %t-title4;
margin: 0;
color: $blue-l3;
}
......@@ -196,13 +196,13 @@ body.index {
margin-top: -($baseline/4);
h3 {
@extend .t-title4;
@extend %t-title4;
margin: 0 0 ($baseline/2) 0;
font-weight: 600;
}
> p {
@extend .t-copy-lead1;
@extend %t-copy-lead1;
color: $gray-d1;
}
......@@ -212,7 +212,7 @@ body.index {
}
.list-proofpoints {
@extend .t-copy-sub1;
@extend %t-copy-sub1;
@include clearfix();
width: flex-grid(9, 9);
margin: ($baseline*1.5) 0 0 0;
......@@ -231,14 +231,14 @@ body.index {
color: $gray-l1;
.title {
@extend .t-copy-base;
@extend %t-copy-base;
margin: 0 0 ($baseline/4) 0;
font-weight: 500;
color: $gray-d3;
}
&:hover {
@extend .fake-link;
@extend %ui-fake-link;
box-shadow: 0 1px ($baseline/10) $shadow-l1;
background: $blue-l5;
top: -($baseline/5);
......@@ -323,7 +323,7 @@ body.index {
text-align: center;
&.action-primary {
@extend .t-action1;
@extend %t-action1;
@include blue-button;
padding: ($baseline*0.75) ($baseline/2);
font-weight: 600;
......@@ -332,7 +332,7 @@ body.index {
}
&.action-secondary {
@extend .t-action3;
@extend %t-action3;
margin-top: ($baseline/2);
}
}
......
......@@ -630,7 +630,7 @@ body.course.outline {
}
label {
@extend .t-copy-sub1;
@extend %t-copy-sub1;
margin-bottom: ($baseline/4);
}
}
......
......@@ -9,7 +9,7 @@ body.course.settings {
}
.content-primary {
@extend .ui-window;
@extend %ui-window;
width: flex-grid(9, 12);
margin-right: flex-gutter();
padding: $baseline ($baseline*1.5);
......@@ -72,7 +72,7 @@ body.course.settings {
}
.tip {
@extend .t-copy-sub2;
@extend %t-copy-sub2;
width: flex-grid(5, 9);
float: right;
margin-top: ($baseline/2);
......@@ -92,12 +92,12 @@ body.course.settings {
// in form -UI hints/tips/messages
.instructions {
@extend .t-copy-sub1;
@extend %t-copy-sub1;
margin: 0 0 $baseline 0;
}
.tip {
@extend .t-copy-sub2;
@extend %t-copy-sub2;
@include transition(color $tmg-f2 ease-in-out 0s);
display: block;
margin-top: ($baseline/4);
......@@ -105,7 +105,7 @@ body.course.settings {
}
.message-error {
@extend .t-copy-sub1;
@extend %t-copy-sub1;
display: block;
margin-top: ($baseline/4);
margin-bottom: ($baseline/2);
......@@ -115,12 +115,12 @@ body.course.settings {
// buttons
.remove-item {
@include white-button;
@extend .t-action3;
@extend %t-action3;
font-weight: 400;
}
.new-button {
// @extend .t-action3; - bad buttons won't render this properly
// @extend %t-action3; - bad buttons won't render this properly
@include font-size(14);
}
......@@ -154,7 +154,7 @@ body.course.settings {
}
label {
@extend .t-copy-sub1;
@extend %t-copy-sub1;
@include transition(color $tmg-f2 ease-in-out 0s);
margin: 0 0 ($baseline/4) 0;
font-weight: 400;
......@@ -165,7 +165,7 @@ body.course.settings {
}
input, textarea {
@extend .t-copy-base;
@extend %t-copy-base;
@include placeholder($gray-l4);
@include size(100%,100%);
padding: ($baseline/2);
......@@ -265,7 +265,7 @@ body.course.settings {
}
input, textarea {
@extend .t-copy-lead1;
@extend %t-copy-lead1;
box-shadow: none;
border: none;
background: none;
......@@ -301,7 +301,7 @@ body.course.settings {
padding: ($baseline/2) 0 0 0;
.title {
@extend .t-copy-sub1;
@extend %t-copy-sub1;
margin: 0 0 ($baseline/10) 0;
padding: 0 ($baseline/2);
......@@ -315,7 +315,7 @@ body.course.settings {
padding: 0 ($baseline/2) ($baseline/2) ($baseline/2);
.link-courseURL {
@extend .t-copy-lead1;
@extend %t-copy-lead1;
@include box-sizing(border-box);
display: block;
width: 100%;
......@@ -337,11 +337,11 @@ body.course.settings {
.action-primary {
@include blue-button();
@extend .t-action3;
@extend %t-action3;
font-weight: 600;
[class^="icon-"] {
@extend .t-icon5;
@extend %t-icon5;
display: inline-block;
vertical-align: middle;
margin-top: -3px;
......@@ -460,7 +460,7 @@ body.course.settings {
}
.msg {
@extend .t-copy-sub2;
@extend %t-copy-sub2;
display: block;
margin-top: ($baseline/2);
color: $gray-l3;
......@@ -478,7 +478,7 @@ body.course.settings {
}
.action-upload-image {
@extend .ui-btn-flat-outline;
@extend %ui-btn-flat-outline;
float: right;
width: flex-grid(2,9);
margin-top: ($baseline/4);
......@@ -820,7 +820,7 @@ body.course.settings {
// specific to code mirror instance in JSON policy editing, need to sync up with other similar code mirror UIs
.CodeMirror {
@extend .t-copy-base;
@extend %t-copy-base;
@include box-sizing(border-box);
box-shadow: 0 1px 2px rgba(0, 0, 0, .1) inset;
@include linear-gradient($lightGrey, tint($lightGrey, 90%));
......
......@@ -171,7 +171,7 @@ body.course.static-pages {
}
.static-page-details {
@extend .ui-window;
@extend %ui-window;
padding: 32px 40px;
.row {
......
......@@ -13,7 +13,7 @@ body.course.textbooks {
margin-right: flex-gutter();
.no-textbook-content {
@extend .ui-well;
@extend %ui-well;
padding: ($baseline*2);
background-color: $gray-l4;
text-align: center;
......@@ -30,7 +30,7 @@ body.course.textbooks {
}
.textbook {
@extend .ui-window;
@extend %ui-window;
position: relative;
.view-textbook {
......@@ -42,7 +42,7 @@ body.course.textbooks {
}
.textbook-title {
@extend .t-title4;
@extend %t-title4;
margin-right: ($baseline*14);
font-weight: bold;
}
......@@ -71,7 +71,7 @@ body.course.textbooks {
margin-left: $baseline;
.chapter {
@extend .t-copy-sub2;
@extend %t-copy-sub2;
margin-bottom: ($baseline/4);
border-bottom: 1px solid $gray-l4;
......@@ -106,16 +106,16 @@ body.course.textbooks {
.view {
@include blue-button;
@extend .t-action4;
@extend %t-action4;
}
.edit {
@include blue-button;
@extend .t-action4;
@extend %t-action4;
}
.delete {
@extend .ui-btn-non;
@extend %ui-btn-non;
}
}
......@@ -160,7 +160,7 @@ body.course.textbooks {
.action-primary {
@include blue-button;
@extend .t-action2;
@extend %t-action2;
@include transition(all .15s);
display: inline-block;
padding: ($baseline/5) $baseline;
......@@ -170,7 +170,7 @@ body.course.textbooks {
.action-secondary {
@include grey-button;
@extend .t-action2;
@extend %t-action2;
@include transition(all .15s);
display: inline-block;
padding: ($baseline/5) $baseline;
......@@ -182,7 +182,7 @@ body.course.textbooks {
}
.copy {
@extend .t-copy-sub2;
@extend %t-copy-sub2;
margin: ($baseline) 0 ($baseline/2) 0;
color: $gray;
......@@ -196,7 +196,7 @@ body.course.textbooks {
.chapters-fields,
.textbook-fields {
@extend .cont-no-list;
@extend %cont-no-list;
.field {
margin: 0 0 ($baseline*0.75) 0;
......@@ -222,7 +222,7 @@ body.course.textbooks {
}
label {
@extend .t-copy-sub1;
@extend %t-copy-sub1;
@include transition(color, 0.15s, ease-in-out);
margin: 0 0 ($baseline/4) 0;
......@@ -233,13 +233,13 @@ body.course.textbooks {
&.add-textbook-name label {
@extend .t-title5;
@extend %t-title5;
}
//this section is borrowed from _account.scss - we should clean up and unify later
input, textarea {
@extend .t-copy-base;
@extend %t-copy-base;
height: 100%;
width: 100%;
padding: ($baseline/2);
......@@ -292,7 +292,7 @@ body.course.textbooks {
}
.tip {
@extend .t-copy-sub2;
@extend %t-copy-sub2;
@include transition(color, 0.15s, ease-in-out);
display: block;
margin-top: ($baseline/4);
......@@ -328,7 +328,7 @@ body.course.textbooks {
}
.action-upload {
@extend .ui-btn-flat-outline;
@extend %ui-btn-flat-outline;
position: absolute;
top: 3px;
right: 0;
......@@ -356,7 +356,7 @@ body.course.textbooks {
.action-add-chapter {
@extend .ui-btn-flat-outline;
@extend %ui-btn-flat-outline;
@include font-size(16);
display: block;
width: 100%;
......
......@@ -466,7 +466,7 @@ body.course.unit {
.action-primary {
@include blue-button;
@extend .t-action2;
@extend %t-action2;
@include transition(all .15s);
display: inline-block;
padding: ($baseline/5) $baseline;
......@@ -476,7 +476,7 @@ body.course.unit {
.action-secondary {
@include grey-button;
@extend .t-action2;
@extend %t-action2;
@include transition(all .15s);
display: inline-block;
padding: ($baseline/5) $baseline;
......@@ -500,7 +500,7 @@ body.course.unit {
//Component Name
.component-name {
@extend .t-copy-sub1;
@extend %t-copy-sub1;
width: 50%;
color: $white;
font-weight: 600;
......@@ -637,7 +637,7 @@ body.course.unit {
}
.setting-label {
@extend .t-copy-sub1;
@extend %t-copy-sub1;
@include transition(color $tmg-f2 ease-in-out 0s);
vertical-align: middle;
display: inline-block;
......@@ -794,8 +794,8 @@ body.course.unit {
}
.create-setting {
@extend .ui-btn-flat-outline;
@extend .t-action3;
@extend %ui-btn-flat-outline;
@extend %t-action3;
display: block;
width: 100%;
padding: ($baseline/2);
......@@ -974,7 +974,7 @@ body.unit {
.unit-id {
.label {
@extend .t-title7;
@extend %t-title7;
margin-bottom: ($baseline/4);
color: $gray-d1;
}
......
......@@ -22,7 +22,7 @@ body.course.users {
.content {
.introduction {
@extend .t-copy-sub1;
@extend %t-copy-sub1;
margin: 0 0 ($baseline*2) 0;
}
}
......@@ -56,7 +56,7 @@ body.course.users {
.action-primary {
@include green-button(); // overwriting for the sake of syncing older green button styles for now
@extend .t-action3;
@extend %t-action3;
padding: ($baseline/2) $baseline;
}
}
......@@ -80,7 +80,7 @@ body.course.users {
.user-list {
.user-item {
@extend .ui-window;
@extend %ui-window;
@include clearfix();
position: relative;
width: flex-grid(9, 9);
......@@ -98,7 +98,7 @@ body.course.users {
// ELEM: item - flag
.flag-role {
@extend .ui-badge;
@extend %ui-badge;
color: $white;
.msg-you {
......@@ -132,7 +132,7 @@ body.course.users {
}
.user-username {
@extend .t-title4;
@extend %t-title4;
@include transition(color $tmg-f2 ease-in-out 0s);
margin: 0 ($baseline/2) ($baseline/10) 0;
color: $gray-d4;
......@@ -140,7 +140,7 @@ body.course.users {
}
.user-email {
@extend .t-title6;
@extend %t-title6;
}
}
......@@ -172,7 +172,7 @@ body.course.users {
}
.delete {
@extend .ui-btn-non;
@extend %ui-btn-non;
}
// HACK: nasty reset needed due to base.scss
......@@ -187,7 +187,7 @@ body.course.users {
&.add-admin-role {
@include blue-button;
@extend .t-action2;
@extend %t-action2;
@include transition(all .15s);
display: inline-block;
padding: ($baseline/5) $baseline;
......@@ -196,7 +196,7 @@ body.course.users {
&.remove-admin-role {
@include grey-button;
@extend .t-action2;
@extend %t-action2;
@include transition(all .15s);
display: inline-block;
padding: ($baseline/5) $baseline;
......@@ -205,7 +205,7 @@ body.course.users {
}
.notoggleforyou {
@extend .t-copy-sub1;
@extend %t-copy-sub1;
color: $gray-l2;
}
}
......
......@@ -6,39 +6,27 @@
<%namespace name='static' file='static_content.html'/>
<%block name="header_extras">
<script type="text/template" id="asset-tpl">
<%static:include path="js/asset.underscore"/>
</script>
</%block>
<%block name="jsextra">
<script src="${static.url('js/vendor/mustache.js')}"></script>
<script src="${static.url('js/vendor/jQuery-File-Upload/js/jquery.iframe-transport.js')}"> </script>
<script src="${static.url('js/vendor/jQuery-File-Upload/js/jquery.fileupload.js')}"> </script>
<script src="${static.url('js/vendor/mustache.js')}"></script>
<script src="${static.url('js/vendor/jQuery-File-Upload/js/jquery.iframe-transport.js')}"> </script>
<script src="${static.url('js/vendor/jQuery-File-Upload/js/jquery.fileupload.js')}"> </script>
<script type="text/javascript">
var assets = new CMS.Models.AssetCollection(${asset_list});
assets.url = "${update_asset_callback_url}";
// TODO remove setting on window object after RequireJS.
window.assetsView = new CMS.Views.Assets({collection: assets, el: $('#asset_table_body')});
</script>
</%block>
<%block name="content">
<script type="text/template" id="new-asset-element">
<tr data-id='{{url}}'>
<td class="thumb-col">
<div class="thumb">
{{#thumb_url}}
<img src="{{thumb_url}}">
{{/thumb_url}}
</div>
</td>
<td class="name-col">
<a data-tooltip="Open/download this file" href="{{url}}" class="filename">{{displayname}}</a>
<div class="embeddable-xml"></div>
</td>
<td class="date-col">
{{uploadDate}}
</td>
<td class="embed-col">
<input type="text" class="embeddable-xml-input" value='{{portable_url}}' readonly>
</td>
<td class="delete-col">
<a href="#" data-tooltip="${_('Delete this asset')}" class="remove-asset-button"><span class="delete-icon"></span></a>
</td>
</tr>
</script>
<div class="wrapper-mast wrapper">
<header class="mast has-actions has-subtitle">
<h1 class="page-header">
......@@ -62,7 +50,7 @@
<div class="page-actions">
<input type="text" class="asset-search-input search wip-box" placeholder="search assets" style="display:none"/>
</div>
<article class="asset-library" data-remove-asset-callback-url='${remove_asset_callback_url}'>
<article class="asset-library">
<table>
<thead>
<tr>
......@@ -73,31 +61,8 @@
<th class="delete-col"></th>
</tr>
</thead>
<tbody id="asset_table_body">
% for asset in assets:
<tr data-id="${asset['url']}">
<td class="thumb-col">
<div class="thumb">
% if asset['thumb_url'] is not None:
<img src="${asset['thumb_url']}">
% endif
</div>
</td>
<td class="name-col">
<a data-tooltip="Open/download this file" href="${asset['url']}" class="filename">${asset['displayname']}</a>
<div class="embeddable-xml"></div>
</td>
<td class="date-col">
${asset['uploadDate']}
</td>
<td class="embed-col">
<input type="text" class="embeddable-xml-input" value="${asset['portable_url']}" readonly>
</td>
<td class="delete-col">
<a href="#" data-tooltip="${_('Delete this asset')}" class="remove-asset-button"><span class="delete-icon"></span></a>
</td>
</tr>
% endfor
<tbody id="asset_table_body" >
</tbody>
</table>
<nav class="pagination wip-box">
......
<td class="thumb-col">
<div class="thumb">
<% if (thumbnail !== '') { %>
<img src="<%= thumbnail %>">
<% } %>
</div>
</td>
<td class="name-col">
<a data-tooltip="<%= gettext('Open/download this file') %>" href="<%= url %>" class="filename"><%= display_name %></a>
<div class="embeddable-xml"></div>
</td>
<td class="date-col">
<%= date_added %>
</td>
<td class="embed-col">
<input type="text" class="embeddable-xml-input" value="<%= portable_url %>" readonly>
</td>
<td class="delete-col">
<a href="#" data-tooltip="<%= gettext('Delete this asset') %>" class="remove-asset-button"><span
class="delete-icon"></span></a>
</td>
......@@ -76,8 +76,8 @@ urlpatterns = ('', # nopep8
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/assets/(?P<name>[^/]+)$',
'contentstore.views.asset_index', name='asset_index'),
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/assets/(?P<name>[^/]+)/remove$',
'contentstore.views.assets.remove_asset', name='remove_asset'),
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/assets/(?P<name>[^/]+)/(?P<asset_id>.+)?.*$',
'contentstore.views.assets.update_asset', name='update_asset'),
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/textbooks/(?P<name>[^/]+)$',
'contentstore.views.textbook_index', name='textbook_index'),
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/textbooks/(?P<name>[^/]+)/new$',
......
......@@ -43,18 +43,18 @@ LOGGER.info("Loading the lettuce acceptance testing terrain file...")
MAX_VALID_BROWSER_ATTEMPTS = 20
def get_username_and_key():
def get_saucelabs_username_and_key():
"""
Returns the Sauce Labs username and access ID as set by environment variables
"""
return {"username": settings.SAUCE.get('USERNAME'), "access-key": settings.SAUCE.get('ACCESS_ID')}
def set_job_status(jobid, passed=True):
def set_saucelabs_job_status(jobid, passed=True):
"""
Sets the job status on sauce labs
"""
config = get_username_and_key()
config = get_saucelabs_username_and_key()
url = 'http://saucelabs.com/rest/v1/{}/jobs/{}'.format(config['username'], world.jobid)
body_content = dumps({"passed": passed})
base64string = encodestring('{}:{}'.format(config['username'], config['access-key']))[:-1]
......@@ -63,7 +63,7 @@ def set_job_status(jobid, passed=True):
return result.status_code == 200
def make_desired_capabilities():
def make_saucelabs_desired_capabilities():
"""
Returns a DesiredCapabilities object corresponding to the environment sauce parameters
"""
......@@ -88,9 +88,9 @@ def initial_setup(server):
"""
Launch the browser once before executing the tests.
"""
world.absorb(settings.SAUCE.get('SAUCE_ENABLED'), 'SAUCE_ENABLED')
world.absorb(settings.LETTUCE_SELENIUM_CLIENT, 'LETTUCE_SELENIUM_CLIENT')
if not world.SAUCE_ENABLED:
if world.LETTUCE_SELENIUM_CLIENT == 'local':
browser_driver = getattr(settings, 'LETTUCE_BROWSER', 'chrome')
# There is an issue with ChromeDriver2 r195627 on Ubuntu
......@@ -121,15 +121,26 @@ def initial_setup(server):
world.browser.driver.set_window_size(1280, 1024)
else:
config = get_username_and_key()
elif world.LETTUCE_SELENIUM_CLIENT == 'saucelabs':
config = get_saucelabs_username_and_key()
world.browser = Browser(
'remote',
url="http://{}:{}@ondemand.saucelabs.com:80/wd/hub".format(config['username'], config['access-key']),
**make_desired_capabilities()
**make_saucelabs_desired_capabilities()
)
world.browser.driver.implicitly_wait(30)
elif world.LETTUCE_SELENIUM_CLIENT == 'grid':
world.browser = Browser(
'remote',
url=settings.SELENIUM_GRID.get('URL'),
browser=settings.SELENIUM_GRID.get('BROWSER'),
)
world.browser.driver.implicitly_wait(30)
else:
raise Exception("Unknown selenium client '{}'".format(world.LETTUCE_SELENIUM_CLIENT))
world.absorb(world.browser.driver.session_id, 'jobid')
......@@ -140,7 +151,7 @@ def reset_data(scenario):
envs/acceptance.py file: mitx_all/db/test_mitx.db
"""
LOGGER.debug("Flushing the test database...")
call_command('flush', interactive=False)
call_command('flush', interactive=False, verbosity=0)
world.absorb({}, 'scenario_dict')
......@@ -183,6 +194,6 @@ def teardown_browser(total):
"""
Quit the browser after executing the tests.
"""
if world.SAUCE_ENABLED:
set_job_status(world.jobid, total.scenarios_ran == total.scenarios_passed)
if world.LETTUCE_SELENIUM_CLIENT == 'saucelabs':
set_saucelabs_job_status(world.jobid, total.scenarios_ran == total.scenarios_passed)
world.browser.quit()
......@@ -138,9 +138,10 @@ def should_have_link_with_path_and_text(step, path, text):
@step(r'should( not)? see "(.*)" (?:somewhere|anywhere) (?:in|on) (?:the|this) page')
def should_see_in_the_page(step, doesnt_appear, text):
multiplier = 1
if world.SAUCE_ENABLED:
if world.LETTUCE_SELENIUM_CLIENT == 'saucelabs':
multiplier = 2
else:
multiplier = 1
if doesnt_appear:
assert world.browser.is_text_not_present(text, wait_time=5*multiplier)
else:
......@@ -152,8 +153,8 @@ def i_am_logged_in(step):
world.create_user('robot', 'test')
world.log_in(username='robot', password='test')
world.browser.visit(django_url('/'))
# You should not see the login link
assert world.is_css_not_present('a#login')
dash_css = 'section.container.dashboard'
assert world.is_css_present(dash_css)
@step(u'I am an edX user$')
......
......@@ -21,6 +21,7 @@ from xmodule.exceptions import NotFoundError, ProcessingError
from xblock.fields import Scope, String, Boolean, Dict, Integer, Float
from .fields import Timedelta, Date
from django.utils.timezone import UTC
from django.utils.translation import ugettext as _
log = logging.getLogger("mitx.courseware")
......@@ -348,7 +349,8 @@ class CapaModule(CapaFields, XModule):
final_check = (self.attempts >= self.max_attempts - 1)
else:
final_check = False
return "Final Submit" if final_check else "Submit"
return _("Final Submit") if final_check else _("Submit")
def should_show_check_button(self):
"""
......
......@@ -14,13 +14,29 @@ import textwrap
log = logging.getLogger("mitx.courseware")
V1_SETTINGS_ATTRIBUTES = [
"display_name", "max_attempts", "graded", "accept_file_upload",
"skip_spelling_checks", "due", "graceperiod", "weight", "min_to_calibrate",
"max_to_calibrate", "peer_grader_count", "required_peer_grading",
"display_name",
"max_attempts",
"graded",
"accept_file_upload",
"skip_spelling_checks",
"due",
"graceperiod",
"weight",
"min_to_calibrate",
"max_to_calibrate",
"peer_grader_count",
"required_peer_grading",
"peer_grade_finished_submissions_when_none_pending",
]
V1_STUDENT_ATTRIBUTES = ["current_task_number", "task_states", "state",
"student_attempts", "ready_to_reset", "old_task_states"]
V1_STUDENT_ATTRIBUTES = [
"current_task_number",
"task_states",
"state",
"student_attempts",
"ready_to_reset",
"old_task_states",
]
V1_ATTRIBUTES = V1_SETTINGS_ATTRIBUTES + V1_STUDENT_ATTRIBUTES
......@@ -288,6 +304,14 @@ class CombinedOpenEndedFields(object):
scope=Scope.settings,
values={"min": 1, "step": "1", "max": 5}
)
peer_grade_finished_submissions_when_none_pending = Boolean(
display_name='Allow "overgrading" of peer submissions',
help=("Allow students to peer grade submissions that already have the requisite number of graders, "
"but ONLY WHEN all submissions they are eligible to grade already have enough graders. "
"This is intended for use when settings for `Required Peer Grading` > `Peer Graders per Response`"),
default=False,
scope=Scope.settings,
)
markdown = String(
help="Markdown source of this module",
default=textwrap.dedent("""\
......
......@@ -15,9 +15,9 @@ from PIL import Image
class StaticContent(object):
def __init__(self, loc, name, content_type, data, last_modified_at=None, thumbnail_location=None, import_path=None,
length=None):
length=None, locked=False):
self.location = loc
self.name = name # a display string which can be edited, and thus not part of the location which needs to be fixed
self.name = name # a display string which can be edited, and thus not part of the location which needs to be fixed
self.content_type = content_type
self._data = data
self.length = length
......@@ -26,6 +26,7 @@ class StaticContent(object):
# optional information about where this file was imported from. This is needed to support import/export
# cycles
self.import_path = import_path
self.locked = locked
@property
def is_thumbnail(self):
......@@ -133,10 +134,10 @@ class StaticContent(object):
class StaticContentStream(StaticContent):
def __init__(self, loc, name, content_type, stream, last_modified_at=None, thumbnail_location=None, import_path=None,
length=None):
length=None, locked=False):
super(StaticContentStream, self).__init__(loc, name, content_type, None, last_modified_at=last_modified_at,
thumbnail_location=thumbnail_location, import_path=import_path,
length=length)
length=length, locked=locked)
self._stream = stream
def stream_data(self):
......@@ -153,7 +154,7 @@ class StaticContentStream(StaticContent):
self._stream.seek(0)
content = StaticContent(self.location, self.name, self.content_type, self._stream.read(),
last_modified_at=self.last_modified_at, thumbnail_location=self.thumbnail_location,
import_path=self.import_path, length=self.length)
import_path=self.import_path, length=self.length, locked=self.locked)
return content
......
......@@ -24,17 +24,19 @@ class MongoContentStore(ContentStore):
self.fs = gridfs.GridFS(_db, bucket)
self.fs_files = _db[bucket + ".files"] # the underlying collection GridFS uses
self.fs_files = _db[bucket + ".files"] # the underlying collection GridFS uses
def save(self, content):
id = content.get_id()
content_id = content.get_id()
# Seems like with the GridFS we can't update existing ID's we have to do a delete/add pair
self.delete(id)
self.delete(content_id)
with self.fs.new_file(_id=id, filename=content.get_url_path(), content_type=content.content_type,
with self.fs.new_file(_id=content_id, filename=content.get_url_path(), content_type=content.content_type,
displayname=content.name, thumbnail_location=content.thumbnail_location,
import_path=content.import_path) as fp:
import_path=content.import_path,
# getattr b/c caching may mean some pickled instances don't have attr
locked=getattr(content, 'locked', False)) as fp:
if hasattr(content.data, '__iter__'):
for chunk in content.data:
fp.write(chunk)
......@@ -43,25 +45,29 @@ class MongoContentStore(ContentStore):
return content
def delete(self, id):
if self.fs.exists({"_id": id}):
self.fs.delete(id)
def delete(self, content_id):
if self.fs.exists({"_id": content_id}):
self.fs.delete(content_id)
def find(self, location, throw_on_not_found=True, as_stream=False):
id = StaticContent.get_id_from_location(location)
content_id = StaticContent.get_id_from_location(location)
try:
if as_stream:
fp = self.fs.get(id)
return StaticContentStream(location, fp.displayname, fp.content_type, fp, last_modified_at=fp.uploadDate,
thumbnail_location=fp.thumbnail_location if hasattr(fp, 'thumbnail_location') else None,
import_path=fp.import_path if hasattr(fp, 'import_path') else None,
length=fp.length)
fp = self.fs.get(content_id)
return StaticContentStream(
location, fp.displayname, fp.content_type, fp, last_modified_at=fp.uploadDate,
thumbnail_location=getattr(fp, 'thumbnail_location', None),
import_path=getattr(fp, 'import_path', None),
length=fp.length, locked=getattr(fp, 'locked', False)
)
else:
with self.fs.get(id) as fp:
return StaticContent(location, fp.displayname, fp.content_type, fp.read(), last_modified_at=fp.uploadDate,
thumbnail_location=fp.thumbnail_location if hasattr(fp, 'thumbnail_location') else None,
import_path=fp.import_path if hasattr(fp, 'import_path') else None,
length=fp.length)
with self.fs.get(content_id) as fp:
return StaticContent(
location, fp.displayname, fp.content_type, fp.read(), last_modified_at=fp.uploadDate,
thumbnail_location=getattr(fp, 'thumbnail_location', None),
import_path=getattr(fp, 'import_path', None),
length=fp.length, locked=getattr(fp, 'locked', False)
)
except NoFile:
if throw_on_not_found:
raise NotFoundError()
......@@ -69,9 +75,9 @@ class MongoContentStore(ContentStore):
return None
def get_stream(self, location):
id = StaticContent.get_id_from_location(location)
content_id = StaticContent.get_id_from_location(location)
try:
handle = self.fs.get(id)
handle = self.fs.get(content_id)
except NoFile:
raise NotFoundError()
......@@ -135,3 +141,61 @@ class MongoContentStore(ContentStore):
# 'borrow' the function 'location_to_query' from the Mongo modulestore implementation
items = self.fs_files.find(location_to_query(course_filter))
return list(items)
def set_attr(self, location, attr, value=True):
"""
Add/set the given attr on the asset at the given location. Does not allow overwriting gridFS built in
attrs such as _id, md5, uploadDate, length. Value can be any type which pymongo accepts.
Returns nothing
Raises NotFoundError if no such item exists
Raises AttributeError is attr is one of the build in attrs.
:param location: a c4x asset location
:param attr: which attribute to set
:param value: the value to set it to (any type pymongo accepts such as datetime, number, string)
"""
self.set_attrs(location, {attr: value})
def get_attr(self, location, attr, default=None):
"""
Get the value of attr set on location. If attr is unset, it returns default. Unlike set, this accessor
does allow getting the value of reserved keywords.
:param location: a c4x asset location
"""
return self.get_attrs(location).get(attr, default)
def set_attrs(self, location, attr_dict):
"""
Like set_attr but sets multiple key value pairs.
Returns nothing.
Raises NotFoundError if no such item exists
Raises AttributeError is attr_dict has any attrs which are one of the build in attrs.
:param location: a c4x asset location
"""
for attr in attr_dict.iterkeys():
if attr in ['_id', 'md5', 'uploadDate', 'length']:
raise AttributeError("{} is a protected attribute.".format(attr))
item = self.fs_files.find_one(location_to_query(location))
if item is None:
raise NotFoundError()
self.fs_files.update({"_id": item["_id"]}, {"$set": attr_dict})
def get_attrs(self, location):
"""
Gets all of the attributes associated with the given asset. Note, returns even built in attrs
such as md5 which you cannot resubmit in an update; so, don't call set_attrs with the result of this
but only with the set of attrs you want to explicitly update.
The attrs will be a superset of _id, contentType, chunkSize, filename, uploadDate, & md5
:param location: a c4x asset location
"""
item = self.fs_files.find_one(location_to_query(location))
if item is None:
raise NotFoundError()
return item
......@@ -365,7 +365,7 @@ section.problem {
li {
display:inline;
margin-left: 50px;
&:first-child {
margin-left: 0px;
}
......@@ -436,10 +436,25 @@ section.problem {
}
table {
margin-bottom: lh();
margin: lh() 0;
border-collapse: collapse;
table-layout: auto;
td, th {
&.cont-justified-left {
text-align: left !important; // nasty, but needed to override the bad specificity of the xmodule css selectors
}
&.cont-justified-right {
text-align: right !important; // nasty, but needed to override the bad specificity of the xmodule css selectors
}
&.cont-justified-center {
text-align: center !important; // nasty, but needed to override the bad specificity of the xmodule css selectorsstyles
}
}
th {
text-align: left;
font-weight: bold;
......@@ -780,7 +795,7 @@ section.problem {
.result-correct {
background: url('../images/correct-icon.png') left 20px no-repeat;
.result-actual-output {
color: #090;
}
......@@ -788,7 +803,7 @@ section.problem {
.result-incorrect {
background: url('../images/incorrect-icon.png') left 20px no-repeat;
.result-actual-output {
color: #B00;
}
......@@ -857,7 +872,7 @@ section.problem {
input[type=radio]:checked + label {
background: #666;
color: white;
color: white;
}
input[class='score-selection'] {
......
......@@ -322,7 +322,7 @@ div.combined-rubric-container {
div.written-feedback {
background: #f6f6f6;
padding: 15px;
padding: 5px;
}
}
......
// HTML component display:
// HTML component display:
* {
line-height: 1.4em;
}
......@@ -92,7 +92,7 @@ ul {
a {
&:link, &:visited, &:hover, &:active {
color: #1d9dd9;
}
}
}
img {
......@@ -116,20 +116,32 @@ code {
}
table {
width: 100%;
width: 100%;
margin: 20px 0;
border-collapse: collapse;
font-size: 16px;
}
th {
background: #eee;
font-weight: bold;
td, th {
margin: 20px 0;
padding: 10px;
border: 1px solid #ccc;
font-size: 14px;
&.cont-justified-left {
text-align: left !important; // nasty, but needed to override the bad specificity of the xmodule css selectors
}
&.cont-justified-right {
text-align: right !important; // nasty, but needed to override the bad specificity of the xmodule css selectors
}
&.cont-justified-center {
text-align: center !important; // nasty, but needed to override the bad specificity of the xmodule css selectorsstyles
}
}
}
table td, th {
margin: 20px 0;
padding: 10px;
border: 1px solid #ccc;
text-align: left;
font-size: 14px;
}
\ No newline at end of file
th {
background: #eee;
font-weight: bold;
}
......@@ -15,6 +15,13 @@ div.video {
@include clearfix;
}
div.focus_grabber {
position: relative;
display: inline;
width: 0px;
height: 0px;
}
article.video-wrapper {
float: left;
margin-right: flex-gutter(9);
......@@ -518,12 +525,19 @@ div.video {
margin-bottom: 8px;
padding: 0;
line-height: lh();
outline-width: 0px;
outline-style: none;
&.current {
color: #333;
font-weight: 700;
}
&.focused {
outline-width: 1px;
outline-style: dotted;
}
&:hover {
color: $blue;
}
......
......@@ -129,7 +129,7 @@ class ErrorDescriptor(ErrorFields, XModuleDescriptor):
@classmethod
def from_descriptor(cls, descriptor, error_msg='Error not available'):
return cls._construct(
descriptor.system,
descriptor.runtime,
str(descriptor),
error_msg,
location=descriptor.location,
......
<h2 class="problem-header">Problem Header</h2>
<h2 class="problem-header">${_("Problem Header")}</h2>
<section class='problem-progress'>
</section>
<section class="problem">
<p>Problem Content</p>
<p>${_("Problem Content")}</p>
<section class="action">
<input type="hidden" name="problem_id" value="1">
......@@ -13,11 +13,11 @@
<span id="display_example_1"></span>
<span id="input_example_1_dynamath"></span>
<input class="check" type="button" value="Check">
<input class="reset" type="button" value="Reset">
<input class="save" type="button" value="Save">
<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>
<input class="check" type="button" value="${_('Check')}">
<input class="reset" type="button" value="${_('Reset')}">
<input class="save" type="button" value="${_('Save')}">
<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>
<section class="submission_feedback"></section>
</section>
</section>
......@@ -13,6 +13,8 @@
data-yt-test-timeout="1500"
data-yt-test-url="https://gdata.youtube.com/feeds/api/videos/"
>
<div class="focus_grabber first"></div>
<div class="tc-wrapper">
<article class="video-wrapper">
<div class="video-player-pre"></div>
......@@ -51,6 +53,8 @@
<ol class="subtitles"><li></li></ol>
</div>
<div class="focus_grabber last"></div>
</div>
</div>
</div>
......
......@@ -16,6 +16,8 @@
data-yt-test-timeout="1500"
data-yt-test-url="https://gdata.youtube.com/feeds/api/videos/"
>
<div class="focus_grabber first"></div>
<div class="tc-wrapper">
<article class="video-wrapper">
<div class="video-player-pre"></div>
......@@ -54,6 +56,8 @@
<ol class="subtitles"><li></li></ol>
</div>
<div class="focus_grabber last"></div>
</div>
</div>
</div>
......
......@@ -16,6 +16,8 @@
data-yt-test-timeout="1500"
data-yt-test-url="https://gdata.youtube.com/feeds/api/videos/"
>
<div class="focus_grabber first"></div>
<div class="tc-wrapper">
<article class="video-wrapper">
<section class="video-player">
......@@ -26,6 +28,8 @@
<ol class="subtitles"><li></li></ol>
</div>
<div class="focus_grabber last"></div>
</div>
</div>
</div>
......
......@@ -13,6 +13,8 @@
data-yt-test-timeout="1500"
data-yt-test-url="https://gdata.youtube.com/feeds/api/videos/"
>
<div class="focus_grabber first"></div>
<div class="tc-wrapper">
<article class="video-wrapper">
<section class="video-player">
......@@ -21,7 +23,9 @@
<section class="video-controls"></section>
</article>
</div>
<div class="focus_grabber last"></div>
</div>
</div>
</div>
</div>
\ No newline at end of file
</div>
......@@ -13,6 +13,8 @@
data-yt-test-timeout="1500"
data-yt-test-url="https://gdata.youtube.com/feeds/api/videos/"
>
<div class="focus_grabber first"></div>
<div class="tc-wrapper">
<article class="video-wrapper">
<div class="video-player-pre"></div>
......@@ -51,6 +53,8 @@
<ol class="subtitles"><li></li></ol>
</div>
<div class="focus_grabber last"></div>
</div>
</div>
</div>
......
......@@ -48,17 +48,32 @@ describe 'CombinedOpenEnded', ->
expect(@combined.task_count).toEqual 2
expect(@combined.task_number).toEqual 1
it 'subelements are made collapsible', ->
it 'subelements are made collapsible', ->
expect(Collapsible.setCollapsibles).toHaveBeenCalled()
describe 'poll', ->
# We will store default window.setTimeout() function here.
oldSetTimeout = null
beforeEach =>
# setup the spies
@combined = new CombinedOpenEnded @element
spyOn(@combined, 'reload').andCallFake -> return 0
# Store original window.setTimeout() function. If we do not do this, then
# all other tests that rely on code which uses window.setTimeout()
# function might (and probably will) fail.
oldSetTimeout = window.setTimeout
# Redefine window.setTimeout() function as a spy.
window.setTimeout = jasmine.createSpy().andCallFake (callback, timeout) -> return 5
afterEach =>
# Reset the default window.setTimeout() function. If we do not do this,
# then all other tests that rely on code which uses window.setTimeout()
# function might (and probably will) fail.
window.setTimeout = oldSetTimeout
it 'polls at the correct intervals', =>
fakeResponseContinue = state: 'not done'
spyOn($, 'postWithPrefix').andCallFake (url, callback) -> callback(fakeResponseContinue)
......@@ -67,19 +82,34 @@ describe 'CombinedOpenEnded', ->
expect(window.queuePollerID).toBe(5)
it 'polling stops properly', =>
fakeResponseDone = state: "done"
fakeResponseDone = state: "done"
spyOn($, 'postWithPrefix').andCallFake (url, callback) -> callback(fakeResponseDone)
@combined.poll()
expect(window.queuePollerID).toBeUndefined()
expect(window.setTimeout).not.toHaveBeenCalled()
describe 'rebind', ->
# We will store default window.setTimeout() function here.
oldSetTimeout = null
beforeEach ->
@combined = new CombinedOpenEnded @element
spyOn(@combined, 'queueing').andCallFake -> return 0
spyOn(@combined, 'skip_post_assessment').andCallFake -> return 0
# Store original window.setTimeout() function. If we do not do this, then
# all other tests that rely on code which uses window.setTimeout()
# function might (and probably will) fail.
oldSetTimeout = window.setTimeout
# Redefine window.setTimeout() function as a spy.
window.setTimeout = jasmine.createSpy().andCallFake (callback, timeout) -> return 5
afterEach =>
# Reset the default window.setTimeout() function. If we do not do this,
# then all other tests that rely on code which uses window.setTimeout()
# function might (and probably will) fail.
window.setTimeout = oldSetTimeout
it 'when our child is in an assessing state', ->
@combined.child_state = 'assessing'
@combined.rebind()
......@@ -87,19 +117,19 @@ describe 'CombinedOpenEnded', ->
expect(@combined.submit_button.val()).toBe("Submit assessment")
expect(@combined.queueing).toHaveBeenCalled()
it 'when our child state is initial', ->
it 'when our child state is initial', ->
@combined.child_state = 'initial'
@combined.rebind()
expect(@combined.answer_area.attr("disabled")).toBeUndefined()
expect(@combined.submit_button.val()).toBe("Submit")
it 'when our child state is post_assessment', ->
it 'when our child state is post_assessment', ->
@combined.child_state = 'post_assessment'
@combined.rebind()
expect(@combined.answer_area.attr("disabled")).toBe("disabled")
expect(@combined.submit_button.val()).toBe("Submit post-assessment")
it 'when our child state is done', ->
it 'when our child state is done', ->
spyOn(@combined, 'next_problem').andCallFake ->
@combined.child_state = 'done'
@combined.rebind()
......@@ -112,7 +142,7 @@ describe 'CombinedOpenEnded', ->
@combined.child_state = 'done'
it 'handling a successful call', ->
fakeResponse =
fakeResponse =
success: true
html: "dummy html"
allow_reset: false
......
......@@ -93,6 +93,7 @@
$('.subtitles li[data-index]').each(function(index, link) {
expect($(link)).toHaveData('index', index);
expect($(link)).toHaveData('start', captionsData.start[index]);
expect($(link)).toHaveAttr('tabindex', 0);
expect($(link)).toHaveText(captionsData.text[index]);
});
});
......@@ -104,7 +105,13 @@
it('bind all the caption link', function() {
$('.subtitles li[data-index]').each(function(index, link) {
expect($(link)).toHandleWith('click', videoCaption.seekPlayer);
expect($(link)).toHandleWith('mouseover', videoCaption.captionMouseOverOut);
expect($(link)).toHandleWith('mouseout', videoCaption.captionMouseOverOut);
expect($(link)).toHandleWith('mousedown', videoCaption.captionMouseDown);
expect($(link)).toHandleWith('click', videoCaption.captionClick);
expect($(link)).toHandleWith('focus', videoCaption.captionFocus);
expect($(link)).toHandleWith('blur', videoCaption.captionBlur);
expect($(link)).toHandleWith('keydown', videoCaption.captionKeyDown);
});
});
......@@ -146,12 +153,27 @@
});
describe('mouse movement', function() {
// We will store default window.setTimeout() function here.
var oldSetTimeout = null;
beforeEach(function() {
// Store original window.setTimeout() function. If we do not do this, then
// all other tests that rely on code which uses window.setTimeout()
// function might (and probably will) fail.
oldSetTimeout = window.setTimeout;
// Redefine window.setTimeout() function as a spy.
window.setTimeout = jasmine.createSpy().andCallFake(function(callback, timeout) { return 5; })
window.setTimeout.andReturn(100);
spyOn(window, 'clearTimeout');
});
afterEach(function () {
// Reset the default window.setTimeout() function. If we do not do this,
// then all other tests that rely on code which uses window.setTimeout()
// function might (and probably will) fail.
window.setTimeout = oldSetTimeout;
});
describe('when cursor is outside of the caption box', function() {
beforeEach(function() {
$(window).trigger(jQuery.Event('mousemove'));
......@@ -263,6 +285,7 @@
$('.subtitles li[data-index]').each(function(index, link) {
expect($(link)).toHaveData('index', index);
expect($(link)).toHaveData('start', captionsData.start[index]);
expect($(link)).toHaveAttr('tabindex', 0);
expect($(link)).toHaveText(captionsData.text[index]);
});
});
......@@ -274,7 +297,13 @@
it('bind all the caption link', function() {
$('.subtitles li[data-index]').each(function(index, link) {
expect($(link)).toHandleWith('click', videoCaption.seekPlayer);
expect($(link)).toHandleWith('mouseover', videoCaption.captionMouseOverOut);
expect($(link)).toHandleWith('mouseout', videoCaption.captionMouseOverOut);
expect($(link)).toHandleWith('mousedown', videoCaption.captionMouseDown);
expect($(link)).toHandleWith('click', videoCaption.captionClick);
expect($(link)).toHandleWith('focus', videoCaption.captionFocus);
expect($(link)).toHandleWith('blur', videoCaption.captionBlur);
expect($(link)).toHandleWith('keydown', videoCaption.captionKeyDown);
});
});
......@@ -543,6 +572,102 @@
});
});
});
describe('caption accessibility', function() {
beforeEach(function() {
initialize();
});
describe('when getting focus through TAB key', function() {
beforeEach(function() {
videoCaption.isMouseFocus = false;
$('.subtitles li[data-index=0]').trigger(jQuery.Event('focus'));
});
it('shows an outline around the caption', function() {
expect($('.subtitles li[data-index=0]')).toHaveClass('focused');
});
it('has automatic scrolling disabled', function() {
expect(videoCaption.autoScrolling).toBe(false);
});
});
describe('when loosing focus through TAB key', function() {
beforeEach(function() {
$('.subtitles li[data-index=0]').trigger(jQuery.Event('blur'));
});
it('does not show an outline around the caption', function() {
expect($('.subtitles li[data-index=0]')).not.toHaveClass('focused');
});
it('has automatic scrolling enabled', function() {
expect(videoCaption.autoScrolling).toBe(true);
});
});
describe('when same caption gets the focus through mouse after having focus through TAB key', function() {
beforeEach(function() {
videoCaption.isMouseFocus = false;
$('.subtitles li[data-index=0]').trigger(jQuery.Event('focus'));
$('.subtitles li[data-index=0]').trigger(jQuery.Event('mousedown'));
});
it('does not show an outline around it', function() {
expect($('.subtitles li[data-index=0]')).not.toHaveClass('focused');
});
it('has automatic scrolling enabled', function() {
expect(videoCaption.autoScrolling).toBe(true);
});
});
describe('when a second caption gets focus through mouse after first had focus through TAB key', function() {
beforeEach(function() {
videoCaption.isMouseFocus = false;
$('.subtitles li[data-index=0]').trigger(jQuery.Event('focus'));
$('.subtitles li[data-index=0]').trigger(jQuery.Event('blur'));
videoCaption.isMouseFocus = true;
$('.subtitles li[data-index=1]').trigger(jQuery.Event('mousedown'));
});
it('does not show an outline around the first', function() {
expect($('.subtitles li[data-index=0]')).not.toHaveClass('focused');
});
it('does not show an outline around the second', function() {
expect($('.subtitles li[data-index=1]')).not.toHaveClass('focused');
});
it('has automatic scrolling enabled', function() {
expect(videoCaption.autoScrolling).toBe(true);
});
});
describe('when enter key is pressed on a caption', function() {
beforeEach(function() {
var e;
spyOn(videoCaption, 'seekPlayer').andCallThrough();
videoCaption.isMouseFocus = false;
$('.subtitles li[data-index=0]').trigger(jQuery.Event('focus'));
e = jQuery.Event('keydown');
e.which = 13; // ENTER key
$('.subtitles li[data-index=0]').trigger(e);
});
// Temporarily disabled due to intermittent failures
// Fails with error: "InvalidStateError: InvalidStateError: An attempt
// was made to use an object that is not, or is no longer, usable"
xit('shows an outline around it', function() {
expect($('.subtitles li[data-index=0]')).toHaveClass('focused');
});
it('calls seekPlayer', function() {
expect(videoCaption.seekPlayer).toHaveBeenCalled();
});
});
});
});
}).call(this);
(function () {
describe('Video FocusGrabber', function () {
var state;
beforeEach(function () {
// https://github.com/pivotal/jasmine/issues/184
//
// This is a known issue. jQuery animations depend on setTimeout
// and the jasmine mock clock stubs that function. You need to turn
// off jQuery animations ($.fx.off()) in a global beforeEach.
//
// I think this is a good pattern - you don't want animations
// messing with your tests. If you need to test with animations on
// I suggest you add incremental browser-based testing to your
// stack.
jQuery.fx.off = true;
loadFixtures('video_html5.html');
state = new Video('#example');
spyOnEvent(state.el, 'mousemove');
spyOn(state.focusGrabber, 'disableFocusGrabber').andCallThrough();
spyOn(state.focusGrabber, 'enableFocusGrabber').andCallThrough();
});
afterEach(function () {
// Turn jQuery animations back on.
jQuery.fx.off = true;
});
it(
'check existence of focus grabber elements and their position',
function () {
var firstFGEl = state.el.find('.focus_grabber.first'),
lastFGEl = state.el.find('.focus_grabber.last'),
tcWrapperEl = state.el.find('.tc-wrapper');
// Existence check.
expect(firstFGEl.length).toBe(1);
expect(lastFGEl.length).toBe(1);
// Position check.
expect(firstFGEl.index() + 1).toBe(tcWrapperEl.index());
expect(lastFGEl.index() - 1).toBe(tcWrapperEl.index());
});
it('from the start, focus grabbers are disabled', function () {
expect(state.focusGrabber.elFirst.attr('tabindex')).toBe(-1);
expect(state.focusGrabber.elLast.attr('tabindex')).toBe(-1);
});
it(
'when first focus grabber is focused "mousemove" event is ' +
'triggered, grabbers are disabled',
function () {
state.focusGrabber.elFirst.triggerHandler('focus');
expect('mousemove').toHaveBeenTriggeredOn(state.el);
expect(state.focusGrabber.disableFocusGrabber).toHaveBeenCalled();
});
it(
'when last focus grabber is focused "mousemove" event is ' +
'triggered, grabbers are disabled',
function () {
state.focusGrabber.elLast.triggerHandler('focus');
expect('mousemove').toHaveBeenTriggeredOn(state.el);
expect(state.focusGrabber.disableFocusGrabber).toHaveBeenCalled();
});
it('after controls hide focus grabbers are enabled', function () {
runs(function () {
// Captions should not be "sticky" for the autohide mechanism
// to work.
state.videoCaption.hideCaptions(true);
// Make sure that the controls are visible. After this event
// is triggered a count down is started to autohide captions.
state.el.triggerHandler('mousemove');
});
// Wait for the autohide to happen. We make it +100ms to make sure
// that there is clearly no race conditions for our expect below.
waits(state.videoControl.fadeOutTimeout + 100);
runs(function () {
expect(
state.focusGrabber.enableFocusGrabber
).toHaveBeenCalled();
});
});
});
}).call(this);
......@@ -145,7 +145,18 @@
});
describe('onStop', function() {
// We will store default window.setTimeout() function here.
var oldSetTimeout = null;
beforeEach(function() {
// Store original window.setTimeout() function. If we do not do this, then
// all other tests that rely on code which uses window.setTimeout()
// function might (and probably will) fail.
oldSetTimeout = window.setTimeout;
// Redefine window.setTimeout() function as a spy.
window.setTimeout = jasmine.createSpy().andCallFake(function(callback, timeout) { return 5; })
window.setTimeout.andReturn(100);
initialize();
spyOn(videoPlayer, 'onSlideSeek').andCallThrough();
videoProgressSlider.onStop({}, {
......@@ -153,6 +164,13 @@
});
});
afterEach(function () {
// Reset the default window.setTimeout() function. If we do not do this,
// then all other tests that rely on code which uses window.setTimeout()
// function might (and probably will) fail.
window.setTimeout = oldSetTimeout;
});
it('freeze the slider', function() {
expect(videoProgressSlider.frozen).toBeTruthy();
});
......@@ -162,7 +180,9 @@
expect(videoPlayer.currentTime).toEqual(20);
});
it('set timeout to unfreeze the slider', function() {
// Temporarily disabled due to intermittent failures
// Fails with error: " Expected true to be falsy."
xit('set timeout to unfreeze the slider', function() {
expect(window.setTimeout).toHaveBeenCalledWith(jasmine.any(Function), 200);
window.setTimeout.mostRecentCall.args[0]();
expect(videoProgressSlider.frozen).toBeFalsy();
......
......@@ -119,6 +119,7 @@ class @CombinedOpenEnded
next_rubric_sel: '.rubric-next-button'
previous_rubric_sel: '.rubric-previous-button'
oe_alert_sel: '.open-ended-alert'
save_button_sel: '.save-button'
constructor: (el) ->
@el=el
......@@ -183,6 +184,7 @@ class @CombinedOpenEnded
@hint_wrapper = @$(@oe).find(@hint_wrapper_sel)
@message_wrapper = @$(@oe).find(@message_wrapper_sel)
@submit_button = @$(@oe).find(@submit_button_sel)
@save_button = @$(@oe).find(@save_button_sel)
@child_state = @oe.data('state')
@child_type = @oe.data('child-type')
if @child_type=="openended"
......@@ -273,6 +275,8 @@ class @CombinedOpenEnded
# rebind to the appropriate function for the current state
@submit_button.unbind('click')
@submit_button.show()
@save_button.unbind('click')
@save_button.hide()
@reset_button.hide()
@hide_file_upload()
@next_problem_button.hide()
......@@ -299,6 +303,8 @@ class @CombinedOpenEnded
@submit_button.prop('value', 'Submit')
@submit_button.click @confirm_save_answer
@setup_file_upload()
@save_button.click @store_answer
@save_button.show()
else if @child_state == 'assessing'
@answer_area.attr("disabled", true)
@replace_text_inputs()
......@@ -338,13 +344,26 @@ class @CombinedOpenEnded
else
@reset_button.show()
find_assessment_elements: ->
@assessment = @$('input[name="grade-selection"]')
find_hint_elements: ->
@hint_area = @$('textarea.post_assessment')
store_answer: (event) =>
event.preventDefault()
if @child_state == 'initial'
data = {'student_answer' : @answer_area.val()}
@save_button.attr("disabled",true)
$.postWithPrefix "#{@ajax_url}/store_answer", data, (response) =>
if response.success
@gentle_alert("Answer saved.")
else
@errors_area.html(response.error)
@save_button.attr("disabled",false)
else
@errors_area.html(@out_of_sync_message)
replace_answer: (response) =>
if response.success
@rubric_wrapper.html(response.rubric_html)
......@@ -364,6 +383,7 @@ class @CombinedOpenEnded
@save_answer(event) if confirm('Please confirm that you wish to submit your work. You will not be able to make any changes after submitting.')
save_answer: (event) =>
@$el.find(@oe_alert_sel).remove()
@submit_button.attr("disabled",true)
@submit_button.hide()
event.preventDefault()
......
/*
* 025_focus_grabber.js
*
* Purpose: Provide a way to focus on autohidden Video controls.
*
*
* Because in HTML player mode we have a feature of autohiding controls on
* mouse inactivity, sometimes focus is lost from the currently selected
* control. What's more, when all controls are autohidden, we can't get to any
* of them because by default browser does not place hidden elements on the
* focus chain.
*
* To get around this minor annoyance, this module will manage 2 placeholder
* elements that will be invisible to the user's eye, but visible to the
* browser. This will allow for a sneaky stealing of focus and placing it where
* we need (on hidden controls).
*
* This code has been moved to a separate module because it provides a concrete
* block of functionality that can be turned on (off).
*/
/*
* "If you want to climb a mountain, begin at the top."
*
* ~ Zen saying
*/
(function (requirejs, require, define) {
// FocusGrabber module.
define(
'video/025_focus_grabber.js',
[],
function () {
return function (state) {
state.focusGrabber = {};
_makeFunctionsPublic(state);
_renderElements(state);
_bindHandlers(state);
};
// Private functions.
function _makeFunctionsPublic(state) {
state.focusGrabber.enableFocusGrabber = _.bind(enableFocusGrabber, state);
state.focusGrabber.disableFocusGrabber = _.bind(disableFocusGrabber, state);
state.focusGrabber.onFocus = _.bind(onFocus, state);
}
function _renderElements(state) {
state.focusGrabber.elFirst = state.el.find('.focus_grabber.first');
state.focusGrabber.elLast = state.el.find('.focus_grabber.last');
// From the start, the Focus Grabber must be disabled so that
// tabbing (switching focus) does not land the user on one of the
// placeholder elements (elFirst, elLast).
state.focusGrabber.disableFocusGrabber();
}
function _bindHandlers(state) {
state.focusGrabber.elFirst.on('focus', state.focusGrabber.onFocus);
state.focusGrabber.elLast.on('focus', state.focusGrabber.onFocus);
// When the video container element receives programmatic focus, then
// on un-focus ('blur' event) we should trigger a 'mousemove' event so
// as to reveal autohidden controls.
state.el.on('blur', function () {
state.el.trigger('mousemove');
});
}
// Public functions.
function enableFocusGrabber() {
var tabIndex;
// When the Focus Grabber is being enabled, there are two different
// scenarios:
//
// 1.) Currently focused element was inside the video player.
// 2.) Currently focused element was somewhere else on the page.
//
// In the first case we must make sure that the video player doesn't
// loose focus, even though the controls are autohidden.
if ($(document.activeElement).parents().hasClass('video')) {
tabIndex = -1;
} else {
tabIndex = 0;
}
this.focusGrabber.elFirst.attr('tabindex', tabIndex);
this.focusGrabber.elLast.attr('tabindex', tabIndex);
// Don't loose focus. We are inside video player on some control, but
// because we can't remain focused on a hidden element, we will shift
// focus to the main video element.
//
// Once the main element will receive the un-focus ('blur') event, a
// 'mousemove' event will be triggered, and the video controls will
// receive focus once again.
if (tabIndex === -1) {
this.el.focus();
this.focusGrabber.elFirst.attr('tabindex', 0);
this.focusGrabber.elLast.attr('tabindex', 0);
}
}
function disableFocusGrabber() {
// Only programmatic focusing on these elements will be available.
// We don't want the user to focus on them (for example with the 'Tab'
// key).
this.focusGrabber.elFirst.attr('tabindex', -1);
this.focusGrabber.elLast.attr('tabindex', -1);
}
function onFocus(event, params) {
// Once the Focus Grabber placeholder elements will gain focus, we will
// trigger 'mousemove' event so that the autohidden controls will
// become visible.
this.el.trigger('mousemove');
this.focusGrabber.disableFocusGrabber();
}
});
}(RequireJS.requirejs, RequireJS.require, RequireJS.define));
......@@ -420,12 +420,15 @@ function (HTML5Video) {
this.videoPlayer.player.setPlaybackRate(this.speed);
}
/* The following has been commented out to make sure autoplay is
disabled for students.
if (
!onTouchBasedDevice() &&
$('.video:first').data('autoplay') === 'True'
) {
this.videoPlayer.play();
}
*/
}
function onStateChange(event) {
......
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