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> ...@@ -86,5 +86,6 @@ Yarko Tymciurak <yarkot1@gmail.com>
Miles Steele <miles@milessteele.com> Miles Steele <miles@milessteele.com>
Kevin Luo <kevluo@edx.org> Kevin Luo <kevluo@edx.org>
Akshay Jagadeesh <akjags@gmail.com> Akshay Jagadeesh <akjags@gmail.com>
Nick Parlante <nick.parlante@cs.stanford.edu>
Marko Seric <marko.seric@math.uzh.ch> 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, ...@@ -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 in roughly chronological order, most recent first. Add your entries at or near
the top. Include a label indicating the component affected. 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. 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 The key is due_date_display_format, and the value should be a format supported by Python's
strftime function. strftime function.
......
...@@ -6,3 +6,8 @@ gem 'neat', '~> 1.3.0' ...@@ -6,3 +6,8 @@ gem 'neat', '~> 1.3.0'
gem 'colorize', '~> 0.5.8' gem 'colorize', '~> 0.5.8'
gem 'launchy', '~> 2.1.2' gem 'launchy', '~> 2.1.2'
gem 'sys-proctable', '~> 0.9.3' 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| ...@@ -22,12 +22,13 @@ Vagrant.configure("2") do |config|
config.vm.provider :virtualbox do |vb| config.vm.provider :virtualbox do |vb|
# Use VBoxManage to customize the VM. For example to change memory: # 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 # This setting makes it so that network access from inside the vagrant guest
# is able to resolve DNS using the hosts VPN connection. # is able to resolve DNS using the hosts VPN connection.
vb.customize ["modifyvm", :id, "--natdnshostresolver1", "on"] vb.customize ["modifyvm", :id, "--natdnshostresolver1", "on"]
end end
config.vm.provision :shell, :path => "scripts/install-acceptance-req.sh"
config.vm.provision :shell, :path => "scripts/vagrant-provisioning.sh" config.vm.provision :shell, :path => "scripts/vagrant-provisioning.sh"
end end
...@@ -5,16 +5,16 @@ Feature: Static Pages ...@@ -5,16 +5,16 @@ Feature: Static Pages
Given I have opened a new course in Studio Given I have opened a new course in Studio
And I go to the static pages page And I go to the static pages page
When I add a new 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 Scenario: Users can delete static pages
Given I have opened a new course in Studio Given I have opened a new course in Studio
And I go to the static pages page And I go to the static pages page
And I add a new 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 Then I am shown a prompt
When I confirm the 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 # Safari won't update the name properly
@skip_safari @skip_safari
...@@ -22,6 +22,6 @@ Feature: Static Pages ...@@ -22,6 +22,6 @@ Feature: Static Pages
Given I have opened a new course in Studio Given I have opened a new course in Studio
And I go to the static pages page And I go to the static pages page
And I add a new 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" 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 @@ ...@@ -2,10 +2,10 @@
#pylint: disable=W0621 #pylint: disable=W0621
from lettuce import world, step 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): def go_to_static(step):
menu_css = 'li.nav-course-courseware' menu_css = 'li.nav-course-courseware'
static_css = 'li.nav-course-courseware-pages a' static_css = 'li.nav-course-courseware-pages a'
...@@ -13,42 +13,29 @@ def go_to_static(step): ...@@ -13,42 +13,29 @@ def go_to_static(step):
world.css_click(static_css) world.css_click(static_css)
@step(u'I add a new page') @step(u'I add a new page$')
def add_page(step): def add_page(step):
button_css = 'a.new-button' button_css = 'a.new-button'
world.css_click(button_css) world.css_click(button_css)
@step(u'I should not see a "([^"]*)" static page$') @step(u'I should see a static page named "([^"]*)"$')
def not_see_page(step, page): def see_a_static_page_named_foo(step, name):
# Either there are no pages, or there are pages but pages_css = 'section.xmodule_StaticTabModule'
# not the one I expect not to exist. 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 @step(u'I "(edit|delete)" the static page$')
# will sometimes exist before the HTML content is loaded def click_edit_or_delete(step, edit_or_delete):
exists_func = lambda(driver): page_exists(page) button_css = 'div.component-actions a.%s-button' % edit_or_delete
world.css_click(button_css)
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 change the name to "([^"]*)"$') @step(u'I change the name to "([^"]*)"$')
...@@ -61,16 +48,3 @@ def change_name(step, new_name): ...@@ -61,16 +48,3 @@ def change_name(step, new_name):
world.trigger_event(input_css) world.trigger_event(input_css)
save_button = 'a.save-button' save_button = 'a.save-button'
world.css_click(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 @@ ...@@ -2,7 +2,10 @@
Unit tests for the asset upload endpoint. Unit tests for the asset upload endpoint.
""" """
import json #pylint: disable=C0111
#pylint: disable=W0621
#pylint: disable=W0212
from datetime import datetime from datetime import datetime
from io import BytesIO from io import BytesIO
from pytz import UTC from pytz import UTC
...@@ -12,7 +15,9 @@ from django.core.urlresolvers import reverse ...@@ -12,7 +15,9 @@ from django.core.urlresolvers import reverse
from contentstore.views import assets from contentstore.views import assets
from xmodule.contentstore.content import StaticContent from xmodule.contentstore.content import StaticContent
from xmodule.modulestore import Location 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): class AssetsTestCase(CourseTestCase):
def setUp(self): def setUp(self):
...@@ -27,22 +32,27 @@ class AssetsTestCase(CourseTestCase): ...@@ -27,22 +32,27 @@ class AssetsTestCase(CourseTestCase):
resp = self.client.get(self.url) resp = self.client.get(self.url)
self.assertEquals(resp.status_code, 200) 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): def test_static_url_generation(self):
location = Location(['i4x', 'foo', 'bar', 'asset', 'my_file_name.jpg']) location = Location(['i4x', 'foo', 'bar', 'asset', 'my_file_name.jpg'])
path = StaticContent.get_static_path_from_location(location) path = StaticContent.get_static_path_from_location(location)
self.assertEquals(path, '/static/my_file_name.jpg') 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): class UploadTestCase(CourseTestCase):
""" """
Unit tests for uploading a file Unit tests for uploading a file
...@@ -71,32 +81,25 @@ class UploadTestCase(CourseTestCase): ...@@ -71,32 +81,25 @@ class UploadTestCase(CourseTestCase):
self.assertEquals(resp.status_code, 405) 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. we can send out to the client via JSON.
""" """
def test_basic(self): def test_basic(self):
upload_date = datetime(2013, 6, 1, 10, 30, tzinfo=UTC) upload_date = datetime(2013, 6, 1, 10, 30, tzinfo=UTC)
asset = {
"displayname": "foo", location = Location(['i4x', 'foo', 'bar', 'asset', 'my_file_name.jpg'])
"chunkSize": 512, thumbnail_location = Location(['i4x', 'foo', 'bar', 'asset', 'my_file_name_thumb.jpg'])
"filename": "foo.png",
"length": 100, output = assets._get_asset_json("my_file", upload_date, location, thumbnail_location)
"uploadDate": upload_date,
"_id": { self.assertEquals(output["display_name"], "my_file")
"course": "course", self.assertEquals(output["date_added"], "Jun 01, 2013 at 10:30 UTC")
"org": "org", self.assertEquals(output["url"], "/i4x/foo/bar/asset/my_file_name.jpg")
"revision": 12, self.assertEquals(output["portable_url"], "/static/my_file_name.jpg")
"category": "category", self.assertEquals(output["thumbnail"], "/i4x/foo/bar/asset/my_file_name_thumb.jpg")
"name": "name", self.assertEquals(output["id"], output["url"])
"tag": "tag",
} output = assets._get_asset_json("name", upload_date, location, None)
} self.assertIsNone(output["thumbnail"])
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")
...@@ -593,9 +593,9 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -593,9 +593,9 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
# go through the website to do the delete, since the soft-delete logic is in the view # 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'}) url = reverse('update_asset', kwargs={'org': 'edX', 'course': 'toy', 'name': '2012_Fall', 'asset_id': '/c4x/edX/toy/asset/sample_static.txt'})
resp = self.client.post(url, {'location': '/c4x/edX/toy/asset/sample_static.txt'}) resp = self.client.delete(url)
self.assertEqual(resp.status_code, 200) self.assertEqual(resp.status_code, 204)
asset_location = StaticContent.get_location_from_path('/c4x/edX/toy/asset/sample_static.txt') asset_location = StaticContent.get_location_from_path('/c4x/edX/toy/asset/sample_static.txt')
...@@ -628,7 +628,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -628,7 +628,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
def test_empty_trashcan(self): 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() content_store = contentstore()
trash_store = contentstore('trashcan') trash_store = contentstore('trashcan')
...@@ -644,9 +644,9 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -644,9 +644,9 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
# go through the website to do the delete, since the soft-delete logic is in the view # 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'}) url = reverse('update_asset', kwargs={'org': 'edX', 'course': 'toy', 'name': '2012_Fall', 'asset_id': '/c4x/edX/toy/asset/sample_static.txt'})
resp = self.client.post(url, {'location': '/c4x/edX/toy/asset/sample_static.txt'}) resp = self.client.delete(url)
self.assertEqual(resp.status_code, 200) self.assertEqual(resp.status_code, 204)
# make sure there's something in the trashcan # make sure there's something in the trashcan
all_assets = trash_store.get_all_content_for_course(course_location) all_assets = trash_store.get_all_content_for_course(course_location)
...@@ -907,7 +907,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -907,7 +907,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
draft_store = modulestore('draft') draft_store = modulestore('draft')
content_store = contentstore() 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') location = CourseDescriptor.id_to_location('edX/toy/2012_Fall')
# get a vertical (and components in it) to copy into an orphan sub dag # get a vertical (and components in it) to copy into an orphan sub dag
...@@ -986,7 +986,9 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -986,7 +986,9 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
delete_course(module_store, content_store, location, commit=True) delete_course(module_store, content_store, location, commit=True)
# reimport # 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])) items = module_store.get_items(Location(['i4x', 'edX', 'toy', 'vertical', None]))
self.assertGreater(len(items), 0) self.assertGreater(len(items), 0)
......
import logging import logging
import json
import os
import tarfile
import shutil
import cgi
import re
from functools import partial from functools import partial
from tempfile import mkdtemp
from path import path
from django.conf import settings from django.http import HttpResponseBadRequest
from django.http import HttpResponse, HttpResponseBadRequest
from django.contrib.auth.decorators import login_required 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_future.csrf import ensure_csrf_cookie
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.core.servers.basehttp import FileWrapper from django.views.decorators.http import require_POST
from django.core.files.temp import NamedTemporaryFile
from django.views.decorators.http import require_POST, require_http_methods
from mitxmako.shortcuts import render_to_response from mitxmako.shortcuts import render_to_response
from cache_toolbox.core import del_cached_content 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.contentstore.django import contentstore
from xmodule.modulestore.xml_exporter import export_to_xml
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.contentstore.content import StaticContent from xmodule.contentstore.content import StaticContent
from xmodule.util.date_utils import get_default_time_display from xmodule.util.date_utils import get_default_time_display
from xmodule.modulestore import InvalidLocationError 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 .access import get_location_and_verify_access
from util.json_request import JsonResponse from util.json_request import JsonResponse
import json
from django.utils.translation import ugettext as _
__all__ = ['asset_index', 'upload_asset'] __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 @login_required
@ensure_csrf_cookie @ensure_csrf_cookie
...@@ -96,32 +52,21 @@ def asset_index(request, org, course, name): ...@@ -96,32 +52,21 @@ def asset_index(request, org, course, name):
# sort in reverse upload date order # sort in reverse upload date order
assets = sorted(assets, key=lambda asset: asset['uploadDate'], reverse=True) assets = sorted(assets, key=lambda asset: asset['uploadDate'], reverse=True)
if request.META.get('HTTP_ACCEPT', "").startswith("application/json"): asset_json = []
return JsonResponse(assets_to_json_dict(assets))
asset_display = []
for asset in assets: for asset in assets:
asset_id = asset['_id'] 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']) 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 # 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 = asset.get('thumbnail_location', None)
thumbnail_location = Location(_thumbnail_location) if _thumbnail_location is not None else 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', { return render_to_response('asset_index.html', {
'context_course': course_module, 'context_course': course_module,
'assets': asset_display, 'asset_list': json.dumps(asset_json),
'upload_asset_callback_url': upload_asset_callback_url, '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, 'org': org,
'course': course, 'course': course,
'name': name 'name': name
...@@ -171,9 +116,6 @@ def upload_asset(request, org, course, coursename): ...@@ -171,9 +116,6 @@ def upload_asset(request, org, course, coursename):
content = sc_partial(upload_file.read()) content = sc_partial(upload_file.read())
tempfile_path = None tempfile_path = None
thumbnail_content = None
thumbnail_location = None
# first let's see if a thumbnail can be created # first let's see if a thumbnail can be created
(thumbnail_content, thumbnail_location) = contentstore().generate_thumbnail( (thumbnail_content, thumbnail_location) = contentstore().generate_thumbnail(
content, content,
...@@ -195,46 +137,38 @@ def upload_asset(request, org, course, coursename): ...@@ -195,46 +137,38 @@ def upload_asset(request, org, course, coursename):
readback = contentstore().find(content.location) readback = contentstore().find(content.location)
response_payload = { response_payload = {
'displayname': content.name, 'asset': _get_asset_json(content.name, readback.last_modified_at, content.location, content.thumbnail_location),
'uploadDate': get_default_time_display(readback.last_modified_at), 'msg': _('Upload completed')
'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'
} }
response = JsonResponse(response_payload) return JsonResponse(response_payload)
return response
@ensure_csrf_cookie @require_http_methods(("DELETE",))
@login_required @login_required
def remove_asset(request, org, course, name): @ensure_csrf_cookie
''' def update_asset(request, org, course, name, asset_id):
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 restful CRUD operations for a course asset.
''' Currently only the DELETE method is implemented.
get_location_and_verify_access(request, org, course, name)
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 # make sure the location is valid
try: try:
loc = StaticContent.get_location_from_path(location) loc = StaticContent.get_location_from_path(asset_id)
except InvalidLocationError: except InvalidLocationError as err:
# return a 'Bad Request' to browser as we have a malformed Location # return a 'Bad Request' to browser as we have a malformed Location
response = HttpResponse() return JsonResponse({"error": err.message}, status=400)
response.status_code = 400
return response
# also make sure the item to delete actually exists # also make sure the item to delete actually exists
try: try:
content = contentstore().find(loc) content = contentstore().find(loc)
except NotFoundError: except NotFoundError:
response = HttpResponse() return JsonResponse(status=404)
response.status_code = 404
return response
# ok, save the content into the trashcan # ok, save the content into the trashcan
contentstore('trashcan').save(content) contentstore('trashcan').save(content)
...@@ -249,13 +183,26 @@ def remove_asset(request, org, course, name): ...@@ -249,13 +183,26 @@ def remove_asset(request, org, course, name):
# remove from any caching # remove from any caching
del_cached_content(thumbnail_content.location) del_cached_content(thumbnail_content.location)
except: except:
pass # OK if this is left dangling logging.warning('Could not delete thumbnail: ' + content.thumbnail_location)
# delete the original # delete the original
contentstore().delete(content.get_id()) contentstore().delete(content.get_id())
# remove from cache # remove from cache
del_cached_content(content.location) del_cached_content(content.location)
return JsonResponse()
return HttpResponse()
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): ...@@ -107,13 +107,13 @@ def preview_module_system(request, preview_id, descriptor):
ajax_url=reverse('preview_dispatch', args=[preview_id, descriptor.location.url(), '']).rstrip('/'), 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? # TODO (cpennington): Do we want to track how instructors are using the preview problems?
track_function=lambda event_type, event: None, 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), get_module=partial(load_preview_module, request, preview_id),
render_template=render_from_lms, render_template=render_from_lms,
debug=True, debug=True,
replace_urls=partial(static_replace.replace_static_urls, data_directory=None, course_id=course_id), replace_urls=partial(static_replace.replace_static_urls, data_directory=None, course_id=course_id),
user=request.user, 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)), can_execute_unsafe_code=(lambda: can_execute_unsafe_code(course_id)),
mixins=settings.XBLOCK_MIXINS, mixins=settings.XBLOCK_MIXINS,
course_id=course_id, course_id=course_id,
......
...@@ -84,12 +84,26 @@ USE_I18N = True ...@@ -84,12 +84,26 @@ USE_I18N = True
# Include the lettuce app for acceptance testing, including the 'harvest' django-admin command # Include the lettuce app for acceptance testing, including the 'harvest' django-admin command
INSTALLED_APPS += ('lettuce.django',) INSTALLED_APPS += ('lettuce.django',)
LETTUCE_APPS = ('contentstore',) 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') 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. # Lastly, see if the developer has any local overrides.
try: try:
from .private import * # pylint: disable=F0401 from .private import * # pylint: disable=F0401
except ImportError: except ImportError:
pass 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 = { ...@@ -254,8 +254,11 @@ PIPELINE_JS = {
'js/models/metadata_model.js', 'js/views/metadata_editor_view.js', 'js/models/metadata_model.js', 'js/views/metadata_editor_view.js',
'js/models/uploads.js', 'js/views/uploads.js', 'js/models/uploads.js', 'js/views/uploads.js',
'js/models/textbook.js', 'js/views/textbook.js', 'js/models/textbook.js', 'js/views/textbook.js',
'js/views/assets.js', 'js/src/utility.js', 'js/src/utility.js',
'js/models/settings/course_grading_policy.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', 'output_filename': 'js/cms-application.js',
'test_order': 0 'test_order': 0
}, },
......
...@@ -25,7 +25,7 @@ describe "CMS.Models.Textbook", -> ...@@ -25,7 +25,7 @@ describe "CMS.Models.Textbook", ->
expect(@model.isEmpty()).toBeTruthy() expect(@model.isEmpty()).toBeTruthy()
it "should have a URL set", -> it "should have a URL set", ->
expect(_.result(@model, "url")).toBeTruthy() expect(@model.url()).toBeTruthy()
it "should be able to reset itself", -> it "should be able to reset itself", ->
@model.set("name", "foobar") @model.set("name", "foobar")
...@@ -124,14 +124,14 @@ describe "CMS.Models.Textbook", -> ...@@ -124,14 +124,14 @@ describe "CMS.Models.Textbook", ->
describe "CMS.Collections.TextbookSet", -> describe "CMS.Collections.TextbookSet", ->
beforeEach -> beforeEach ->
CMS.URL.TEXTBOOK = "/textbooks" CMS.URL.TEXTBOOKS = "/textbooks"
@collection = new CMS.Collections.TextbookSet() @collection = new CMS.Collections.TextbookSet()
afterEach -> afterEach ->
delete CMS.URL.TEXTBOOK delete CMS.URL.TEXTBOOKS
it "should have a url set", -> it "should have a url set", ->
expect(_.result(@collection, "url"), "/textbooks") expect(@collection.url()).toEqual("/textbooks")
it "can call save", -> it "can call save", ->
spyOn(@collection, "sync") 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", -> ...@@ -11,15 +11,11 @@ describe "CMS.Views.UploadDialog", ->
@model = new CMS.Models.FileUpload( @model = new CMS.Models.FileUpload(
mimeTypes: ['application/pdf'] mimeTypes: ['application/pdf']
) )
@chapter = new CMS.Models.Chapter() @dialogResponse = dialogResponse = []
@view = new CMS.Views.UploadDialog( @view = new CMS.Views.UploadDialog(
model: @model, model: @model,
onSuccess: (response) => onSuccess: (response) =>
options = {} dialogResponse.push(response.response)
if !@chapter.get('name')
options.name = response.displayname
options.asset_path = response.url
@chapter.set(options)
) )
spyOn(@view, 'remove').andCallThrough() spyOn(@view, 'remove').andCallThrough()
...@@ -66,7 +62,6 @@ describe "CMS.Views.UploadDialog", -> ...@@ -66,7 +62,6 @@ describe "CMS.Views.UploadDialog", ->
expect(@view.$el).toContain("#upload_error") expect(@view.$el).toContain("#upload_error")
expect(@view.$(".action-upload")).toHaveClass("disabled") expect(@view.$(".action-upload")).toHaveClass("disabled")
it "adds body class on show()", -> it "adds body class on show()", ->
@view.show() @view.show()
expect(@view.options.shown).toBeTruthy() expect(@view.options.shown).toBeTruthy()
...@@ -99,11 +94,10 @@ describe "CMS.Views.UploadDialog", -> ...@@ -99,11 +94,10 @@ describe "CMS.Views.UploadDialog", ->
expect(request.method).toEqual("POST") expect(request.method).toEqual("POST")
request.respond(200, {"Content-Type": "application/json"}, request.respond(200, {"Content-Type": "application/json"},
'{"displayname": "starfish", "url": "/uploaded/starfish.pdf"}') '{"response": "dummy_response"}')
expect(@model.get("uploading")).toBeFalsy() expect(@model.get("uploading")).toBeFalsy()
expect(@model.get("finished")).toBeTruthy() expect(@model.get("finished")).toBeTruthy()
expect(@chapter.get("name")).toEqual("starfish") expect(@dialogResponse.pop()).toEqual("dummy_response")
expect(@chapter.get("asset_path")).toEqual("/uploaded/starfish.pdf")
it "can handle upload errors", -> it "can handle upload errors", ->
@view.upload() @view.upload()
...@@ -114,7 +108,7 @@ describe "CMS.Views.UploadDialog", -> ...@@ -114,7 +108,7 @@ describe "CMS.Views.UploadDialog", ->
it "removes itself after two seconds on successful upload", -> it "removes itself after two seconds on successful upload", ->
@view.upload() @view.upload()
@requests[0].respond(200, {"Content-Type": "application/json"}, @requests[0].respond(200, {"Content-Type": "application/json"},
'{"displayname": "starfish", "url": "/uploaded/starfish.pdf"}') '{"response": "dummy_response"}')
expect(@view.remove).not.toHaveBeenCalled() expect(@view.remove).not.toHaveBeenCalled()
@clock.tick(2001) @clock.tick(2001)
expect(@view.remove).toHaveBeenCalled() expect(@view.remove).toHaveBeenCalled()
...@@ -421,11 +421,6 @@ function _deleteItem($el, type) { ...@@ -421,11 +421,6 @@ function _deleteItem($el, type) {
confirm.show(); confirm.show();
} }
function markAsLoaded() {
$('.upload-modal .copy-button').css('display', 'inline-block');
$('.upload-modal .progress-bar').addClass('loaded');
}
function hideModal(e) { function hideModal(e) {
if (e) { if (e) {
e.preventDefault(); e.preventDefault();
...@@ -434,7 +429,6 @@ function hideModal(e) { ...@@ -434,7 +429,6 @@ function hideModal(e) {
// of the editor. Users must press Cancel or Save to exit the editor. // of the editor. Users must press Cancel or Save to exit the editor.
// module_edit adds and removes the "is-fixed" class. // module_edit adds and removes the "is-fixed" class.
if (!$modalCover.hasClass("is-fixed")) { if (!$modalCover.hasClass("is-fixed")) {
$('.file-input').unbind('change', startUpload);
$modal.hide(); $modal.hide();
$modalCover.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() { $(document).ready(function() {
$('.uploads .upload-button').bind('click', showUploadModal); $('.uploads .upload-button').bind('click', showUploadModal);
$('.upload-modal .close-button').bind('click', hideModal); $('.upload-modal .close-button').bind('click', hideModal);
$('.upload-modal .choose-file-button').bind('click', showFileSelectionMenu); $('.upload-modal .choose-file-button').bind('click', showFileSelectionMenu);
$('.remove-asset-button').bind('click', removeAsset);
}); });
function removeAsset(e){ var showUploadModal = function (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) {
e.preventDefault(); e.preventDefault();
resetUploadModal(); resetUploadModal();
// $modal has to be global for hideModal to work.
$modal = $('.upload-modal').show(); $modal = $('.upload-modal').show();
$('.file-input').bind('change', startUpload);
$('.upload-modal .file-chooser').fileupload({ $('.upload-modal .file-chooser').fileupload({
dataType: 'json', dataType: 'json',
type: 'POST', type: 'POST',
...@@ -75,73 +33,58 @@ function showUploadModal(e) { ...@@ -75,73 +33,58 @@ function showUploadModal(e) {
} }
}); });
$('.file-input').bind('change', startUpload);
$modalCover.show(); $modalCover.show();
} };
function showFileSelectionMenu(e) { var showFileSelectionMenu = function(e) {
e.preventDefault(); e.preventDefault();
$('.file-input').click(); $('.file-input').click();
} };
function startUpload(e) { var startUpload = function (e) {
var files = $('.file-input').get(0).files; var file = e.target.value;
if (files.length === 0)
return;
$('.upload-modal h1').html(gettext('Uploading…')); $('.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 .choose-file-button').hide();
$('.upload-modal .progress-bar').removeClass('loaded').show(); $('.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%'; var percentVal = '0%';
$('.upload-modal .progress-fill').width(percentVal); $('.upload-modal .progress-fill').width(percentVal);
$('.upload-modal .progress-fill').html(percentVal); $('.upload-modal .progress-fill').html(percentVal);
} $('.upload-modal .progress-bar').hide();
function resetUploadModal() { $('.upload-modal .file-name').show();
// Reset modal so it no longer displays information about previously
// completed uploads.
resetUploadBar();
$('.upload-modal .file-name').html(''); $('.upload-modal .file-name').html('');
$('.upload-modal h1').html(gettext('Upload New File'));
$('.upload-modal .choose-file-button').html(gettext('Choose File')); $('.upload-modal .choose-file-button').html(gettext('Choose File'));
$('.upload-modal .embeddable-xml-input').val(''); $('.upload-modal .embeddable-xml-input').val('');
$('.upload-modal .embeddable').hide(); $('.upload-modal .embeddable').hide();
} };
function showUploadFeedback(event, percentComplete) { var showUploadFeedback = function (event, percentComplete) {
var percentVal = percentComplete + '%'; var percentVal = percentComplete + '%';
$('.upload-modal .progress-fill').width(percentVal); $('.upload-modal .progress-fill').width(percentVal);
$('.upload-modal .progress-fill').html(percentVal); $('.upload-modal .progress-fill').html(percentVal);
} };
function displayFinishedUpload(resp) { var displayFinishedUpload = function (resp) {
if (resp.status == 200) { var asset = resp.asset;
markAsLoaded();
}
$('.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 .embeddable').show();
$('.upload-modal .file-name').hide(); $('.upload-modal .file-name').hide();
$('.upload-modal .progress-fill').html(resp.msg); $('.upload-modal .progress-fill').html(resp.msg);
$('.upload-modal .choose-file-button').html(gettext('Load Another File')).show(); $('.upload-modal .choose-file-button').html(gettext('Load Another File')).show();
$('.upload-modal .progress-fill').width('100%'); $('.upload-modal .progress-fill').width('100%');
// see if this id already exists, if so, then user must have updated an existing piece of content // TODO remove setting on window object after RequireJS.
$("tr[data-id='" + resp.url + "']").remove(); window.assetsView.addAsset(new CMS.Models.Asset(asset));
};
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
});
}
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({ ...@@ -60,7 +60,7 @@ CMS.Views.Settings.Details = CMS.Views.ValidatingView.extend({
this.$el.find('#' + this.fieldToSelectorMap['effort']).val(this.model.get('effort')); this.$el.find('#' + this.fieldToSelectorMap['effort']).val(this.model.get('effort'));
var imageURL = this.model.get('course_image_asset_path'); 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); this.$el.find('#course-image').attr('src', imageURL);
return this; return this;
...@@ -262,9 +262,9 @@ CMS.Views.Settings.Details = CMS.Views.ValidatingView.extend({ ...@@ -262,9 +262,9 @@ CMS.Views.Settings.Details = CMS.Views.ValidatingView.extend({
model: upload, model: upload,
onSuccess: function(response) { onSuccess: function(response) {
var options = { var options = {
'course_image_name': response.displayname, 'course_image_name': response.asset.display_name,
'course_image_asset_path': response.url 'course_image_asset_path': response.asset.url
} };
self.model.set(options); self.model.set(options);
self.render(); self.render();
$('#course-image').attr('src', self.model.get('course_image_asset_path')) $('#course-image').attr('src', self.model.get('course_image_asset_path'))
......
...@@ -156,7 +156,6 @@ CMS.Views.ListTextbooks = Backbone.View.extend({ ...@@ -156,7 +156,6 @@ CMS.Views.ListTextbooks = Backbone.View.extend({
initialize: function() { initialize: function() {
this.emptyTemplate = _.template($("#no-textbooks-tpl").text()); this.emptyTemplate = _.template($("#no-textbooks-tpl").text());
this.listenTo(this.collection, 'all', this.render); this.listenTo(this.collection, 'all', this.render);
this.listenTo(this.collection, 'destroy', this.handleDestroy);
}, },
tagName: "div", tagName: "div",
className: "textbooks-list", className: "textbooks-list",
...@@ -185,9 +184,6 @@ CMS.Views.ListTextbooks = Backbone.View.extend({ ...@@ -185,9 +184,6 @@ CMS.Views.ListTextbooks = Backbone.View.extend({
addOne: function(e) { addOne: function(e) {
if(e && e.preventDefault) { e.preventDefault(); } if(e && e.preventDefault) { e.preventDefault(); }
this.collection.add([{editing: true}]); this.collection.add([{editing: true}]);
},
handleDestroy: function(model, collection, options) {
collection.remove(model);
} }
}); });
CMS.Views.EditChapter = Backbone.View.extend({ CMS.Views.EditChapter = Backbone.View.extend({
...@@ -252,11 +248,11 @@ CMS.Views.EditChapter = Backbone.View.extend({ ...@@ -252,11 +248,11 @@ CMS.Views.EditChapter = Backbone.View.extend({
onSuccess: function(response) { onSuccess: function(response) {
var options = {}; var options = {};
if(!that.model.get('name')) { 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); that.model.set(options);
}, }
}); });
$(".wrapper-view").after(view.show().el); $(".wrapper-view").after(view.show().el);
} }
......
...@@ -8,7 +8,7 @@ html { ...@@ -8,7 +8,7 @@ html {
} }
body { body {
@extend .t-copy-base; @extend %t-copy-base;
min-width: $fg-min-width; min-width: $fg-min-width;
background: $gray-l5; background: $gray-l5;
color: $gray-d2; color: $gray-d2;
...@@ -29,7 +29,7 @@ a { ...@@ -29,7 +29,7 @@ a {
} }
h1 { h1 {
@extend .t-title4; @extend %t-title4;
font-weight: 300; font-weight: 300;
} }
...@@ -51,13 +51,13 @@ h1 { ...@@ -51,13 +51,13 @@ h1 {
// typography - basic // typography - basic
.page-header { .page-header {
@extend .t-title3; @extend %t-title3;
display: block; display: block;
font-weight: 600; font-weight: 600;
color: $gray-d3; color: $gray-d3;
.subtitle { .subtitle {
@extend .t-title7; @extend %t-title7;
position: relative; position: relative;
top: ($baseline/4); top: ($baseline/4);
display: block; display: block;
...@@ -67,29 +67,29 @@ h1 { ...@@ -67,29 +67,29 @@ h1 {
} }
.section-header { .section-header {
@extend .t-title4; @extend %t-title4;
font-weight: 600; font-weight: 600;
.subtitle { .subtitle {
@extend .t-title7; @extend %t-title7;
} }
} }
.area-header { .area-header {
@extend .t-title6; @extend %t-title6;
font-weight: 600; font-weight: 600;
.subtitle { .subtitle {
@extend .t-title8; @extend %t-title8;
} }
} }
.area-subheader { .area-subheader {
@extend .t-title7; @extend %t-title7;
font-weight: 600; font-weight: 600;
.subtitle { .subtitle {
@extend .t-title9; @extend %t-title9;
} }
} }
...@@ -198,35 +198,35 @@ h1 { ...@@ -198,35 +198,35 @@ h1 {
// typography - loose headings (BT: needs to be removed once html is clean) // typography - loose headings (BT: needs to be removed once html is clean)
.title-1 { .title-1 {
@extend .t-title3; @extend %t-title3;
margin-bottom: ($baseline*1.5); margin-bottom: ($baseline*1.5);
} }
.title-2 { .title-2 {
@extend .t-title4; @extend %t-title4;
margin-bottom: $baseline; margin-bottom: $baseline;
} }
.title-3 { .title-3 {
@extend .t-title5; @extend %t-title5;
margin-bottom: ($baseline/2); margin-bottom: ($baseline/2);
} }
.title-4 { .title-4 {
@extend .t-title7; @extend %t-title7;
margin-bottom: $baseline; margin-bottom: $baseline;
font-weight: 500 font-weight: 500
} }
.title-5 { .title-5 {
@extend .t-title7; @extend %t-title7;
color: $gray-l1; color: $gray-l1;
margin-bottom: $baseline; margin-bottom: $baseline;
font-weight: 500 font-weight: 500
} }
.title-6 { .title-6 {
@extend .t-title7; @extend %t-title7;
color: $gray-l2; color: $gray-l2;
margin-bottom: $baseline; margin-bottom: $baseline;
font-weight: 500 font-weight: 500
...@@ -340,7 +340,7 @@ p, ul, ol, dl { ...@@ -340,7 +340,7 @@ p, ul, ol, dl {
.content { .content {
@include clearfix(); @include clearfix();
@extend .t-copy-base; @extend %t-copy-base;
max-width: $fg-max-width; max-width: $fg-max-width;
min-width: $fg-min-width; min-width: $fg-min-width;
width: flex-grid(12); width: flex-grid(12);
...@@ -354,14 +354,14 @@ p, ul, ol, dl { ...@@ -354,14 +354,14 @@ p, ul, ol, dl {
padding-bottom: ($baseline/2); padding-bottom: ($baseline/2);
.title-sub { .title-sub {
@extend .t-copy-sub1; @extend %t-copy-sub1;
display: block; display: block;
margin: 0; margin: 0;
color: $gray-l2; color: $gray-l2;
} }
.title-1 { .title-1 {
@extend .t-title3; @extend %t-title3;
margin: 0; margin: 0;
padding: 0; padding: 0;
font-weight: 600; font-weight: 600;
...@@ -378,16 +378,16 @@ p, ul, ol, dl { ...@@ -378,16 +378,16 @@ p, ul, ol, dl {
.content-primary { .content-primary {
.title-1 { .title-1 {
@extend .t-title3; @extend %t-title3;
} }
.title-2 { .title-2 {
@extend .t-title4; @extend %t-title4;
margin: 0 0 ($baseline/2) 0; margin: 0 0 ($baseline/2) 0;
} }
.title-3 { .title-3 {
@extend .t-title6; @extend %t-title6;
margin: 0 0 ($baseline/2) 0; margin: 0 0 ($baseline/2) 0;
} }
...@@ -401,7 +401,7 @@ p, ul, ol, dl { ...@@ -401,7 +401,7 @@ p, ul, ol, dl {
} }
.tip { .tip {
@extend .t-copy-sub2; @extend %t-copy-sub2;
width: flex-grid(7, 12); width: flex-grid(7, 12);
float: right; float: right;
margin-top: ($baseline/2); margin-top: ($baseline/2);
...@@ -419,7 +419,7 @@ p, ul, ol, dl { ...@@ -419,7 +419,7 @@ p, ul, ol, dl {
} }
.bit { .bit {
@extend .t-copy-sub1; @extend %t-copy-sub1;
margin: 0 0 $baseline 0; margin: 0 0 $baseline 0;
border-bottom: 1px solid $gray-l4; border-bottom: 1px solid $gray-l4;
padding: 0 0 $baseline 0; padding: 0 0 $baseline 0;
...@@ -432,7 +432,7 @@ p, ul, ol, dl { ...@@ -432,7 +432,7 @@ p, ul, ol, dl {
} }
h3 { h3 {
@extend .t-title7; @extend %t-title7;
margin: 0 0 ($baseline/4) 0; margin: 0 0 ($baseline/4) 0;
color: $gray-d2; color: $gray-d2;
font-weight: 600; font-weight: 600;
...@@ -448,7 +448,7 @@ p, ul, ol, dl { ...@@ -448,7 +448,7 @@ p, ul, ol, dl {
// actions // actions
.list-actions { .list-actions {
@extend .cont-no-list; @extend %cont-no-list;
.action-item { .action-item {
margin-bottom: ($baseline/4); margin-bottom: ($baseline/4);
...@@ -558,7 +558,7 @@ p, ul, ol, dl { ...@@ -558,7 +558,7 @@ p, ul, ol, dl {
// misc // misc
hr.divide { hr.divide {
@extend .cont-text-sr; @extend %cont-text-sr;
} }
.item-details { .item-details {
...@@ -719,7 +719,7 @@ hr.divide { ...@@ -719,7 +719,7 @@ hr.divide {
.new-button { .new-button {
@include green-button; @include green-button;
@extend .t-action4; @extend %t-action4;
padding: 8px 20px 10px; padding: 8px 20px 10px;
text-align: center; text-align: center;
...@@ -735,7 +735,7 @@ hr.divide { ...@@ -735,7 +735,7 @@ hr.divide {
.view-button { .view-button {
@include blue-button; @include blue-button;
@extend .t-copy-base; @extend %t-action4;
text-align: center; text-align: center;
&.big { &.big {
...@@ -754,7 +754,7 @@ hr.divide { ...@@ -754,7 +754,7 @@ hr.divide {
.edit-button.standard, .edit-button.standard,
.delete-button.standard { .delete-button.standard {
@extend .t-action4; @extend %t-action4;
@include white-button; @include white-button;
float: left; float: left;
padding: 3px 10px 4px; padding: 3px 10px 4px;
...@@ -806,7 +806,7 @@ hr.divide { ...@@ -806,7 +806,7 @@ hr.divide {
// basic utility // basic utility
.sr { .sr {
@extend .cont-text-sr; @extend %cont-text-sr;
} }
.fake-link { .fake-link {
...@@ -827,7 +827,7 @@ hr.divide { ...@@ -827,7 +827,7 @@ hr.divide {
} }
hr.divider { hr.divider {
@extend .sr; @extend %cont-text-sr;
} }
// ==================== // ====================
...@@ -859,7 +859,7 @@ body.js { ...@@ -859,7 +859,7 @@ body.js {
text-align: center; text-align: center;
.label { .label {
@extend .cont-text-sr; @extend %cont-text-sr;
} }
[class^="icon-"] { [class^="icon-"] {
...@@ -882,14 +882,14 @@ body.js { ...@@ -882,14 +882,14 @@ body.js {
} }
.title { .title {
@extend .t-title5; @extend %t-title5;
margin: 0 0 ($baseline/2) 0; margin: 0 0 ($baseline/2) 0;
font-weight: 600; font-weight: 600;
color: $gray-d3; color: $gray-d3;
} }
.description { .description {
@extend .t-copy-sub2; @extend %t-copy-sub2;
margin-top: ($baseline/2); margin-top: ($baseline/2);
color: $gray-l1; color: $gray-l1;
} }
......
...@@ -17,7 +17,7 @@ ...@@ -17,7 +17,7 @@
} }
// canned animation - use if you want out of the box/non-customized anim // canned animation - use if you want out of the box/non-customized anim
.anim-fadeIn { %anim-fadeIn {
@include animation(fadeIn $tmg-f2 linear 1); @include animation(fadeIn $tmg-f2 linear 1);
} }
...@@ -38,7 +38,7 @@ ...@@ -38,7 +38,7 @@
} }
// canned animation - use if you want out of the box/non-customized anim // canned animation - use if you want out of the box/non-customized anim
.anim-fadeOut { %anim-fadeOut {
@include animation(fadeOut $tmg-f2 linear 1); @include animation(fadeOut $tmg-f2 linear 1);
} }
...@@ -62,7 +62,7 @@ ...@@ -62,7 +62,7 @@
} }
// canned animation - use if you want out of the box/non-customized anim // 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); @include animation(rotateUp $tmg-f2 ease-in-out 1);
} }
...@@ -83,7 +83,7 @@ ...@@ -83,7 +83,7 @@
} }
// canned animation - use if you want out of the box/non-customized anim // 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); @include animation(rotateDown $tmg-f2 ease-in-out 1);
} }
...@@ -104,7 +104,7 @@ ...@@ -104,7 +104,7 @@
} }
// canned animation - use if you want out of the box/non-customized anim // canned animation - use if you want out of the box/non-customized anim
.anim-rotateCW { %anim-rotateCW {
@include animation(rotateCW $tmg-s1 linear infinite); @include animation(rotateCW $tmg-s1 linear infinite);
} }
...@@ -125,7 +125,7 @@ ...@@ -125,7 +125,7 @@
} }
// canned animation - use if you want out of the box/non-customized anim // canned animation - use if you want out of the box/non-customized anim
.anim-rotateCCW { %anim-rotateCCW {
@include animation(rotateCCW $tmg-s1 linear infinite); @include animation(rotateCCW $tmg-s1 linear infinite);
} }
...@@ -185,7 +185,7 @@ ...@@ -185,7 +185,7 @@
} }
// canned animation - use if you want out of the box/non-customized anim // 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); @include animation(bounceIn $tmg-f1 ease-in-out 1);
} }
...@@ -208,6 +208,6 @@ ...@@ -208,6 +208,6 @@
} }
// canned animation - use if you want out of the box/non-customized anim // 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); @include animation(bounceOut $tmg-f1 ease-in-out 1);
} }
.content-type { %content-type {
display: inline-block; display: inline-block;
width: 14px; width: 14px;
height: 16px; height: 16px;
...@@ -9,61 +9,61 @@ ...@@ -9,61 +9,61 @@
} }
.videosequence-icon { .videosequence-icon {
@extend .content-type; @extend %content-type;
background-image: url('../img/content-types/videosequence.png'); background-image: url('../img/content-types/videosequence.png');
} }
.video-icon { .video-icon {
@extend .content-type; @extend %content-type;
background-image: url('../img/content-types/video.png'); background-image: url('../img/content-types/video.png');
} }
.problemset-icon { .problemset-icon {
@extend .content-type; @extend %content-type;
background-image: url('../img/content-types/problemset.png'); background-image: url('../img/content-types/problemset.png');
} }
.problem-icon { .problem-icon {
@extend .content-type; @extend %content-type;
background-image: url('../img/content-types/problem.png'); background-image: url('../img/content-types/problem.png');
} }
.lab-icon { .lab-icon {
@extend .content-type; @extend %content-type;
background-image: url('../img/content-types/lab.png'); background-image: url('../img/content-types/lab.png');
} }
.tab-icon { .tab-icon {
@extend .content-type; @extend %content-type;
background-image: url('../img/content-types/lab.png'); background-image: url('../img/content-types/lab.png');
} }
.html-icon { .html-icon {
@extend .content-type; @extend %content-type;
background-image: url('../img/content-types/html.png'); background-image: url('../img/content-types/html.png');
} }
.vertical-icon { .vertical-icon {
@extend .content-type; @extend %content-type;
background-image: url('../img/content-types/vertical.png'); background-image: url('../img/content-types/vertical.png');
} }
.sequential-icon { .sequential-icon {
@extend .content-type; @extend %content-type;
background-image: url('../img/content-types/sequential.png'); background-image: url('../img/content-types/sequential.png');
} }
.chapter-icon { .chapter-icon {
@extend .content-type; @extend %content-type;
background-image: url('../img/content-types/chapter.png'); background-image: url('../img/content-types/chapter.png');
} }
.module-icon { .module-icon {
@extend .content-type; @extend %content-type;
background-image: url('../img/content-types/module.png'); background-image: url('../img/content-types/module.png');
} }
.module-icon { .module-icon {
@extend .content-type; @extend %content-type;
background-image: url('../img/content-types/module.png'); background-image: url('../img/content-types/module.png');
} }
...@@ -2,8 +2,8 @@ ...@@ -2,8 +2,8 @@
// ==================== // ====================
// gray primary button // gray primary button
.btn-primary-gray { %btn-primary-gray {
@extend .ui-btn-primary; @extend %ui-btn-primary;
background: $gray-l1; background: $gray-l1;
border-color: $gray-l2; border-color: $gray-l2;
color: $white; color: $white;
...@@ -24,8 +24,8 @@ ...@@ -24,8 +24,8 @@
} }
// blue primary button // blue primary button
.btn-primary-blue { %btn-primary-blue {
@extend .ui-btn-primary; @extend %ui-btn-primary;
background: $blue; background: $blue;
border-color: $blue-s1; border-color: $blue-s1;
color: $white; color: $white;
...@@ -47,8 +47,8 @@ ...@@ -47,8 +47,8 @@
} }
// green primary button // green primary button
.btn-primary-green { %btn-primary-green {
@extend .ui-btn-primary; @extend %ui-btn-primary;
background: $green; background: $green;
border-color: $green; border-color: $green;
color: $white; color: $white;
...@@ -70,8 +70,8 @@ ...@@ -70,8 +70,8 @@
} }
// gray secondary button // gray secondary button
.btn-secondary-gray { %btn-secondary-gray {
@extend .ui-btn-secondary; @extend %ui-btn-secondary;
border-color: $gray-l3; border-color: $gray-l3;
color: $gray-l1; color: $gray-l1;
...@@ -91,8 +91,8 @@ ...@@ -91,8 +91,8 @@
} }
// blue secondary button // blue secondary button
.btn-secondary-blue { %btn-secondary-blue {
@extend .ui-btn-secondary; @extend %ui-btn-secondary;
border-color: $blue-l3; border-color: $blue-l3;
color: $blue; color: $blue;
...@@ -113,8 +113,8 @@ ...@@ -113,8 +113,8 @@
} }
// green secondary button // green secondary button
.btn-secondary-green { %btn-secondary-green {
@extend .ui-btn-secondary; @extend %ui-btn-secondary;
border-color: $green-l4; border-color: $green-l4;
color: $green-l2; color: $green-l2;
...@@ -148,9 +148,9 @@ ...@@ -148,9 +148,9 @@
// ==================== // ====================
// simple dropdown button styling - should we move this elsewhere? // simple dropdown button styling - should we move this elsewhere?
.ui-btn-dd { %ui-btn-dd {
@extend .ui-btn; @extend %ui-btn;
@extend .ui-btn-pill; @extend %ui-btn-pill;
padding:($baseline/4) ($baseline/2); padding:($baseline/4) ($baseline/2);
border-width: 1px; border-width: 1px;
border-style: solid; border-style: solid;
...@@ -158,7 +158,7 @@ ...@@ -158,7 +158,7 @@
text-align: center; text-align: center;
&:hover, &:active { &:hover, &:active {
@extend .ui-fake-link; @extend %ui-fake-link;
border-color: $gray-l3; border-color: $gray-l3;
} }
...@@ -169,8 +169,8 @@ ...@@ -169,8 +169,8 @@
} }
// layout-based buttons - nav dd // layout-based buttons - nav dd
.ui-btn-dd-nav-primary { %ui-btn-dd-nav-primary {
@extend .ui-btn-dd; @extend %ui-btn-dd;
background: $white; background: $white;
border-color: $white; border-color: $white;
color: $gray-d1; color: $gray-d1;
...@@ -197,6 +197,6 @@ ...@@ -197,6 +197,6 @@
// ==================== // ====================
// specific buttons - view live // specific buttons - view live
.view-live-button { %view-live-button {
@extend .t-action4; @extend %t-action4;
} }
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
padding: $baseline; padding: $baseline;
footer.primary { footer.primary {
@extend .t-copy-sub2; @extend %t-copy-sub2;
@include clearfix(); @include clearfix();
max-width: $fg-max-width; max-width: $fg-max-width;
min-width: $fg-min-width; min-width: $fg-min-width;
......
...@@ -115,10 +115,10 @@ form { ...@@ -115,10 +115,10 @@ form {
// ELEM: form // ELEM: form
// form styling for creating a new content item (course, user, textbook) // form styling for creating a new content item (course, user, textbook)
form[class^="create-"] { form[class^="create-"] {
@extend .ui-window; @extend %ui-window;
.title { .title {
@extend .t-title4; @extend %t-title4;
font-weight: 600; font-weight: 600;
padding: $baseline ($baseline*1.5) 0 ($baseline*1.5); padding: $baseline ($baseline*1.5) 0 ($baseline*1.5);
} }
...@@ -129,7 +129,7 @@ form[class^="create-"] { ...@@ -129,7 +129,7 @@ form[class^="create-"] {
.list-input { .list-input {
@extend .cont-no-list; @extend %cont-no-list;
.field { .field {
margin: 0 0 ($baseline*0.75) 0; margin: 0 0 ($baseline*0.75) 0;
...@@ -155,7 +155,7 @@ form[class^="create-"] { ...@@ -155,7 +155,7 @@ form[class^="create-"] {
} }
label { label {
@extend .t-copy-sub1; @extend %t-copy-sub1;
@include transition(color $tmg-f3 ease-in-out 0s); @include transition(color $tmg-f3 ease-in-out 0s);
margin: 0 0 ($baseline/4) 0; margin: 0 0 ($baseline/4) 0;
...@@ -166,7 +166,7 @@ form[class^="create-"] { ...@@ -166,7 +166,7 @@ form[class^="create-"] {
input, textarea { input, textarea {
@extend .t-copy-base; @extend %t-copy-base;
@include transition(all $tmg-f2 ease-in-out 0s); @include transition(all $tmg-f2 ease-in-out 0s);
height: 100%; height: 100%;
width: 100%; width: 100%;
...@@ -208,7 +208,7 @@ form[class^="create-"] { ...@@ -208,7 +208,7 @@ form[class^="create-"] {
} }
.tip { .tip {
@extend .t-copy-sub2; @extend %t-copy-sub2;
@include transition(color, 0.15s, ease-in-out); @include transition(color, 0.15s, ease-in-out);
display: block; display: block;
margin-top: ($baseline/4); margin-top: ($baseline/4);
...@@ -226,11 +226,11 @@ form[class^="create-"] { ...@@ -226,11 +226,11 @@ form[class^="create-"] {
} }
.is-showing { .is-showing {
@extend .anim-fadeIn; @extend %anim-fadeIn;
} }
.is-hiding { .is-hiding {
@extend .anim-fadeOut; @extend %anim-fadeOut;
} }
.tip-error { .tip-error {
...@@ -311,12 +311,12 @@ form[class^="create-"] { ...@@ -311,12 +311,12 @@ form[class^="create-"] {
.action-primary { .action-primary {
@include blue-button; @include blue-button;
@extend .t-action2; @extend %t-action2;
} }
.action-secondary { .action-secondary {
@include grey-button; @include grey-button;
@extend .t-action2; @extend %t-action2;
} }
} }
} }
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
// ==================== // ====================
.wrapper-header { .wrapper-header {
@extend .ui-depth3; @extend %ui-depth3;
position: relative; position: relative;
width: 100%; width: 100%;
box-shadow: 0 1px 2px 0 $shadow-l1; box-shadow: 0 1px 2px 0 $shadow-l1;
...@@ -51,7 +51,7 @@ ...@@ -51,7 +51,7 @@
nav { nav {
> ol > .nav-item { > ol > .nav-item {
@extend .t-action3; @extend %t-action3;
display: inline-block; display: inline-block;
vertical-align: middle; vertical-align: middle;
font-weight: 600; font-weight: 600;
...@@ -74,8 +74,8 @@ ...@@ -74,8 +74,8 @@
.nav-dd { .nav-dd {
.title { .title {
@extend .t-action2; @extend %t-action2;
@extend .ui-btn-dd-nav-primary; @extend %ui-btn-dd-nav-primary;
@include transition(all $tmg-f2 ease-in-out 0s); @include transition(all $tmg-f2 ease-in-out 0s);
.label, .icon-caret-down { .label, .icon-caret-down {
...@@ -133,7 +133,7 @@ ...@@ -133,7 +133,7 @@
padding: ($baseline*0.75) flex-gutter() ($baseline*0.75) 0; padding: ($baseline*0.75) flex-gutter() ($baseline*0.75) 0;
.course-org, .course-number { .course-org, .course-number {
@extend .t-action4; @extend %t-action4;
display: inline-block; display: inline-block;
vertical-align: middle; vertical-align: middle;
max-width: 45%; max-width: 45%;
...@@ -148,7 +148,7 @@ ...@@ -148,7 +148,7 @@
} }
.course-title { .course-title {
@extend .t-action2; @extend %t-action2;
display: block; display: block;
width: 100%; width: 100%;
overflow: hidden; overflow: hidden;
......
...@@ -23,8 +23,8 @@ ...@@ -23,8 +23,8 @@
width: 100%; width: 100%;
} }
.ui-badge { %ui-badge {
@extend .t-title9; @extend %t-title9;
position: relative; position: relative;
border-bottom-right-radius: ($baseline/10); border-bottom-right-radius: ($baseline/10);
border-bottom-left-radius: ($baseline/10); border-bottom-left-radius: ($baseline/10);
...@@ -39,7 +39,7 @@ ...@@ -39,7 +39,7 @@
// OPTION: add this class for a visual hanging display // OPTION: add this class for a visual hanging display
&.is-hanging { &.is-hanging {
@include box-sizing(border-box); @include box-sizing(border-box);
@extend .ui-depth2; @extend %ui-depth2;
top: -($baseline/4); top: -($baseline/4);
&:after { &:after {
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
// ==================== // ====================
.modal-cover { .modal-cover {
@extend .ui-depth3; @extend %ui-depth3;
display: none; display: none;
position: fixed; position: fixed;
top: 0; top: 0;
...@@ -13,7 +13,7 @@ ...@@ -13,7 +13,7 @@
} }
.modal { .modal {
@extend .ui-depth4; @extend %ui-depth4;
display: none; display: none;
position: fixed; position: fixed;
top: 60px; top: 60px;
...@@ -61,7 +61,7 @@ ...@@ -61,7 +61,7 @@
// lean modal alternative // lean modal alternative
#lean_overlay { #lean_overlay {
@extend .ui-depth4; @extend %ui-depth4;
position: fixed; position: fixed;
top: 0px; top: 0px;
left: 0px; left: 0px;
......
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
nav { nav {
ol, ul { ol, ul {
@extend .cont-no-list; @extend %cont-no-list;
} }
.nav-item { .nav-item {
...@@ -111,7 +111,7 @@ nav { ...@@ -111,7 +111,7 @@ nav {
} }
.nav-item { .nav-item {
@extend .t-action3; @extend %t-action3;
display: block; display: block;
margin: 0 0 ($baseline/4) 0; margin: 0 0 ($baseline/4) 0;
border-bottom: 1px solid $gray-l5; border-bottom: 1px solid $gray-l5;
......
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
.wrapper-inner { .wrapper-inner {
@include linear-gradient($gray-d3 0%, $gray-d3 98%, $black 100%); @include linear-gradient($gray-d3 0%, $gray-d3 98%, $black 100%);
@extend .ui-depth0; @extend %ui-depth0;
display: none; display: none;
width: 100% !important; width: 100% !important;
border-bottom: 1px solid $white; border-bottom: 1px solid $white;
...@@ -19,7 +19,7 @@ ...@@ -19,7 +19,7 @@
// sock - actions // sock - actions
.list-cta { .list-cta {
@extend .ui-depth1; @extend %ui-depth1;
position: absolute; position: absolute;
top: -($baseline*0.75); top: -($baseline*0.75);
width: 100%; width: 100%;
...@@ -27,8 +27,8 @@ ...@@ -27,8 +27,8 @@
text-align: center; text-align: center;
.cta-show-sock { .cta-show-sock {
@extend .ui-btn-pill; @extend %ui-btn-pill;
@extend .t-action4; @extend %t-action4;
background: $gray-l5; background: $gray-l5;
padding: ($baseline/2) $baseline; padding: ($baseline/2) $baseline;
color: $gray; color: $gray;
...@@ -48,7 +48,7 @@ ...@@ -48,7 +48,7 @@
// sock - additional help // sock - additional help
.sock { .sock {
@include clearfix(); @include clearfix();
@extend .t-copy-sub2; @extend %t-copy-sub2;
max-width: $fg-max-width; max-width: $fg-max-width;
min-width: $fg-min-width; min-width: $fg-min-width;
width: flex-grid(12); width: flex-grid(12);
...@@ -60,7 +60,7 @@ ...@@ -60,7 +60,7 @@
header { header {
.title { .title {
@extend .t-title4; @extend %t-title4;
} }
} }
...@@ -70,13 +70,13 @@ ...@@ -70,13 +70,13 @@
@include box-sizing(border-box); @include box-sizing(border-box);
.title { .title {
@extend .t-title6; @extend %t-title6;
color: $white; color: $white;
margin-bottom: ($baseline/2); margin-bottom: ($baseline/2);
} }
.copy { .copy {
@extend .t-copy-sub2; @extend %t-copy-sub2;
margin: 0 0 $baseline 0; margin: 0 0 $baseline 0;
} }
...@@ -92,7 +92,7 @@ ...@@ -92,7 +92,7 @@
} }
.action { .action {
@extend .t-action4; @extend %t-action4;
display: block; display: block;
[class^="icon-"] { [class^="icon-"] {
...@@ -108,13 +108,13 @@ ...@@ -108,13 +108,13 @@
} }
.tip { .tip {
@extend .sr; @extend %cont-text-sr;
} }
} }
.action-primary { .action-primary {
@extend .btn-primary-blue; @extend %btn-primary-blue;
@extend .t-action3; @extend %t-action3;
} }
} }
} }
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
// messages // messages
.message { .message {
@extend .t-copy-sub1; @extend %t-copy-sub1;
display: block; display: block;
} }
...@@ -49,7 +49,7 @@ ...@@ -49,7 +49,7 @@
@include box-sizing(border-box); @include box-sizing(border-box);
.copy { .copy {
@extend .t-copy-sub1; @extend %t-copy-sub1;
} }
} }
...@@ -186,7 +186,7 @@ ...@@ -186,7 +186,7 @@
// prompts // prompts
.wrapper-prompt { .wrapper-prompt {
@extend .ui-depth5; @extend %ui-depth5;
@include transition(all $tmg-f3 ease-in-out 0s); @include transition(all $tmg-f3 ease-in-out 0s);
position: fixed; position: fixed;
top: 0; top: 0;
...@@ -233,12 +233,12 @@ ...@@ -233,12 +233,12 @@
} }
.action-primary { .action-primary {
@extend .t-action4; @extend %t-action4;
font-weight: 600; font-weight: 600;
} }
.action-secondary { .action-secondary {
@extend .t-action4; @extend %t-action4;
} }
} }
} }
...@@ -284,7 +284,7 @@ ...@@ -284,7 +284,7 @@
// notifications // notifications
.wrapper-notification { .wrapper-notification {
@extend .ui-depth5; @extend %ui-depth5;
@include clearfix(); @include clearfix();
box-shadow: 0 -1px 3px $shadow, inset 0 3px 1px $blue; box-shadow: 0 -1px 3px $shadow, inset 0 3px 1px $blue;
position: fixed; position: fixed;
...@@ -417,12 +417,12 @@ ...@@ -417,12 +417,12 @@
} }
.copy { .copy {
@extend .t-copy-sub1; @extend %t-copy-sub1;
width: flex-grid(10, 12); width: flex-grid(10, 12);
color: $gray-l2; color: $gray-l2;
.title { .title {
@extend .t-title7; @extend %t-title7;
margin-bottom: 0; margin-bottom: 0;
color: $white; color: $white;
} }
...@@ -465,7 +465,7 @@ ...@@ -465,7 +465,7 @@
.action-secondary { .action-secondary {
@extend .t-action4; @extend %t-action4;
} }
} }
...@@ -486,7 +486,7 @@ ...@@ -486,7 +486,7 @@
} }
.copy p { .copy p {
@extend .cont-text-sr; @extend %cont-text-sr;
} }
} }
} }
...@@ -495,7 +495,7 @@ ...@@ -495,7 +495,7 @@
// alerts // alerts
.wrapper-alert { .wrapper-alert {
@extend .ui-depth2; @extend %ui-depth2;
@include box-sizing(border-box); @include box-sizing(border-box);
box-shadow: 0 1px 1px $white, inset 0 2px 2px $shadow-d1, inset 0 -4px 1px $blue; box-shadow: 0 1px 1px $white, inset 0 2px 2px $shadow-d1, inset 0 -4px 1px $blue;
position: relative; position: relative;
...@@ -585,7 +585,7 @@ ...@@ -585,7 +585,7 @@
color: $gray-l2; color: $gray-l2;
.title { .title {
@extend .t-title7; @extend %t-title7;
margin-bottom: 0; margin-bottom: 0;
color: $white; color: $white;
} }
...@@ -619,12 +619,12 @@ ...@@ -619,12 +619,12 @@
} }
.action-primary { .action-primary {
@extend .t-action4; @extend %t-action4;
font-weight: 600; font-weight: 600;
} }
.action-secondary { .action-secondary {
@extend .t-action4; @extend %t-action4;
} }
} }
} }
...@@ -641,7 +641,7 @@ ...@@ -641,7 +641,7 @@
text-align: center; text-align: center;
.label { .label {
@extend .cont-text-sr; @extend %cont-text-sr;
} }
[class^="icon"] { [class^="icon"] {
...@@ -738,7 +738,7 @@ body.uxdesign.alerts { ...@@ -738,7 +738,7 @@ body.uxdesign.alerts {
} }
.content-primary { .content-primary {
@extend .ui-window; @extend %ui-window;
width: flex-grid(12, 12); width: flex-grid(12, 12);
margin-right: flex-gutter(); margin-right: flex-gutter();
padding: $baseline ($baseline*1.5); padding: $baseline ($baseline*1.5);
......
...@@ -7,12 +7,12 @@ ...@@ -7,12 +7,12 @@
margin-bottom: $baseline; margin-bottom: $baseline;
.title { .title {
@extend .t-title4; @extend %t-title4;
font-weight: 600; font-weight: 600;
} }
.copy { .copy {
@extend .t-copy-sub1; @extend %t-copy-sub1;
} }
strong { strong {
...@@ -30,14 +30,14 @@ ...@@ -30,14 +30,14 @@
} }
.nav-introduction-supplementary { .nav-introduction-supplementary {
@extend .t-copy-sub2; @extend %t-copy-sub2;
float: right; float: right;
width: flex-grid(4,12); width: flex-grid(4,12);
display: block; display: block;
text-align: right; text-align: right;
.icon { .icon {
@extend .t-action3; @extend %t-action3;
display: inline-block; display: inline-block;
vertical-align: middle; vertical-align: middle;
margin-right: ($baseline/4); margin-right: ($baseline/4);
...@@ -48,20 +48,20 @@ ...@@ -48,20 +48,20 @@
// notices - in-context: to be used as notices to users within the context of a form/action // notices - in-context: to be used as notices to users within the context of a form/action
.notice-incontext { .notice-incontext {
@extend .ui-well; @extend %ui-well;
border-radius: ($baseline/10); border-radius: ($baseline/10);
position: relative; position: relative;
overflow: hidden; overflow: hidden;
margin-bottom: $baseline; margin-bottom: $baseline;
.title { .title {
@extend .t-title6; @extend %t-title6;
margin-bottom: ($baseline/2); margin-bottom: ($baseline/2);
font-weight: 700; font-weight: 700;
} }
.copy { .copy {
@extend .t-copy-sub1; @extend %t-copy-sub1;
@include transition(opacity $tmg-f2 ease-in-out 0s); @include transition(opacity $tmg-f2 ease-in-out 0s);
opacity: 0.75; opacity: 0.75;
margin-bottom: $baseline; margin-bottom: $baseline;
...@@ -99,8 +99,8 @@ ...@@ -99,8 +99,8 @@
} }
.action-primary { .action-primary {
@extend .btn-primary-blue; @extend %btn-primary-blue;
@extend .t-action3; @extend %t-action3;
} }
} }
} }
...@@ -160,8 +160,8 @@ ...@@ -160,8 +160,8 @@
} }
.action-primary { .action-primary {
@extend .btn-primary-blue; @extend %btn-primary-blue;
@extend .t-action3; @extend %t-action3;
} }
} }
} }
...@@ -188,8 +188,8 @@ ...@@ -188,8 +188,8 @@
} }
.action-primary { .action-primary {
@extend .btn-primary-green; @extend %btn-primary-green;
@extend .t-action3; @extend %t-action3;
} }
} }
} }
......
...@@ -4,59 +4,59 @@ ...@@ -4,59 +4,59 @@
// Scale - (6, 7, 8, 9, 10, 11, 12, 14, 16, 18, 21, 24, 36, 48, 60, 72) // Scale - (6, 7, 8, 9, 10, 11, 12, 14, 16, 18, 21, 24, 36, 48, 60, 72)
// headings/titles // headings/titles
.t-title { %t-title {
font-family: $f-sans-serif; font-family: $f-sans-serif;
} }
.t-title1 { %t-title1 {
@extend .t-title; @extend %t-title;
@include font-size(60); @include font-size(60);
@include line-height(60); @include line-height(60);
} }
.t-title2 { %t-title2 {
@extend .t-title; @extend %t-title;
@include font-size(48); @include font-size(48);
@include line-height(48); @include line-height(48);
} }
.t-title3 { %t-title3 {
@include font-size(36); @include font-size(36);
@include line-height(36); @include line-height(36);
} }
.t-title4 { %t-title4 {
@extend .t-title; @extend %t-title;
@include font-size(24); @include font-size(24);
@include line-height(24); @include line-height(24);
} }
.t-title5 { %t-title5 {
@extend .t-title; @extend %t-title;
@include font-size(18); @include font-size(18);
@include line-height(18); @include line-height(18);
} }
.t-title6 { %t-title6 {
@extend .t-title; @extend %t-title;
@include font-size(16); @include font-size(16);
@include line-height(16); @include line-height(16);
} }
.t-title7 { %t-title7 {
@extend .t-title; @extend %t-title;
@include font-size(14); @include font-size(14);
@include line-height(14); @include line-height(14);
} }
.t-title8 { %t-title8 {
@extend .t-title; @extend %t-title;
@include font-size(12); @include font-size(12);
@include line-height(12); @include line-height(12);
} }
.t-title9 { %t-title9 {
@extend .t-title; @extend %t-title;
@include font-size(11); @include font-size(11);
@include line-height(11); @include line-height(11);
} }
...@@ -64,36 +64,36 @@ ...@@ -64,36 +64,36 @@
// ==================== // ====================
// copy // copy
.t-copy { %t-copy {
font-family: $f-sans-serif; font-family: $f-sans-serif;
} }
.t-copy-base { %t-copy-base {
@extend .t-copy; @extend %t-copy;
@include font-size(16); @include font-size(16);
@include line-height(16); @include line-height(16);
} }
.t-copy-lead1 { %t-copy-lead1 {
@extend .t-copy; @extend %t-copy;
@include font-size(18); @include font-size(18);
@include line-height(18); @include line-height(18);
} }
.t-copy-lead2 { %t-copy-lead2 {
@extend .t-copy; @extend %t-copy;
@include font-size(24); @include font-size(24);
@include line-height(24); @include line-height(24);
} }
.t-copy-sub1 { %t-copy-sub1 {
@extend .t-copy; @extend %t-copy;
@include font-size(14); @include font-size(14);
@include line-height(14); @include line-height(14);
} }
.t-copy-sub2 { %t-copy-sub2 {
@extend .t-copy; @extend %t-copy;
@include font-size(12); @include font-size(12);
@include line-height(12); @include line-height(12);
} }
...@@ -101,22 +101,22 @@ ...@@ -101,22 +101,22 @@
// ==================== // ====================
// actions/labels // actions/labels
.t-action1 { %t-action1 {
@include font-size(18); @include font-size(18);
@include line-height(18); @include line-height(18);
} }
.t-action2 { %t-action2 {
@include font-size(16); @include font-size(16);
@include line-height(16); @include line-height(16);
} }
.t-action3 { %t-action3 {
@include font-size(14); @include font-size(14);
@include line-height(14); @include line-height(14);
} }
.t-action4 { %t-action4 {
@include font-size(12); @include font-size(12);
@include line-height(12); @include line-height(12);
} }
...@@ -125,54 +125,54 @@ ...@@ -125,54 +125,54 @@
// ==================== // ====================
// code // code
.t-code { %t-code {
font-family: $f-monospace; font-family: $f-monospace;
} }
// ==================== // ====================
// icons // icons
.t-icon1 { %t-icon1 {
@include font-size(48); @include font-size(48);
@include line-height(48); @include line-height(48);
} }
.t-icon2 { %t-icon2 {
@include font-size(36); @include font-size(36);
@include line-height(36); @include line-height(36);
} }
.t-icon3 { %t-icon3 {
@include font-size(24); @include font-size(24);
@include line-height(24); @include line-height(24);
} }
.t-icon4 { %t-icon4 {
@include font-size(18); @include font-size(18);
@include line-height(18); @include line-height(18);
} }
.t-icon5 { %t-icon5 {
@include font-size(16); @include font-size(16);
@include line-height(16); @include line-height(16);
} }
.t-icon6 { %t-icon6 {
@include font-size(14); @include font-size(14);
@include line-height(14); @include line-height(14);
} }
.t-icon7 { %t-icon7 {
@include font-size(12); @include font-size(12);
@include line-height(12); @include line-height(12);
} }
.t-icon8 { %t-icon8 {
@include font-size(11); @include font-size(11);
@include line-height(11); @include line-height(11);
} }
.t-icon9 { %t-icon9 {
@include font-size(10); @include font-size(10);
@include line-height(10); @include line-height(10);
} }
...@@ -5,7 +5,7 @@ body.course.feature-upload { ...@@ -5,7 +5,7 @@ body.course.feature-upload {
// dialog // dialog
.wrapper-dialog { .wrapper-dialog {
@extend .ui-depth5; @extend %ui-depth5;
@include transition(all 0.05s ease-in-out); @include transition(all 0.05s ease-in-out);
position: fixed; position: fixed;
top: 0; top: 0;
...@@ -34,14 +34,14 @@ body.course.feature-upload { ...@@ -34,14 +34,14 @@ body.course.feature-upload {
text-align: left; text-align: left;
.title { .title {
@extend .t-title5; @extend %t-title5;
margin-bottom: ($baseline/2); margin-bottom: ($baseline/2);
font-weight: 600; font-weight: 600;
color: $black; color: $black;
} }
.message { .message {
@extend .t-copy-sub2; @extend %t-copy-sub2;
color: $gray; color: $gray;
} }
...@@ -59,7 +59,7 @@ body.course.feature-upload { ...@@ -59,7 +59,7 @@ body.course.feature-upload {
} }
input[type="file"] { input[type="file"] {
@extend .t-copy-sub2; @extend %t-copy-sub2;
} }
.status-upload { .status-upload {
...@@ -140,7 +140,7 @@ body.course.feature-upload { ...@@ -140,7 +140,7 @@ body.course.feature-upload {
.action-item { .action-item {
@extend .t-action4; @extend %t-action4;
display: inline-block; display: inline-block;
margin-right: ($baseline*0.75); margin-right: ($baseline*0.75);
......
...@@ -12,7 +12,7 @@ body.signup, body.signin { ...@@ -12,7 +12,7 @@ body.signup, body.signin {
.content { .content {
@include clearfix(); @include clearfix();
@extend .t-copy-base; @extend %t-copy-base;
max-width: $fg-max-width; max-width: $fg-max-width;
min-width: $fg-min-width; min-width: $fg-min-width;
width: flex-grid(12); width: flex-grid(12);
...@@ -26,14 +26,14 @@ body.signup, body.signin { ...@@ -26,14 +26,14 @@ body.signup, body.signin {
padding-bottom: ($baseline/2); padding-bottom: ($baseline/2);
h1 { h1 {
@extend .t-title3; @extend %t-title3;
margin: 0; margin: 0;
padding: 0; padding: 0;
font-weight: 600; font-weight: 600;
} }
.action { .action {
@extend .t-action3; @extend %t-action3;
position: absolute; position: absolute;
right: 0; right: 0;
top: 40%; top: 40%;
...@@ -41,7 +41,7 @@ body.signup, body.signin { ...@@ -41,7 +41,7 @@ body.signup, body.signin {
} }
.introduction { .introduction {
@extend .t-copy-sub1; @extend %t-copy-sub1;
margin: 0 0 $baseline 0; margin: 0 0 $baseline 0;
} }
} }
...@@ -69,7 +69,7 @@ body.signup, body.signin { ...@@ -69,7 +69,7 @@ body.signup, body.signin {
.action-primary { .action-primary {
@include blue-button; @include blue-button;
@extend .t-action2; @extend %t-action2;
@include transition(all $tmg-f3 linear 0s); @include transition(all $tmg-f3 linear 0s);
display: block; display: block;
width: 100%; width: 100%;
...@@ -108,7 +108,7 @@ body.signup, body.signin { ...@@ -108,7 +108,7 @@ body.signup, body.signin {
} }
label { label {
@extend .t-copy-sub1; @extend %t-copy-sub1;
@include transition(color $tmg-f3 ease-in-out 0s); @include transition(color $tmg-f3 ease-in-out 0s);
margin: 0 0 ($baseline/4) 0; margin: 0 0 ($baseline/4) 0;
...@@ -118,7 +118,7 @@ body.signup, body.signin { ...@@ -118,7 +118,7 @@ body.signup, body.signin {
} }
input, textarea { input, textarea {
@extend .t-copy-base; @extend %t-copy-base;
height: 100%; height: 100%;
width: 100%; width: 100%;
padding: ($baseline/2); padding: ($baseline/2);
...@@ -171,7 +171,7 @@ body.signup, body.signin { ...@@ -171,7 +171,7 @@ body.signup, body.signin {
} }
.tip { .tip {
@extend .t-copy-sub2; @extend %t-copy-sub2;
@include transition(color $tmg-f3 ease-in-out 0s); @include transition(color $tmg-f3 ease-in-out 0s);
display: block; display: block;
margin-top: ($baseline/4); margin-top: ($baseline/4);
...@@ -212,7 +212,7 @@ body.signup, body.signin { ...@@ -212,7 +212,7 @@ body.signup, body.signin {
width: flex-grid(4, 12); width: flex-grid(4, 12);
.bit { .bit {
@extend .t-copy-sub1; @extend %t-copy-sub1;
margin: 0 0 $baseline 0; margin: 0 0 $baseline 0;
border-bottom: 1px solid $gray-l4; border-bottom: 1px solid $gray-l4;
padding: 0 0 $baseline 0; padding: 0 0 $baseline 0;
...@@ -225,7 +225,7 @@ body.signup, body.signin { ...@@ -225,7 +225,7 @@ body.signup, body.signin {
} }
h3 { h3 {
@extend .t-title7; @extend %t-title7;
margin: 0 0 ($baseline/4) 0; margin: 0 0 ($baseline/4) 0;
color: $gray-d2; color: $gray-d2;
font-weight: 600; font-weight: 600;
...@@ -245,7 +245,7 @@ body.signup, body.signin { ...@@ -245,7 +245,7 @@ body.signup, body.signin {
position: relative; position: relative;
.action-forgotpassword { .action-forgotpassword {
@extend .t-action3; @extend %t-action3;
position: absolute; position: absolute;
top: 0; top: 0;
right: 0; right: 0;
......
...@@ -14,7 +14,7 @@ body.course.checklists { ...@@ -14,7 +14,7 @@ body.course.checklists {
// checklists - general // checklists - general
.course-checklist { .course-checklist {
@extend .ui-window; @extend %ui-window;
margin: 0 0 ($baseline*2) 0; margin: 0 0 ($baseline*2) 0;
&:last-child { &:last-child {
...@@ -23,7 +23,7 @@ body.course.checklists { ...@@ -23,7 +23,7 @@ body.course.checklists {
// visual status // visual status
.viz-checklist-status { .viz-checklist-status {
@extend .cont-text-hide; @extend %cont-text-hide;
@include size(100%,($baseline/4)); @include size(100%,($baseline/4));
position: relative; position: relative;
display: block; display: block;
...@@ -40,7 +40,7 @@ body.course.checklists { ...@@ -40,7 +40,7 @@ body.course.checklists {
background: $green; background: $green;
.int { .int {
@extend .cont-text-sr; @extend %cont-text-sr;
} }
} }
} }
...@@ -83,7 +83,7 @@ body.course.checklists { ...@@ -83,7 +83,7 @@ body.course.checklists {
} }
.checklist-status { .checklist-status {
@extend .t-copy-sub1; @extend %t-copy-sub1;
width: flex-grid(3, 9); width: flex-grid(3, 9);
float: right; float: right;
margin-top: ($baseline/2); margin-top: ($baseline/2);
...@@ -99,7 +99,7 @@ body.course.checklists { ...@@ -99,7 +99,7 @@ body.course.checklists {
} }
.status-count { .status-count {
@extend .t-copy-base; @extend %t-copy-base;
margin-left: ($baseline/4); margin-left: ($baseline/4);
margin-right: ($baseline/4); margin-right: ($baseline/4);
color: $gray-d3; color: $gray-d3;
...@@ -107,7 +107,7 @@ body.course.checklists { ...@@ -107,7 +107,7 @@ body.course.checklists {
} }
.status-amount { .status-amount {
@extend .t-copy-base; @extend %t-copy-base;
margin-left: ($baseline/4); margin-left: ($baseline/4);
color: $gray-d3; color: $gray-d3;
font-weight: 600; font-weight: 600;
...@@ -138,7 +138,7 @@ body.course.checklists { ...@@ -138,7 +138,7 @@ body.course.checklists {
.action-secondary { .action-secondary {
@include grey-button(); @include grey-button();
@extend .t-action3; @extend %t-action3;
font-weight: 400; font-weight: 400;
float: right; float: right;
...@@ -245,13 +245,13 @@ body.course.checklists { ...@@ -245,13 +245,13 @@ body.course.checklists {
} }
.task-description { .task-description {
@extend .t-copy-sub1; @extend %t-copy-sub1;
@include transition(color $tmg-f2 ease-in-out 0s); @include transition(color $tmg-f2 ease-in-out 0s);
color: $gray-l2; color: $gray-l2;
} }
.task-support { .task-support {
@extend .t-copy-sub2; @extend %t-copy-sub2;
@include transition(opacity $tmg-f2 ease-in-out 0s); @include transition(opacity $tmg-f2 ease-in-out 0s);
opacity: 0.0; opacity: 0.0;
pointer-events: none; pointer-events: none;
...@@ -272,13 +272,13 @@ body.course.checklists { ...@@ -272,13 +272,13 @@ body.course.checklists {
.action-primary { .action-primary {
@include blue-button; @include blue-button;
@extend .t-action4; @extend %t-action4;
font-weight: 600; font-weight: 600;
text-align: center; text-align: center;
} }
.action-secondary { .action-secondary {
@extend .t-action4; @extend %t-action4;
margin-top: ($baseline/2); margin-top: ($baseline/2);
} }
} }
...@@ -319,7 +319,7 @@ body.course.checklists { ...@@ -319,7 +319,7 @@ body.course.checklists {
.action-primary { .action-primary {
@include grey-button; @include grey-button;
@extend .t-action4; @extend %t-action4;
font-weight: 600; font-weight: 600;
text-align: center; text-align: center;
} }
......
...@@ -58,8 +58,8 @@ body.dashboard { ...@@ -58,8 +58,8 @@ body.dashboard {
} }
.action-create-course { .action-create-course {
@extend .btn-primary-green; @extend %btn-primary-green;
@extend .t-action3; @extend %t-action3;
} }
} }
} }
...@@ -72,8 +72,8 @@ body.dashboard { ...@@ -72,8 +72,8 @@ body.dashboard {
overflow: hidden; overflow: hidden;
.ui-toggle-control { .ui-toggle-control {
@extend .ui-depth2; @extend %ui-depth2;
@extend .btn-secondary-gray; @extend %btn-secondary-gray;
@include clearfix(); @include clearfix();
display: block; display: block;
text-align: left; text-align: left;
...@@ -85,14 +85,14 @@ body.dashboard { ...@@ -85,14 +85,14 @@ body.dashboard {
} }
.label { .label {
@extend .t-action3; @extend %t-action3;
float: left; float: left;
width: flex-grid(8, 9); width: flex-grid(8, 9);
margin: 3px flex-gutter() 0 0; margin: 3px flex-gutter() 0 0;
} }
.icon-remove-sign { .icon-remove-sign {
@extend .t-action1; @extend %t-action1;
@include transform(rotate(45deg)); @include transform(rotate(45deg));
@include transform-origin(center center); @include transform-origin(center center);
@include transition(all $tmg-f1 linear 0s); @include transition(all $tmg-f1 linear 0s);
...@@ -102,7 +102,7 @@ body.dashboard { ...@@ -102,7 +102,7 @@ body.dashboard {
} }
.ui-toggle-target { .ui-toggle-target {
@extend .ui-depth1; @extend %ui-depth1;
@include transition(opacity $tmg-f1 ease-in-out 0s); @include transition(opacity $tmg-f1 ease-in-out 0s);
position: relative; position: relative;
top: -2px; top: -2px;
...@@ -136,7 +136,7 @@ body.dashboard { ...@@ -136,7 +136,7 @@ body.dashboard {
margin-top: $baseline; margin-top: $baseline;
.title { .title {
@extend .t-title7; @extend %t-title7;
margin-bottom: ($baseline/4); margin-bottom: ($baseline/4);
font-weight: 700; font-weight: 700;
color: $gray-d1; color: $gray-d1;
...@@ -154,8 +154,8 @@ body.dashboard { ...@@ -154,8 +154,8 @@ body.dashboard {
} }
.action-primary { .action-primary {
@extend .btn-primary-blue; @extend %btn-primary-blue;
@extend .t-action3; @extend %t-action3;
} }
// specific - request button // specific - request button
...@@ -203,7 +203,7 @@ body.dashboard { ...@@ -203,7 +203,7 @@ body.dashboard {
.status-update { .status-update {
.label { .label {
@extend .cont-text-sr; @extend %cont-text-sr;
} }
.value { .value {
...@@ -232,7 +232,7 @@ body.dashboard { ...@@ -232,7 +232,7 @@ body.dashboard {
} }
.value-formal { .value-formal {
@extend .t-title5; @extend %t-title5;
margin: ($baseline/2); margin: ($baseline/2);
font-weight: 700; font-weight: 700;
...@@ -242,7 +242,7 @@ body.dashboard { ...@@ -242,7 +242,7 @@ body.dashboard {
} }
.value-description { .value-description {
@extend .t-copy-sub1; @extend %t-copy-sub1;
position: relative; position: relative;
color: $white; color: $white;
opacity: 0.85; opacity: 0.85;
...@@ -253,7 +253,7 @@ body.dashboard { ...@@ -253,7 +253,7 @@ body.dashboard {
&.is-unrequested { &.is-unrequested {
.title { .title {
@extend .cont-text-sr; @extend %cont-text-sr;
} }
} }
...@@ -336,21 +336,21 @@ body.dashboard { ...@@ -336,21 +336,21 @@ body.dashboard {
// encompassing course link // encompassing course link
.course-link { .course-link {
@extend .ui-depth2; @extend %ui-depth2;
width: flex-grid(7, 9); width: flex-grid(7, 9);
margin-right: flex-gutter(); margin-right: flex-gutter();
} }
// course title // course title
.course-title { .course-title {
@extend .t-title4; @extend %t-title4;
margin: 0 ($baseline*2) ($baseline/4) 0; margin: 0 ($baseline*2) ($baseline/4) 0;
font-weight: 300; font-weight: 300;
} }
// course metadata // course metadata
.course-metadata { .course-metadata {
@extend .t-copy-sub1; @extend %t-copy-sub1;
@include transition(opacity $tmg-f1 ease-in-out 0); @include transition(opacity $tmg-f1 ease-in-out 0);
color: $gray; color: $gray;
opacity: 0.75; opacity: 0.75;
...@@ -375,20 +375,20 @@ body.dashboard { ...@@ -375,20 +375,20 @@ body.dashboard {
} }
.label { .label {
@extend .cont-text-sr; @extend %cont-text-sr;
} }
} }
} }
.course-actions { .course-actions {
@extend .ui-depth3; @extend %ui-depth3;
position: static; position: static;
width: flex-grid(2, 9); width: flex-grid(2, 9);
text-align: right; text-align: right;
// view live button // view live button
.view-live-button { .view-live-button {
@extend .ui-depth3; @extend %ui-depth3;
@include transition(opacity $tmg-f2 ease-in-out 0); @include transition(opacity $tmg-f2 ease-in-out 0);
@include box-sizing(border-box); @include box-sizing(border-box);
padding: ($baseline/2); padding: ($baseline/2);
...@@ -447,7 +447,7 @@ body.dashboard { ...@@ -447,7 +447,7 @@ body.dashboard {
} }
label { label {
@extend .t-title7; @extend %t-title7;
display: block; display: block;
font-weight: 700; font-weight: 700;
} }
...@@ -460,7 +460,7 @@ body.dashboard { ...@@ -460,7 +460,7 @@ body.dashboard {
} }
.new-course-name { .new-course-name {
@extend .t-title5; @extend %t-title5;
font-weight: 300; font-weight: 300;
} }
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
body.course.export { body.course.export {
.export-overview { .export-overview {
@extend .ui-window; @extend %ui-window;
@include clearfix; @include clearfix;
padding: 30px 40px; padding: 30px 40px;
} }
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
body.course.import { body.course.import {
.import-overview { .import-overview {
@extend .ui-window; @extend %ui-window;
@include clearfix; @include clearfix;
padding: 30px 40px; padding: 30px 40px;
} }
......
...@@ -18,7 +18,7 @@ body.index { ...@@ -18,7 +18,7 @@ body.index {
} }
.content { .content {
@extend .t-copy-base; @extend %t-copy-base;
@include clearfix(); @include clearfix();
max-width: $fg-max-width; max-width: $fg-max-width;
min-width: $fg-min-width; min-width: $fg-min-width;
...@@ -62,7 +62,7 @@ body.index { ...@@ -62,7 +62,7 @@ body.index {
color: $white; color: $white;
h1 { h1 {
@extend .t-title2; @extend %t-title2;
float: none; float: none;
margin: 0 0 ($baseline/2) 0; margin: 0 0 ($baseline/2) 0;
border-bottom: 1px solid $blue-l1; border-bottom: 1px solid $blue-l1;
...@@ -81,7 +81,7 @@ body.index { ...@@ -81,7 +81,7 @@ body.index {
} }
.tagline { .tagline {
@extend .t-title4; @extend %t-title4;
margin: 0; margin: 0;
color: $blue-l3; color: $blue-l3;
} }
...@@ -196,13 +196,13 @@ body.index { ...@@ -196,13 +196,13 @@ body.index {
margin-top: -($baseline/4); margin-top: -($baseline/4);
h3 { h3 {
@extend .t-title4; @extend %t-title4;
margin: 0 0 ($baseline/2) 0; margin: 0 0 ($baseline/2) 0;
font-weight: 600; font-weight: 600;
} }
> p { > p {
@extend .t-copy-lead1; @extend %t-copy-lead1;
color: $gray-d1; color: $gray-d1;
} }
...@@ -212,7 +212,7 @@ body.index { ...@@ -212,7 +212,7 @@ body.index {
} }
.list-proofpoints { .list-proofpoints {
@extend .t-copy-sub1; @extend %t-copy-sub1;
@include clearfix(); @include clearfix();
width: flex-grid(9, 9); width: flex-grid(9, 9);
margin: ($baseline*1.5) 0 0 0; margin: ($baseline*1.5) 0 0 0;
...@@ -231,14 +231,14 @@ body.index { ...@@ -231,14 +231,14 @@ body.index {
color: $gray-l1; color: $gray-l1;
.title { .title {
@extend .t-copy-base; @extend %t-copy-base;
margin: 0 0 ($baseline/4) 0; margin: 0 0 ($baseline/4) 0;
font-weight: 500; font-weight: 500;
color: $gray-d3; color: $gray-d3;
} }
&:hover { &:hover {
@extend .fake-link; @extend %ui-fake-link;
box-shadow: 0 1px ($baseline/10) $shadow-l1; box-shadow: 0 1px ($baseline/10) $shadow-l1;
background: $blue-l5; background: $blue-l5;
top: -($baseline/5); top: -($baseline/5);
...@@ -323,7 +323,7 @@ body.index { ...@@ -323,7 +323,7 @@ body.index {
text-align: center; text-align: center;
&.action-primary { &.action-primary {
@extend .t-action1; @extend %t-action1;
@include blue-button; @include blue-button;
padding: ($baseline*0.75) ($baseline/2); padding: ($baseline*0.75) ($baseline/2);
font-weight: 600; font-weight: 600;
...@@ -332,7 +332,7 @@ body.index { ...@@ -332,7 +332,7 @@ body.index {
} }
&.action-secondary { &.action-secondary {
@extend .t-action3; @extend %t-action3;
margin-top: ($baseline/2); margin-top: ($baseline/2);
} }
} }
......
...@@ -630,7 +630,7 @@ body.course.outline { ...@@ -630,7 +630,7 @@ body.course.outline {
} }
label { label {
@extend .t-copy-sub1; @extend %t-copy-sub1;
margin-bottom: ($baseline/4); margin-bottom: ($baseline/4);
} }
} }
......
...@@ -9,7 +9,7 @@ body.course.settings { ...@@ -9,7 +9,7 @@ body.course.settings {
} }
.content-primary { .content-primary {
@extend .ui-window; @extend %ui-window;
width: flex-grid(9, 12); width: flex-grid(9, 12);
margin-right: flex-gutter(); margin-right: flex-gutter();
padding: $baseline ($baseline*1.5); padding: $baseline ($baseline*1.5);
...@@ -72,7 +72,7 @@ body.course.settings { ...@@ -72,7 +72,7 @@ body.course.settings {
} }
.tip { .tip {
@extend .t-copy-sub2; @extend %t-copy-sub2;
width: flex-grid(5, 9); width: flex-grid(5, 9);
float: right; float: right;
margin-top: ($baseline/2); margin-top: ($baseline/2);
...@@ -92,12 +92,12 @@ body.course.settings { ...@@ -92,12 +92,12 @@ body.course.settings {
// in form -UI hints/tips/messages // in form -UI hints/tips/messages
.instructions { .instructions {
@extend .t-copy-sub1; @extend %t-copy-sub1;
margin: 0 0 $baseline 0; margin: 0 0 $baseline 0;
} }
.tip { .tip {
@extend .t-copy-sub2; @extend %t-copy-sub2;
@include transition(color $tmg-f2 ease-in-out 0s); @include transition(color $tmg-f2 ease-in-out 0s);
display: block; display: block;
margin-top: ($baseline/4); margin-top: ($baseline/4);
...@@ -105,7 +105,7 @@ body.course.settings { ...@@ -105,7 +105,7 @@ body.course.settings {
} }
.message-error { .message-error {
@extend .t-copy-sub1; @extend %t-copy-sub1;
display: block; display: block;
margin-top: ($baseline/4); margin-top: ($baseline/4);
margin-bottom: ($baseline/2); margin-bottom: ($baseline/2);
...@@ -115,12 +115,12 @@ body.course.settings { ...@@ -115,12 +115,12 @@ body.course.settings {
// buttons // buttons
.remove-item { .remove-item {
@include white-button; @include white-button;
@extend .t-action3; @extend %t-action3;
font-weight: 400; font-weight: 400;
} }
.new-button { .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); @include font-size(14);
} }
...@@ -154,7 +154,7 @@ body.course.settings { ...@@ -154,7 +154,7 @@ body.course.settings {
} }
label { label {
@extend .t-copy-sub1; @extend %t-copy-sub1;
@include transition(color $tmg-f2 ease-in-out 0s); @include transition(color $tmg-f2 ease-in-out 0s);
margin: 0 0 ($baseline/4) 0; margin: 0 0 ($baseline/4) 0;
font-weight: 400; font-weight: 400;
...@@ -165,7 +165,7 @@ body.course.settings { ...@@ -165,7 +165,7 @@ body.course.settings {
} }
input, textarea { input, textarea {
@extend .t-copy-base; @extend %t-copy-base;
@include placeholder($gray-l4); @include placeholder($gray-l4);
@include size(100%,100%); @include size(100%,100%);
padding: ($baseline/2); padding: ($baseline/2);
...@@ -265,7 +265,7 @@ body.course.settings { ...@@ -265,7 +265,7 @@ body.course.settings {
} }
input, textarea { input, textarea {
@extend .t-copy-lead1; @extend %t-copy-lead1;
box-shadow: none; box-shadow: none;
border: none; border: none;
background: none; background: none;
...@@ -301,7 +301,7 @@ body.course.settings { ...@@ -301,7 +301,7 @@ body.course.settings {
padding: ($baseline/2) 0 0 0; padding: ($baseline/2) 0 0 0;
.title { .title {
@extend .t-copy-sub1; @extend %t-copy-sub1;
margin: 0 0 ($baseline/10) 0; margin: 0 0 ($baseline/10) 0;
padding: 0 ($baseline/2); padding: 0 ($baseline/2);
...@@ -315,7 +315,7 @@ body.course.settings { ...@@ -315,7 +315,7 @@ body.course.settings {
padding: 0 ($baseline/2) ($baseline/2) ($baseline/2); padding: 0 ($baseline/2) ($baseline/2) ($baseline/2);
.link-courseURL { .link-courseURL {
@extend .t-copy-lead1; @extend %t-copy-lead1;
@include box-sizing(border-box); @include box-sizing(border-box);
display: block; display: block;
width: 100%; width: 100%;
...@@ -337,11 +337,11 @@ body.course.settings { ...@@ -337,11 +337,11 @@ body.course.settings {
.action-primary { .action-primary {
@include blue-button(); @include blue-button();
@extend .t-action3; @extend %t-action3;
font-weight: 600; font-weight: 600;
[class^="icon-"] { [class^="icon-"] {
@extend .t-icon5; @extend %t-icon5;
display: inline-block; display: inline-block;
vertical-align: middle; vertical-align: middle;
margin-top: -3px; margin-top: -3px;
...@@ -460,7 +460,7 @@ body.course.settings { ...@@ -460,7 +460,7 @@ body.course.settings {
} }
.msg { .msg {
@extend .t-copy-sub2; @extend %t-copy-sub2;
display: block; display: block;
margin-top: ($baseline/2); margin-top: ($baseline/2);
color: $gray-l3; color: $gray-l3;
...@@ -478,7 +478,7 @@ body.course.settings { ...@@ -478,7 +478,7 @@ body.course.settings {
} }
.action-upload-image { .action-upload-image {
@extend .ui-btn-flat-outline; @extend %ui-btn-flat-outline;
float: right; float: right;
width: flex-grid(2,9); width: flex-grid(2,9);
margin-top: ($baseline/4); margin-top: ($baseline/4);
...@@ -820,7 +820,7 @@ body.course.settings { ...@@ -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 // specific to code mirror instance in JSON policy editing, need to sync up with other similar code mirror UIs
.CodeMirror { .CodeMirror {
@extend .t-copy-base; @extend %t-copy-base;
@include box-sizing(border-box); @include box-sizing(border-box);
box-shadow: 0 1px 2px rgba(0, 0, 0, .1) inset; box-shadow: 0 1px 2px rgba(0, 0, 0, .1) inset;
@include linear-gradient($lightGrey, tint($lightGrey, 90%)); @include linear-gradient($lightGrey, tint($lightGrey, 90%));
......
...@@ -171,7 +171,7 @@ body.course.static-pages { ...@@ -171,7 +171,7 @@ body.course.static-pages {
} }
.static-page-details { .static-page-details {
@extend .ui-window; @extend %ui-window;
padding: 32px 40px; padding: 32px 40px;
.row { .row {
......
...@@ -13,7 +13,7 @@ body.course.textbooks { ...@@ -13,7 +13,7 @@ body.course.textbooks {
margin-right: flex-gutter(); margin-right: flex-gutter();
.no-textbook-content { .no-textbook-content {
@extend .ui-well; @extend %ui-well;
padding: ($baseline*2); padding: ($baseline*2);
background-color: $gray-l4; background-color: $gray-l4;
text-align: center; text-align: center;
...@@ -30,7 +30,7 @@ body.course.textbooks { ...@@ -30,7 +30,7 @@ body.course.textbooks {
} }
.textbook { .textbook {
@extend .ui-window; @extend %ui-window;
position: relative; position: relative;
.view-textbook { .view-textbook {
...@@ -42,7 +42,7 @@ body.course.textbooks { ...@@ -42,7 +42,7 @@ body.course.textbooks {
} }
.textbook-title { .textbook-title {
@extend .t-title4; @extend %t-title4;
margin-right: ($baseline*14); margin-right: ($baseline*14);
font-weight: bold; font-weight: bold;
} }
...@@ -71,7 +71,7 @@ body.course.textbooks { ...@@ -71,7 +71,7 @@ body.course.textbooks {
margin-left: $baseline; margin-left: $baseline;
.chapter { .chapter {
@extend .t-copy-sub2; @extend %t-copy-sub2;
margin-bottom: ($baseline/4); margin-bottom: ($baseline/4);
border-bottom: 1px solid $gray-l4; border-bottom: 1px solid $gray-l4;
...@@ -106,16 +106,16 @@ body.course.textbooks { ...@@ -106,16 +106,16 @@ body.course.textbooks {
.view { .view {
@include blue-button; @include blue-button;
@extend .t-action4; @extend %t-action4;
} }
.edit { .edit {
@include blue-button; @include blue-button;
@extend .t-action4; @extend %t-action4;
} }
.delete { .delete {
@extend .ui-btn-non; @extend %ui-btn-non;
} }
} }
...@@ -160,7 +160,7 @@ body.course.textbooks { ...@@ -160,7 +160,7 @@ body.course.textbooks {
.action-primary { .action-primary {
@include blue-button; @include blue-button;
@extend .t-action2; @extend %t-action2;
@include transition(all .15s); @include transition(all .15s);
display: inline-block; display: inline-block;
padding: ($baseline/5) $baseline; padding: ($baseline/5) $baseline;
...@@ -170,7 +170,7 @@ body.course.textbooks { ...@@ -170,7 +170,7 @@ body.course.textbooks {
.action-secondary { .action-secondary {
@include grey-button; @include grey-button;
@extend .t-action2; @extend %t-action2;
@include transition(all .15s); @include transition(all .15s);
display: inline-block; display: inline-block;
padding: ($baseline/5) $baseline; padding: ($baseline/5) $baseline;
...@@ -182,7 +182,7 @@ body.course.textbooks { ...@@ -182,7 +182,7 @@ body.course.textbooks {
} }
.copy { .copy {
@extend .t-copy-sub2; @extend %t-copy-sub2;
margin: ($baseline) 0 ($baseline/2) 0; margin: ($baseline) 0 ($baseline/2) 0;
color: $gray; color: $gray;
...@@ -196,7 +196,7 @@ body.course.textbooks { ...@@ -196,7 +196,7 @@ body.course.textbooks {
.chapters-fields, .chapters-fields,
.textbook-fields { .textbook-fields {
@extend .cont-no-list; @extend %cont-no-list;
.field { .field {
margin: 0 0 ($baseline*0.75) 0; margin: 0 0 ($baseline*0.75) 0;
...@@ -222,7 +222,7 @@ body.course.textbooks { ...@@ -222,7 +222,7 @@ body.course.textbooks {
} }
label { label {
@extend .t-copy-sub1; @extend %t-copy-sub1;
@include transition(color, 0.15s, ease-in-out); @include transition(color, 0.15s, ease-in-out);
margin: 0 0 ($baseline/4) 0; margin: 0 0 ($baseline/4) 0;
...@@ -233,13 +233,13 @@ body.course.textbooks { ...@@ -233,13 +233,13 @@ body.course.textbooks {
&.add-textbook-name label { &.add-textbook-name label {
@extend .t-title5; @extend %t-title5;
} }
//this section is borrowed from _account.scss - we should clean up and unify later //this section is borrowed from _account.scss - we should clean up and unify later
input, textarea { input, textarea {
@extend .t-copy-base; @extend %t-copy-base;
height: 100%; height: 100%;
width: 100%; width: 100%;
padding: ($baseline/2); padding: ($baseline/2);
...@@ -292,7 +292,7 @@ body.course.textbooks { ...@@ -292,7 +292,7 @@ body.course.textbooks {
} }
.tip { .tip {
@extend .t-copy-sub2; @extend %t-copy-sub2;
@include transition(color, 0.15s, ease-in-out); @include transition(color, 0.15s, ease-in-out);
display: block; display: block;
margin-top: ($baseline/4); margin-top: ($baseline/4);
...@@ -328,7 +328,7 @@ body.course.textbooks { ...@@ -328,7 +328,7 @@ body.course.textbooks {
} }
.action-upload { .action-upload {
@extend .ui-btn-flat-outline; @extend %ui-btn-flat-outline;
position: absolute; position: absolute;
top: 3px; top: 3px;
right: 0; right: 0;
...@@ -356,7 +356,7 @@ body.course.textbooks { ...@@ -356,7 +356,7 @@ body.course.textbooks {
.action-add-chapter { .action-add-chapter {
@extend .ui-btn-flat-outline; @extend %ui-btn-flat-outline;
@include font-size(16); @include font-size(16);
display: block; display: block;
width: 100%; width: 100%;
......
...@@ -466,7 +466,7 @@ body.course.unit { ...@@ -466,7 +466,7 @@ body.course.unit {
.action-primary { .action-primary {
@include blue-button; @include blue-button;
@extend .t-action2; @extend %t-action2;
@include transition(all .15s); @include transition(all .15s);
display: inline-block; display: inline-block;
padding: ($baseline/5) $baseline; padding: ($baseline/5) $baseline;
...@@ -476,7 +476,7 @@ body.course.unit { ...@@ -476,7 +476,7 @@ body.course.unit {
.action-secondary { .action-secondary {
@include grey-button; @include grey-button;
@extend .t-action2; @extend %t-action2;
@include transition(all .15s); @include transition(all .15s);
display: inline-block; display: inline-block;
padding: ($baseline/5) $baseline; padding: ($baseline/5) $baseline;
...@@ -500,7 +500,7 @@ body.course.unit { ...@@ -500,7 +500,7 @@ body.course.unit {
//Component Name //Component Name
.component-name { .component-name {
@extend .t-copy-sub1; @extend %t-copy-sub1;
width: 50%; width: 50%;
color: $white; color: $white;
font-weight: 600; font-weight: 600;
...@@ -637,7 +637,7 @@ body.course.unit { ...@@ -637,7 +637,7 @@ body.course.unit {
} }
.setting-label { .setting-label {
@extend .t-copy-sub1; @extend %t-copy-sub1;
@include transition(color $tmg-f2 ease-in-out 0s); @include transition(color $tmg-f2 ease-in-out 0s);
vertical-align: middle; vertical-align: middle;
display: inline-block; display: inline-block;
...@@ -794,8 +794,8 @@ body.course.unit { ...@@ -794,8 +794,8 @@ body.course.unit {
} }
.create-setting { .create-setting {
@extend .ui-btn-flat-outline; @extend %ui-btn-flat-outline;
@extend .t-action3; @extend %t-action3;
display: block; display: block;
width: 100%; width: 100%;
padding: ($baseline/2); padding: ($baseline/2);
...@@ -974,7 +974,7 @@ body.unit { ...@@ -974,7 +974,7 @@ body.unit {
.unit-id { .unit-id {
.label { .label {
@extend .t-title7; @extend %t-title7;
margin-bottom: ($baseline/4); margin-bottom: ($baseline/4);
color: $gray-d1; color: $gray-d1;
} }
......
...@@ -22,7 +22,7 @@ body.course.users { ...@@ -22,7 +22,7 @@ body.course.users {
.content { .content {
.introduction { .introduction {
@extend .t-copy-sub1; @extend %t-copy-sub1;
margin: 0 0 ($baseline*2) 0; margin: 0 0 ($baseline*2) 0;
} }
} }
...@@ -56,7 +56,7 @@ body.course.users { ...@@ -56,7 +56,7 @@ body.course.users {
.action-primary { .action-primary {
@include green-button(); // overwriting for the sake of syncing older green button styles for now @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; padding: ($baseline/2) $baseline;
} }
} }
...@@ -80,7 +80,7 @@ body.course.users { ...@@ -80,7 +80,7 @@ body.course.users {
.user-list { .user-list {
.user-item { .user-item {
@extend .ui-window; @extend %ui-window;
@include clearfix(); @include clearfix();
position: relative; position: relative;
width: flex-grid(9, 9); width: flex-grid(9, 9);
...@@ -98,7 +98,7 @@ body.course.users { ...@@ -98,7 +98,7 @@ body.course.users {
// ELEM: item - flag // ELEM: item - flag
.flag-role { .flag-role {
@extend .ui-badge; @extend %ui-badge;
color: $white; color: $white;
.msg-you { .msg-you {
...@@ -132,7 +132,7 @@ body.course.users { ...@@ -132,7 +132,7 @@ body.course.users {
} }
.user-username { .user-username {
@extend .t-title4; @extend %t-title4;
@include transition(color $tmg-f2 ease-in-out 0s); @include transition(color $tmg-f2 ease-in-out 0s);
margin: 0 ($baseline/2) ($baseline/10) 0; margin: 0 ($baseline/2) ($baseline/10) 0;
color: $gray-d4; color: $gray-d4;
...@@ -140,7 +140,7 @@ body.course.users { ...@@ -140,7 +140,7 @@ body.course.users {
} }
.user-email { .user-email {
@extend .t-title6; @extend %t-title6;
} }
} }
...@@ -172,7 +172,7 @@ body.course.users { ...@@ -172,7 +172,7 @@ body.course.users {
} }
.delete { .delete {
@extend .ui-btn-non; @extend %ui-btn-non;
} }
// HACK: nasty reset needed due to base.scss // HACK: nasty reset needed due to base.scss
...@@ -187,7 +187,7 @@ body.course.users { ...@@ -187,7 +187,7 @@ body.course.users {
&.add-admin-role { &.add-admin-role {
@include blue-button; @include blue-button;
@extend .t-action2; @extend %t-action2;
@include transition(all .15s); @include transition(all .15s);
display: inline-block; display: inline-block;
padding: ($baseline/5) $baseline; padding: ($baseline/5) $baseline;
...@@ -196,7 +196,7 @@ body.course.users { ...@@ -196,7 +196,7 @@ body.course.users {
&.remove-admin-role { &.remove-admin-role {
@include grey-button; @include grey-button;
@extend .t-action2; @extend %t-action2;
@include transition(all .15s); @include transition(all .15s);
display: inline-block; display: inline-block;
padding: ($baseline/5) $baseline; padding: ($baseline/5) $baseline;
...@@ -205,7 +205,7 @@ body.course.users { ...@@ -205,7 +205,7 @@ body.course.users {
} }
.notoggleforyou { .notoggleforyou {
@extend .t-copy-sub1; @extend %t-copy-sub1;
color: $gray-l2; color: $gray-l2;
} }
} }
......
...@@ -6,39 +6,27 @@ ...@@ -6,39 +6,27 @@
<%namespace name='static' file='static_content.html'/> <%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"> <%block name="jsextra">
<script src="${static.url('js/vendor/mustache.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.iframe-transport.js')}"> </script>
<script src="${static.url('js/vendor/jQuery-File-Upload/js/jquery.fileupload.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>
<%block name="content"> <%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"> <div class="wrapper-mast wrapper">
<header class="mast has-actions has-subtitle"> <header class="mast has-actions has-subtitle">
<h1 class="page-header"> <h1 class="page-header">
...@@ -62,7 +50,7 @@ ...@@ -62,7 +50,7 @@
<div class="page-actions"> <div class="page-actions">
<input type="text" class="asset-search-input search wip-box" placeholder="search assets" style="display:none"/> <input type="text" class="asset-search-input search wip-box" placeholder="search assets" style="display:none"/>
</div> </div>
<article class="asset-library" data-remove-asset-callback-url='${remove_asset_callback_url}'> <article class="asset-library">
<table> <table>
<thead> <thead>
<tr> <tr>
...@@ -73,31 +61,8 @@ ...@@ -73,31 +61,8 @@
<th class="delete-col"></th> <th class="delete-col"></th>
</tr> </tr>
</thead> </thead>
<tbody id="asset_table_body"> <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> </tbody>
</table> </table>
<nav class="pagination wip-box"> <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 ...@@ -76,8 +76,8 @@ urlpatterns = ('', # nopep8
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/assets/(?P<name>[^/]+)$', url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/assets/(?P<name>[^/]+)$',
'contentstore.views.asset_index', name='asset_index'), 'contentstore.views.asset_index', name='asset_index'),
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/assets/(?P<name>[^/]+)/remove$', url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/assets/(?P<name>[^/]+)/(?P<asset_id>.+)?.*$',
'contentstore.views.assets.remove_asset', name='remove_asset'), 'contentstore.views.assets.update_asset', name='update_asset'),
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/textbooks/(?P<name>[^/]+)$', url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/textbooks/(?P<name>[^/]+)$',
'contentstore.views.textbook_index', name='textbook_index'), 'contentstore.views.textbook_index', name='textbook_index'),
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/textbooks/(?P<name>[^/]+)/new$', url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/textbooks/(?P<name>[^/]+)/new$',
......
...@@ -43,18 +43,18 @@ LOGGER.info("Loading the lettuce acceptance testing terrain file...") ...@@ -43,18 +43,18 @@ LOGGER.info("Loading the lettuce acceptance testing terrain file...")
MAX_VALID_BROWSER_ATTEMPTS = 20 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 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')} 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 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) url = 'http://saucelabs.com/rest/v1/{}/jobs/{}'.format(config['username'], world.jobid)
body_content = dumps({"passed": passed}) body_content = dumps({"passed": passed})
base64string = encodestring('{}:{}'.format(config['username'], config['access-key']))[:-1] base64string = encodestring('{}:{}'.format(config['username'], config['access-key']))[:-1]
...@@ -63,7 +63,7 @@ def set_job_status(jobid, passed=True): ...@@ -63,7 +63,7 @@ def set_job_status(jobid, passed=True):
return result.status_code == 200 return result.status_code == 200
def make_desired_capabilities(): def make_saucelabs_desired_capabilities():
""" """
Returns a DesiredCapabilities object corresponding to the environment sauce parameters Returns a DesiredCapabilities object corresponding to the environment sauce parameters
""" """
...@@ -88,9 +88,9 @@ def initial_setup(server): ...@@ -88,9 +88,9 @@ def initial_setup(server):
""" """
Launch the browser once before executing the tests. 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') browser_driver = getattr(settings, 'LETTUCE_BROWSER', 'chrome')
# There is an issue with ChromeDriver2 r195627 on Ubuntu # There is an issue with ChromeDriver2 r195627 on Ubuntu
...@@ -121,15 +121,26 @@ def initial_setup(server): ...@@ -121,15 +121,26 @@ def initial_setup(server):
world.browser.driver.set_window_size(1280, 1024) world.browser.driver.set_window_size(1280, 1024)
else: elif world.LETTUCE_SELENIUM_CLIENT == 'saucelabs':
config = get_username_and_key() config = get_saucelabs_username_and_key()
world.browser = Browser( world.browser = Browser(
'remote', 'remote',
url="http://{}:{}@ondemand.saucelabs.com:80/wd/hub".format(config['username'], config['access-key']), 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) 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') world.absorb(world.browser.driver.session_id, 'jobid')
...@@ -140,7 +151,7 @@ def reset_data(scenario): ...@@ -140,7 +151,7 @@ def reset_data(scenario):
envs/acceptance.py file: mitx_all/db/test_mitx.db envs/acceptance.py file: mitx_all/db/test_mitx.db
""" """
LOGGER.debug("Flushing the test database...") LOGGER.debug("Flushing the test database...")
call_command('flush', interactive=False) call_command('flush', interactive=False, verbosity=0)
world.absorb({}, 'scenario_dict') world.absorb({}, 'scenario_dict')
...@@ -183,6 +194,6 @@ def teardown_browser(total): ...@@ -183,6 +194,6 @@ def teardown_browser(total):
""" """
Quit the browser after executing the tests. Quit the browser after executing the tests.
""" """
if world.SAUCE_ENABLED: if world.LETTUCE_SELENIUM_CLIENT == 'saucelabs':
set_job_status(world.jobid, total.scenarios_ran == total.scenarios_passed) set_saucelabs_job_status(world.jobid, total.scenarios_ran == total.scenarios_passed)
world.browser.quit() world.browser.quit()
...@@ -138,9 +138,10 @@ def should_have_link_with_path_and_text(step, path, text): ...@@ -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') @step(r'should( not)? see "(.*)" (?:somewhere|anywhere) (?:in|on) (?:the|this) page')
def should_see_in_the_page(step, doesnt_appear, text): def should_see_in_the_page(step, doesnt_appear, text):
multiplier = 1 if world.LETTUCE_SELENIUM_CLIENT == 'saucelabs':
if world.SAUCE_ENABLED:
multiplier = 2 multiplier = 2
else:
multiplier = 1
if doesnt_appear: if doesnt_appear:
assert world.browser.is_text_not_present(text, wait_time=5*multiplier) assert world.browser.is_text_not_present(text, wait_time=5*multiplier)
else: else:
...@@ -152,8 +153,8 @@ def i_am_logged_in(step): ...@@ -152,8 +153,8 @@ def i_am_logged_in(step):
world.create_user('robot', 'test') world.create_user('robot', 'test')
world.log_in(username='robot', password='test') world.log_in(username='robot', password='test')
world.browser.visit(django_url('/')) world.browser.visit(django_url('/'))
# You should not see the login link dash_css = 'section.container.dashboard'
assert world.is_css_not_present('a#login') assert world.is_css_present(dash_css)
@step(u'I am an edX user$') @step(u'I am an edX user$')
......
...@@ -21,6 +21,7 @@ from xmodule.exceptions import NotFoundError, ProcessingError ...@@ -21,6 +21,7 @@ from xmodule.exceptions import NotFoundError, ProcessingError
from xblock.fields import Scope, String, Boolean, Dict, Integer, Float from xblock.fields import Scope, String, Boolean, Dict, Integer, Float
from .fields import Timedelta, Date from .fields import Timedelta, Date
from django.utils.timezone import UTC from django.utils.timezone import UTC
from django.utils.translation import ugettext as _
log = logging.getLogger("mitx.courseware") log = logging.getLogger("mitx.courseware")
...@@ -348,7 +349,8 @@ class CapaModule(CapaFields, XModule): ...@@ -348,7 +349,8 @@ class CapaModule(CapaFields, XModule):
final_check = (self.attempts >= self.max_attempts - 1) final_check = (self.attempts >= self.max_attempts - 1)
else: else:
final_check = False 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): def should_show_check_button(self):
""" """
......
...@@ -14,13 +14,29 @@ import textwrap ...@@ -14,13 +14,29 @@ import textwrap
log = logging.getLogger("mitx.courseware") log = logging.getLogger("mitx.courseware")
V1_SETTINGS_ATTRIBUTES = [ V1_SETTINGS_ATTRIBUTES = [
"display_name", "max_attempts", "graded", "accept_file_upload", "display_name",
"skip_spelling_checks", "due", "graceperiod", "weight", "min_to_calibrate", "max_attempts",
"max_to_calibrate", "peer_grader_count", "required_peer_grading", "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", V1_STUDENT_ATTRIBUTES = [
"student_attempts", "ready_to_reset", "old_task_states"] "current_task_number",
"task_states",
"state",
"student_attempts",
"ready_to_reset",
"old_task_states",
]
V1_ATTRIBUTES = V1_SETTINGS_ATTRIBUTES + V1_STUDENT_ATTRIBUTES V1_ATTRIBUTES = V1_SETTINGS_ATTRIBUTES + V1_STUDENT_ATTRIBUTES
...@@ -288,6 +304,14 @@ class CombinedOpenEndedFields(object): ...@@ -288,6 +304,14 @@ class CombinedOpenEndedFields(object):
scope=Scope.settings, scope=Scope.settings,
values={"min": 1, "step": "1", "max": 5} 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( markdown = String(
help="Markdown source of this module", help="Markdown source of this module",
default=textwrap.dedent("""\ default=textwrap.dedent("""\
......
...@@ -15,9 +15,9 @@ from PIL import Image ...@@ -15,9 +15,9 @@ from PIL import Image
class StaticContent(object): class StaticContent(object):
def __init__(self, loc, name, content_type, data, last_modified_at=None, thumbnail_location=None, import_path=None, 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.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.content_type = content_type
self._data = data self._data = data
self.length = length self.length = length
...@@ -26,6 +26,7 @@ class StaticContent(object): ...@@ -26,6 +26,7 @@ class StaticContent(object):
# optional information about where this file was imported from. This is needed to support import/export # optional information about where this file was imported from. This is needed to support import/export
# cycles # cycles
self.import_path = import_path self.import_path = import_path
self.locked = locked
@property @property
def is_thumbnail(self): def is_thumbnail(self):
...@@ -133,10 +134,10 @@ class StaticContent(object): ...@@ -133,10 +134,10 @@ class StaticContent(object):
class StaticContentStream(StaticContent): class StaticContentStream(StaticContent):
def __init__(self, loc, name, content_type, stream, last_modified_at=None, thumbnail_location=None, import_path=None, 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, super(StaticContentStream, self).__init__(loc, name, content_type, None, last_modified_at=last_modified_at,
thumbnail_location=thumbnail_location, import_path=import_path, thumbnail_location=thumbnail_location, import_path=import_path,
length=length) length=length, locked=locked)
self._stream = stream self._stream = stream
def stream_data(self): def stream_data(self):
...@@ -153,7 +154,7 @@ class StaticContentStream(StaticContent): ...@@ -153,7 +154,7 @@ class StaticContentStream(StaticContent):
self._stream.seek(0) self._stream.seek(0)
content = StaticContent(self.location, self.name, self.content_type, self._stream.read(), content = StaticContent(self.location, self.name, self.content_type, self._stream.read(),
last_modified_at=self.last_modified_at, thumbnail_location=self.thumbnail_location, 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 return content
......
...@@ -24,17 +24,19 @@ class MongoContentStore(ContentStore): ...@@ -24,17 +24,19 @@ class MongoContentStore(ContentStore):
self.fs = gridfs.GridFS(_db, bucket) 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): 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 # 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, 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__'): if hasattr(content.data, '__iter__'):
for chunk in content.data: for chunk in content.data:
fp.write(chunk) fp.write(chunk)
...@@ -43,25 +45,29 @@ class MongoContentStore(ContentStore): ...@@ -43,25 +45,29 @@ class MongoContentStore(ContentStore):
return content return content
def delete(self, id): def delete(self, content_id):
if self.fs.exists({"_id": id}): if self.fs.exists({"_id": content_id}):
self.fs.delete(id) self.fs.delete(content_id)
def find(self, location, throw_on_not_found=True, as_stream=False): 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: try:
if as_stream: if as_stream:
fp = self.fs.get(id) fp = self.fs.get(content_id)
return StaticContentStream(location, fp.displayname, fp.content_type, fp, last_modified_at=fp.uploadDate, return StaticContentStream(
thumbnail_location=fp.thumbnail_location if hasattr(fp, 'thumbnail_location') else None, location, fp.displayname, fp.content_type, fp, last_modified_at=fp.uploadDate,
import_path=fp.import_path if hasattr(fp, 'import_path') else None, thumbnail_location=getattr(fp, 'thumbnail_location', None),
length=fp.length) import_path=getattr(fp, 'import_path', None),
length=fp.length, locked=getattr(fp, 'locked', False)
)
else: else:
with self.fs.get(id) as fp: with self.fs.get(content_id) as fp:
return StaticContent(location, fp.displayname, fp.content_type, fp.read(), last_modified_at=fp.uploadDate, return StaticContent(
thumbnail_location=fp.thumbnail_location if hasattr(fp, 'thumbnail_location') else None, location, fp.displayname, fp.content_type, fp.read(), last_modified_at=fp.uploadDate,
import_path=fp.import_path if hasattr(fp, 'import_path') else None, thumbnail_location=getattr(fp, 'thumbnail_location', None),
length=fp.length) import_path=getattr(fp, 'import_path', None),
length=fp.length, locked=getattr(fp, 'locked', False)
)
except NoFile: except NoFile:
if throw_on_not_found: if throw_on_not_found:
raise NotFoundError() raise NotFoundError()
...@@ -69,9 +75,9 @@ class MongoContentStore(ContentStore): ...@@ -69,9 +75,9 @@ class MongoContentStore(ContentStore):
return None return None
def get_stream(self, location): def get_stream(self, location):
id = StaticContent.get_id_from_location(location) content_id = StaticContent.get_id_from_location(location)
try: try:
handle = self.fs.get(id) handle = self.fs.get(content_id)
except NoFile: except NoFile:
raise NotFoundError() raise NotFoundError()
...@@ -135,3 +141,61 @@ class MongoContentStore(ContentStore): ...@@ -135,3 +141,61 @@ class MongoContentStore(ContentStore):
# 'borrow' the function 'location_to_query' from the Mongo modulestore implementation # 'borrow' the function 'location_to_query' from the Mongo modulestore implementation
items = self.fs_files.find(location_to_query(course_filter)) items = self.fs_files.find(location_to_query(course_filter))
return list(items) 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 { ...@@ -365,7 +365,7 @@ section.problem {
li { li {
display:inline; display:inline;
margin-left: 50px; margin-left: 50px;
&:first-child { &:first-child {
margin-left: 0px; margin-left: 0px;
} }
...@@ -436,10 +436,25 @@ section.problem { ...@@ -436,10 +436,25 @@ section.problem {
} }
table { table {
margin-bottom: lh(); margin: lh() 0;
border-collapse: collapse; border-collapse: collapse;
table-layout: auto; 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 { th {
text-align: left; text-align: left;
font-weight: bold; font-weight: bold;
...@@ -780,7 +795,7 @@ section.problem { ...@@ -780,7 +795,7 @@ section.problem {
.result-correct { .result-correct {
background: url('../images/correct-icon.png') left 20px no-repeat; background: url('../images/correct-icon.png') left 20px no-repeat;
.result-actual-output { .result-actual-output {
color: #090; color: #090;
} }
...@@ -788,7 +803,7 @@ section.problem { ...@@ -788,7 +803,7 @@ section.problem {
.result-incorrect { .result-incorrect {
background: url('../images/incorrect-icon.png') left 20px no-repeat; background: url('../images/incorrect-icon.png') left 20px no-repeat;
.result-actual-output { .result-actual-output {
color: #B00; color: #B00;
} }
...@@ -857,7 +872,7 @@ section.problem { ...@@ -857,7 +872,7 @@ section.problem {
input[type=radio]:checked + label { input[type=radio]:checked + label {
background: #666; background: #666;
color: white; color: white;
} }
input[class='score-selection'] { input[class='score-selection'] {
......
...@@ -322,7 +322,7 @@ div.combined-rubric-container { ...@@ -322,7 +322,7 @@ div.combined-rubric-container {
div.written-feedback { div.written-feedback {
background: #f6f6f6; background: #f6f6f6;
padding: 15px; padding: 5px;
} }
} }
......
// HTML component display: // HTML component display:
* { * {
line-height: 1.4em; line-height: 1.4em;
} }
...@@ -92,7 +92,7 @@ ul { ...@@ -92,7 +92,7 @@ ul {
a { a {
&:link, &:visited, &:hover, &:active { &:link, &:visited, &:hover, &:active {
color: #1d9dd9; color: #1d9dd9;
} }
} }
img { img {
...@@ -116,20 +116,32 @@ code { ...@@ -116,20 +116,32 @@ code {
} }
table { table {
width: 100%; width: 100%;
margin: 20px 0;
border-collapse: collapse; border-collapse: collapse;
font-size: 16px; font-size: 16px;
}
th { td, th {
background: #eee; margin: 20px 0;
font-weight: bold; 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 { th {
margin: 20px 0; background: #eee;
padding: 10px; font-weight: bold;
border: 1px solid #ccc; }
text-align: left;
font-size: 14px;
}
\ No newline at end of file
...@@ -15,6 +15,13 @@ div.video { ...@@ -15,6 +15,13 @@ div.video {
@include clearfix; @include clearfix;
} }
div.focus_grabber {
position: relative;
display: inline;
width: 0px;
height: 0px;
}
article.video-wrapper { article.video-wrapper {
float: left; float: left;
margin-right: flex-gutter(9); margin-right: flex-gutter(9);
...@@ -518,12 +525,19 @@ div.video { ...@@ -518,12 +525,19 @@ div.video {
margin-bottom: 8px; margin-bottom: 8px;
padding: 0; padding: 0;
line-height: lh(); line-height: lh();
outline-width: 0px;
outline-style: none;
&.current { &.current {
color: #333; color: #333;
font-weight: 700; font-weight: 700;
} }
&.focused {
outline-width: 1px;
outline-style: dotted;
}
&:hover { &:hover {
color: $blue; color: $blue;
} }
......
...@@ -129,7 +129,7 @@ class ErrorDescriptor(ErrorFields, XModuleDescriptor): ...@@ -129,7 +129,7 @@ class ErrorDescriptor(ErrorFields, XModuleDescriptor):
@classmethod @classmethod
def from_descriptor(cls, descriptor, error_msg='Error not available'): def from_descriptor(cls, descriptor, error_msg='Error not available'):
return cls._construct( return cls._construct(
descriptor.system, descriptor.runtime,
str(descriptor), str(descriptor),
error_msg, error_msg,
location=descriptor.location, location=descriptor.location,
......
<h2 class="problem-header">Problem Header</h2> <h2 class="problem-header">${_("Problem Header")}</h2>
<section class='problem-progress'> <section class='problem-progress'>
</section> </section>
<section class="problem"> <section class="problem">
<p>Problem Content</p> <p>${_("Problem Content")}</p>
<section class="action"> <section class="action">
<input type="hidden" name="problem_id" value="1"> <input type="hidden" name="problem_id" value="1">
...@@ -13,11 +13,11 @@ ...@@ -13,11 +13,11 @@
<span id="display_example_1"></span> <span id="display_example_1"></span>
<span id="input_example_1_dynamath"></span> <span id="input_example_1_dynamath"></span>
<input class="check" type="button" value="Check"> <input class="check" type="button" value="${_('Check')}">
<input class="reset" type="button" value="Reset"> <input class="reset" type="button" value="${_('Reset')}">
<input class="save" type="button" value="Save"> <input class="save" type="button" value="${_('Save')}">
<button class="show"><span class="show-label">Show Answer(s)</span> <span class="sr">(for question(s) above - adjacent to each field)</span></button> <button class="show"><span class="show-label">${_("Show Answer(s)")}</span> <span class="sr">(for question(s) above - adjacent to each field)</span></button>
<a href="/courseware/6.002_Spring_2012/${ explain }" class="new-page">Explanation</a> <a href="/courseware/6.002_Spring_2012/${ explain }" class="new-page">${_("Explanation")}</a>
<section class="submission_feedback"></section> <section class="submission_feedback"></section>
</section> </section>
</section> </section>
...@@ -13,6 +13,8 @@ ...@@ -13,6 +13,8 @@
data-yt-test-timeout="1500" data-yt-test-timeout="1500"
data-yt-test-url="https://gdata.youtube.com/feeds/api/videos/" data-yt-test-url="https://gdata.youtube.com/feeds/api/videos/"
> >
<div class="focus_grabber first"></div>
<div class="tc-wrapper"> <div class="tc-wrapper">
<article class="video-wrapper"> <article class="video-wrapper">
<div class="video-player-pre"></div> <div class="video-player-pre"></div>
...@@ -51,6 +53,8 @@ ...@@ -51,6 +53,8 @@
<ol class="subtitles"><li></li></ol> <ol class="subtitles"><li></li></ol>
</div> </div>
<div class="focus_grabber last"></div>
</div> </div>
</div> </div>
</div> </div>
......
...@@ -16,6 +16,8 @@ ...@@ -16,6 +16,8 @@
data-yt-test-timeout="1500" data-yt-test-timeout="1500"
data-yt-test-url="https://gdata.youtube.com/feeds/api/videos/" data-yt-test-url="https://gdata.youtube.com/feeds/api/videos/"
> >
<div class="focus_grabber first"></div>
<div class="tc-wrapper"> <div class="tc-wrapper">
<article class="video-wrapper"> <article class="video-wrapper">
<div class="video-player-pre"></div> <div class="video-player-pre"></div>
...@@ -54,6 +56,8 @@ ...@@ -54,6 +56,8 @@
<ol class="subtitles"><li></li></ol> <ol class="subtitles"><li></li></ol>
</div> </div>
<div class="focus_grabber last"></div>
</div> </div>
</div> </div>
</div> </div>
......
...@@ -16,6 +16,8 @@ ...@@ -16,6 +16,8 @@
data-yt-test-timeout="1500" data-yt-test-timeout="1500"
data-yt-test-url="https://gdata.youtube.com/feeds/api/videos/" data-yt-test-url="https://gdata.youtube.com/feeds/api/videos/"
> >
<div class="focus_grabber first"></div>
<div class="tc-wrapper"> <div class="tc-wrapper">
<article class="video-wrapper"> <article class="video-wrapper">
<section class="video-player"> <section class="video-player">
...@@ -26,6 +28,8 @@ ...@@ -26,6 +28,8 @@
<ol class="subtitles"><li></li></ol> <ol class="subtitles"><li></li></ol>
</div> </div>
<div class="focus_grabber last"></div>
</div> </div>
</div> </div>
</div> </div>
......
...@@ -13,6 +13,8 @@ ...@@ -13,6 +13,8 @@
data-yt-test-timeout="1500" data-yt-test-timeout="1500"
data-yt-test-url="https://gdata.youtube.com/feeds/api/videos/" data-yt-test-url="https://gdata.youtube.com/feeds/api/videos/"
> >
<div class="focus_grabber first"></div>
<div class="tc-wrapper"> <div class="tc-wrapper">
<article class="video-wrapper"> <article class="video-wrapper">
<section class="video-player"> <section class="video-player">
...@@ -21,7 +23,9 @@ ...@@ -21,7 +23,9 @@
<section class="video-controls"></section> <section class="video-controls"></section>
</article> </article>
</div> </div>
<div class="focus_grabber last"></div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
\ No newline at end of file
...@@ -13,6 +13,8 @@ ...@@ -13,6 +13,8 @@
data-yt-test-timeout="1500" data-yt-test-timeout="1500"
data-yt-test-url="https://gdata.youtube.com/feeds/api/videos/" data-yt-test-url="https://gdata.youtube.com/feeds/api/videos/"
> >
<div class="focus_grabber first"></div>
<div class="tc-wrapper"> <div class="tc-wrapper">
<article class="video-wrapper"> <article class="video-wrapper">
<div class="video-player-pre"></div> <div class="video-player-pre"></div>
...@@ -51,6 +53,8 @@ ...@@ -51,6 +53,8 @@
<ol class="subtitles"><li></li></ol> <ol class="subtitles"><li></li></ol>
</div> </div>
<div class="focus_grabber last"></div>
</div> </div>
</div> </div>
</div> </div>
......
...@@ -48,17 +48,32 @@ describe 'CombinedOpenEnded', -> ...@@ -48,17 +48,32 @@ describe 'CombinedOpenEnded', ->
expect(@combined.task_count).toEqual 2 expect(@combined.task_count).toEqual 2
expect(@combined.task_number).toEqual 1 expect(@combined.task_number).toEqual 1
it 'subelements are made collapsible', -> it 'subelements are made collapsible', ->
expect(Collapsible.setCollapsibles).toHaveBeenCalled() expect(Collapsible.setCollapsibles).toHaveBeenCalled()
describe 'poll', -> describe 'poll', ->
# We will store default window.setTimeout() function here.
oldSetTimeout = null
beforeEach => beforeEach =>
# setup the spies # setup the spies
@combined = new CombinedOpenEnded @element @combined = new CombinedOpenEnded @element
spyOn(@combined, 'reload').andCallFake -> return 0 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 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', => it 'polls at the correct intervals', =>
fakeResponseContinue = state: 'not done' fakeResponseContinue = state: 'not done'
spyOn($, 'postWithPrefix').andCallFake (url, callback) -> callback(fakeResponseContinue) spyOn($, 'postWithPrefix').andCallFake (url, callback) -> callback(fakeResponseContinue)
...@@ -67,19 +82,34 @@ describe 'CombinedOpenEnded', -> ...@@ -67,19 +82,34 @@ describe 'CombinedOpenEnded', ->
expect(window.queuePollerID).toBe(5) expect(window.queuePollerID).toBe(5)
it 'polling stops properly', => it 'polling stops properly', =>
fakeResponseDone = state: "done" fakeResponseDone = state: "done"
spyOn($, 'postWithPrefix').andCallFake (url, callback) -> callback(fakeResponseDone) spyOn($, 'postWithPrefix').andCallFake (url, callback) -> callback(fakeResponseDone)
@combined.poll() @combined.poll()
expect(window.queuePollerID).toBeUndefined() expect(window.queuePollerID).toBeUndefined()
expect(window.setTimeout).not.toHaveBeenCalled() expect(window.setTimeout).not.toHaveBeenCalled()
describe 'rebind', -> describe 'rebind', ->
# We will store default window.setTimeout() function here.
oldSetTimeout = null
beforeEach -> beforeEach ->
@combined = new CombinedOpenEnded @element @combined = new CombinedOpenEnded @element
spyOn(@combined, 'queueing').andCallFake -> return 0 spyOn(@combined, 'queueing').andCallFake -> return 0
spyOn(@combined, 'skip_post_assessment').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 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', -> it 'when our child is in an assessing state', ->
@combined.child_state = 'assessing' @combined.child_state = 'assessing'
@combined.rebind() @combined.rebind()
...@@ -87,19 +117,19 @@ describe 'CombinedOpenEnded', -> ...@@ -87,19 +117,19 @@ describe 'CombinedOpenEnded', ->
expect(@combined.submit_button.val()).toBe("Submit assessment") expect(@combined.submit_button.val()).toBe("Submit assessment")
expect(@combined.queueing).toHaveBeenCalled() expect(@combined.queueing).toHaveBeenCalled()
it 'when our child state is initial', -> it 'when our child state is initial', ->
@combined.child_state = 'initial' @combined.child_state = 'initial'
@combined.rebind() @combined.rebind()
expect(@combined.answer_area.attr("disabled")).toBeUndefined() expect(@combined.answer_area.attr("disabled")).toBeUndefined()
expect(@combined.submit_button.val()).toBe("Submit") 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.child_state = 'post_assessment'
@combined.rebind() @combined.rebind()
expect(@combined.answer_area.attr("disabled")).toBe("disabled") expect(@combined.answer_area.attr("disabled")).toBe("disabled")
expect(@combined.submit_button.val()).toBe("Submit post-assessment") 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 -> spyOn(@combined, 'next_problem').andCallFake ->
@combined.child_state = 'done' @combined.child_state = 'done'
@combined.rebind() @combined.rebind()
...@@ -112,7 +142,7 @@ describe 'CombinedOpenEnded', -> ...@@ -112,7 +142,7 @@ describe 'CombinedOpenEnded', ->
@combined.child_state = 'done' @combined.child_state = 'done'
it 'handling a successful call', -> it 'handling a successful call', ->
fakeResponse = fakeResponse =
success: true success: true
html: "dummy html" html: "dummy html"
allow_reset: false allow_reset: false
......
...@@ -93,6 +93,7 @@ ...@@ -93,6 +93,7 @@
$('.subtitles li[data-index]').each(function(index, link) { $('.subtitles li[data-index]').each(function(index, link) {
expect($(link)).toHaveData('index', index); expect($(link)).toHaveData('index', index);
expect($(link)).toHaveData('start', captionsData.start[index]); expect($(link)).toHaveData('start', captionsData.start[index]);
expect($(link)).toHaveAttr('tabindex', 0);
expect($(link)).toHaveText(captionsData.text[index]); expect($(link)).toHaveText(captionsData.text[index]);
}); });
}); });
...@@ -104,7 +105,13 @@ ...@@ -104,7 +105,13 @@
it('bind all the caption link', function() { it('bind all the caption link', function() {
$('.subtitles li[data-index]').each(function(index, link) { $('.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 @@ ...@@ -146,12 +153,27 @@
}); });
describe('mouse movement', function() { describe('mouse movement', function() {
// We will store default window.setTimeout() function here.
var oldSetTimeout = null;
beforeEach(function() { 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 = jasmine.createSpy().andCallFake(function(callback, timeout) { return 5; })
window.setTimeout.andReturn(100); window.setTimeout.andReturn(100);
spyOn(window, 'clearTimeout'); 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() { describe('when cursor is outside of the caption box', function() {
beforeEach(function() { beforeEach(function() {
$(window).trigger(jQuery.Event('mousemove')); $(window).trigger(jQuery.Event('mousemove'));
...@@ -263,6 +285,7 @@ ...@@ -263,6 +285,7 @@
$('.subtitles li[data-index]').each(function(index, link) { $('.subtitles li[data-index]').each(function(index, link) {
expect($(link)).toHaveData('index', index); expect($(link)).toHaveData('index', index);
expect($(link)).toHaveData('start', captionsData.start[index]); expect($(link)).toHaveData('start', captionsData.start[index]);
expect($(link)).toHaveAttr('tabindex', 0);
expect($(link)).toHaveText(captionsData.text[index]); expect($(link)).toHaveText(captionsData.text[index]);
}); });
}); });
...@@ -274,7 +297,13 @@ ...@@ -274,7 +297,13 @@
it('bind all the caption link', function() { it('bind all the caption link', function() {
$('.subtitles li[data-index]').each(function(index, link) { $('.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 @@ ...@@ -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); }).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 @@ ...@@ -145,7 +145,18 @@
}); });
describe('onStop', function() { describe('onStop', function() {
// We will store default window.setTimeout() function here.
var oldSetTimeout = null;
beforeEach(function() { 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(); initialize();
spyOn(videoPlayer, 'onSlideSeek').andCallThrough(); spyOn(videoPlayer, 'onSlideSeek').andCallThrough();
videoProgressSlider.onStop({}, { videoProgressSlider.onStop({}, {
...@@ -153,6 +164,13 @@ ...@@ -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() { it('freeze the slider', function() {
expect(videoProgressSlider.frozen).toBeTruthy(); expect(videoProgressSlider.frozen).toBeTruthy();
}); });
...@@ -162,7 +180,9 @@ ...@@ -162,7 +180,9 @@
expect(videoPlayer.currentTime).toEqual(20); 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); expect(window.setTimeout).toHaveBeenCalledWith(jasmine.any(Function), 200);
window.setTimeout.mostRecentCall.args[0](); window.setTimeout.mostRecentCall.args[0]();
expect(videoProgressSlider.frozen).toBeFalsy(); expect(videoProgressSlider.frozen).toBeFalsy();
......
...@@ -119,6 +119,7 @@ class @CombinedOpenEnded ...@@ -119,6 +119,7 @@ class @CombinedOpenEnded
next_rubric_sel: '.rubric-next-button' next_rubric_sel: '.rubric-next-button'
previous_rubric_sel: '.rubric-previous-button' previous_rubric_sel: '.rubric-previous-button'
oe_alert_sel: '.open-ended-alert' oe_alert_sel: '.open-ended-alert'
save_button_sel: '.save-button'
constructor: (el) -> constructor: (el) ->
@el=el @el=el
...@@ -183,6 +184,7 @@ class @CombinedOpenEnded ...@@ -183,6 +184,7 @@ class @CombinedOpenEnded
@hint_wrapper = @$(@oe).find(@hint_wrapper_sel) @hint_wrapper = @$(@oe).find(@hint_wrapper_sel)
@message_wrapper = @$(@oe).find(@message_wrapper_sel) @message_wrapper = @$(@oe).find(@message_wrapper_sel)
@submit_button = @$(@oe).find(@submit_button_sel) @submit_button = @$(@oe).find(@submit_button_sel)
@save_button = @$(@oe).find(@save_button_sel)
@child_state = @oe.data('state') @child_state = @oe.data('state')
@child_type = @oe.data('child-type') @child_type = @oe.data('child-type')
if @child_type=="openended" if @child_type=="openended"
...@@ -273,6 +275,8 @@ class @CombinedOpenEnded ...@@ -273,6 +275,8 @@ class @CombinedOpenEnded
# rebind to the appropriate function for the current state # rebind to the appropriate function for the current state
@submit_button.unbind('click') @submit_button.unbind('click')
@submit_button.show() @submit_button.show()
@save_button.unbind('click')
@save_button.hide()
@reset_button.hide() @reset_button.hide()
@hide_file_upload() @hide_file_upload()
@next_problem_button.hide() @next_problem_button.hide()
...@@ -299,6 +303,8 @@ class @CombinedOpenEnded ...@@ -299,6 +303,8 @@ class @CombinedOpenEnded
@submit_button.prop('value', 'Submit') @submit_button.prop('value', 'Submit')
@submit_button.click @confirm_save_answer @submit_button.click @confirm_save_answer
@setup_file_upload() @setup_file_upload()
@save_button.click @store_answer
@save_button.show()
else if @child_state == 'assessing' else if @child_state == 'assessing'
@answer_area.attr("disabled", true) @answer_area.attr("disabled", true)
@replace_text_inputs() @replace_text_inputs()
...@@ -338,13 +344,26 @@ class @CombinedOpenEnded ...@@ -338,13 +344,26 @@ class @CombinedOpenEnded
else else
@reset_button.show() @reset_button.show()
find_assessment_elements: -> find_assessment_elements: ->
@assessment = @$('input[name="grade-selection"]') @assessment = @$('input[name="grade-selection"]')
find_hint_elements: -> find_hint_elements: ->
@hint_area = @$('textarea.post_assessment') @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) => replace_answer: (response) =>
if response.success if response.success
@rubric_wrapper.html(response.rubric_html) @rubric_wrapper.html(response.rubric_html)
...@@ -364,6 +383,7 @@ class @CombinedOpenEnded ...@@ -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) 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) => save_answer: (event) =>
@$el.find(@oe_alert_sel).remove()
@submit_button.attr("disabled",true) @submit_button.attr("disabled",true)
@submit_button.hide() @submit_button.hide()
event.preventDefault() 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) { ...@@ -420,12 +420,15 @@ function (HTML5Video) {
this.videoPlayer.player.setPlaybackRate(this.speed); this.videoPlayer.player.setPlaybackRate(this.speed);
} }
/* The following has been commented out to make sure autoplay is
disabled for students.
if ( if (
!onTouchBasedDevice() && !onTouchBasedDevice() &&
$('.video:first').data('autoplay') === 'True' $('.video:first').data('autoplay') === 'True'
) { ) {
this.videoPlayer.play(); this.videoPlayer.play();
} }
*/
} }
function onStateChange(event) { 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