Commit c95b44d8 by cahrens

Merge branch 'master' into feature/christina/metadata-ui

Conflicts:
	cms/envs/common.py
	cms/templates/base.html
	common/lib/xmodule/xmodule/combined_open_ended_module.py
parents c8d6d5d2 a4ad495b
...@@ -26,6 +26,7 @@ Gemfile.lock ...@@ -26,6 +26,7 @@ Gemfile.lock
conf/locale/en/LC_MESSAGES/*.po conf/locale/en/LC_MESSAGES/*.po
!messages.po !messages.po
lms/static/sass/*.css lms/static/sass/*.css
lms/static/sass/application.scss
cms/static/sass/*.css cms/static/sass/*.css
lms/lib/comment_client/python lms/lib/comment_client/python
nosetests.xml nosetests.xml
......
REVIEWBOARD_URL = "https://rbcommons.com/s/edx/"
GUESS_FIELDS = True
...@@ -42,7 +42,7 @@ COMPONENT_TYPES = ['customtag', 'discussion', 'html', 'problem', 'video'] ...@@ -42,7 +42,7 @@ COMPONENT_TYPES = ['customtag', 'discussion', 'html', 'problem', 'video']
OPEN_ENDED_COMPONENT_TYPES = ["combinedopenended", "peergrading"] OPEN_ENDED_COMPONENT_TYPES = ["combinedopenended", "peergrading"]
NOTE_COMPONENT_TYPES = ['notes'] NOTE_COMPONENT_TYPES = ['notes']
ADVANCED_COMPONENT_TYPES = ['annotatable' + 'word_cloud'] + OPEN_ENDED_COMPONENT_TYPES + NOTE_COMPONENT_TYPES ADVANCED_COMPONENT_TYPES = ['annotatable', 'word_cloud'] + OPEN_ENDED_COMPONENT_TYPES + NOTE_COMPONENT_TYPES
ADVANCED_COMPONENT_CATEGORY = 'advanced' ADVANCED_COMPONENT_CATEGORY = 'advanced'
ADVANCED_COMPONENT_POLICY_KEY = 'advanced_modules' ADVANCED_COMPONENT_POLICY_KEY = 'advanced_modules'
......
...@@ -8,7 +8,7 @@ from mitxmako.shortcuts import render_to_response ...@@ -8,7 +8,7 @@ from mitxmako.shortcuts import render_to_response
from external_auth.views import ssl_login_shortcut from external_auth.views import ssl_login_shortcut
from .user import index from .user import index
__all__ = ['signup', 'old_login_redirect', 'login_page', 'howitworks', 'ux_alerts'] __all__ = ['signup', 'old_login_redirect', 'login_page', 'howitworks']
""" """
Public views Public views
...@@ -49,10 +49,3 @@ def howitworks(request): ...@@ -49,10 +49,3 @@ def howitworks(request):
return index(request) return index(request)
else: else:
return render_to_response('howitworks.html', {}) return render_to_response('howitworks.html', {})
def ux_alerts(request):
"""
static/proof-of-concept views
"""
return render_to_response('ux-alerts.html', {})
...@@ -21,7 +21,7 @@ def event(request): ...@@ -21,7 +21,7 @@ def event(request):
A noop to swallow the analytics call so that cms methods don't spook and poor developers looking at A noop to swallow the analytics call so that cms methods don't spook and poor developers looking at
console logs don't get distracted :-) console logs don't get distracted :-)
''' '''
return HttpResponse(True) return HttpResponse(status=204)
def get_request_method(request): def get_request_method(request):
......
...@@ -2,6 +2,11 @@ ...@@ -2,6 +2,11 @@
This config file extends the test environment configuration This config file extends the test environment configuration
so that we can run the lettuce acceptance tests. so that we can run the lettuce acceptance tests.
""" """
# We intentionally define lots of variables that aren't used, and
# want to import all variables from base settings files
# pylint: disable=W0401, W0614
from .test import * from .test import *
# You need to start the server in debug mode, # You need to start the server in debug mode,
......
""" """
This is the default template for our main set of AWS servers. This is the default template for our main set of AWS servers.
""" """
# We intentionally define lots of variables that aren't used, and
# want to import all variables from base settings files
# pylint: disable=W0401, W0614
import json import json
from .common import * from .common import *
......
...@@ -19,6 +19,10 @@ Longer TODO: ...@@ -19,6 +19,10 @@ Longer TODO:
multiple sites, but we do need a way to map their data assets. multiple sites, but we do need a way to map their data assets.
""" """
# We intentionally define lots of variables that aren't used, and
# want to import all variables from base settings files
# pylint: disable=W0401, W0614
import sys import sys
import lms.envs.common import lms.envs.common
from path import path from path import path
...@@ -217,7 +221,10 @@ PIPELINE_JS = { ...@@ -217,7 +221,10 @@ PIPELINE_JS = {
'source_filenames': sorted( 'source_filenames': sorted(
rooted_glob(COMMON_ROOT / 'static/', 'coffee/src/**/*.js') + rooted_glob(COMMON_ROOT / 'static/', 'coffee/src/**/*.js') +
rooted_glob(PROJECT_ROOT / 'static/', 'coffee/src/**/*.js') rooted_glob(PROJECT_ROOT / 'static/', 'coffee/src/**/*.js')
) + ['js/hesitate.js', 'js/base.js', 'js/models/metadata_model.js'], ) + ['js/hesitate.js', 'js/base.js',
'js/models/feedback.js', 'js/views/feedback.js',
'js/models/section.js', 'js/views/section.js',
'js/models/metadata_model.js'],
'output_filename': 'js/cms-application.js', 'output_filename': 'js/cms-application.js',
'test_order': 0 'test_order': 0
}, },
......
""" """
This config file runs the simplest dev environment""" This config file runs the simplest dev environment"""
# We intentionally define lots of variables that aren't used, and
# want to import all variables from base settings files
# pylint: disable=W0401, W0614
from .common import * from .common import *
from logsettings import get_logger_config from logsettings import get_logger_config
......
# We intentionally define lots of variables that aren't used, and
# want to import all variables from base settings files
# pylint: disable=W0401, W0614
# dev environment for ichuang/mit # dev environment for ichuang/mit
# FORCE_SCRIPT_NAME = '/cms' # FORCE_SCRIPT_NAME = '/cms'
......
...@@ -8,6 +8,10 @@ The worker can be executed using: ...@@ -8,6 +8,10 @@ The worker can be executed using:
django_admin.py celery worker django_admin.py celery worker
""" """
# We intentionally define lots of variables that aren't used, and
# want to import all variables from base settings files
# pylint: disable=W0401, W0614
from dev import * from dev import *
################################# CELERY ###################################### ################################# CELERY ######################################
......
...@@ -2,6 +2,10 @@ ...@@ -2,6 +2,10 @@
This configuration is used for running jasmine tests This configuration is used for running jasmine tests
""" """
# We intentionally define lots of variables that aren't used, and
# want to import all variables from base settings files
# pylint: disable=W0401, W0614
from .test import * from .test import *
from logsettings import get_logger_config from logsettings import get_logger_config
......
...@@ -7,6 +7,11 @@ sessions. Assumes structure: ...@@ -7,6 +7,11 @@ sessions. Assumes structure:
/mitx # The location of this repo /mitx # The location of this repo
/log # Where we're going to write log files /log # Where we're going to write log files
""" """
# We intentionally define lots of variables that aren't used, and
# want to import all variables from base settings files
# pylint: disable=W0401, W0614
from .common import * from .common import *
import os import os
from path import path from path import path
......
...@@ -11,11 +11,11 @@ ...@@ -11,11 +11,11 @@
<span class="int"><%= percentChecked %></span>% of checklist completed</span></span> <span class="int"><%= percentChecked %></span>% of checklist completed</span></span>
<header> <header>
<h3 class="checklist-title title-2 is-selectable" title="Collapse/Expand this Checklist"> <h3 class="checklist-title title-2 is-selectable" title="Collapse/Expand this Checklist">
<i class="ss-icon ss-symbolicons-standard icon-arrow ui-toggle-expansion">&#x25BE;</i> <i class="icon-caret-down ui-toggle-expansion"></i>
<%= checklistShortDescription %></h3> <%= checklistShortDescription %></h3>
<span class="checklist-status status"> <span class="checklist-status status">
Tasks Completed: <span class="status-count"><%= itemsChecked %></span>/<span class="status-amount"><%= items.length %></span> Tasks Completed: <span class="status-count"><%= itemsChecked %></span>/<span class="status-amount"><%= items.length %></span>
<i class="ss-icon ss-symbolicons-standard icon-confirm">&#x2713;</i> <i class="icon-ok"></i>
</span> </span>
</header> </header>
...@@ -58,4 +58,4 @@ ...@@ -58,4 +58,4 @@
<% taskIndex+=1; }) %> <% taskIndex+=1; }) %>
</ul> </ul>
</section> </section>
\ No newline at end of file
...@@ -8,6 +8,8 @@ ...@@ -8,6 +8,8 @@
"js/vendor/json2.js", "js/vendor/json2.js",
"js/vendor/underscore-min.js", "js/vendor/underscore-min.js",
"js/vendor/backbone-min.js", "js/vendor/backbone-min.js",
"js/vendor/jquery.leanModal.min.js" "js/vendor/jquery.leanModal.min.js",
"js/vendor/sinon-1.7.1.js",
"js/test/i18n.js"
] ]
} }
../../../templates/js/section-name-edit.underscore
\ No newline at end of file
../../../templates/js/system-feedback.underscore
\ No newline at end of file
jasmine.getFixtures().fixturesPath = 'fixtures'
# Stub jQuery.cookie # Stub jQuery.cookie
@stubCookies = @stubCookies =
csrftoken: "stubCSRFToken" csrftoken: "stubCSRFToken"
......
...@@ -22,3 +22,37 @@ describe "main helper", -> ...@@ -22,3 +22,37 @@ describe "main helper", ->
it "setup AJAX CSRF token", -> it "setup AJAX CSRF token", ->
expect($.ajaxSettings.headers["X-CSRFToken"]).toEqual("stubCSRFToken") expect($.ajaxSettings.headers["X-CSRFToken"]).toEqual("stubCSRFToken")
describe "AJAX Errors", ->
tpl = readFixtures('system-feedback.underscore')
beforeEach ->
setFixtures($("<script>", {id: "system-feedback-tpl", type: "text/template"}).text(tpl))
appendSetFixtures(sandbox({id: "page-notification"}))
@requests = requests = []
@xhr = sinon.useFakeXMLHttpRequest()
@xhr.onCreate = (xhr) -> requests.push(xhr)
afterEach ->
@xhr.restore()
it "successful AJAX request does not pop an error notification", ->
expect($("#page-notification")).toBeEmpty()
$.ajax("/test")
expect($("#page-notification")).toBeEmpty()
@requests[0].respond(200)
expect($("#page-notification")).toBeEmpty()
it "AJAX request with error should pop an error notification", ->
$.ajax("/test")
@requests[0].respond(500)
expect($("#page-notification")).not.toBeEmpty()
expect($("#page-notification")).toContain('div.wrapper-notification-error')
it "can override AJAX request with error so it does not pop an error notification", ->
$.ajax
url: "/test"
notifyOnError: false
@requests[0].respond(500)
expect($("#page-notification")).toBeEmpty()
describe "CMS.Models.SystemFeedback", ->
beforeEach ->
@model = new CMS.Models.SystemFeedback()
it "should have an empty message by default", ->
expect(@model.get("message")).toEqual("")
it "should have an empty title by default", ->
expect(@model.get("title")).toEqual("")
it "should not have an intent set by default", ->
expect(@model.get("intent")).toBeNull()
describe "CMS.Models.WarningMessage", ->
beforeEach ->
@model = new CMS.Models.WarningMessage()
it "should have the correct intent", ->
expect(@model.get("intent")).toEqual("warning")
describe "CMS.Models.ErrorMessage", ->
beforeEach ->
@model = new CMS.Models.ErrorMessage()
it "should have the correct intent", ->
expect(@model.get("intent")).toEqual("error")
describe "CMS.Models.ConfirmationMessage", ->
beforeEach ->
@model = new CMS.Models.ConfirmationMessage()
it "should have the correct intent", ->
expect(@model.get("intent")).toEqual("confirmation")
describe "CMS.Models.Section", ->
describe "basic", ->
beforeEach ->
@model = new CMS.Models.Section({
id: 42,
name: "Life, the Universe, and Everything"
})
it "should take an id argument", ->
expect(@model.get("id")).toEqual(42)
it "should take a name argument", ->
expect(@model.get("name")).toEqual("Life, the Universe, and Everything")
it "should have a URL set", ->
expect(@model.url).toEqual("/save_item")
it "should serialize to JSON correctly", ->
expect(@model.toJSON()).toEqual({
id: 42,
metadata: {
display_name: "Life, the Universe, and Everything"
}
})
describe "XHR", ->
beforeEach ->
spyOn(CMS.Models.Section.prototype, 'showNotification')
spyOn(CMS.Models.Section.prototype, 'hideNotification')
@model = new CMS.Models.Section({
id: 42,
name: "Life, the Universe, and Everything"
})
@requests = requests = []
@xhr = sinon.useFakeXMLHttpRequest()
@xhr.onCreate = (xhr) -> requests.push(xhr)
afterEach ->
@xhr.restore()
it "show/hide a notification when it saves to the server", ->
@model.save()
expect(CMS.Models.Section.prototype.showNotification).toHaveBeenCalled()
@requests[0].respond(200)
expect(CMS.Models.Section.prototype.hideNotification).toHaveBeenCalled()
it "don't hide notification when saving fails", ->
# this is handled by the global AJAX error handler
@model.save()
@requests[0].respond(500)
expect(CMS.Models.Section.prototype.hideNotification).not.toHaveBeenCalled()
tpl = readFixtures('system-feedback.underscore')
beforeEach ->
setFixtures(sandbox({id: "page-alert"}))
appendSetFixtures(sandbox({id: "page-notification"}))
appendSetFixtures(sandbox({id: "page-prompt"}))
appendSetFixtures($("<script>", {id: "system-feedback-tpl", type: "text/template"}).text(tpl))
@addMatchers
toBeShown: ->
@actual.hasClass("is-shown") and not @actual.hasClass("is-hiding")
toBeHiding: ->
@actual.hasClass("is-hiding") and not @actual.hasClass("is-shown")
toContainText: (text) ->
# remove this when we upgrade jasmine-jquery
trimmedText = $.trim(@actual.text())
if text and $.isFunction(text.test)
return text.test(trimmedText)
else
return trimmedText.indexOf(text) != -1;
describe "CMS.Views.Alert as base class", ->
beforeEach ->
@model = new CMS.Models.ConfirmationMessage({
title: "Portal"
message: "Welcome to the Aperture Science Computer-Aided Enrichment Center"
})
# it will be interesting to see when this.render is called, so lets spy on it
spyOn(CMS.Views.Alert.prototype, 'render').andCallThrough()
it "renders on initalize", ->
view = new CMS.Views.Alert({model: @model})
expect(view.render).toHaveBeenCalled()
it "renders the template", ->
view = new CMS.Views.Alert({model: @model})
expect(view.$(".action-close")).toBeDefined()
expect(view.$('.wrapper')).toBeShown()
expect(view.$el).toContainText(@model.get("title"))
expect(view.$el).toContainText(@model.get("message"))
it "close button sends a .hide() message", ->
spyOn(CMS.Views.Alert.prototype, 'hide').andCallThrough()
view = new CMS.Views.Alert({model: @model})
view.$(".action-close").click()
expect(CMS.Views.Alert.prototype.hide).toHaveBeenCalled()
expect(view.$('.wrapper')).toBeHiding()
describe "CMS.Views.Prompt", ->
beforeEach ->
@model = new CMS.Models.ConfirmationMessage({
title: "Portal"
message: "Welcome to the Aperture Science Computer-Aided Enrichment Center"
})
# for some reason, expect($("body")) blows up the test runner, so this test
# just exercises the Prompt rather than asserting on anything. Best I can
# do for now. :(
it "changes class on body", ->
# expect($("body")).not.toHaveClass("prompt-is-shown")
view = new CMS.Views.Prompt({model: @model})
# expect($("body")).toHaveClass("prompt-is-shown")
view.hide()
# expect($("body")).not.toHaveClass("prompt-is-shown")
describe "CMS.Views.Alert click events", ->
beforeEach ->
@model = new CMS.Models.WarningMessage(
title: "Unsaved",
message: "Your content is currently Unsaved.",
actions:
primary:
text: "Save",
class: "save-button",
click: jasmine.createSpy('primaryClick')
secondary: [{
text: "Revert",
class: "cancel-button",
click: jasmine.createSpy('secondaryClick')
}]
)
@view = new CMS.Views.Alert({model: @model})
it "should trigger the primary event on a primary click", ->
@view.primaryClick()
expect(@model.get('actions').primary.click).toHaveBeenCalled()
it "should trigger the secondary event on a secondary click", ->
@view.secondaryClick()
expect(@model.get('actions').secondary[0].click).toHaveBeenCalled()
it "should apply class to primary action", ->
expect(@view.$(".action-primary")).toHaveClass("save-button")
it "should apply class to secondary action", ->
expect(@view.$(".action-secondary")).toHaveClass("cancel-button")
describe "CMS.Views.Notification minShown and maxShown", ->
beforeEach ->
@model = new CMS.Models.SystemFeedback(
intent: "saving"
title: "Saving"
)
spyOn(CMS.Views.Notification.prototype, 'show').andCallThrough()
spyOn(CMS.Views.Notification.prototype, 'hide').andCallThrough()
@clock = sinon.useFakeTimers()
afterEach ->
@clock.restore()
it "a minShown view should not hide too quickly", ->
view = new CMS.Views.Notification({model: @model, minShown: 1000})
expect(CMS.Views.Notification.prototype.show).toHaveBeenCalled()
expect(view.$('.wrapper')).toBeShown()
# call hide() on it, but the minShown should prevent it from hiding right away
view.hide()
expect(view.$('.wrapper')).toBeShown()
# wait for the minShown timeout to expire, and check again
@clock.tick(1001)
expect(view.$('.wrapper')).toBeHiding()
it "a maxShown view should hide by itself", ->
view = new CMS.Views.Notification({model: @model, maxShown: 1000})
expect(CMS.Views.Notification.prototype.show).toHaveBeenCalled()
expect(view.$('.wrapper')).toBeShown()
# wait for the maxShown timeout to expire, and check again
@clock.tick(1001)
expect(view.$('.wrapper')).toBeHiding()
it "a minShown view can stay visible longer", ->
view = new CMS.Views.Notification({model: @model, minShown: 1000})
expect(CMS.Views.Notification.prototype.show).toHaveBeenCalled()
expect(view.$('.wrapper')).toBeShown()
# wait for the minShown timeout to expire, and check again
@clock.tick(1001)
expect(CMS.Views.Notification.prototype.hide).not.toHaveBeenCalled()
expect(view.$('.wrapper')).toBeShown()
# can now hide immediately
view.hide()
expect(view.$('.wrapper')).toBeHiding()
it "a maxShown view can hide early", ->
view = new CMS.Views.Notification({model: @model, maxShown: 1000})
expect(CMS.Views.Notification.prototype.show).toHaveBeenCalled()
expect(view.$('.wrapper')).toBeShown()
# wait 50 milliseconds, and hide it early
@clock.tick(50)
view.hide()
expect(view.$('.wrapper')).toBeHiding()
# wait for timeout to expire, make sure it doesn't do anything weird
@clock.tick(1000)
expect(view.$('.wrapper')).toBeHiding()
it "a view can have both maxShown and minShown", ->
view = new CMS.Views.Notification({model: @model, minShown: 1000, maxShown: 2000})
# can't hide early
@clock.tick(50)
view.hide()
expect(view.$('.wrapper')).toBeShown()
@clock.tick(1000)
expect(view.$('.wrapper')).toBeHiding()
# show it again, and let it hide automatically
view.show()
@clock.tick(1050)
expect(view.$('.wrapper')).toBeShown()
@clock.tick(1000)
expect(view.$('.wrapper')).toBeHiding()
describe "CMS.Views.SectionShow", ->
describe "Basic", ->
beforeEach ->
spyOn(CMS.Views.SectionShow.prototype, "switchToEditView")
.andCallThrough()
@model = new CMS.Models.Section({
id: 42
name: "Life, the Universe, and Everything"
})
@view = new CMS.Views.SectionShow({model: @model})
@view.render()
it "should contain the model name", ->
expect(@view.$el).toHaveText(@model.get('name'))
it "should call switchToEditView when clicked", ->
@view.$el.click()
expect(@view.switchToEditView).toHaveBeenCalled()
it "should pass the same element to SectionEdit when switching views", ->
spyOn(CMS.Views.SectionEdit.prototype, 'initialize').andCallThrough()
@view.switchToEditView()
expect(CMS.Views.SectionEdit.prototype.initialize).toHaveBeenCalled()
expect(CMS.Views.SectionEdit.prototype.initialize.mostRecentCall.args[0].el).toEqual(@view.el)
describe "CMS.Views.SectionEdit", ->
describe "Basic", ->
tpl = readFixtures('section-name-edit.underscore')
feedback_tpl = readFixtures('system-feedback.underscore')
beforeEach ->
setFixtures($("<script>", {id: "section-name-edit-tpl", type: "text/template"}).text(tpl))
appendSetFixtures($("<script>", {id: "system-feedback-tpl", type: "text/template"}).text(feedback_tpl))
spyOn(CMS.Views.SectionEdit.prototype, "switchToShowView")
.andCallThrough()
spyOn(CMS.Views.SectionEdit.prototype, "showInvalidMessage")
.andCallThrough()
window.analytics = jasmine.createSpyObj('analytics', ['track'])
window.course_location_analytics = jasmine.createSpy()
@requests = requests = []
@xhr = sinon.useFakeXMLHttpRequest()
@xhr.onCreate = (xhr) -> requests.push(xhr)
@model = new CMS.Models.Section({
id: 42
name: "Life, the Universe, and Everything"
})
@view = new CMS.Views.SectionEdit({model: @model})
@view.render()
afterEach ->
@xhr.restore()
delete window.analytics
delete window.course_location_analytics
it "should have the model name as the default text value", ->
expect(@view.$("input[type=text]").val()).toEqual(@model.get('name'))
it "should call switchToShowView when cancel button is clicked", ->
@view.$("input.cancel-button").click()
expect(@view.switchToShowView).toHaveBeenCalled()
it "should save model when save button is clicked", ->
spyOn(@model, 'save')
@view.$("input[type=submit]").click()
expect(@model.save).toHaveBeenCalled()
it "should call switchToShowView when save() is successful", ->
@view.$("input[type=submit]").click()
@requests[0].respond(200)
expect(@view.switchToShowView).toHaveBeenCalled()
it "should call showInvalidMessage when validation is unsuccessful", ->
spyOn(@model, 'validate').andReturn("BLARRGH")
@view.$("input[type=submit]").click()
expect(@view.showInvalidMessage).toHaveBeenCalledWith(
jasmine.any(Object), "BLARRGH", jasmine.any(Object))
expect(@view.switchToShowView).not.toHaveBeenCalled()
it "should not save when validation is unsuccessful", ->
spyOn(@model, 'validate').andReturn("BLARRGH")
@view.$("input[type=text]").val("changed")
@view.$("input[type=submit]").click()
expect(@model.get('name')).not.toEqual("changed")
...@@ -15,6 +15,15 @@ $ -> ...@@ -15,6 +15,15 @@ $ ->
headers : { 'X-CSRFToken': $.cookie 'csrftoken' } headers : { 'X-CSRFToken': $.cookie 'csrftoken' }
dataType: 'json' dataType: 'json'
$(document).ajaxError (event, jqXHR, ajaxSettings, thrownError) ->
if ajaxSettings.notifyOnError is false
return
msg = new CMS.Models.ErrorMessage(
"title": gettext("Studio's having trouble saving your work")
"message": jqXHR.responseText || gettext("This may be happening because of an error with our server or your internet connection. Try refreshing the page or making sure you are online.")
)
new CMS.Views.Notification({model: msg})
window.onTouchBasedDevice = -> window.onTouchBasedDevice = ->
navigator.userAgent.match /iPhone|iPod|iPad/i navigator.userAgent.match /iPhone|iPod|iPad/i
......
This source diff could not be displayed because it is too large. You can view the blob instead.
...@@ -140,11 +140,6 @@ $(document).ready(function() { ...@@ -140,11 +140,6 @@ $(document).ready(function() {
$('.new-course-button').bind('click', addNewCourse); $('.new-course-button').bind('click', addNewCourse);
// section name editing
$('.section-name').bind('click', editSectionName);
$('.edit-section-name-cancel').bind('click', cancelEditSectionName);
// $('.edit-section-name-save').bind('click', saveEditSectionName);
// section date setting // section date setting
$('.set-publish-date').bind('click', setSectionScheduleDate); $('.set-publish-date').bind('click', setSectionScheduleDate);
$('.edit-section-start-cancel').bind('click', cancelSetSectionScheduleDate); $('.edit-section-start-cancel').bind('click', cancelSetSectionScheduleDate);
...@@ -209,8 +204,8 @@ function toggleSections(e) { ...@@ -209,8 +204,8 @@ function toggleSections(e) {
$section = $('.courseware-section'); $section = $('.courseware-section');
sectionCount = $section.length; sectionCount = $section.length;
$button = $(this); $button = $(this);
$labelCollapsed = $('<i class="ss-icon ss-symbolicons-block">up</i> <span class="label">Collapse All Sections</span>'); $labelCollapsed = $('<i class="icon-arrow-up"></i> <span class="label">Collapse All Sections</span>');
$labelExpanded = $('<i class="ss-icon ss-symbolicons-block">down</i> <span class="label">Expand All Sections</span>'); $labelExpanded = $('<i class="icon-arrow-down"></i> <span class="label">Expand All Sections</span>');
var buttonLabel = $button.hasClass('is-activated') ? $labelCollapsed : $labelExpanded; var buttonLabel = $button.hasClass('is-activated') ? $labelCollapsed : $labelExpanded;
$button.toggleClass('is-activated').html(buttonLabel); $button.toggleClass('is-activated').html(buttonLabel);
...@@ -763,72 +758,6 @@ function cancelNewSubsection(e) { ...@@ -763,72 +758,6 @@ function cancelNewSubsection(e) {
$(this).parents('li.branch').remove(); $(this).parents('li.branch').remove();
} }
function editSectionName(e) {
e.preventDefault();
$(this).unbind('click', editSectionName);
$(this).children('.section-name-edit').show();
$(this).find('.edit-section-name').focus();
$(this).children('.section-name-span').hide();
$(this).find('.section-name-edit').bind('submit', saveEditSectionName);
$(this).find('.edit-section-name-cancel').bind('click', cancelNewSection);
$body.bind('keyup', {
$cancelButton: $(this).find('.edit-section-name-cancel')
}, checkForCancel);
}
function cancelEditSectionName(e) {
e.preventDefault();
$(this).parent().hide();
$(this).parent().siblings('.section-name-span').show();
$(this).closest('.section-name').bind('click', editSectionName);
e.stopPropagation();
}
function saveEditSectionName(e) {
e.preventDefault();
$(this).closest('.section-name').unbind('click', editSectionName);
var id = $(this).closest('.courseware-section').data('id');
var display_name = $.trim($(this).find('.edit-section-name').val());
$(this).closest('.courseware-section .section-name').append($spinner);
$spinner.show();
if (display_name == '') {
alert("You must specify a name before saving.");
return;
}
analytics.track('Edited Section Name', {
'course': course_location_analytics,
'display_name': display_name,
'id': id
});
var $_this = $(this);
// call into server to commit the new order
$.ajax({
url: "/save_item",
type: "POST",
dataType: "json",
contentType: "application/json",
data: JSON.stringify({
'id': id,
'metadata': {
'display_name': display_name
}
})
}).success(function() {
$spinner.delay(250).fadeOut(250);
$_this.closest('h3').find('.section-name-span').html(display_name).show();
$_this.hide();
$_this.closest('.section-name').bind('click', editSectionName);
e.stopPropagation();
});
}
function setSectionScheduleDate(e) { function setSectionScheduleDate(e) {
e.preventDefault(); e.preventDefault();
$(this).closest("h4").hide(); $(this).closest("h4").hide();
......
CMS.Models.SystemFeedback = Backbone.Model.extend({
defaults: {
"intent": null, // "warning", "confirmation", "error", "announcement", "step-required", etc
"title": "",
"message": ""
/* could also have an "actions" hash: here is an example demonstrating
the expected structure
"actions": {
"primary": {
"text": "Save",
"class": "action-save",
"click": function() {
// do something when Save is clicked
// `this` refers to the model
}
},
"secondary": [
{
"text": "Cancel",
"class": "action-cancel",
"click": function() {}
}, {
"text": "Discard Changes",
"class": "action-discard",
"click": function() {}
}
]
}
*/
}
});
CMS.Models.WarningMessage = CMS.Models.SystemFeedback.extend({
defaults: $.extend({}, CMS.Models.SystemFeedback.prototype.defaults, {
"intent": "warning"
})
});
CMS.Models.ErrorMessage = CMS.Models.SystemFeedback.extend({
defaults: $.extend({}, CMS.Models.SystemFeedback.prototype.defaults, {
"intent": "error"
})
});
CMS.Models.ConfirmationMessage = CMS.Models.SystemFeedback.extend({
defaults: $.extend({}, CMS.Models.SystemFeedback.prototype.defaults, {
"intent": "confirmation"
})
});
CMS.Models.Section = Backbone.Model.extend({
defaults: {
"name": ""
},
validate: function(attrs, options) {
if (!attrs.name) {
return gettext("You must specify a name");
}
},
url: "/save_item",
toJSON: function() {
return {
id: this.get("id"),
metadata: {
display_name: this.get("name")
}
};
},
initialize: function() {
this.listenTo(this, "request", this.showNotification);
this.listenTo(this, "sync", this.hideNotification);
},
showNotification: function() {
if(!this.msg) {
this.msg = new CMS.Models.SystemFeedback({
intent: "saving",
title: gettext("Saving&hellip;")
});
}
if(!this.msgView) {
this.msgView = new CMS.Views.Notification({
model: this.msg,
closeIcon: false,
minShown: 1250
});
}
this.msgView.show();
},
hideNotification: function() {
if(!this.msgView) { return; }
this.msgView.hide();
}
});
...@@ -40,7 +40,6 @@ CMS.Models.Settings.Advanced = Backbone.Model.extend({ ...@@ -40,7 +40,6 @@ CMS.Models.Settings.Advanced = Backbone.Model.extend({
// data // data
data : JSON.stringify({ deleteKeys : self.deleteKeys}) data : JSON.stringify({ deleteKeys : self.deleteKeys})
}) })
.fail(function(hdr, status, error) { CMS.ServerError(self, "Deleting keys:" + status); })
.done(function(data, status, error) { .done(function(data, status, error) {
// clear deleteKeys on success // clear deleteKeys on success
self.deleteKeys = []; self.deleteKeys = [];
......
...@@ -22,8 +22,7 @@ CMS.Views.Checklists = Backbone.View.extend({ ...@@ -22,8 +22,7 @@ CMS.Views.Checklists = Backbone.View.extend({
} }
); );
}, },
reset: true, reset: true
error: CMS.ServerError
} }
); );
}, },
...@@ -90,8 +89,7 @@ CMS.Views.Checklists = Backbone.View.extend({ ...@@ -90,8 +89,7 @@ CMS.Views.Checklists = Backbone.View.extend({
'task': model.attributes.items[task_index].short_description, 'task': model.attributes.items[task_index].short_description,
'state': model.attributes.items[task_index].is_checked 'state': model.attributes.items[task_index].is_checked
}); });
}, }
error : CMS.ServerError
}); });
} }
}); });
...@@ -105,7 +105,7 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({ ...@@ -105,7 +105,7 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
var targetModel = this.eventModel(event); var targetModel = this.eventModel(event);
targetModel.set({ date : this.dateEntry(event).val(), content : this.$codeMirror.getValue() }); targetModel.set({ date : this.dateEntry(event).val(), content : this.$codeMirror.getValue() });
// push change to display, hide the editor, submit the change // push change to display, hide the editor, submit the change
targetModel.save({}, {error : CMS.ServerError}); targetModel.save({});
this.closeEditor(this); this.closeEditor(this);
analytics.track('Saved Course Update', { analytics.track('Saved Course Update', {
...@@ -166,11 +166,9 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({ ...@@ -166,11 +166,9 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
success: function() { success: function() {
cacheThis.render(); cacheThis.render();
}, },
reset: true, reset: true
error: CMS.ServerError
}); });
}, }
error : CMS.ServerError
}); });
}, },
...@@ -254,8 +252,7 @@ CMS.Views.ClassInfoHandoutsView = Backbone.View.extend({ ...@@ -254,8 +252,7 @@ CMS.Views.ClassInfoHandoutsView = Backbone.View.extend({
} }
); );
}, },
reset: true, reset: true
error: CMS.ServerError
}); });
}, },
...@@ -296,7 +293,7 @@ CMS.Views.ClassInfoHandoutsView = Backbone.View.extend({ ...@@ -296,7 +293,7 @@ CMS.Views.ClassInfoHandoutsView = Backbone.View.extend({
onSave: function(event) { onSave: function(event) {
this.model.set('data', this.$codeMirror.getValue()); this.model.set('data', this.$codeMirror.getValue());
this.render(); this.render();
this.model.save({}, {error: CMS.ServerError}); this.model.save({});
this.$form.hide(); this.$form.hide();
this.closeEditor(this); this.closeEditor(this);
......
CMS.Views.Alert = Backbone.View.extend({
options: {
type: "alert",
shown: true, // is this view currently being shown?
icon: true, // should we render an icon related to the message intent?
closeIcon: true, // should we render a close button in the top right corner?
minShown: 0, // length of time after this view has been shown before it can be hidden (milliseconds)
maxShown: Infinity // length of time after this view has been shown before it will be automatically hidden (milliseconds)
},
initialize: function() {
var tpl = $("#system-feedback-tpl").text();
if(!tpl) {
console.error("Couldn't load system-feedback template");
}
this.template = _.template(tpl);
this.setElement($("#page-"+this.options.type));
this.listenTo(this.model, 'change', this.render);
return this.show();
},
render: function() {
var attrs = $.extend({}, this.options, this.model.attributes);
this.$el.html(this.template(attrs));
return this;
},
events: {
"click .action-close": "hide",
"click .action-primary": "primaryClick",
"click .action-secondary": "secondaryClick"
},
show: function() {
clearTimeout(this.hideTimeout);
this.options.shown = true;
this.shownAt = new Date();
this.render();
if($.isNumeric(this.options.maxShown)) {
this.hideTimeout = setTimeout($.proxy(this.hide, this),
this.options.maxShown);
}
return this;
},
hide: function() {
if(this.shownAt && $.isNumeric(this.options.minShown) &&
this.options.minShown > new Date() - this.shownAt)
{
clearTimeout(this.hideTimeout);
this.hideTimeout = setTimeout($.proxy(this.hide, this),
this.options.minShown - (new Date() - this.shownAt));
} else {
this.options.shown = false;
delete this.shownAt;
this.render();
}
return this;
},
primaryClick: function() {
var actions = this.model.get("actions");
if(!actions) { return; }
var primary = actions.primary;
if(!primary) { return; }
if(primary.click) {
primary.click.call(this.model, this);
}
},
secondaryClick: function(e) {
var actions = this.model.get("actions");
if(!actions) { return; }
var secondaryList = actions.secondary;
if(!secondaryList) { return; }
// which secondary action was clicked?
var i = 0; // default to the first secondary action (easier for testing)
if(e && e.target) {
i = _.indexOf(this.$(".action-secondary"), e.target);
}
var secondary = this.model.get("actions").secondary[i];
if(secondary.click) {
secondary.click.call(this.model, this);
}
}
});
CMS.Views.Notification = CMS.Views.Alert.extend({
options: $.extend({}, CMS.Views.Alert.prototype.options, {
type: "notification",
closeIcon: false
})
});
CMS.Views.Prompt = CMS.Views.Alert.extend({
options: $.extend({}, CMS.Views.Alert.prototype.options, {
type: "prompt",
closeIcon: false,
icon: false
}),
render: function() {
if(!window.$body) { window.$body = $(document.body); }
if(this.options.shown) {
$body.addClass('prompt-is-shown');
} else {
$body.removeClass('prompt-is-shown');
}
// super() in Javascript has awkward syntax :(
return CMS.Views.Alert.prototype.render.apply(this, arguments);
}
});
...@@ -35,7 +35,7 @@ CMS.Views.OverviewAssignmentGrader = Backbone.View.extend({ ...@@ -35,7 +35,7 @@ CMS.Views.OverviewAssignmentGrader = Backbone.View.extend({
// TODO move to a template file // TODO move to a template file
'<h4 class="status-label"><%= assignmentType %></h4>' + '<h4 class="status-label"><%= assignmentType %></h4>' +
'<a data-tooltip="Mark/unmark this subsection as graded" class="menu-toggle" href="#">' + '<a data-tooltip="Mark/unmark this subsection as graded" class="menu-toggle" href="#">' +
'<% if (!hideSymbol) {%><span class="ss-icon ss-standard">&#x2713;</span><%};%>' + '<% if (!hideSymbol) {%><i class="icon-ok"></i><%};%>' +
'</a>' + '</a>' +
'<ul class="menu">' + '<ul class="menu">' +
'<% graders.each(function(option) { %>' + '<% graders.each(function(option) { %>' +
......
CMS.Views.SectionShow = Backbone.View.extend({
template: _.template('<span data-tooltip="<%= gettext("Edit this section\'s name") %>" class="section-name-span"><%= name %></span>'),
render: function() {
var attrs = {
name: this.model.escape('name')
};
this.$el.html(this.template(attrs));
this.delegateEvents();
return this;
},
events: {
"click": "switchToEditView"
},
switchToEditView: function() {
if(!this.editView) {
this.editView = new CMS.Views.SectionEdit({
model: this.model, el: this.el, showView: this});
}
this.undelegateEvents();
this.editView.render();
}
});
CMS.Views.SectionEdit = Backbone.View.extend({
render: function() {
var attrs = {
name: this.model.escape('name')
};
this.$el.html(this.template(attrs));
this.delegateEvents();
return this;
},
initialize: function() {
this.template = _.template($("#section-name-edit-tpl").text());
this.listenTo(this.model, "invalid", this.showInvalidMessage);
this.render();
},
events: {
"click .save-button": "saveName",
"submit": "saveName",
"click .cancel-button": "switchToShowView"
},
saveName: function(e) {
if (e) { e.preventDefault(); }
var name = this.$("input[type=text]").val();
var that = this;
this.model.save("name", name, {
success: function() {
analytics.track('Edited Section Name', {
'course': course_location_analytics,
'display_name': that.model.get('name'),
'id': that.model.get('id')
});
that.switchToShowView();
}
});
},
switchToShowView: function() {
if(!this.showView) {
this.showView = new CMS.Views.SectionShow({
model: this.model, el: this.el, editView: this});
}
this.undelegateEvents();
this.stopListening();
this.showView.render();
},
showInvalidMessage: function(model, error, options) {
model.set("name", model.previous("name"));
var that = this;
var msg = new CMS.Models.ErrorMessage({
title: gettext("Your change could not be saved"),
message: error,
actions: {
primary: {
text: gettext("Return and resolve this issue"),
click: function(view) {
view.hide();
that.$("input[type=text]").focus();
}
}
}
});
new CMS.Views.Prompt({model: msg});
}
});
CMS.ServerError = function(model, error) {
// this handler is for the client:server communication not the validation errors which handleValidationError catches
window.alert("Server Error: " + error.responseText);
};
...@@ -23,7 +23,6 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({ ...@@ -23,7 +23,6 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({
// because these are outside of this.$el, they can't be in the event hash // because these are outside of this.$el, they can't be in the event hash
$('.save-button').on('click', this, this.saveView); $('.save-button').on('click', this, this.saveView);
$('.cancel-button').on('click', this, this.revertView); $('.cancel-button').on('click', this, this.revertView);
this.listenTo(this.model, 'error', CMS.ServerError);
this.listenTo(this.model, 'invalid', this.handleValidationError); this.listenTo(this.model, 'invalid', this.handleValidationError);
}, },
render: function() { render: function() {
...@@ -144,8 +143,7 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({ ...@@ -144,8 +143,7 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({
'course': course_location_analytics 'course': course_location_analytics
}); });
}, }
error : CMS.ServerError
}); });
}, },
revertView : function(event) { revertView : function(event) {
...@@ -155,8 +153,7 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({ ...@@ -155,8 +153,7 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({
self.model.clear({silent : true}); self.model.clear({silent : true});
self.model.fetch({ self.model.fetch({
success : function() { self.render(); }, success : function() { self.render(); },
reset: true, reset: true
error : CMS.ServerError
}); });
}, },
renderTemplate: function (key, value) { renderTemplate: function (key, value) {
......
...@@ -16,7 +16,7 @@ CMS.Views.Settings.Details = CMS.Views.ValidatingView.extend({ ...@@ -16,7 +16,7 @@ CMS.Views.Settings.Details = CMS.Views.ValidatingView.extend({
}, },
initialize : function() { initialize : function() {
this.fileAnchorTemplate = _.template('<a href="<%= fullpath %>"> <i class="ss-icon ss-standard">&#x1F4C4;</i><%= filename %></a>'); this.fileAnchorTemplate = _.template('<a href="<%= fullpath %>"> <i class="icon-file"></i><%= filename %></a>');
// fill in fields // fill in fields
this.$el.find("#course-name").val(this.model.get('location').get('name')); this.$el.find("#course-name").val(this.model.get('location').get('name'));
this.$el.find("#course-organization").val(this.model.get('location').get('org')); this.$el.find("#course-organization").val(this.model.get('location').get('org'));
...@@ -26,7 +26,6 @@ CMS.Views.Settings.Details = CMS.Views.ValidatingView.extend({ ...@@ -26,7 +26,6 @@ CMS.Views.Settings.Details = CMS.Views.ValidatingView.extend({
var dateIntrospect = new Date(); var dateIntrospect = new Date();
this.$el.find('#timezone').html("(" + dateIntrospect.getTimezone() + ")"); this.$el.find('#timezone').html("(" + dateIntrospect.getTimezone() + ")");
this.listenTo(this.model, 'error', CMS.ServerError);
this.listenTo(this.model, 'invalid', this.handleValidationError); this.listenTo(this.model, 'invalid', this.handleValidationError);
this.selectorToField = _.invert(this.fieldToSelectorMap); this.selectorToField = _.invert(this.fieldToSelectorMap);
}, },
......
...@@ -44,7 +44,6 @@ CMS.Views.Settings.Grading = CMS.Views.ValidatingView.extend({ ...@@ -44,7 +44,6 @@ CMS.Views.Settings.Grading = CMS.Views.ValidatingView.extend({
self.render(); self.render();
} }
); );
this.listenTo(this.model, 'error', CMS.ServerError);
this.listenTo(this.model, 'invalid', this.handleValidationError); this.listenTo(this.model, 'invalid', this.handleValidationError);
this.model.get('graders').on('remove', this.render, this); this.model.get('graders').on('remove', this.render, this);
this.model.get('graders').on('reset', this.render, this); this.model.get('graders').on('reset', this.render, this);
...@@ -65,7 +64,7 @@ CMS.Views.Settings.Grading = CMS.Views.ValidatingView.extend({ ...@@ -65,7 +64,7 @@ CMS.Views.Settings.Grading = CMS.Views.ValidatingView.extend({
gradeCollection.each(function(gradeModel) { gradeCollection.each(function(gradeModel) {
$(gradelist).append(self.template({model : gradeModel })); $(gradelist).append(self.template({model : gradeModel }));
var newEle = gradelist.children().last(); var newEle = gradelist.children().last();
var newView = new CMS.Views.Settings.GraderView({el: newEle, var newView = new CMS.Views.Settings.GraderView({el: newEle,
model : gradeModel, collection : gradeCollection }); model : gradeModel, collection : gradeCollection });
}); });
...@@ -119,7 +118,7 @@ CMS.Views.Settings.Grading = CMS.Views.ValidatingView.extend({ ...@@ -119,7 +118,7 @@ CMS.Views.Settings.Grading = CMS.Views.ValidatingView.extend({
gradeBarWidth : null, // cache of value since it won't change (more certain) gradeBarWidth : null, // cache of value since it won't change (more certain)
renderCutoffBar: function() { renderCutoffBar: function() {
var gradeBar =this.$el.find('.grade-bar'); var gradeBar =this.$el.find('.grade-bar');
this.gradeBarWidth = gradeBar.width(); this.gradeBarWidth = gradeBar.width();
var gradelist = gradeBar.children('.grades'); var gradelist = gradeBar.children('.grades');
// HACK fixing a duplicate call issue by undoing previous call effect. Need to figure out why called 2x // HACK fixing a duplicate call issue by undoing previous call effect. Need to figure out why called 2x
...@@ -128,11 +127,11 @@ CMS.Views.Settings.Grading = CMS.Views.ValidatingView.extend({ ...@@ -128,11 +127,11 @@ CMS.Views.Settings.Grading = CMS.Views.ValidatingView.extend({
// Can probably be simplified to one variable now. // Can probably be simplified to one variable now.
var removable = false; var removable = false;
var draggable = false; // first and last are not removable, first is not draggable var draggable = false; // first and last are not removable, first is not draggable
_.each(this.descendingCutoffs, _.each(this.descendingCutoffs,
function(cutoff, index) { function(cutoff, index) {
var newBar = this.gradeCutoffTemplate({ var newBar = this.gradeCutoffTemplate({
descriptor : cutoff['designation'] , descriptor : cutoff['designation'] ,
width : nextWidth, width : nextWidth,
removable : removable }); removable : removable });
gradelist.append(newBar); gradelist.append(newBar);
if (draggable) { if (draggable) {
...@@ -152,7 +151,7 @@ CMS.Views.Settings.Grading = CMS.Views.ValidatingView.extend({ ...@@ -152,7 +151,7 @@ CMS.Views.Settings.Grading = CMS.Views.ValidatingView.extend({
}, },
this); this);
// add fail which is not in data // add fail which is not in data
var failBar = this.gradeCutoffTemplate({ descriptor : this.failLabel(), var failBar = this.gradeCutoffTemplate({ descriptor : this.failLabel(),
width : nextWidth, removable : false}); width : nextWidth, removable : false});
$(failBar).find("span[contenteditable=true]").attr("contenteditable", false); $(failBar).find("span[contenteditable=true]").attr("contenteditable", false);
gradelist.append(failBar); gradelist.append(failBar);
...@@ -221,12 +220,12 @@ CMS.Views.Settings.Grading = CMS.Views.ValidatingView.extend({ ...@@ -221,12 +220,12 @@ CMS.Views.Settings.Grading = CMS.Views.ValidatingView.extend({
}, },
saveCutoffs: function() { saveCutoffs: function() {
this.model.save('grade_cutoffs', this.model.save('grade_cutoffs',
_.reduce(this.descendingCutoffs, _.reduce(this.descendingCutoffs,
function(object, cutoff) { function(object, cutoff) {
object[cutoff['designation']] = cutoff['cutoff'] / 100.0; object[cutoff['designation']] = cutoff['cutoff'] / 100.0;
return object; return object;
}, },
{})); {}));
}, },
...@@ -244,7 +243,7 @@ CMS.Views.Settings.Grading = CMS.Views.ValidatingView.extend({ ...@@ -244,7 +243,7 @@ CMS.Views.Settings.Grading = CMS.Views.ValidatingView.extend({
this.descendingCutoffs.push({designation: this.GRADES[gradeLength], cutoff: failBarWidth}); this.descendingCutoffs.push({designation: this.GRADES[gradeLength], cutoff: failBarWidth});
this.descendingCutoffs[gradeLength - 1]['cutoff'] = Math.round(targetWidth); this.descendingCutoffs[gradeLength - 1]['cutoff'] = Math.round(targetWidth);
var $newGradeBar = this.gradeCutoffTemplate({ descriptor : this.GRADES[gradeLength], var $newGradeBar = this.gradeCutoffTemplate({ descriptor : this.GRADES[gradeLength],
width : targetWidth, removable : true }); width : targetWidth, removable : true });
var gradeDom = this.$el.find('.grades'); var gradeDom = this.$el.find('.grades');
gradeDom.children().last().before($newGradeBar); gradeDom.children().last().before($newGradeBar);
...@@ -317,7 +316,6 @@ CMS.Views.Settings.GraderView = CMS.Views.ValidatingView.extend({ ...@@ -317,7 +316,6 @@ CMS.Views.Settings.GraderView = CMS.Views.ValidatingView.extend({
'blur :input' : "inputUnfocus" 'blur :input' : "inputUnfocus"
}, },
initialize : function() { initialize : function() {
this.listenTo(this.model, 'error', CMS.ServerError);
this.listenTo(this.model, 'invalid', this.handleValidationError); this.listenTo(this.model, 'invalid', this.handleValidationError);
this.selectorToField = _.invert(this.fieldToSelectorMap); this.selectorToField = _.invert(this.fieldToSelectorMap);
this.render(); this.render();
...@@ -362,9 +360,8 @@ CMS.Views.Settings.GraderView = CMS.Views.ValidatingView.extend({ ...@@ -362,9 +360,8 @@ CMS.Views.Settings.GraderView = CMS.Views.ValidatingView.extend({
} }
}, },
deleteModel : function(e) { deleteModel : function(e) {
this.model.destroy( this.model.destroy();
{ error : CMS.ServerError});
e.preventDefault(); e.preventDefault();
} }
}); });
\ No newline at end of file
...@@ -3,7 +3,6 @@ CMS.Views.ValidatingView = Backbone.View.extend({ ...@@ -3,7 +3,6 @@ CMS.Views.ValidatingView = Backbone.View.extend({
// decorates the fields. Needs wiring per class, but this initialization shows how // decorates the fields. Needs wiring per class, but this initialization shows how
// either have your init call this one or copy the contents // either have your init call this one or copy the contents
initialize : function() { initialize : function() {
this.listenTo(this.model, 'error', CMS.ServerError);
this.listenTo(this.model, 'invalid', this.handleValidationError); this.listenTo(this.model, 'invalid', this.handleValidationError);
this.selectorToField = _.invert(this.fieldToSelectorMap); this.selectorToField = _.invert(this.fieldToSelectorMap);
}, },
......
...@@ -314,7 +314,7 @@ p, ul, ol, dl { ...@@ -314,7 +314,7 @@ p, ul, ol, dl {
} }
.upload-button .icon-create { .upload-button .icon-plus {
@extend .t-action2; @extend .t-action2;
line-height: 0 !important; line-height: 0 !important;
} }
...@@ -750,11 +750,11 @@ hr.divide { ...@@ -750,11 +750,11 @@ hr.divide {
display: block; display: block;
} }
.icon-create { .icon-plus {
display: inline-block; display: inline-block;
vertical-align: middle; vertical-align: middle;
margin-right: ($baseline/4); margin-right: ($baseline/4);
margin-top: ($baseline/10); margin-top: -2px;
line-height: 0; line-height: 0;
} }
} }
...@@ -768,12 +768,12 @@ hr.divide { ...@@ -768,12 +768,12 @@ hr.divide {
display: block; display: block;
} }
.icon-view { .icon-eye-open {
@include font-size(15); @include font-size(16);
display: inline-block; display: inline-block;
vertical-align: middle; vertical-align: middle;
margin-right: ($baseline/2); margin-right: 8px;
margin-top: ($baseline/5); margin-top: -3px;
line-height: 0; line-height: 0;
} }
} }
...@@ -888,7 +888,7 @@ body.js { ...@@ -888,7 +888,7 @@ body.js {
@extend .text-sr; @extend .text-sr;
} }
.ss-icon { [class^="icon-"] {
@include font-size(18); @include font-size(18);
color: $white; color: $white;
} }
......
...@@ -5,6 +5,12 @@ ...@@ -5,6 +5,12 @@
// talbs: we need to slowly ween ourselves off of these // talbs: we need to slowly ween ourselves off of these
// ==================== // ====================
// line-height (old way)
@function lh($amount: 1) {
@return $body-line-height * $amount;
}
// inherited - vertical and horizontal centering // inherited - vertical and horizontal centering
@mixin vertically-and-horizontally-centered ($height, $width) { @mixin vertically-and-horizontally-centered ($height, $width) {
left: 50%; left: 50%;
......
...@@ -92,37 +92,6 @@ ...@@ -92,37 +92,6 @@
// ==================== // ====================
// notifications slide up then down
@mixin notificationsSlideUpDown {
0%, 100% {
@include transform(translateY(0));
}
15%, 85% {
@include transform(translateY(-($notification-height)));
}
20%, 80% {
@include transform(translateY(-($notification-height*0.99)));
}
}
@-moz-keyframes notificationsSlideUpDown { @include notificationsSlideUpDown(); }
@-webkit-keyframes notificationsSlideUpDown { @include notificationsSlideUpDown(); }
@-o-keyframes notificationsSlideUpDown { @include notificationsSlideUpDown(); }
@keyframes notificationsSlideUpDown { @include notificationsSlideUpDown();}
@mixin anim-notificationsSlideUpDown($duration, $timing: ease-in-out, $count: 1, $delay: 0) {
@include animation-name(notificationsSlideUpDown);
@include animation-duration($duration);
@include animation-delay($delay);
@include animation-timing-function($timing);
@include animation-iteration-count($count);
@include animation-fill-mode(both);
}
// ====================
// bounce in // bounce in
@mixin bounceIn { @mixin bounceIn {
0% { 0% {
......
...@@ -10,6 +10,7 @@ ...@@ -10,6 +10,7 @@
// ==================== // ====================
@import 'vendor/normalize'; @import 'vendor/normalize';
@import 'reset'; @import 'reset';
@import 'vendor/font-awesome';
// BASE *default edX offerings* // BASE *default edX offerings*
......
...@@ -48,7 +48,7 @@ ...@@ -48,7 +48,7 @@
padding: ($baseline/2) ($baseline*0.75); padding: ($baseline/2) ($baseline*0.75);
background: transparent; background: transparent;
.ss-icon { [class^="icon-"] {
@include transition(top .25s ease-in-out .25s); @include transition(top .25s ease-in-out .25s);
@include font-size(15); @include font-size(15);
display: inline-block; display: inline-block;
...@@ -60,7 +60,7 @@ ...@@ -60,7 +60,7 @@
&:hover, &:active { &:hover, &:active {
color: $gray-d2; color: $gray-d2;
.ss-icon { [class^="icon-"] {
color: $gray-d2; color: $gray-d2;
} }
} }
......
...@@ -219,10 +219,11 @@ ...@@ -219,10 +219,11 @@
.nav-item { .nav-item {
position: relative; position: relative;
.icon-expand { .icon-caret-down {
@include font-size(14); @include font-size(14);
@include transition (color 0.5s ease-in-out, opacity 0.5s ease-in-out); @include transition (color 0.5s ease-in-out, opacity 0.5s ease-in-out);
display: inline-block; display: inline-block;
vertical-align: middle;
margin-left: 2px; margin-left: 2px;
opacity: 0.5; opacity: 0.5;
color: $gray-l2; color: $gray-l2;
...@@ -230,7 +231,7 @@ ...@@ -230,7 +231,7 @@
&:hover { &:hover {
.icon-expand { .icon-caret-down {
color: $blue; color: $blue;
opacity: 1.0; opacity: 1.0;
} }
......
...@@ -5,12 +5,12 @@ ...@@ -5,12 +5,12 @@
} }
.ss-icon { [class^="icon-"] {
} }
.icon-inline { .icon-inline {
display: inline-block; display: inline-block;
vertical-align: middle; vertical-align: middle;
margin-right: ($baseline/4); margin-right: ($baseline/4);
} }
\ No newline at end of file
...@@ -2,22 +2,22 @@ ...@@ -2,22 +2,22 @@
// ==================== // ====================
.modal-cover { .modal-cover {
@extend .depth3;
display: none; display: none;
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
z-index: 1000;
width: 100%; width: 100%;
height: 100%; height: 100%;
background: rgba(0, 0, 0, .8); background: rgba(0, 0, 0, .8);
} }
.modal { .modal {
@extend .depth4;
display: none; display: none;
position: fixed; position: fixed;
top: 60px; top: 60px;
left: 50%; left: 50%;
z-index: 1001;
width: 930px; width: 930px;
height: 540px; height: 540px;
margin-left: -465px; margin-left: -465px;
...@@ -61,12 +61,12 @@ ...@@ -61,12 +61,12 @@
// lean modal alternative // lean modal alternative
#lean_overlay { #lean_overlay {
@extend .depth4;
position: fixed; position: fixed;
z-index: 10000;
top: 0px; top: 0px;
left: 0px; left: 0px;
display: none; display: none;
height: 100%; height: 100%;
width: 100%; width: 100%;
background: $black; background: $black;
} }
\ No newline at end of file
...@@ -33,8 +33,9 @@ ...@@ -33,8 +33,9 @@
padding: ($baseline/2) $baseline; padding: ($baseline/2) $baseline;
color: $gray; color: $gray;
.icon { [class^="icon-"] {
@include font-size(14); @include font-size(14);
margin-right: ($baseline/4);
} }
&:hover { &:hover {
...@@ -62,10 +63,6 @@ ...@@ -62,10 +63,6 @@
@extend .t-title4; @extend .t-title4;
} }
.ss-icon {
@extend .t-icon;
@extend .icon-inline;
}
} }
// shared elements // shared elements
...@@ -98,8 +95,11 @@ ...@@ -98,8 +95,11 @@
@extend .t-action4; @extend .t-action4;
display: block; display: block;
.icon { [class^="icon-"] {
@include font-size(18); @include font-size(18);
vertical-align: middle;
margin-right: $baseline/4;
} }
&:hover, &:active { &:hover, &:active {
......
...@@ -144,7 +144,7 @@ ...@@ -144,7 +144,7 @@
// prompts // prompts
.wrapper-prompt { .wrapper-prompt {
@extend .depth4; @extend .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;
...@@ -204,7 +204,7 @@ ...@@ -204,7 +204,7 @@
// types of prompts - error // types of prompts - error
.prompt.error { .prompt.error {
.icon-error { [class^="icon"] {
color: $red-l1; color: $red-l1;
} }
...@@ -216,7 +216,7 @@ ...@@ -216,7 +216,7 @@
// types of prompts - confirmation // types of prompts - confirmation
.prompt.confirmation { .prompt.confirmation {
.icon-error { [class^="icon"] {
color: $green; color: $green;
} }
...@@ -228,7 +228,7 @@ ...@@ -228,7 +228,7 @@
// types of prompts - error // types of prompts - error
.prompt.warning { .prompt.warning {
.icon-warning { [class^="icon"] {
color: $orange; color: $orange;
} }
...@@ -242,7 +242,7 @@ ...@@ -242,7 +242,7 @@
// notifications // notifications
.wrapper-notification { .wrapper-notification {
@extend .depth3; @extend .depth5;
@include clearfix(); @include clearfix();
@include box-shadow(0 -1px 3px $shadow, inset 0 3px 1px $blue); @include box-shadow(0 -1px 3px $shadow, inset 0 3px 1px $blue);
position: fixed; position: fixed;
...@@ -253,7 +253,7 @@ ...@@ -253,7 +253,7 @@
&.wrapper-notification-warning { &.wrapper-notification-warning {
@include box-shadow(0 -1px 3px $shadow, inset 0 3px 1px $orange); @include box-shadow(0 -1px 3px $shadow, inset 0 3px 1px $orange);
.icon-warning { [class^="icon"] {
color: $orange; color: $orange;
} }
} }
...@@ -261,7 +261,7 @@ ...@@ -261,7 +261,7 @@
&.wrapper-notification-error { &.wrapper-notification-error {
@include box-shadow(0 -1px 3px $shadow, inset 0 3px 1px $red-l1); @include box-shadow(0 -1px 3px $shadow, inset 0 3px 1px $red-l1);
.icon-error { [class^="icon"] {
color: $red-l1; color: $red-l1;
} }
} }
...@@ -269,7 +269,7 @@ ...@@ -269,7 +269,7 @@
&.wrapper-notification-confirmation { &.wrapper-notification-confirmation {
@include box-shadow(0 -1px 3px $shadow, inset 0 3px 1px $green); @include box-shadow(0 -1px 3px $shadow, inset 0 3px 1px $green);
.icon-confirmation { [class^="icon"] {
color: $green; color: $green;
} }
} }
...@@ -294,13 +294,13 @@ ...@@ -294,13 +294,13 @@
max-width: none; max-width: none;
min-width: none; min-width: none;
.icon, .copy { [class^="icon"], .copy {
float: none; float: none;
display: inline-block; display: inline-block;
vertical-align: middle; vertical-align: middle;
} }
.icon { [class^="icon"] {
width: $baseline; width: $baseline;
height: ($baseline*1.25); height: ($baseline*1.25);
margin-right: ($baseline*0.75); margin-right: ($baseline*0.75);
...@@ -329,7 +329,7 @@ ...@@ -329,7 +329,7 @@
max-width: none; max-width: none;
min-width: none; min-width: none;
.icon-help { [class^="icon"] {
width: $baseline; width: $baseline;
margin-right: ($baseline*0.75); margin-right: ($baseline*0.75);
} }
...@@ -357,13 +357,13 @@ ...@@ -357,13 +357,13 @@
font-weight: 700; font-weight: 700;
} }
.icon, .copy { [class^="icon"], .copy {
float: left; float: left;
display: inline-block; display: inline-block;
vertical-align: middle; vertical-align: middle;
} }
.icon { [class^="icon"] {
@include transition (color 0.5s ease-in-out); @include transition (color 0.5s ease-in-out);
@include font-size(22); @include font-size(22);
width: flex-grid(1, 12); width: flex-grid(1, 12);
...@@ -389,7 +389,7 @@ ...@@ -389,7 +389,7 @@
// with actions // with actions
&.has-actions { &.has-actions {
.icon { [class^="icon"] {
width: flex-grid(1, 12); width: flex-grid(1, 12);
} }
...@@ -436,9 +436,11 @@ ...@@ -436,9 +436,11 @@
&.saving { &.saving {
.icon-saving { [class^="icon"] {
@include anim-rotateClockwise(3s, linear, infinite); @include anim-rotateClockwise(3s, linear, infinite);
width: 22px; width: 25px;
margin: -4px 10px 0 0;
@include transform-origin(52% 60%);
} }
.copy p { .copy p {
...@@ -472,7 +474,7 @@ ...@@ -472,7 +474,7 @@
&.wrapper-alert-warning { &.wrapper-alert-warning {
@include box-shadow(0 1px 1px $white, inset 0 2px 2px $shadow-d1, inset 0 -4px 1px $orange); @include box-shadow(0 1px 1px $white, inset 0 2px 2px $shadow-d1, inset 0 -4px 1px $orange);
.icon-warning { [class^="icon"] {
color: $orange; color: $orange;
} }
} }
...@@ -480,7 +482,7 @@ ...@@ -480,7 +482,7 @@
&.wrapper-alert-error { &.wrapper-alert-error {
@include box-shadow(0 1px 1px $white, inset 0 2px 2px $shadow-d1, inset 0 -4px 1px $red-l1); @include box-shadow(0 1px 1px $white, inset 0 2px 2px $shadow-d1, inset 0 -4px 1px $red-l1);
.icon-error { [class^="icon"] {
color: $red-l1; color: $red-l1;
} }
} }
...@@ -488,7 +490,7 @@ ...@@ -488,7 +490,7 @@
&.wrapper-alert-confirmation { &.wrapper-alert-confirmation {
@include box-shadow(0 1px 1px $white, inset 0 2px 2px $shadow-d1, inset 0 -4px 1px $green); @include box-shadow(0 1px 1px $white, inset 0 2px 2px $shadow-d1, inset 0 -4px 1px $green);
.icon-confirmation { [class^="icon"] {
color: $green; color: $green;
} }
} }
...@@ -496,7 +498,7 @@ ...@@ -496,7 +498,7 @@
&.wrapper-alert-announcement { &.wrapper-alert-announcement {
@include box-shadow(0 1px 1px $white, inset 0 2px 2px $shadow-d1, inset 0 -4px 1px $blue); @include box-shadow(0 1px 1px $white, inset 0 2px 2px $shadow-d1, inset 0 -4px 1px $blue);
.icon-announcement { [class^="icon"] {
color: $blue; color: $blue;
} }
} }
...@@ -504,7 +506,7 @@ ...@@ -504,7 +506,7 @@
&.wrapper-alert-step-required { &.wrapper-alert-step-required {
@include box-shadow(0 1px 1px $white, inset 0 2px 2px $shadow-d1, inset 0 -4px 1px $pink); @include box-shadow(0 1px 1px $white, inset 0 2px 2px $shadow-d1, inset 0 -4px 1px $pink);
.icon-step-required { [class^="icon"] {
color: $pink; color: $pink;
} }
} }
...@@ -524,11 +526,11 @@ ...@@ -524,11 +526,11 @@
font-weight: 700; font-weight: 700;
} }
.icon, .copy { [class^="icon"], .copy {
float: left; float: left;
} }
.icon { [class^="icon"] {
@include transition (color 0.5s ease-in-out); @include transition (color 0.5s ease-in-out);
@include font-size(22); @include font-size(22);
width: flex-grid(1, 12); width: flex-grid(1, 12);
...@@ -550,7 +552,7 @@ ...@@ -550,7 +552,7 @@
// with actions // with actions
&.has-actions { &.has-actions {
.icon { [class^="icon"] {
width: flex-grid(1, 12); width: flex-grid(1, 12);
} }
...@@ -600,7 +602,7 @@ ...@@ -600,7 +602,7 @@
@extend .text-sr; @extend .text-sr;
} }
.icon { [class^="icon"] {
@include font-size(14); @include font-size(14);
color: $white; color: $white;
width: auto; width: auto;
...@@ -669,10 +671,6 @@ ...@@ -669,10 +671,6 @@
&.is-hiding { &.is-hiding {
@include anim-notificationsSlideDown(0.25s); @include anim-notificationsSlideDown(0.25s);
} }
&.is-fleeting {
@include anim-notificationsSlideUpDown(2s);
}
} }
} }
......
...@@ -11,54 +11,54 @@ ...@@ -11,54 +11,54 @@
.t-title1 { .t-title1 {
@extend .t-title; @extend .t-title;
@include font-size(60); @include font-size(60);
@include lh(60); @include line-height(60);
} }
.t-title2 { .t-title2 {
@extend .t-title; @extend .t-title;
@include font-size(48); @include font-size(48);
@include lh(48); @include line-height(48);
} }
.t-title3 { .t-title3 {
@include font-size(36); @include font-size(36);
@include lh(36); @include line-height(36);
} }
.t-title4 { .t-title4 {
@extend .t-title; @extend .t-title;
@include font-size(24); @include font-size(24);
@include lh(24); @include line-height(24);
} }
.t-title5 { .t-title5 {
@extend .t-title; @extend .t-title;
@include font-size(18); @include font-size(18);
@include lh(18); @include line-height(18);
} }
.t-title6 { .t-title6 {
@extend .t-title; @extend .t-title;
@include font-size(16); @include font-size(16);
@include lh(16); @include line-height(16);
} }
.t-title7 { .t-title7 {
@extend .t-title; @extend .t-title;
@include font-size(14); @include font-size(14);
@include lh(14); @include line-height(14);
} }
.t-title8 { .t-title8 {
@extend .t-title; @extend .t-title;
@include font-size(12); @include font-size(12);
@include lh(12); @include line-height(12);
} }
.t-title9 { .t-title9 {
@extend .t-title; @extend .t-title;
@include font-size(11); @include font-size(11);
@include lh(11); @include line-height(11);
} }
// ==================== // ====================
...@@ -71,31 +71,31 @@ ...@@ -71,31 +71,31 @@
.t-copy-base { .t-copy-base {
@extend .t-copy; @extend .t-copy;
@include font-size(16); @include font-size(16);
@include lh(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 lh(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 lh(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 lh(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 lh(12); @include line-height(12);
} }
// ==================== // ====================
...@@ -103,22 +103,22 @@ ...@@ -103,22 +103,22 @@
// actions/labels // actions/labels
.t-action1 { .t-action1 {
@include font-size(18); @include font-size(18);
@include lh(18); @include line-height(18);
} }
.t-action2 { .t-action2 {
@include font-size(16); @include font-size(16);
@include lh(16); @include line-height(16);
} }
.t-action3 { .t-action3 {
@include font-size(14); @include font-size(14);
@include lh(14); @include line-height(14);
} }
.t-action4 { .t-action4 {
@include font-size(12); @include font-size(12);
@include lh(12); @include line-height(12);
} }
...@@ -131,7 +131,48 @@ ...@@ -131,7 +131,48 @@
// ==================== // ====================
// misc // icons
.t-icon { .t-icon1 {
line-height: 0; @include font-size(48);
@include line-height(48);
}
.t-icon2 {
@include font-size(36);
@include line-height(36);
}
.t-icon3 {
@include font-size(24);
@include line-height(24);
}
.t-icon4 {
@include font-size(18);
@include line-height(18);
}
.t-icon5 {
@include font-size(16);
@include line-height(16);
}
.t-icon6 {
@include font-size(14);
@include line-height(14);
}
.t-icon7 {
@include font-size(12);
@include line-height(12);
}
.t-icon8 {
@include font-size(11);
@include line-height(11);
}
.t-icon9 {
@include font-size(10);
@include line-height(10);
} }
/*!
* Font Awesome 3.1.0
* the iconic font designed for Bootstrap
* -------------------------------------------------------
* The full suite of pictographic icons, examples, and documentation
* can be found at: http://fontawesome.io
*
* License
* -------------------------------------------------------
* - The Font Awesome font is licensed under the SIL Open Font License v1.1 -
* http://scripts.sil.org/OFL
* - Font Awesome CSS, LESS, and SASS files are licensed under the MIT License -
* http://opensource.org/licenses/mit-license.html
* - Font Awesome documentation licensed under CC BY 3.0 License -
* http://creativecommons.org/licenses/by/3.0/
* - Attribution is no longer required in Font Awesome 3.0, but much appreciated:
* "Font Awesome by Dave Gandy - http://fontawesome.io"
* Contact
* -------------------------------------------------------
* Email: dave@fontawesome.io
* Twitter: http://twitter.com/fortaweso_me
* Work: Lead Product Designer @ http://kyruus.com
*/
/* FONT PATH
* -------------------------- */
@font-face {
font-family: 'FontAwesome';
src: url('../fonts/vendor/fontawesome-webfont.eot?v=3.1.0');
src: url('../fonts/vendor/fontawesome-webfont.eot?#iefix&v=3.1.0') format('embedded-opentype'), url('../fonts/vendor/fontawesome-webfont.woff?v=3.1.0') format('woff'), url('../fonts/vendor/fontawesome-webfont.ttf?v=3.1.0') format('truetype'), url('../fonts/vendor/fontawesome-webfont.svg#fontawesomeregular?v=3.1.0') format('svg');
font-weight: normal;
font-style: normal;
}
/* FONT AWESOME CORE
* -------------------------- */
[class^="icon-"],
[class*=" icon-"] {
font-family: FontAwesome;
font-weight: normal;
font-style: normal;
text-decoration: inherit;
-webkit-font-smoothing: antialiased;
*margin-right: .3em;
}
[class^="icon-"]:before,
[class*=" icon-"]:before {
text-decoration: inherit;
display: inline-block;
speak: none;
}
/* makes the font 33% larger relative to the icon container */
.icon-large:before {
vertical-align: -10%;
font-size: 1.3333333333333333em;
}
/* makes sure icons active on rollover in links */
a [class^="icon-"],
a [class*=" icon-"],
a [class^="icon-"]:before,
a [class*=" icon-"]:before {
display: inline;
}
/* increased font size for icon-large */
[class^="icon-"].icon-fixed-width,
[class*=" icon-"].icon-fixed-width {
display: inline-block;
width: 1.2857142857142858em;
text-align: center;
}
[class^="icon-"].icon-fixed-width.icon-large,
[class*=" icon-"].icon-fixed-width.icon-large {
width: 1.5714285714285714em;
}
ul.icons-ul {
list-style-type: none;
text-indent: -0.7142857142857143em;
margin-left: 2.142857142857143em;
}
ul.icons-ul > li .icon-li {
width: 0.7142857142857143em;
display: inline-block;
text-align: center;
}
[class^="icon-"].hide,
[class*=" icon-"].hide {
display: none;
}
.icon-muted {
color: #eeeeee;
}
.icon-light {
color: #ffffff;
}
.icon-dark {
color: #333333;
}
.icon-border {
border: solid 1px #eeeeee;
padding: .2em .25em .15em;
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
border-radius: 3px;
}
.icon-2x {
font-size: 2em;
}
.icon-2x.icon-border {
border-width: 2px;
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
border-radius: 4px;
}
.icon-3x {
font-size: 3em;
}
.icon-3x.icon-border {
border-width: 3px;
-webkit-border-radius: 5px;
-moz-border-radius: 5px;
border-radius: 5px;
}
.icon-4x {
font-size: 4em;
}
.icon-4x.icon-border {
border-width: 4px;
-webkit-border-radius: 6px;
-moz-border-radius: 6px;
border-radius: 6px;
}
.icon-5x {
font-size: 5em;
}
.icon-5x.icon-border {
border-width: 5px;
-webkit-border-radius: 7px;
-moz-border-radius: 7px;
border-radius: 7px;
}
.pull-right {
float: right;
}
.pull-left {
float: left;
}
[class^="icon-"].pull-left,
[class*=" icon-"].pull-left {
margin-right: .3em;
}
[class^="icon-"].pull-right,
[class*=" icon-"].pull-right {
margin-left: .3em;
}
/* BOOTSTRAP SPECIFIC CLASSES
* -------------------------- */
/* Bootstrap 2.0 sprites.less reset */
[class^="icon-"],
[class*=" icon-"] {
display: inline;
width: auto;
height: auto;
line-height: normal;
vertical-align: baseline;
background-image: none;
background-position: 0% 0%;
background-repeat: repeat;
margin-top: 0;
}
/* more sprites.less reset */
.icon-white,
.nav-pills > .active > a > [class^="icon-"],
.nav-pills > .active > a > [class*=" icon-"],
.nav-list > .active > a > [class^="icon-"],
.nav-list > .active > a > [class*=" icon-"],
.navbar-inverse .nav > .active > a > [class^="icon-"],
.navbar-inverse .nav > .active > a > [class*=" icon-"],
.dropdown-menu > li > a:hover > [class^="icon-"],
.dropdown-menu > li > a:hover > [class*=" icon-"],
.dropdown-menu > .active > a > [class^="icon-"],
.dropdown-menu > .active > a > [class*=" icon-"],
.dropdown-submenu:hover > a > [class^="icon-"],
.dropdown-submenu:hover > a > [class*=" icon-"] {
background-image: none;
}
/* keeps Bootstrap styles with and without icons the same */
.btn [class^="icon-"].icon-large,
.nav [class^="icon-"].icon-large,
.btn [class*=" icon-"].icon-large,
.nav [class*=" icon-"].icon-large {
line-height: .9em;
}
.btn [class^="icon-"].icon-spin,
.nav [class^="icon-"].icon-spin,
.btn [class*=" icon-"].icon-spin,
.nav [class*=" icon-"].icon-spin {
display: inline-block;
}
.nav-tabs [class^="icon-"],
.nav-pills [class^="icon-"],
.nav-tabs [class*=" icon-"],
.nav-pills [class*=" icon-"],
.nav-tabs [class^="icon-"].icon-large,
.nav-pills [class^="icon-"].icon-large,
.nav-tabs [class*=" icon-"].icon-large,
.nav-pills [class*=" icon-"].icon-large {
line-height: .9em;
}
.btn [class^="icon-"].pull-left.icon-2x,
.btn [class*=" icon-"].pull-left.icon-2x,
.btn [class^="icon-"].pull-right.icon-2x,
.btn [class*=" icon-"].pull-right.icon-2x {
margin-top: .18em;
}
.btn [class^="icon-"].icon-spin.icon-large,
.btn [class*=" icon-"].icon-spin.icon-large {
line-height: .8em;
}
.btn.btn-small [class^="icon-"].pull-left.icon-2x,
.btn.btn-small [class*=" icon-"].pull-left.icon-2x,
.btn.btn-small [class^="icon-"].pull-right.icon-2x,
.btn.btn-small [class*=" icon-"].pull-right.icon-2x {
margin-top: .25em;
}
.btn.btn-large [class^="icon-"],
.btn.btn-large [class*=" icon-"] {
margin-top: 0;
}
.btn.btn-large [class^="icon-"].pull-left.icon-2x,
.btn.btn-large [class*=" icon-"].pull-left.icon-2x,
.btn.btn-large [class^="icon-"].pull-right.icon-2x,
.btn.btn-large [class*=" icon-"].pull-right.icon-2x {
margin-top: .05em;
}
.btn.btn-large [class^="icon-"].pull-left.icon-2x,
.btn.btn-large [class*=" icon-"].pull-left.icon-2x {
margin-right: .2em;
}
.btn.btn-large [class^="icon-"].pull-right.icon-2x,
.btn.btn-large [class*=" icon-"].pull-right.icon-2x {
margin-left: .2em;
}
/* EXTRAS
* -------------------------- */
/* Stacked and layered icon */
.icon-stack {
position: relative;
display: inline-block;
width: 2em;
height: 2em;
line-height: 2em;
vertical-align: -35%;
}
.icon-stack [class^="icon-"],
.icon-stack [class*=" icon-"] {
display: block;
text-align: center;
position: absolute;
width: 100%;
height: 100%;
font-size: 1em;
line-height: inherit;
*line-height: 2em;
}
.icon-stack .icon-stack-base {
font-size: 2em;
*line-height: 1em;
}
/* Animated rotating icon */
.icon-spin {
display: inline-block;
-moz-animation: spin 2s infinite linear;
-o-animation: spin 2s infinite linear;
-webkit-animation: spin 2s infinite linear;
animation: spin 2s infinite linear;
}
@-moz-keyframes spin {
0% {
-moz-transform: rotate(0deg);
}
100% {
-moz-transform: rotate(359deg);
}
}
@-webkit-keyframes spin {
0% {
-webkit-transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(359deg);
}
}
@-o-keyframes spin {
0% {
-o-transform: rotate(0deg);
}
100% {
-o-transform: rotate(359deg);
}
}
@-ms-keyframes spin {
0% {
-ms-transform: rotate(0deg);
}
100% {
-ms-transform: rotate(359deg);
}
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(359deg);
}
}
/* Icon rotations and mirroring */
.icon-rotate-90:before {
-webkit-transform: rotate(90deg);
-moz-transform: rotate(90deg);
-ms-transform: rotate(90deg);
-o-transform: rotate(90deg);
transform: rotate(90deg);
filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=1);
}
.icon-rotate-180:before {
-webkit-transform: rotate(180deg);
-moz-transform: rotate(180deg);
-ms-transform: rotate(180deg);
-o-transform: rotate(180deg);
transform: rotate(180deg);
filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=2);
}
.icon-rotate-270:before {
-webkit-transform: rotate(270deg);
-moz-transform: rotate(270deg);
-ms-transform: rotate(270deg);
-o-transform: rotate(270deg);
transform: rotate(270deg);
filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=3);
}
.icon-flip-horizontal:before {
-webkit-transform: scale(-1, 1);
-moz-transform: scale(-1, 1);
-ms-transform: scale(-1, 1);
-o-transform: scale(-1, 1);
transform: scale(-1, 1);
}
.icon-flip-vertical:before {
-webkit-transform: scale(1, -1);
-moz-transform: scale(1, -1);
-ms-transform: scale(1, -1);
-o-transform: scale(1, -1);
transform: scale(1, -1);
}
/* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen
readers do not read off random characters that represent icons */
.icon-glass:before {
content: "\f000";
}
.icon-music:before {
content: "\f001";
}
.icon-search:before {
content: "\f002";
}
.icon-envelope:before {
content: "\f003";
}
.icon-heart:before {
content: "\f004";
}
.icon-star:before {
content: "\f005";
}
.icon-star-empty:before {
content: "\f006";
}
.icon-user:before {
content: "\f007";
}
.icon-film:before {
content: "\f008";
}
.icon-th-large:before {
content: "\f009";
}
.icon-th:before {
content: "\f00a";
}
.icon-th-list:before {
content: "\f00b";
}
.icon-ok:before {
content: "\f00c";
}
.icon-remove:before {
content: "\f00d";
}
.icon-zoom-in:before {
content: "\f00e";
}
.icon-zoom-out:before {
content: "\f010";
}
.icon-off:before {
content: "\f011";
}
.icon-signal:before {
content: "\f012";
}
.icon-cog:before {
content: "\f013";
}
.icon-trash:before {
content: "\f014";
}
.icon-home:before {
content: "\f015";
}
.icon-file:before {
content: "\f016";
}
.icon-time:before {
content: "\f017";
}
.icon-road:before {
content: "\f018";
}
.icon-download-alt:before {
content: "\f019";
}
.icon-download:before {
content: "\f01a";
}
.icon-upload:before {
content: "\f01b";
}
.icon-inbox:before {
content: "\f01c";
}
.icon-play-circle:before {
content: "\f01d";
}
.icon-repeat:before,
.icon-rotate-right:before {
content: "\f01e";
}
/* F020 doesn't work in Safari. all shifted one down */
.icon-refresh:before {
content: "\f021";
}
.icon-list-alt:before {
content: "\f022";
}
.icon-lock:before {
content: "\f023";
}
.icon-flag:before {
content: "\f024";
}
.icon-headphones:before {
content: "\f025";
}
.icon-volume-off:before {
content: "\f026";
}
.icon-volume-down:before {
content: "\f027";
}
.icon-volume-up:before {
content: "\f028";
}
.icon-qrcode:before {
content: "\f029";
}
.icon-barcode:before {
content: "\f02a";
}
.icon-tag:before {
content: "\f02b";
}
.icon-tags:before {
content: "\f02c";
}
.icon-book:before {
content: "\f02d";
}
.icon-bookmark:before {
content: "\f02e";
}
.icon-print:before {
content: "\f02f";
}
.icon-camera:before {
content: "\f030";
}
.icon-font:before {
content: "\f031";
}
.icon-bold:before {
content: "\f032";
}
.icon-italic:before {
content: "\f033";
}
.icon-text-height:before {
content: "\f034";
}
.icon-text-width:before {
content: "\f035";
}
.icon-align-left:before {
content: "\f036";
}
.icon-align-center:before {
content: "\f037";
}
.icon-align-right:before {
content: "\f038";
}
.icon-align-justify:before {
content: "\f039";
}
.icon-list:before {
content: "\f03a";
}
.icon-indent-left:before {
content: "\f03b";
}
.icon-indent-right:before {
content: "\f03c";
}
.icon-facetime-video:before {
content: "\f03d";
}
.icon-picture:before {
content: "\f03e";
}
.icon-pencil:before {
content: "\f040";
}
.icon-map-marker:before {
content: "\f041";
}
.icon-adjust:before {
content: "\f042";
}
.icon-tint:before {
content: "\f043";
}
.icon-edit:before {
content: "\f044";
}
.icon-share:before {
content: "\f045";
}
.icon-check:before {
content: "\f046";
}
.icon-move:before {
content: "\f047";
}
.icon-step-backward:before {
content: "\f048";
}
.icon-fast-backward:before {
content: "\f049";
}
.icon-backward:before {
content: "\f04a";
}
.icon-play:before {
content: "\f04b";
}
.icon-pause:before {
content: "\f04c";
}
.icon-stop:before {
content: "\f04d";
}
.icon-forward:before {
content: "\f04e";
}
.icon-fast-forward:before {
content: "\f050";
}
.icon-step-forward:before {
content: "\f051";
}
.icon-eject:before {
content: "\f052";
}
.icon-chevron-left:before {
content: "\f053";
}
.icon-chevron-right:before {
content: "\f054";
}
.icon-plus-sign:before {
content: "\f055";
}
.icon-minus-sign:before {
content: "\f056";
}
.icon-remove-sign:before {
content: "\f057";
}
.icon-ok-sign:before {
content: "\f058";
}
.icon-question-sign:before {
content: "\f059";
}
.icon-info-sign:before {
content: "\f05a";
}
.icon-screenshot:before {
content: "\f05b";
}
.icon-remove-circle:before {
content: "\f05c";
}
.icon-ok-circle:before {
content: "\f05d";
}
.icon-ban-circle:before {
content: "\f05e";
}
.icon-arrow-left:before {
content: "\f060";
}
.icon-arrow-right:before {
content: "\f061";
}
.icon-arrow-up:before {
content: "\f062";
}
.icon-arrow-down:before {
content: "\f063";
}
.icon-share-alt:before,
.icon-mail-forward:before {
content: "\f064";
}
.icon-resize-full:before {
content: "\f065";
}
.icon-resize-small:before {
content: "\f066";
}
.icon-plus:before {
content: "\f067";
}
.icon-minus:before {
content: "\f068";
}
.icon-asterisk:before {
content: "\f069";
}
.icon-exclamation-sign:before {
content: "\f06a";
}
.icon-gift:before {
content: "\f06b";
}
.icon-leaf:before {
content: "\f06c";
}
.icon-fire:before {
content: "\f06d";
}
.icon-eye-open:before {
content: "\f06e";
}
.icon-eye-close:before {
content: "\f070";
}
.icon-warning-sign:before {
content: "\f071";
}
.icon-plane:before {
content: "\f072";
}
.icon-calendar:before {
content: "\f073";
}
.icon-random:before {
content: "\f074";
}
.icon-comment:before {
content: "\f075";
}
.icon-magnet:before {
content: "\f076";
}
.icon-chevron-up:before {
content: "\f077";
}
.icon-chevron-down:before {
content: "\f078";
}
.icon-retweet:before {
content: "\f079";
}
.icon-shopping-cart:before {
content: "\f07a";
}
.icon-folder-close:before {
content: "\f07b";
}
.icon-folder-open:before {
content: "\f07c";
}
.icon-resize-vertical:before {
content: "\f07d";
}
.icon-resize-horizontal:before {
content: "\f07e";
}
.icon-bar-chart:before {
content: "\f080";
}
.icon-twitter-sign:before {
content: "\f081";
}
.icon-facebook-sign:before {
content: "\f082";
}
.icon-camera-retro:before {
content: "\f083";
}
.icon-key:before {
content: "\f084";
}
.icon-cogs:before {
content: "\f085";
}
.icon-comments:before {
content: "\f086";
}
.icon-thumbs-up:before {
content: "\f087";
}
.icon-thumbs-down:before {
content: "\f088";
}
.icon-star-half:before {
content: "\f089";
}
.icon-heart-empty:before {
content: "\f08a";
}
.icon-signout:before {
content: "\f08b";
}
.icon-linkedin-sign:before {
content: "\f08c";
}
.icon-pushpin:before {
content: "\f08d";
}
.icon-external-link:before {
content: "\f08e";
}
.icon-signin:before {
content: "\f090";
}
.icon-trophy:before {
content: "\f091";
}
.icon-github-sign:before {
content: "\f092";
}
.icon-upload-alt:before {
content: "\f093";
}
.icon-lemon:before {
content: "\f094";
}
.icon-phone:before {
content: "\f095";
}
.icon-check-empty:before {
content: "\f096";
}
.icon-bookmark-empty:before {
content: "\f097";
}
.icon-phone-sign:before {
content: "\f098";
}
.icon-twitter:before {
content: "\f099";
}
.icon-facebook:before {
content: "\f09a";
}
.icon-github:before {
content: "\f09b";
}
.icon-unlock:before {
content: "\f09c";
}
.icon-credit-card:before {
content: "\f09d";
}
.icon-rss:before {
content: "\f09e";
}
.icon-hdd:before {
content: "\f0a0";
}
.icon-bullhorn:before {
content: "\f0a1";
}
.icon-bell:before {
content: "\f0a2";
}
.icon-certificate:before {
content: "\f0a3";
}
.icon-hand-right:before {
content: "\f0a4";
}
.icon-hand-left:before {
content: "\f0a5";
}
.icon-hand-up:before {
content: "\f0a6";
}
.icon-hand-down:before {
content: "\f0a7";
}
.icon-circle-arrow-left:before {
content: "\f0a8";
}
.icon-circle-arrow-right:before {
content: "\f0a9";
}
.icon-circle-arrow-up:before {
content: "\f0aa";
}
.icon-circle-arrow-down:before {
content: "\f0ab";
}
.icon-globe:before {
content: "\f0ac";
}
.icon-wrench:before {
content: "\f0ad";
}
.icon-tasks:before {
content: "\f0ae";
}
.icon-filter:before {
content: "\f0b0";
}
.icon-briefcase:before {
content: "\f0b1";
}
.icon-fullscreen:before {
content: "\f0b2";
}
.icon-group:before {
content: "\f0c0";
}
.icon-link:before {
content: "\f0c1";
}
.icon-cloud:before {
content: "\f0c2";
}
.icon-beaker:before {
content: "\f0c3";
}
.icon-cut:before {
content: "\f0c4";
}
.icon-copy:before {
content: "\f0c5";
}
.icon-paper-clip:before {
content: "\f0c6";
}
.icon-save:before {
content: "\f0c7";
}
.icon-sign-blank:before {
content: "\f0c8";
}
.icon-reorder:before {
content: "\f0c9";
}
.icon-list-ul:before {
content: "\f0ca";
}
.icon-list-ol:before {
content: "\f0cb";
}
.icon-strikethrough:before {
content: "\f0cc";
}
.icon-underline:before {
content: "\f0cd";
}
.icon-table:before {
content: "\f0ce";
}
.icon-magic:before {
content: "\f0d0";
}
.icon-truck:before {
content: "\f0d1";
}
.icon-pinterest:before {
content: "\f0d2";
}
.icon-pinterest-sign:before {
content: "\f0d3";
}
.icon-google-plus-sign:before {
content: "\f0d4";
}
.icon-google-plus:before {
content: "\f0d5";
}
.icon-money:before {
content: "\f0d6";
}
.icon-caret-down:before {
content: "\f0d7";
}
.icon-caret-up:before {
content: "\f0d8";
}
.icon-caret-left:before {
content: "\f0d9";
}
.icon-caret-right:before {
content: "\f0da";
}
.icon-columns:before {
content: "\f0db";
}
.icon-sort:before {
content: "\f0dc";
}
.icon-sort-down:before {
content: "\f0dd";
}
.icon-sort-up:before {
content: "\f0de";
}
.icon-envelope-alt:before {
content: "\f0e0";
}
.icon-linkedin:before {
content: "\f0e1";
}
.icon-undo:before,
.icon-rotate-left:before {
content: "\f0e2";
}
.icon-legal:before {
content: "\f0e3";
}
.icon-dashboard:before {
content: "\f0e4";
}
.icon-comment-alt:before {
content: "\f0e5";
}
.icon-comments-alt:before {
content: "\f0e6";
}
.icon-bolt:before {
content: "\f0e7";
}
.icon-sitemap:before {
content: "\f0e8";
}
.icon-umbrella:before {
content: "\f0e9";
}
.icon-paste:before {
content: "\f0ea";
}
.icon-lightbulb:before {
content: "\f0eb";
}
.icon-exchange:before {
content: "\f0ec";
}
.icon-cloud-download:before {
content: "\f0ed";
}
.icon-cloud-upload:before {
content: "\f0ee";
}
.icon-user-md:before {
content: "\f0f0";
}
.icon-stethoscope:before {
content: "\f0f1";
}
.icon-suitcase:before {
content: "\f0f2";
}
.icon-bell-alt:before {
content: "\f0f3";
}
.icon-coffee:before {
content: "\f0f4";
}
.icon-food:before {
content: "\f0f5";
}
.icon-file-alt:before {
content: "\f0f6";
}
.icon-building:before {
content: "\f0f7";
}
.icon-hospital:before {
content: "\f0f8";
}
.icon-ambulance:before {
content: "\f0f9";
}
.icon-medkit:before {
content: "\f0fa";
}
.icon-fighter-jet:before {
content: "\f0fb";
}
.icon-beer:before {
content: "\f0fc";
}
.icon-h-sign:before {
content: "\f0fd";
}
.icon-plus-sign-alt:before {
content: "\f0fe";
}
.icon-double-angle-left:before {
content: "\f100";
}
.icon-double-angle-right:before {
content: "\f101";
}
.icon-double-angle-up:before {
content: "\f102";
}
.icon-double-angle-down:before {
content: "\f103";
}
.icon-angle-left:before {
content: "\f104";
}
.icon-angle-right:before {
content: "\f105";
}
.icon-angle-up:before {
content: "\f106";
}
.icon-angle-down:before {
content: "\f107";
}
.icon-desktop:before {
content: "\f108";
}
.icon-laptop:before {
content: "\f109";
}
.icon-tablet:before {
content: "\f10a";
}
.icon-mobile-phone:before {
content: "\f10b";
}
.icon-circle-blank:before {
content: "\f10c";
}
.icon-quote-left:before {
content: "\f10d";
}
.icon-quote-right:before {
content: "\f10e";
}
.icon-spinner:before {
content: "\f110";
}
.icon-circle:before {
content: "\f111";
}
.icon-reply:before,
.icon-mail-reply:before {
content: "\f112";
}
.icon-folder-close-alt:before {
content: "\f114";
}
.icon-folder-open-alt:before {
content: "\f115";
}
.icon-expand-alt:before {
content: "\f116";
}
.icon-collapse-alt:before {
content: "\f117";
}
.icon-smile:before {
content: "\f118";
}
.icon-frown:before {
content: "\f119";
}
.icon-meh:before {
content: "\f11a";
}
.icon-gamepad:before {
content: "\f11b";
}
.icon-keyboard:before {
content: "\f11c";
}
.icon-flag-alt:before {
content: "\f11d";
}
.icon-flag-checkered:before {
content: "\f11e";
}
.icon-terminal:before {
content: "\f120";
}
.icon-code:before {
content: "\f121";
}
.icon-reply-all:before {
content: "\f122";
}
.icon-mail-reply-all:before {
content: "\f122";
}
.icon-star-half-full:before,
.icon-star-half-empty:before {
content: "\f123";
}
.icon-location-arrow:before {
content: "\f124";
}
.icon-crop:before {
content: "\f125";
}
.icon-code-fork:before {
content: "\f126";
}
.icon-unlink:before {
content: "\f127";
}
.icon-question:before {
content: "\f128";
}
.icon-info:before {
content: "\f129";
}
.icon-exclamation:before {
content: "\f12a";
}
.icon-superscript:before {
content: "\f12b";
}
.icon-subscript:before {
content: "\f12c";
}
.icon-eraser:before {
content: "\f12d";
}
.icon-puzzle-piece:before {
content: "\f12e";
}
.icon-microphone:before {
content: "\f130";
}
.icon-microphone-off:before {
content: "\f131";
}
.icon-shield:before {
content: "\f132";
}
.icon-calendar-empty:before {
content: "\f133";
}
.icon-fire-extinguisher:before {
content: "\f134";
}
.icon-rocket:before {
content: "\f135";
}
.icon-maxcdn:before {
content: "\f136";
}
.icon-chevron-sign-left:before {
content: "\f137";
}
.icon-chevron-sign-right:before {
content: "\f138";
}
.icon-chevron-sign-up:before {
content: "\f139";
}
.icon-chevron-sign-down:before {
content: "\f13a";
}
.icon-html5:before {
content: "\f13b";
}
.icon-css3:before {
content: "\f13c";
}
.icon-anchor:before {
content: "\f13d";
}
.icon-unlock-alt:before {
content: "\f13e";
}
.icon-bullseye:before {
content: "\f140";
}
.icon-ellipsis-horizontal:before {
content: "\f141";
}
.icon-ellipsis-vertical:before {
content: "\f142";
}
.icon-rss-sign:before {
content: "\f143";
}
.icon-play-sign:before {
content: "\f144";
}
.icon-ticket:before {
content: "\f145";
}
.icon-minus-sign-alt:before {
content: "\f146";
}
.icon-check-minus:before {
content: "\f147";
}
.icon-level-up:before {
content: "\f148";
}
.icon-level-down:before {
content: "\f149";
}
.icon-check-sign:before {
content: "\f14a";
}
.icon-edit-sign:before {
content: "\f14b";
}
.icon-external-link-sign:before {
content: "\f14c";
}
.icon-share-sign:before {
content: "\f14d";
}
...@@ -272,9 +272,9 @@ body.signup, body.signin { ...@@ -272,9 +272,9 @@ body.signup, body.signin {
background: $yellow-d1; background: $yellow-d1;
color: $white; color: $white;
.ss-icon { [class^="icon-"] {
position: relative; position: relative;
top: 3px; top: 1px;
@include font-size(16); @include font-size(16);
display: inline-block; display: inline-block;
margin-right: ($baseline/2); margin-right: ($baseline/2);
......
...@@ -2,13 +2,25 @@ ...@@ -2,13 +2,25 @@
// ==================== // ====================
body.course.uploads { body.course.uploads {
.nav-actions {
.icon-cloud-upload {
@include font-size(16);
vertical-align: bottom;
margin-right: ($baseline/5);
}
}
input.asset-search-input { input.asset-search-input {
float: left; float: left;
width: 260px; width: 260px;
background-color: #fff; background-color: #fff;
} }
.asset-library { .asset-library {
@include clearfix; @include clearfix;
...@@ -102,7 +114,7 @@ body.course.uploads { ...@@ -102,7 +114,7 @@ body.course.uploads {
} }
.upload-modal { .upload-modal {
display: none; display: none;
width: 640px !important; width: 640px !important;
margin-left: -320px !important; margin-left: -320px !important;
...@@ -187,4 +199,4 @@ body.course.uploads { ...@@ -187,4 +199,4 @@ body.course.uploads {
display: none; display: none;
margin-bottom: 100px; margin-bottom: 100px;
} }
} }
\ No newline at end of file
...@@ -62,7 +62,7 @@ body.course.checklists { ...@@ -62,7 +62,7 @@ body.course.checklists {
.ui-toggle-expansion { .ui-toggle-expansion {
@include transition(rotate .15s ease-in-out .25s); @include transition(rotate .15s ease-in-out .25s);
@include font-size(14); @include font-size(21);
display: inline-block; display: inline-block;
vertical-align: middle; vertical-align: middle;
margin-right: ($baseline/2); margin-right: ($baseline/2);
...@@ -91,10 +91,9 @@ body.course.checklists { ...@@ -91,10 +91,9 @@ body.course.checklists {
color: $gray-l2; color: $gray-l2;
.icon-confirm { .icon-ok {
@include font-size(20); @include font-size(20);
display: inline-block; display: inline-block;
vertical-align: middle;
margin-left: ($baseline/2); margin-left: ($baseline/2);
color: $gray-l4; color: $gray-l4;
} }
...@@ -184,13 +183,13 @@ body.course.checklists { ...@@ -184,13 +183,13 @@ body.course.checklists {
header { header {
.checklist-title, .icon-confirm { .checklist-title, .icon-caret-down {
color: $green; color: $green;
} }
.checklist-status { .checklist-status {
.status-count, .status-amount, .icon-confirm { .status-count, .status-amount, .icon-ok {
color: $green; color: $green;
} }
} }
......
...@@ -164,11 +164,11 @@ body.index { ...@@ -164,11 +164,11 @@ body.index {
right: ($baseline/2); right: ($baseline/2);
opacity: 0; opacity: 0;
.ss-icon { [class^="icon-"] {
@include font-size(18); @include font-size(18);
@include border-top-radius(3px); @include border-top-radius(3px);
display: inline-block; display: inline-block;
padding: ($baseline/4) ($baseline/2); padding: ($baseline/2);
background: $blue; background: $blue;
color: $white; color: $white;
text-align: center; text-align: center;
......
...@@ -56,6 +56,12 @@ body.course.outline { ...@@ -56,6 +56,12 @@ body.course.outline {
} }
} }
[class^="icon-"] {
vertical-align: middle;
margin-top: -5px;
display: inline-block;
}
.menu { .menu {
@include font-size(12); @include font-size(12);
@include border-radius(4px); @include border-radius(4px);
...@@ -331,6 +337,7 @@ body.course.outline { ...@@ -331,6 +337,7 @@ body.course.outline {
&:hover, &.is-active { &:hover, &.is-active {
color: $blue; color: $blue;
} }
} }
.menu { .menu {
...@@ -533,7 +540,7 @@ body.course.outline { ...@@ -533,7 +540,7 @@ body.course.outline {
display: block; display: block;
} }
.ss-icon { [class^="icon-"] {
@include font-size(11); @include font-size(11);
@include border-radius(20px); @include border-radius(20px);
position: relative; position: relative;
......
...@@ -328,11 +328,12 @@ body.course.settings { ...@@ -328,11 +328,12 @@ body.course.settings {
@extend .t-action-3; @extend .t-action-3;
font-weight: 600; font-weight: 600;
.icon { [class^="icon-"] {
@extend .t-icon; @extend .t-icon;
@include font-size(16); @include font-size(16);
display: inline-block; display: inline-block;
vertical-align: middle; vertical-align: middle;
margin-top: -3px;
} }
} }
} }
......
...@@ -10,6 +10,16 @@ body.course.static-pages { ...@@ -10,6 +10,16 @@ body.course.static-pages {
padding: 12px 0; padding: 12px 0;
} }
.nav-introduction-supplementary {
.icon-question-sign {
display: inline-block;
vertical-align: baseline;
margin-right: ($baseline/4);
}
}
.unit-body { .unit-body {
padding: 0; padding: 0;
......
...@@ -44,7 +44,7 @@ ...@@ -44,7 +44,7 @@
<h3 class="sr">Page Actions</h3> <h3 class="sr">Page Actions</h3>
<ul> <ul>
<li class="nav-item"> <li class="nav-item">
<a href="#" class="button upload-button new-button"><i class="ss-icon ss-symbolicons-standard icon icon-create">&#xEB40;</i> Upload New File</a> <a href="#" class="button upload-button new-button"><i class="icon-cloud-upload"></i> Upload New File</a>
</li> </li>
</ul> </ul>
</nav> </nav>
......
...@@ -31,6 +31,13 @@ ...@@ -31,6 +31,13 @@
<body class="<%block name='bodyclass'></%block> hide-wip"> <body class="<%block name='bodyclass'></%block> hide-wip">
<%include file="courseware_vendor_js.html"/> <%include file="courseware_vendor_js.html"/>
## js templates
<script id="system-feedback-tpl" type="text/template">
<%static:include path="js/system-feedback.underscore" />
</script>
## javascript
<script type="text/javascript" src="/jsi18n/"></script> <script type="text/javascript" src="/jsi18n/"></script>
<script type="text/javascript" src="${static.url('js/vendor/json2.js')}"></script> <script type="text/javascript" src="${static.url('js/vendor/json2.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/underscore-min.js')}"></script> <script type="text/javascript" src="${static.url('js/vendor/underscore-min.js')}"></script>
...@@ -41,7 +48,6 @@ ...@@ -41,7 +48,6 @@
<script src="${static.url('js/vendor/symbolset.ss-symbolicons.js')}"></script> <script src="${static.url('js/vendor/symbolset.ss-symbolicons.js')}"></script>
<script src="${static.url('js/vendor/html5-input-polyfills/number-polyfill.js')}"></script> <script src="${static.url('js/vendor/html5-input-polyfills/number-polyfill.js')}"></script>
<%static:js group='main'/> <%static:js group='main'/>
<%static:js group='module-js'/> <%static:js group='module-js'/>
<script src="${static.url('js/vendor/jquery.inlineedit.js')}"></script> <script src="${static.url('js/vendor/jquery.inlineedit.js')}"></script>
...@@ -52,6 +58,7 @@ ...@@ -52,6 +58,7 @@
<script src="${static.url('js/vendor/jquery.smooth-scroll.min.js')}"></script> <script src="${static.url('js/vendor/jquery.smooth-scroll.min.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/CodeMirror/htmlmixed.js')}"></script> <script type="text/javascript" src="${static.url('js/vendor/CodeMirror/htmlmixed.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/CodeMirror/css.js')}"></script> <script type="text/javascript" src="${static.url('js/vendor/CodeMirror/css.js')}"></script>
<script type="text/javascript" src="//www.youtube.com/player_api"></script>
<!--TODO: not the right place--> <!--TODO: not the right place-->
<script type="text/javascript" src="${static.url('js/models/metadata_model.js')}"></script> <script type="text/javascript" src="${static.url('js/models/metadata_model.js')}"></script>
...@@ -62,12 +69,14 @@ ...@@ -62,12 +69,14 @@
document.location.protocol + '//www.youtube.com/player_api">\x3C/script>'); document.location.protocol + '//www.youtube.com/player_api">\x3C/script>');
</script> </script>
<script src="${static.url('js/models/feedback.js')}"></script>
<script src="${static.url('js/views/feedback.js')}"></script>
<!-- view --> <!-- view -->
<div class="wrapper wrapper-view"> <div class="wrapper wrapper-view">
<%include file="widgets/header.html" /> <%include file="widgets/header.html" />
<%block name="view_alerts"></%block> <div id="page-alert"></div>
<%block name="view_banners"></%block>
<%block name="content"></%block> <%block name="content"></%block>
...@@ -78,10 +87,10 @@ ...@@ -78,10 +87,10 @@
<%include file="widgets/footer.html" /> <%include file="widgets/footer.html" />
<%include file="widgets/tender.html" /> <%include file="widgets/tender.html" />
<%block name="view_notifications"></%block> <div id="page-notification"></div>
</div> </div>
<%block name="view_prompts"></%block> <div id="page-prompt"></div>
<%block name="jsextra"></%block> <%block name="jsextra"></%block>
</body> </body>
......
...@@ -53,7 +53,7 @@ ...@@ -53,7 +53,7 @@
<h3 class="sr">Page Actions</h3> <h3 class="sr">Page Actions</h3>
<ul> <ul>
<li class="nav-item"> <li class="nav-item">
<a href="#" class=" button new-button new-update-button"><i class="ss-icon ss-symbolicons-standard icon icon-create">&#x002B;</i> New Update</a> <a href="#" class=" button new-button new-update-button"><i class="icon-plus"></i> New Update</a>
</li> </li>
</ul> </ul>
</nav> </nav>
......
...@@ -27,7 +27,7 @@ ...@@ -27,7 +27,7 @@
<h3 class="sr">Page Actions</h3> <h3 class="sr">Page Actions</h3>
<ul> <ul>
<li class="nav-item"> <li class="nav-item">
<a href="#" class="button new-button new-tab"><i class="ss-icon ss-symbolicons-standard icon icon-create">&#x002B;</i> New Page</a> <a href="#" class="button new-button new-tab"><i class="icon-plus"></i> New Page</a>
</li> </li>
</ul> </ul>
</nav> </nav>
...@@ -41,7 +41,7 @@ ...@@ -41,7 +41,7 @@
<nav class="nav-introduction-supplementary"> <nav class="nav-introduction-supplementary">
<ul> <ul>
<li class="nav-item"> <li class="nav-item">
<a rel="modal" href="#preview-lms-staticpages"><i class="ss-icon ss-symbolicons-block icon icon-information">&#x2753;</i>How do Static Pages look to students in my course?</a> <a rel="modal" href="#preview-lms-staticpages"><i class="icon-question-sign"></i>How do Static Pages look to students in my course?</a>
</li> </li>
</ul> </ul>
</nav> </nav>
...@@ -76,7 +76,7 @@ ...@@ -76,7 +76,7 @@
</figure> </figure>
<a href="#" rel="view" class="action action-modal-close"> <a href="#" rel="view" class="action action-modal-close">
<i class="ss-icon ss-symbolicons-block icon-close icon">&#x2421;</i> <i class="icon-remove-sign"></i>
<span class="label">close modal</span> <span class="label">close modal</span>
</a> </a>
</div> </div>
......
...@@ -28,7 +28,7 @@ ...@@ -28,7 +28,7 @@
<img src="/static/img/thumb-hiw-feature1.png" alt="Studio Helps You Keep Your Courses Organized" /> <img src="/static/img/thumb-hiw-feature1.png" alt="Studio Helps You Keep Your Courses Organized" />
<figcaption class="sr">Studio Helps You Keep Your Courses Organized</figcaption> <figcaption class="sr">Studio Helps You Keep Your Courses Organized</figcaption>
<span class="action-zoom"> <span class="action-zoom">
<i class="ss-icon ss-symbolicons-block icon icon-zoom">&#xE002;</i> <i class="icon-zoom-in"></i>
</span> </span>
</a> </a>
</figure> </figure>
...@@ -62,7 +62,7 @@ ...@@ -62,7 +62,7 @@
<img src="/static/img/thumb-hiw-feature2.png" alt="Learning is More than Just Lectures" /> <img src="/static/img/thumb-hiw-feature2.png" alt="Learning is More than Just Lectures" />
<figcaption class="sr">Learning is More than Just Lectures</figcaption> <figcaption class="sr">Learning is More than Just Lectures</figcaption>
<span class="action-zoom"> <span class="action-zoom">
<i class="ss-icon ss-symbolicons-block icon icon-zoom">&#xE002;</i> <i class="icon-zoom-in"></i>
</span> </span>
</a> </a>
</figure> </figure>
...@@ -96,7 +96,7 @@ ...@@ -96,7 +96,7 @@
<img src="/static/img/thumb-hiw-feature3.png" alt="Studio Gives You Simple, Fast, and Incremental Publishing. With Friends." /> <img src="/static/img/thumb-hiw-feature3.png" alt="Studio Gives You Simple, Fast, and Incremental Publishing. With Friends." />
<figcaption class="sr">Studio Gives You Simple, Fast, and Incremental Publishing. With Friends.</figcaption> <figcaption class="sr">Studio Gives You Simple, Fast, and Incremental Publishing. With Friends.</figcaption>
<span class="action-zoom"> <span class="action-zoom">
<i class="ss-icon ss-symbolicons-block icon icon-zoom">&#xE002;</i> <i class="icon-zoom-in"></i>
</span> </span>
</a> </a>
</figure> </figure>
...@@ -132,7 +132,7 @@ ...@@ -132,7 +132,7 @@
<header> <header>
<h2 class="sr">Sign Up for Studio Today!</h2> <h2 class="sr">Sign Up for Studio Today!</h2>
</header> </header>
<ul class="list-actions"> <ul class="list-actions">
<li class="action-item"> <li class="action-item">
<a href="${reverse('signup')}" class="action action-primary">Sign Up &amp; Start Making an edX Course</a> <a href="${reverse('signup')}" class="action action-primary">Sign Up &amp; Start Making an edX Course</a>
...@@ -152,7 +152,7 @@ ...@@ -152,7 +152,7 @@
</figure> </figure>
<a href="" rel="view" class="action action-modal-close"> <a href="" rel="view" class="action action-modal-close">
<i class="ss-icon ss-symbolicons-block icon icon-close">&#x2421;</i> <i class="icon-remove-sign"></i>
<span class="label">close modal</span> <span class="label">close modal</span>
</a> </a>
</div> </div>
...@@ -165,7 +165,7 @@ ...@@ -165,7 +165,7 @@
</figure> </figure>
<a href="" rel="view" class="action action-modal-close"> <a href="" rel="view" class="action action-modal-close">
<i class="ss-icon ss-symbolicons-block icon icon-close">&#x2421;</i> <i class="icon-remove-sign"></i>
<span class="label">close modal</span> <span class="label">close modal</span>
</a> </a>
</div> </div>
...@@ -178,8 +178,8 @@ ...@@ -178,8 +178,8 @@
</figure> </figure>
<a href="" rel="view" class="action action-modal-close"> <a href="" rel="view" class="action action-modal-close">
<i class="ss-icon ss-symbolicons-block icon icon-close">&#x2421;</i> <i class="icon-remove-sign"></i>
<span class="label">close modal</span> <span class="label">close modal</span>
</a> </a>
</div> </div>
</%block> </%block>
\ No newline at end of file
...@@ -45,7 +45,7 @@ ...@@ -45,7 +45,7 @@
<ul> <ul>
<li class="nav-item"> <li class="nav-item">
% if not disable_course_creation: % if not disable_course_creation:
<a href="#" class="button new-button new-course-button"><i class="ss-icon ss-symbolicons-standard icon icon-create">&#x002B;</i> ${_("New Course")}</a> <a href="#" class="button new-button new-course-button"><i class="icon-plus"></i> ${_("New Course")}</a>
% elif settings.MITX_FEATURES.get('STAFF_EMAIL',''): % elif settings.MITX_FEATURES.get('STAFF_EMAIL',''):
<a href="mailto:${settings.MITX_FEATURES.get('STAFF_EMAIL','')}">${_("Email staff to create course")}</a> <a href="mailto:${settings.MITX_FEATURES.get('STAFF_EMAIL','')}">${_("Email staff to create course")}</a>
% endif % endif
...@@ -76,7 +76,7 @@ ...@@ -76,7 +76,7 @@
<a class="class-link" href="${url}" class="class-name"> <a class="class-link" href="${url}" class="class-name">
<span class="class-name">${course}</span> <span class="class-name">${course}</span>
</a> </a>
<a href="${lms_link}" rel="external" class="button view-button view-live-button"><i class="ss-icon ss-symbolicons-block icon icon-view">&#xE010;</i>View Live</a> <a href="${lms_link}" rel="external" class="button view-button view-live-button">View Live</a>
</li> </li>
%endfor %endfor
</ul> </ul>
......
<form class="section-name-edit">
<input type="text" value="<%= name %>" autocomplete="off"/>
<input type="submit" class="save-button" value="<%= gettext("Save") %>" />
<input type="button" class="cancel-button" value="<%= gettext("Cancel") %>" />
</form>
<div class="wrapper wrapper-<%= type %> wrapper-<%= type %>-<%= intent %>
<% if(obj.shown) { %>is-shown<% } else { %>is-hiding<% } %>
<% if(_.contains(['help', 'saving'], intent)) { %>wrapper-<%= type %>-status<% } %>"
id="<%= type %>-<%= intent %>"
aria-hidden="<% if(obj.shown) { %>false<% } else { %>true<% } %>"
aria-labelledby="<%= type %>-<%= intent %>-title"
<% if (obj.message) { %>aria-describedby="<%= type %>-<%= intent %>-description" <% } %>
<% if (obj.actions) { %>role="dialog"<% } %>
>
<div class="<%= type %> <%= intent %> <% if(obj.actions) { %>has-actions<% } %>">
<% if(obj.icon) { %>
<% var iconClass = {"warning": "warning-sign", "confirmation": "ok", "error": "warning-sign", "announcement": "bullhorn", "step-required": "exclamation-sign", "help": "question-sign", "saving": "cog"} %>
<i class="icon-<%= iconClass[intent] %>"></i>
<% } %>
<div class="copy">
<h2 class="title title-3" id="<%= type %>-<%= intent %>-title"><%= title %></h2>
<% if(obj.message) { %><p class="message" id="<%= type %>-<%= intent %>-description"><%= message %></p><% } %>
</div>
<% if(obj.actions) { %>
<nav class="nav-actions">
<h3 class="sr"><%= type %> Actions</h3>
<ul>
<% if(actions.primary) { %>
<li class="nav-item">
<a href="#" class="button action-primary <%= actions.primary.class %>"><%= actions.primary.text %></a>
</li>
<% } %>
<% if(actions.secondary) {
_.each(actions.secondary, function(secondary) { %>
<li class="nav-item">
<a href="#" class="button action-secondary <%= secondary.class %>"><%= secondary.text %></a>
</li>
<% });
} %>
</ul>
</nav>
<% } %>
<% if(obj.closeIcon) { %>
<a href="#" rel="view" class="action action-close action-<%= type %>-close">
<i class="icon-remove-sign"></i>
<span class="label">close <%= type %></span>
</a>
<% } %>
</div>
</div>
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
<ul> <ul>
%if allow_actions: %if allow_actions:
<li class="nav-item"> <li class="nav-item">
<a href="#" class="button new-button new-user-button"><i class="ss-icon ss-symbolicons-standard icon icon-create">&#x002B;</i> New User</a> <a href="#" class="button new-button new-user-button"><i class="icon-plus"></i> New User</a>
</li> </li>
%endif %endif
</ul> </ul>
......
...@@ -20,6 +20,12 @@ ...@@ -20,6 +20,12 @@
<script type="text/javascript" src="${static.url('js/views/overview.js')}"></script> <script type="text/javascript" src="${static.url('js/views/overview.js')}"></script>
<script type="text/javascript" src="${static.url('js/models/settings/course_grading_policy.js')}"></script> <script type="text/javascript" src="${static.url('js/models/settings/course_grading_policy.js')}"></script>
<script type="text/template" id="section-name-edit-tpl">
<%static:include path="js/section-name-edit.underscore" />
</script>
<script type="text/javascript" src="${static.url('js/models/section.js')}"></script>
<script type="text/javascript" src="${static.url('js/views/section.js')}"></script>
<script type="text/javascript"> <script type="text/javascript">
$(document).ready(function(){ $(document).ready(function(){
// TODO figure out whether these should be in window or someplace else or whether they're only needed as local vars // TODO figure out whether these should be in window or someplace else or whether they're only needed as local vars
...@@ -37,6 +43,14 @@ ...@@ -37,6 +43,14 @@
graders : window.graderTypes graders : window.graderTypes
}); });
}); });
$(".section-name").each(function() {
var model = new CMS.Models.Section({
id: $(this).parent(".item-details").data("id"),
name: $(this).data("name")
});
new CMS.Views.SectionShow({model: model, el: this}).render();
})
}); });
</script> </script>
...@@ -115,13 +129,13 @@ ...@@ -115,13 +129,13 @@
<h3 class="sr">Page Actions</h3> <h3 class="sr">Page Actions</h3>
<ul> <ul>
<li class="nav-item"> <li class="nav-item">
<a href="#" class="toggle-button toggle-button-sections"><i class="ss-icon ss-symbolicons-block icon">up</i> <span class="label">Collapse All Sections</span></a> <a href="#" class="toggle-button toggle-button-sections"><i class="icon-arrow-up"></i> <span class="label">Collapse All Sections</span></a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a href="#" class="button new-button new-courseware-section-button"><i class="ss-icon ss-symbolicons-standard icon icon-create">&#x002B;</i> New Section</a> <a href="#" class="button new-button new-courseware-section-button"><i class="icon-plus"></i> New Section</a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a href="${lms_link}" rel="external" class="button view-button view-live-button"><i class="ss-icon ss-symbolicons-block icon icon-view">&#xE010;</i>View Live</a> <a href="${lms_link}" rel="external" class="button view-button view-live-button">View Live</a>
</li> </li>
</ul> </ul>
</nav> </nav>
...@@ -137,14 +151,7 @@ ...@@ -137,14 +151,7 @@
<a href="#" data-tooltip="Expand/collapse this section" class="expand-collapse-icon collapse"></a> <a href="#" data-tooltip="Expand/collapse this section" class="expand-collapse-icon collapse"></a>
<div class="item-details" data-id="${section.location}"> <div class="item-details" data-id="${section.location}">
<h3 class="section-name"> <h3 class="section-name" data-name="${section.display_name_with_default | h}"></h3>
<span data-tooltip="Edit this section's name" class="section-name-span">${section.display_name_with_default}</span>
<form class="section-name-edit" style="display:none">
<input type="text" value="${section.display_name_with_default | h}" class="edit-section-name" autocomplete="off"/>
<input type="submit" class="save-button edit-section-name-save" value="Save" />
<input type="button" class="cancel-button edit-section-name-cancel" value="Cancel" />
</form>
</h3>
<div class="section-published-date"> <div class="section-published-date">
<% <%
start_date_str = get_time_struct_display(section.lms.start, '%m/%d/%Y') start_date_str = get_time_struct_display(section.lms.start, '%m/%d/%Y')
......
...@@ -92,7 +92,7 @@ from contentstore import utils ...@@ -92,7 +92,7 @@ from contentstore import utils
<ul class="list-actions"> <ul class="list-actions">
<li class="action-item"> <li class="action-item">
<a title="Send a note to students via email" href="mailto:someone@domain.com?Subject=Enroll%20in%20${context_course.display_name_with_default}&body=The%20course%20&quot;${context_course.display_name_with_default}&quot;,%20provided%20by%20edX,%20is%20open%20for%20enrollment.%20Please%20navigate%20to%20this%20course%20at%20https:${utils.get_lms_link_for_about_page(course_location)}%20to%20enroll." class="action action-primary"><i class="ss-icon icon ss-symbolicons-standard icon icon-inline icon-announcement">&#x2709;</i> Invite your students</a> <a title="Send a note to students via email" href="mailto:someone@domain.com?Subject=Enroll%20in%20${context_course.display_name_with_default}&body=The%20course%20&quot;${context_course.display_name_with_default}&quot;,%20provided%20by%20edX,%20is%20open%20for%20enrollment.%20Please%20navigate%20to%20this%20course%20at%20https:${utils.get_lms_link_for_about_page(course_location)}%20to%20enroll." class="action action-primary"><i class="icon-envelope-alt icon-inline"></i> Invite your students</a>
</li> </li>
</ul> </ul>
</div> </div>
......
...@@ -110,7 +110,7 @@ editor.render(); ...@@ -110,7 +110,7 @@ editor.render();
<!-- notification: change has been made and a save is needed --> <!-- notification: change has been made and a save is needed -->
<div class="wrapper wrapper-notification wrapper-notification-warning" aria-hidden="true" role="dialog" aria-labelledby="notification-changesMade-title" aria-describedby="notification-changesMade-description"> <div class="wrapper wrapper-notification wrapper-notification-warning" aria-hidden="true" role="dialog" aria-labelledby="notification-changesMade-title" aria-describedby="notification-changesMade-description">
<div class="notification warning has-actions"> <div class="notification warning has-actions">
<i class="ss-icon ss-symbolicons-block icon icon-warning">&#x26A0;</i> <i class="icon-warning-sign"></i>
<div class="copy"> <div class="copy">
<h2 class="title title-3" id="notification-changesMade-title">You've Made Some Changes</h2> <h2 class="title title-3" id="notification-changesMade-title">You've Made Some Changes</h2>
...@@ -136,7 +136,7 @@ editor.render(); ...@@ -136,7 +136,7 @@ editor.render();
<!-- alert: save confirmed with close --> <!-- alert: save confirmed with close -->
<div class="wrapper wrapper-alert wrapper-alert-confirmation" role="status"> <div class="wrapper wrapper-alert wrapper-alert-confirmation" role="status">
<div class="alert confirmation"> <div class="alert confirmation">
<i class="ss-icon ss-symbolicons-standard icon icon-confirmation">&#x2713;</i> <i class="icon-ok"></i>
<div class="copy"> <div class="copy">
<h2 class="title title-3">Your policy changes have been saved.</h2> <h2 class="title title-3">Your policy changes have been saved.</h2>
...@@ -144,7 +144,7 @@ editor.render(); ...@@ -144,7 +144,7 @@ editor.render();
</div> </div>
<a href="" rel="view" class="action action-alert-close"> <a href="" rel="view" class="action action-alert-close">
<i class="ss-icon ss-symbolicons-block icon icon-close">&#x2421;</i> <i class="icon-remove-sign"></i>
<span class="label">close alert</span> <span class="label">close alert</span>
</a> </a>
</div> </div>
...@@ -153,7 +153,7 @@ editor.render(); ...@@ -153,7 +153,7 @@ editor.render();
<!-- alert: error --> <!-- alert: error -->
<div class="wrapper wrapper-alert wrapper-alert-error" role="status"> <div class="wrapper wrapper-alert wrapper-alert-error" role="status">
<div class="alert error"> <div class="alert error">
<i class="ss-icon ss-symbolicons-block icon icon-error">&#x26A0;</i> <i class="icon-warning-sign"></i>
<div class="copy"> <div class="copy">
<h2 class="title title-3">There was an error saving your information</h2> <h2 class="title title-3">There was an error saving your information</h2>
......
...@@ -117,7 +117,7 @@ from contentstore import utils ...@@ -117,7 +117,7 @@ from contentstore import utils
<div class="actions"> <div class="actions">
<a href="#" class="new-button new-course-grading-item add-grading-data"> <a href="#" class="new-button new-course-grading-item add-grading-data">
<span class="plus-icon white"></span>New Assignment Type <i class="icon-plus"></i>New Assignment Type
</a> </a>
</div> </div>
</section> </section>
......
<%inherit file="base.html" />
<%block name="title">Studio Alerts</%block>
<%block name="bodyclass">is-signedin course uxdesign alerts</%block>
<%block name="jsextra">
<script type="text/javascript">
// notifications - demo
$(document).ready(function() {
// alert and notifications - manual close
$('.action-alert-close, .alert.has-actions .nav-actions a').click(function(e) {
(e).preventDefault();
console.log('closing alert');
$(this).closest('.wrapper-alert').removeClass('is-shown');
});
// alert and notifications - manual & action-based close
$('.action-notification-close').click(function(e) {
(e).preventDefault();
$(this).closest('.wrapper-notification').removeClass('is-shown').addClass('is-hiding');
});
// prompt pop
$('.action-prompt').click(function(e){
(e).preventDefault();
$body.toggleClass('prompt-is-shown');
$(this).closest('.wrapper-prompt').attr('aria-hidden','false');
});
// prompt close
$('.prompt .action-cancel, .prompt .action-proceed').click(function(e) {
(e).preventDefault();
$body.removeClass('prompt-is-shown');
$(this).closest('.wrapper-prompt').attr('aria-hidden','true');
});
$('.hide-notification').click(function(e) {
(e).preventDefault();
$('.wrapper-notification').removeClass('is-hiding is-shown').attr('aria-hidden','true');
$(this.hash).addClass('is-hiding').attr('aria-hidden','true');
});
$('.show-notification').click(function(e) {
(e).preventDefault();
$('.wrapper-notification').removeClass('is-shown is-hiding');
$(this.hash).addClass('is-shown').attr('aria-hidden','false');
});
$('.show-notification-fleeting').click(function(e) {
(e).preventDefault();
$('.wrapper-notification').removeClass('is-fleeting').attr('aria-hidden','true');
$(this.hash).addClass('is-fleeting').attr('aria-hidden','false');
});
$('.hide-alert').click(function(e) {
(e).preventDefault();
$('.wrapper-alert').removeClass('is-hiding');
$(this.hash).addClass('is-hiding');
});
$('.show-alert').click(function(e) {
(e).preventDefault();
$('.wrapper-alert').removeClass('is-shown');
$(this.hash).addClass('is-shown');
});
$('.hide-prompt').click(function(e){
(e).preventDefault();
$body.removeClass('prompt-is-shown');
});
$('.show-prompt').click(function(e) {
(e).preventDefault();
$body.toggleClass('prompt-is-shown');
$('.wrapper-prompt').removeClass('is-shown');
$(this.hash).addClass('is-shown').attr('aria-hidden','false');
});
});
</script>
</%block>
<%block name="content">
<div class="wrapper-mast wrapper">
<header class="mast has-actions has-subtitle">
<h1 class="page-header">
<small class="subtitle">UX Design</small>
<span class="sr">&gt; </span>System Notifications
</h1>
</header>
</div>
<div class="wrapper-content wrapper">
<section class="content">
<article class="content-primary" role="main">
<section>
<header>
<h2 class="title-2">Alerts</h2>
<span class="tip">persistant, static messages to the user</span>
</header>
<p>In Studio, alerts are 1) general warnings/notes (e.g. drafts, published content, next steps) about the current view a user is interacting with or 2) notes about the status (e.g. saved confirmations, errors, next system steps) of any previous state that need to communicated to the user when arriving at the current view.</p>
<h3 class="title-3">Different Static Examples of Alerts</h3>
<p>Note: alerts will probably never been shown based on click or page action and will primarily be loaded along with a pageload and initial display</p>
<ul>
<li><a href="#alert-draft" class="show-alert">Show Draft</a></li>
<li><a href="#alert-version" class="show-alert">Show Version</a></li>
<li><a href="#alert-saved" class="show-alert">Show Previous View/Action</a></li>
<li><a href="#alert-close" class="show-alert">Show Alert with Close Option</a></li>
<li><a href="#alert-removed" class="show-alert">Show Previous View/Action Removed</a></li>
<li><a href="#alert-system-error" class="show-alert">Show System Error</a></li>
<li><a href="#alert-user-error" class="show-alert">Show User Error</a></li>
<li><a href="#alert-announcement2" class="show-alert">Show Announcement</a></li>
<li><a href="#alert-announcement1" class="show-alert">Show Announcement with Actions</a></li>
<li><a href="#alert-activation" class="show-alert">Show Activiation</a></li>
<li><a href="#alert-threeActions" class="show-alert">Alert with three actions</a></li>
</ul>
</section>
<section>
<header>
<h2 class="title-2">Notifications</h2>
<span class="tip">contextual, feedback-based, and temporal messages to the user</span>
</header>
<p>In Studio, notifications are meant to inform the user of 1) any system status (e.g. saving, processing/validating) occurring based on any action they have taken or 2) any decisions (e.g. saving/discarding) a user must make to confirm.</p>
<h3 class="title-3">Different Static Examples of Notifications</h3>
<ul>
<li>
<a href="#notification-changesMade" class="show-notification">Show Changes Made (used in Advanced Settings)</a>
<a href="#notification-changesMade" class="hide-notification">Hide Changes Made (used in Advanced Settings)</a>
</li>
<li>
<a href="#notification-version" class="show-notification">Show New Version Warning</a>
<a href="#notification-version" class="hide-notification">Hide New Version Warning</a>
</li>
<li>
<a href="#notification-dangerous" class="show-notification">Show Editing Warning</a>
<a href="#notification-dangerous" class="hide-notification">Hide Editing Warning</a>
</li>
<li>
<a href="#notification-confirmation" class="show-notification-fleeting">Show Confirmation (Fleeting Notification)</a>
</li>
<li>
<a href="#notification-saving" class="show-notification">Show Saving</a>
<a href="#notification-saving" class="hide-notification">Hide Saving</a>
</li>
<li>
<a href="#notification-help" class="show-notification">Show Help</a>
<a href="#notification-help" class="hide-notification">Hide Help</a>
</li>
</ul>
</section>
<section>
<header>
<h2 class="title-2">Prompts</h2>
<span class="tip">presents a user with a choice, based on their previous interaction, that must be decided before they can proceed</span>
</header>
<p>In Studio, prompts are dialogs that are presented above all other page components and present a user with a choice, based on their previous interaction, that must be decided before they can proceed (or return to the previous interaction step).</p>
<h3 class="title-3">Different Static Examples of Prompts</h3>
<ul>
<li>
<a href="#prompt-confirm" class="show-prompt">Show Confirm Prompt</a>
</li>
<li>
<a href="#prompt-warning" class="show-prompt">Show Warning Prompt</a>
</li>
<li>
<a href="#prompt-error" class="show-prompt">Show Error Prompt</a>
</li>
</ul>
</section>
</article>
</section>
</div>
</%block>
<%block name="view_alerts">
<!-- alert: 3 actions -->
<div class="wrapper wrapper-alert wrapper-alert-warning" id="alert-threeActions">
<div class="alert warning has-actions">
<i class="ss-icon ss-symbolicons-block icon icon-warning">&#x26A0;</i>
<div class="copy">
<h2 class="title title-3">You are editing a draft</h2>
<p class="message">Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa.</p>
</div>
<nav class="nav-actions">
<h3 class="sr">Alert Actions</h3>
<ul>
<li class="nav-item">
<a href="#" class="action action-save action-primary">Save Draft</a>
</li>
<li class="nav-item">
<a href="#" class="action action-cancel action-secondary">Disgard Draft</a>
</li>
<li class="nav-item">
<a href="#" class="action action-secondary">Do Something Elsee</a>
</li>
</ul>
</nav>
</div>
</div>
<!-- alert: you're editing a draft -->
<div class="wrapper wrapper-alert wrapper-alert-warning" id="alert-draft">
<div class="alert warning has-actions">
<i class="ss-icon ss-symbolicons-block icon icon-warning">&#x26A0;</i>
<div class="copy">
<h2 class="title title-3">You are editing a draft</h2>
<p class="message">Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa.</p>
</div>
<nav class="nav-actions">
<h3 class="sr">Alert Actions</h3>
<ul>
<li class="nav-item">
<a href="#" class="action action-save action-primary">Save Draft</a>
</li>
<li class="nav-item">
<a href="#" class="action action-cancel action-secondary">Disgard Draft</a>
</li>
</ul>
</nav>
</div>
</div>
<!-- alert: newer version -->
<div class="wrapper wrapper-alert wrapper-alert-warning" id="alert-version">
<div class="alert warning has-actions">
<i class="ss-icon ss-symbolicons-block icon icon-warning">&#x26A0;</i>
<div class="copy">
<h2 class="title title-3">A Newer Version of This Exists</h2>
<p class="message">Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa.</p>
</div>
<nav class="nav-actions">
<h3 class="sr">Alert Actions</h3>
<ul>
<li class="nav-item">
<a href="#" class="action action-save action-primary">Go to Newer Version</a>
</li>
<li class="nav-item">
<a href="#" class="action action-cancel action-secondary">Continue Editing</a>
</li>
</ul>
</nav>
</div>
</div>
<!-- alert: save confirmed -->
<div class="wrapper wrapper-alert wrapper-alert-confirmation" id="alert-saved">
<div class="alert confirmation">
<i class="ss-icon ss-symbolicons-standard icon icon-confirmation">&#x2713;</i>
<div class="copy">
<h2 class="title title-3">Your changes have been saved</h2>
<p class="message">Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. <a rel="page" href="#test">Please see below</a></p>
</div>
</div>
</div>
<!-- alert: save confirmed with close -->
<div class="wrapper wrapper-alert wrapper-alert-confirmation" id="alert-close">
<div class="alert confirmation">
<i class="ss-icon ss-symbolicons-standard icon icon-confirmation">&#x2713;</i>
<div class="copy">
<h2 class="title title-3">Your changes have been saved</h2>
<p class="message">Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. <a rel="page" href="#test">Please see below</a></p>
</div>
<a href="#" rel="view" class="action action-alert-close">
<i class="ss-icon ss-symbolicons-block icon icon-close">&#x2421;</i>
<span class="label">close alert</span>
</a>
</div>
</div>
<!-- alert: delete confirmed -->
<div class="wrapper wrapper-alert wrapper-alert-confirmation" id="alert-removed">
<div class="alert confirmation">
<i class="ss-icon ss-symbolicons-standard icon icon-confirmation">&#x2713;</i>
<div class="copy">
<h2 class="title title-3">X Has been removed</h2>
<p class="message">Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa.</p>
</div>
</div>
</div>
<!-- alert: system error -->
<div class="wrapper wrapper-alert wrapper-alert-error" id="alert-system-error">
<div class="alert error">
<i class="ss-icon ss-symbolicons-block icon icon-error">&#x26A0;</i>
<div class="copy">
<h2 class="title title-3">We're sorry, there was a error with Studio</h2>
<p class="message">Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa.</p>
</div>
</div>
</div>
<!-- alert: user error -->
<div class="wrapper wrapper-alert wrapper-alert-error" id="alert-user-error">
<div class="alert error has-actions">
<i class="ss-icon ss-symbolicons-block icon icon-error">&#x26A0;</i>
<div class="copy">
<h2 class="title title-3">There was an error in your submission</h2>
<p class="message">Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. <a rel="page" href="#test">Please see below</a></p>
</div>
<nav class="nav-actions">
<h3 class="sr">Alert Actions</h3>
<ul>
<li class="nav-item">
<a href="#" class="action action-cancel action-primary">Cancel Your Submission</a>
</li>
</ul>
</nav>
</div>
</div>
<!-- alert: announcement w/ actions -->
<div class="wrapper wrapper-alert wrapper-alert-announcement" id="alert-announcement1">
<div class="alert announcement has-actions">
<i class="ss-icon ss-symbolicons-block icon icon-announcement">&#x1F4E2;</i>
<div class="copy">
<h2 class="title title-3">Studio will be unavailable this weekend</h2>
<p class="message">Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa.</p>
</div>
<nav class="nav-actions">
<h3 class="sr">Alert Actions</h3>
<ul>
<li class="nav-item">
<a href="#" rel="external" class="action-primary">Contact edX Staff</a>
</li>
<li class="nav-item">
<a href="#" class="action-secondary">Manage Your edX Notifications</a>
</li>
</ul>
</nav>
</div>
</div>
<!-- alert: announcement -->
<div class="wrapper wrapper-alert wrapper-alert-announcement" id="alert-announcement2">
<div class="alert announcement">
<i class="ss-icon ss-symbolicons-block icon icon-announcement">&#x1F4E2;</i>
<div class="copy">
<h2 class="title title-3">Studio will be unavailable this weekend</h2>
<p class="message">Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa.</p>
</div>
</div>
</div>
<!-- alert: step required -->
<div class="wrapper wrapper-alert wrapper-alert-step-required" id="alert-activation">
<div class="alert step-required has-actions">
<i class="ss-icon ss-symbolicons-block icon icon-step-required">&#xE0D1;</i>
<div class="copy">
<h2 class="title title-3">Your Studio account has been created, but needs to be activated</h2>
<p class="message">Donec sed odio dui. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Etiam porta sem malesuada magna mollis euismod. Cras mattis consectetur purus sit amet fermentum. Curabitur blandit tempus porttitor.</p>
</div>
<nav class="nav-actions">
<h3 class="sr">Alert Actions</h3>
<ul>
<li class="nav-item">
<a href="#" class="action-primary">Re-send Activation Message</a>
</li>
<li class="nav-item">
<a href="#" rel="external" class="action-secondary">Contact edX Support</a>
</li>
</ul>
</nav>
</div>
</div>
</%block>
<%block name="view_notifications">
<!-- notification: change has been made and a save is needed -->
<div class="wrapper wrapper-notification wrapper-notification-warning" aria-hidden="true" role="dialog" aria-labelledby="notification-changesMade-title" aria-describedby="notification-changesMade-description" id="notification-changesMade">
<div class="notification warning has-actions">
<i class="ss-icon ss-symbolicons-block icon icon-warning">&#x26A0;</i>
<div class="copy">
<h2 class="title title-3" id="notification-changesMade-title">You've Made Some Changes</h2>
<p id="notification-changesMade-description">Your changes will not take effect until you <strong>save your progress</strong>. Take care with key and value formatting, as validation is <strong>not implemented</strong>.</p>
</div>
<nav class="nav-actions">
<h3 class="sr">Notification Actions</h3>
<ul>
<li class="nav-item">
<a href="" class="action action-save action-primary">Save Changes</a>
</li>
<li class="nav-item">
<a href="" class="action action-cancel action-secondary">Cancel</a>
</li>
</ul>
</nav>
</div>
</div>
<!-- notification: newer version exists -->
<div class="wrapper wrapper-notification wrapper-notification-warning" id="notification-version" aria-hidden="true" role="dialog" aria-labelledby="notification-warning-title" aria-describedby="notification-warning-description">
<div class="notification warning has-actions">
<i class="ss-icon ss-symbolicons-block icon icon-warning">&#x26A0;</i>
<div class="copy">
<h2 class="title title-3" id="notification-warning-title">A Newer Version of This Exists</h2>
<p class="message" id="notification-warning-description">Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa.</p>
</div>
<nav class="nav-actions">
<h3 class="sr">Notification Actions</h3>
<ul>
<li class="nav-item">
<a href="#" class="action action-save action-primary">Go to Newer Version</a>
</li>
<li class="nav-item">
<a href="#" class="action action-cancel action-secondary">Continue Editing</a>
</li>
</ul>
</nav>
</div>
</div>
<!-- notification: warning about editing something dangerous -->
<div class="wrapper wrapper-notification wrapper-notification-warning" id="notification-dangerous" aria-hidden="true" role="dialog" aria-labelledby="notification-dangerous-title" aria-describedby="notification-dangerous-description">
<div class="notification warning has-actions">
<i class="ss-icon ss-symbolicons-block icon icon-warning">&#x26A0;</i>
<div class="copy">
<h2 class="title title-3" id="notification-dangerous-title">Are You Sure You Want to Edit That?</h2>
<p class="message" id="notification-dangerous-description">Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa.</p>
</div>
<nav class="nav-actions">
<h3 class="sr">Notification Actions</h3>
<ul>
<li class="nav-item">
<a href="#" class="action action-proceed action-primary">Yes, I want to Edit X</a>
</li>
<li class="nav-item">
<a href="#" class="action action-cancel action-secondary">No, I do not</a>
</li>
</ul>
</nav>
</div>
</div>
<!-- notification: status - saving -->
<div class="wrapper wrapper-notification wrapper-notification-status wrapper-notification-saving" id="notification-saving">
<div class="notification saving">
<i class="ss-icon ss-symbolicons-block icon icon-saving">&#x2699;</i>
<div class="copy">
<h2 class="title title-3" role="status">Saving &hellip;</h2>
</div>
</div>
</div>
<!-- notification: status- confirmed -->
<div class="wrapper wrapper-notification wrapper-notification-confirmation" id="notification-confirmation">
<div class="notification confirmation">
<i class="ss-icon ss-symbolicons-standard icon icon-confirmation">&#x2713;</i>
<div class="copy">
<h2 class="title title-3" role="status"><a href="#">Your Section</a> Has Been Created</h2>
</div>
</div>
</div>
<!-- notification: help - DYK -->
<div class="wrapper wrapper-notification wrapper-notification-help" id="notification-help">
<div class="notification help">
<i class="ss-icon ss-symbolicons-block icon icon-help">&#x2753;</i>
<div class="copy">
<h2 class="title title-3">Fun Fact:</h2>
<p class="message">Using the checkmark will allow you make a subsection gradable as an assignment, which counts towards a student's total grade</p>
</div>
<a href="#" rel="view" class="action action-notification-close">
<i class="ss-icon ss-symbolicons-block icon icon-close">&#x2421;</i>
<span class="label">close notification</span>
</a>
</div>
</div>
</%block>
<%block name="view_prompts">
<!-- prompt - confirm deletion -->
<div class="wrapper wrapper-prompt wrapper-prompt-confirm" id="prompt-confirm" aria-hidden="true" role="dialog" aria-labelledby="prompt-confirm-sectionDelete-title" aria-describedby="prompt-confirm-sectionDelete-description">
<div class="prompt confirm">
<div class="copy">
<h2 class="title title-3" id="prompt-confirm-sectionDelete-title">Delete "Introduction &amp; Overview"?</h2>
<p class="message" id="prompt-confirm-sectionDelete-description">Deleting a section cannot be undone and its contents cannot be recovered.</p>
</div>
<nav class="nav-actions">
<h3 class="sr">Prompt Actions</h3>
<ul>
<li class="nav-item">
<a href="#" class="action-primary action-proceed">Yes, delete this section</a>
</li>
<li class="nav-item">
<a href="#" class="action-secondary action-cancel">Cancel</a>
</li>
</ul>
</nav>
</div>
</div>
<!-- prompt - warning -->
<div class="wrapper wrapper-prompt wrapper-prompt-warning" id="prompt-warning" aria-hidden="true" role="dialog" aria-labelledby="prompt-warning-useAdvanced-title" aria-describedby="prompt-warning-useAdvanced-description">
<div class="prompt warning">
<div class="copy">
<h2 class="title title-3" id="prompt-warning-useAdvanced-title">Use Advanced Problem Editor?</h2>
<p class="message" id="prompt-warning-useAdvanced-description">If you proceed, you cannot edit this problem using the simple problem editor.</p>
</div>
<nav class="nav-actions">
<h3 class="sr">Prompt Actions</h3>
<ul>
<li class="nav-item">
<a href="#" class="action-primary action-proceed">Continue to Advanced Editor</a>
</li>
<li class="nav-item">
<a href="#" class="action-secondary action-cancel">Cancel</a>
</li>
</ul>
</nav>
</div>
</div>
<!-- prompt - error -->
<div class="wrapper wrapper-prompt wrapper-prompt-error" id="prompt-error" aria-hidden="true" role="dialog" aria-labelledby="prompt-errorUser-title" aria-describedby="prompt-errorUser-description">
<div class="prompt error">
<div class="copy">
<h2 class="title title-3" id="prompt-errorUser-title">There Were Errors in Your Submission</h2>
<p class="message" id="prompt-errorUser-description">Please correct the errors noted on the page and try again.</p>
</div>
<nav class="nav-actions">
<h3 class="sr">Prompt Actions</h3>
<ul>
<li class="nav-item">
<a href="#" class="action-primary action-proceed">Correct errors &amp; try again</a>
</li>
</ul>
</nav>
</div>
</div>
</%block>
...@@ -21,7 +21,7 @@ ...@@ -21,7 +21,7 @@
<ol> <ol>
<li class="nav-item nav-course-courseware"> <li class="nav-item nav-course-courseware">
<h3 class="title"><span class="label-prefix">Course </span>Content <i class="ss-icon ss-symbolicons-block icon-expand">&#x25BE;</i></h3> <h3 class="title"><span class="label-prefix">Course </span>Content <i class="icon-caret-down"></i></h3>
<div class="wrapper wrapper-nav-sub"> <div class="wrapper wrapper-nav-sub">
<div class="nav-sub"> <div class="nav-sub">
...@@ -36,7 +36,7 @@ ...@@ -36,7 +36,7 @@
</li> </li>
<li class="nav-item nav-course-settings"> <li class="nav-item nav-course-settings">
<h3 class="title"><span class="label-prefix">Course </span>Settings <i class="ss-icon ss-symbolicons-block icon-expand">&#x25BE;</i></h3> <h3 class="title"><span class="label-prefix">Course </span>Settings <i class="icon-caret-down"></i></h3>
<div class="wrapper wrapper-nav-sub"> <div class="wrapper wrapper-nav-sub">
<div class="nav-sub"> <div class="nav-sub">
...@@ -51,7 +51,7 @@ ...@@ -51,7 +51,7 @@
</li> </li>
<li class="nav-item nav-course-tools"> <li class="nav-item nav-course-tools">
<h3 class="title">Tools <i class="ss-icon ss-symbolicons-block icon-expand">&#x25BE;</i></h3> <h3 class="title">Tools <i class="icon-caret-down"></i></h3>
<div class="wrapper wrapper-nav-sub"> <div class="wrapper wrapper-nav-sub">
<div class="nav-sub"> <div class="nav-sub">
...@@ -76,10 +76,10 @@ ...@@ -76,10 +76,10 @@
<li class="nav-item nav-account-username"> <li class="nav-item nav-account-username">
<a href="#" class="title"> <a href="#" class="title">
<span class="account-username"> <span class="account-username">
<i class="ss-icon ss-symbolicons-standard icon-user">&#x1F464;</i> <i class="icon-user"></i>
${ user.username } ${ user.username }
</span> </span>
<i class="ss-icon ss-symbolicons-block icon-expand">&#x25BE;</i> <i class="icon-caret-down"></i>
</a> </a>
<div class="wrapper wrapper-nav-sub"> <div class="wrapper wrapper-nav-sub">
......
...@@ -2,14 +2,14 @@ ...@@ -2,14 +2,14 @@
<div class="wrapper-sock wrapper"> <div class="wrapper-sock wrapper">
<ul class="list-actions list-cta"> <ul class="list-actions list-cta">
<li class="action-item"> <li class="action-item">
<a href="#sock" class="cta cta-show-sock"><i class="ss-icon ss-symbolicons-block icon icon-inline icon-help">&#x2753;</i> <span class="copy">Looking for Help with Studio?</span></a> <a href="#sock" class="cta cta-show-sock"><i class="icon-question-sign"></i> <span class="copy">Looking for Help with Studio?</span></a>
</li> </li>
</ul> </ul>
<div class="wrapper-inner wrapper"> <div class="wrapper-inner wrapper">
<section class="sock" id="sock"> <section class="sock" id="sock">
<header> <header>
<h2 class="title sr"><i class="ss-icon ss-symbolicons-block icon icon-inline icon-help">&#x2753;</i> edX Studio Help</h2> <h2 class="title sr">edX Studio Help</h2>
</header> </header>
<div class="support"> <div class="support">
...@@ -21,15 +21,15 @@ ...@@ -21,15 +21,15 @@
<ul class="list-actions"> <ul class="list-actions">
<li class="action-item"> <li class="action-item">
<a href="http://files.edx.org/Getting_Started_with_Studio.pdf" class="action action-primary" title="This is a PDF Document"><i class="ss-icon icon ss-symbolicons-block icon icon-inline icon-pdf">&#xEC00;</i> Download Studio Documentation</a> <a href="http://files.edx.org/Getting_Started_with_Studio.pdf" class="action action-primary" title="This is a PDF Document">Download Studio Documentation</a>
<span class="tip">How to use Studio to build your course</span> <span class="tip">How to use Studio to build your course</span>
</li> </li>
<li class="action-item"> <li class="action-item">
<a href="http://help.edge.edx.org/" rel="external" class="action action-primary"><i class="ss-icon icon ss-symbolicons-block icon icon-inline icon-help">&#xEE11;</i> Studio Help Center</a> <a href="http://help.edge.edx.org/" rel="external" class="action action-primary">Studio Help Center</a>
<span class="tip"><i class="ss-icon ss-symbolicons-block icon-help">&#xEE11;</i> Studio Help Center</span> <span class="tip">Studio Help Center</span>
</li> </li>
<li class="action-item"> <li class="action-item">
<a href="https://edge.edx.org/courses/edX/edX101/How_to_Create_an_edX_Course/about" rel="external" class="action action-primary"><i class="ss-icon icon ss-symbolicons-block icon icon-inline icon-enroll">&#x1F393;</i> Enroll in edX101</a> <a href="https://edge.edx.org/courses/edX/edX101/How_to_Create_an_edX_Course/about" rel="external" class="action action-primary">Enroll in edX101</a>
<span class="tip">How to use Studio to build your course</span> <span class="tip">How to use Studio to build your course</span>
</li> </li>
</ul> </ul>
...@@ -45,7 +45,7 @@ ...@@ -45,7 +45,7 @@
<ul class="list-actions"> <ul class="list-actions">
<li class="action-item"> <li class="action-item">
<a href="http://help.edge.edx.org/discussion/new" class="action action-primary show-tender" title="Use our feedback tool, Tender, to share your feedback"><i class="ss-icon ss-symbolicons-block icon icon-inline icon-feedback">&#xE398;</i> Contact Us</a> <a href="http://help.edge.edx.org/discussion/new" class="action action-primary show-tender" title="Use our feedback tool, Tender, to share your feedback"><i class="icon-comments"></i> Contact Us</a>
</li> </li>
</ul> </ul>
</div> </div>
......
...@@ -115,9 +115,6 @@ urlpatterns += ( ...@@ -115,9 +115,6 @@ urlpatterns += (
url(r'^login_post$', 'student.views.login_user', name='login_post'), url(r'^login_post$', 'student.views.login_user', name='login_post'),
url(r'^logout$', 'student.views.logout_user', name='logout'), url(r'^logout$', 'student.views.logout_user', name='logout'),
# static/proof-of-concept views
url(r'^ux-alerts$', 'contentstore.views.ux_alerts', name='ux-alerts')
) )
js_info_dict = { js_info_dict = {
......
...@@ -28,3 +28,8 @@ except: ...@@ -28,3 +28,8 @@ except:
% endfor % endfor
%endif %endif
</%def> </%def>
<%def name="include(path)"><%
from django.template.loaders.filesystem import _loader
source, template_path = _loader.load_template_source(path)
%>${source}</%def>
...@@ -74,6 +74,9 @@ def initial_setup(server): ...@@ -74,6 +74,9 @@ def initial_setup(server):
if not success: if not success:
raise IOError("Could not acquire valid ChromeDriver browser session.") raise IOError("Could not acquire valid ChromeDriver browser session.")
# Set the browser size to 1280x1024
world.browser.driver.set_window_size(1280, 1024)
@before.each_scenario @before.each_scenario
def reset_data(scenario): def reset_data(scenario):
......
...@@ -2,7 +2,7 @@ from setuptools import setup ...@@ -2,7 +2,7 @@ from setuptools import setup
setup( setup(
name="calc", name="calc",
version="0.1", version="0.1.1",
py_modules=["calc"], py_modules=["calc"],
install_requires=[ install_requires=[
"pyparsing==1.5.6", "pyparsing==1.5.6",
......
...@@ -475,12 +475,11 @@ class LoncapaProblem(object): ...@@ -475,12 +475,11 @@ class LoncapaProblem(object):
msg = "Error while executing script code: %s" % str(err).replace('<', '&lt;') msg = "Error while executing script code: %s" % str(err).replace('<', '&lt;')
raise responsetypes.LoncapaProblemError(msg) raise responsetypes.LoncapaProblemError(msg)
# store code source in context # Store code source in context, along with the Python path needed to run it correctly.
context['script_code'] = all_code context['script_code'] = all_code
context['python_path'] = python_path
return context return context
def _extract_html(self, problemtree): # private def _extract_html(self, problemtree): # private
''' '''
Main (private) function which converts Problem XML tree to HTML. Main (private) function which converts Problem XML tree to HTML.
......
...@@ -286,7 +286,7 @@ class LoncapaResponse(object): ...@@ -286,7 +286,7 @@ class LoncapaResponse(object):
} }
try: try:
safe_exec.safe_exec(code, globals_dict) safe_exec.safe_exec(code, globals_dict, python_path=self.context['python_path'])
except Exception as err: except Exception as err:
msg = 'Error %s in evaluating hint function %s' % (err, hintfn) msg = 'Error %s in evaluating hint function %s' % (err, hintfn)
msg += "\nSee XML source line %s" % getattr( msg += "\nSee XML source line %s" % getattr(
...@@ -972,7 +972,7 @@ class CustomResponse(LoncapaResponse): ...@@ -972,7 +972,7 @@ class CustomResponse(LoncapaResponse):
'ans': ans, 'ans': ans,
} }
globals_dict.update(kwargs) globals_dict.update(kwargs)
safe_exec.safe_exec(code, globals_dict, cache=self.system.cache) safe_exec.safe_exec(code, globals_dict, python_path=self.context['python_path'])
return globals_dict['cfn_return'] return globals_dict['cfn_return']
return check_function return check_function
......
...@@ -39,6 +39,9 @@ class TestLazyMod(unittest.TestCase): ...@@ -39,6 +39,9 @@ class TestLazyMod(unittest.TestCase):
self.assertEqual(hsv[0], 0.25) self.assertEqual(hsv[0], 0.25)
def test_dotted(self): def test_dotted(self):
self.assertNotIn("email.utils", sys.modules) # wsgiref is a module with submodules that is not already imported.
email_utils = LazyModule("email.utils") # Any similar module would do. This test demonstrates that the module
self.assertEqual(email_utils.quote('"hi"'), r'\"hi\"') # is not already im
self.assertNotIn("wsgiref.util", sys.modules)
wsgiref_util = LazyModule("wsgiref.util")
self.assertEqual(wsgiref_util.guess_scheme({}), "http")
...@@ -2,7 +2,7 @@ from setuptools import setup ...@@ -2,7 +2,7 @@ from setuptools import setup
setup( setup(
name="chem", name="chem",
version="0.1", version="0.1.1",
packages=["chem"], packages=["chem"],
install_requires=[ install_requires=[
"pyparsing==1.5.6", "pyparsing==1.5.6",
......
...@@ -2,7 +2,7 @@ from setuptools import setup ...@@ -2,7 +2,7 @@ from setuptools import setup
setup( setup(
name="sandbox-packages", name="sandbox-packages",
version="0.1", version="0.1.1",
packages=[ packages=[
"verifiers", "verifiers",
], ],
......
...@@ -48,13 +48,14 @@ class VersionInteger(Integer): ...@@ -48,13 +48,14 @@ class VersionInteger(Integer):
class CombinedOpenEndedFields(object): class CombinedOpenEndedFields(object):
display_name = String(help="Display name for this module", default="Open Ended Grading", scope=Scope.settings)
current_task_number = Integer(help="Current task that the student is on.", default=0, scope=Scope.user_state) current_task_number = Integer(help="Current task that the student is on.", default=0, scope=Scope.user_state)
task_states = List(help="List of state dictionaries of each task within this module.", scope=Scope.user_state) task_states = List(help="List of state dictionaries of each task within this module.", scope=Scope.user_state)
state = String(help="Which step within the current task that the student is on.", default="initial", state = String(help="Which step within the current task that the student is on.", default="initial",
scope=Scope.user_state) scope=Scope.user_state)
student_attempts = Integer(help="Number of attempts taken by the student on this problem", default=0, student_attempts = StringyInteger(help="Number of attempts taken by the student on this problem", default=0,
scope=Scope.user_state) scope=Scope.user_state)
ready_to_reset = Boolean(help="If the problem is ready to be reset or not.", default=False, ready_to_reset = StringyBoolean(help="If the problem is ready to be reset or not.", default=False,
scope=Scope.user_state) scope=Scope.user_state)
attempts = StringyInteger(display_name="Maximum Attempts", attempts = StringyInteger(display_name="Maximum Attempts",
help="The number of times the student can try to answer this problem.", default=1, help="The number of times the student can try to answer this problem.", default=1,
...@@ -226,10 +227,3 @@ class CombinedOpenEndedDescriptor(CombinedOpenEndedFields, RawDescriptor): ...@@ -226,10 +227,3 @@ class CombinedOpenEndedDescriptor(CombinedOpenEndedFields, RawDescriptor):
has_score = True has_score = True
always_recalculate_grades = True always_recalculate_grades = True
template_dir_name = "combinedopenended" template_dir_name = "combinedopenended"
@property
def non_editable_metadata_fields(self):
non_editable_fields = super(CombinedOpenEndedDescriptor, self).non_editable_metadata_fields
non_editable_fields.extend([CombinedOpenEndedDescriptor.due, CombinedOpenEndedDescriptor.graceperiod,
CombinedOpenEndedDescriptor.version])
return non_editable_fields
...@@ -27,16 +27,19 @@ class @VideoCaption extends Subview ...@@ -27,16 +27,19 @@ class @VideoCaption extends Subview
@fetchCaption() @fetchCaption()
fetchCaption: -> fetchCaption: ->
$.getWithPrefix @captionURL(), (captions) => $.ajaxWithPrefix
@captions = captions.text url: @captionURL()
@start = captions.start notifyOnError: false
success: (captions) =>
@loaded = true @captions = captions.text
@start = captions.start
if onTouchBasedDevice()
$('.subtitles li').html "Caption will be displayed when you start playing the video." @loaded = true
else
@renderCaption() if onTouchBasedDevice()
$('.subtitles li').html "Caption will be displayed when you start playing the video."
else
@renderCaption()
renderCaption: -> renderCaption: ->
container = $('<ol>') container = $('<ol>')
......
...@@ -27,16 +27,19 @@ class @VideoCaptionAlpha extends SubviewAlpha ...@@ -27,16 +27,19 @@ class @VideoCaptionAlpha extends SubviewAlpha
@fetchCaption() @fetchCaption()
fetchCaption: -> fetchCaption: ->
$.getWithPrefix @captionURL(), (captions) => $.ajaxWithPrefix
@captions = captions.text url: @captionURL()
@start = captions.start notifyOnError: false
success: (captions) =>
@loaded = true @captions = captions.text
@start = captions.start
if onTouchBasedDevice()
$('.subtitles li').html "Caption will be displayed when you start playing the video." @loaded = true
else
@renderCaption() if onTouchBasedDevice()
$('.subtitles li').html "Caption will be displayed when you start playing the video."
else
@renderCaption()
renderCaption: -> renderCaption: ->
container = $('<ol>') container = $('<ol>')
......
...@@ -290,7 +290,6 @@ class XMLModuleStore(ModuleStoreBase): ...@@ -290,7 +290,6 @@ class XMLModuleStore(ModuleStoreBase):
if course_dirs is None: if course_dirs is None:
course_dirs = sorted([d for d in os.listdir(self.data_dir) if course_dirs = sorted([d for d in os.listdir(self.data_dir) if
os.path.exists(self.data_dir / d / "course.xml")]) os.path.exists(self.data_dir / d / "course.xml")])
for course_dir in course_dirs: for course_dir in course_dirs:
self.try_load_course(course_dir) self.try_load_course(course_dir)
......
...@@ -6,7 +6,7 @@ log = logging.getLogger(__name__) ...@@ -6,7 +6,7 @@ log = logging.getLogger(__name__)
class ControllerQueryService(GradingService): class ControllerQueryService(GradingService):
""" """
Interface to staff grading backend. Interface to controller query backend.
""" """
def __init__(self, config, system): def __init__(self, config, system):
...@@ -77,6 +77,50 @@ class ControllerQueryService(GradingService): ...@@ -77,6 +77,50 @@ class ControllerQueryService(GradingService):
return response return response
class MockControllerQueryService(object):
"""
Mock controller query service for testing
"""
def __init__(self, config, system):
pass
def check_if_name_is_unique(self, **params):
"""
Mock later if needed. Stub function for now.
@param params:
@return:
"""
pass
def check_for_eta(self, **params):
"""
Mock later if needed. Stub function for now.
@param params:
@return:
"""
pass
def check_combined_notifications(self, **params):
combined_notifications = '{"flagged_submissions_exist": false, "version": 1, "new_student_grading_to_view": false, "success": true, "staff_needs_to_grade": false, "student_needs_to_peer_grade": true, "overall_need_to_check": true}'
return combined_notifications
def get_grading_status_list(self, **params):
grading_status_list = '{"version": 1, "problem_list": [{"problem_name": "Science Question -- Machine Assessed", "grader_type": "NA", "eta_available": true, "state": "Waiting to be Graded", "eta": 259200, "location": "i4x://MITx/oe101x/combinedopenended/Science_SA_ML"}, {"problem_name": "Humanities Question -- Peer Assessed", "grader_type": "NA", "eta_available": true, "state": "Waiting to be Graded", "eta": 259200, "location": "i4x://MITx/oe101x/combinedopenended/Humanities_SA_Peer"}], "success": true}'
return grading_status_list
def get_flagged_problem_list(self, **params):
flagged_problem_list = '{"version": 1, "success": false, "error": "No flagged submissions exist for course: MITx/oe101x/2012_Fall"}'
return flagged_problem_list
def take_action_on_flags(self, **params):
"""
Mock later if needed. Stub function for now.
@param params:
@return:
"""
pass
def convert_seconds_to_human_readable(seconds): def convert_seconds_to_human_readable(seconds):
if seconds < 60: if seconds < 60:
human_string = "{0} seconds".format(seconds) human_string = "{0} seconds".format(seconds)
......
window.gettext = window.ngettext = function(){};
This source diff could not be displayed because it is too large. You can view the blob instead.
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
} }
// mixins - line height // mixins - line height
@mixin lh($fontSize: auto){ @mixin line-height($fontSize: auto){
line-height: ($fontSize*1.48) + px; line-height: ($fontSize*1.48) + px;
line-height: (($fontSize/10)*1.48) + rem; line-height: (($fontSize/10)*1.48) + rem;
} }
......
...@@ -414,6 +414,11 @@ def modx_dispatch(request, dispatch, location, course_id): ...@@ -414,6 +414,11 @@ def modx_dispatch(request, dispatch, location, course_id):
through the part before the first '?'. through the part before the first '?'.
- location -- the module location. Used to look up the XModule instance - location -- the module location. Used to look up the XModule instance
- course_id -- defines the course context for this request. - course_id -- defines the course context for this request.
Raises PermissionDenied if the user is not logged in. Raises Http404 if
the location and course_id do not identify a valid module, the module is
not accessible by the user, or the module raises NotFoundError. If the
module raises any other error, it will escape this function.
''' '''
# ''' (fix emacs broken parsing) # ''' (fix emacs broken parsing)
...@@ -442,8 +447,19 @@ def modx_dispatch(request, dispatch, location, course_id): ...@@ -442,8 +447,19 @@ def modx_dispatch(request, dispatch, location, course_id):
return HttpResponse(json.dumps({'success': file_too_big_msg})) return HttpResponse(json.dumps({'success': file_too_big_msg}))
p[fileinput_id] = inputfiles p[fileinput_id] = inputfiles
try:
descriptor = modulestore().get_instance(course_id, location)
except ItemNotFoundError:
log.warn(
"Invalid location for course id {course_id}: {location}".format(
course_id=course_id,
location=location
)
)
raise Http404
model_data_cache = ModelDataCache.cache_for_descriptor_descendents(course_id, model_data_cache = ModelDataCache.cache_for_descriptor_descendents(course_id,
request.user, modulestore().get_instance(course_id, location)) request.user, descriptor)
instance = get_module(request.user, request, location, model_data_cache, course_id, grade_bucket_type='ajax') instance = get_module(request.user, request, location, model_data_cache, course_id, grade_bucket_type='ajax')
if instance is None: if instance is None:
......
...@@ -69,19 +69,38 @@ class ModuleRenderTestCase(LoginEnrollmentTestCase): ...@@ -69,19 +69,38 @@ class ModuleRenderTestCase(LoginEnrollmentTestCase):
json.dumps({'success': 'Submission aborted! Your file "%s" is too large (max size: %d MB)' % json.dumps({'success': 'Submission aborted! Your file "%s" is too large (max size: %d MB)' %
(inputfile.name, settings.STUDENT_FILEUPLOAD_MAX_SIZE / (1000 ** 2))})) (inputfile.name, settings.STUDENT_FILEUPLOAD_MAX_SIZE / (1000 ** 2))}))
mock_request_3 = MagicMock() mock_request_3 = MagicMock()
mock_request_3.POST.copy.return_value = {} mock_request_3.POST.copy.return_value = {'position': 1}
mock_request_3.FILES = False mock_request_3.FILES = False
mock_request_3.user = UserFactory() mock_request_3.user = UserFactory()
inputfile_2 = Stub() inputfile_2 = Stub()
inputfile_2.size = 1 inputfile_2.size = 1
inputfile_2.name = 'name' inputfile_2.name = 'name'
self.assertRaises(ItemNotFoundError, render.modx_dispatch,
mock_request_3, 'dummy', self.location, 'toy')
self.assertRaises(Http404, render.modx_dispatch, mock_request_3, 'dummy',
self.location, self.course_id)
mock_request_3.POST.copy.return_value = {'position': 1}
self.assertIsInstance(render.modx_dispatch(mock_request_3, 'goto_position', self.assertIsInstance(render.modx_dispatch(mock_request_3, 'goto_position',
self.location, self.course_id), HttpResponse) self.location, self.course_id), HttpResponse)
self.assertRaises(
Http404,
render.modx_dispatch,
mock_request_3,
'goto_position',
self.location,
'bad_course_id'
)
self.assertRaises(
Http404,
render.modx_dispatch,
mock_request_3,
'goto_position',
['i4x', 'edX', 'toy', 'chapter', 'bad_location'],
self.course_id
)
self.assertRaises(
Http404,
render.modx_dispatch,
mock_request_3,
'bad_dispatch',
self.location,
self.course_id
)
def test_get_score_bucket(self): def test_get_score_bucket(self):
self.assertEquals(render.get_score_bucket(0, 10), 'incorrect') self.assertEquals(render.get_score_bucket(0, 10), 'incorrect')
......
...@@ -174,6 +174,9 @@ def forum_form_discussion(request, course_id): ...@@ -174,6 +174,9 @@ def forum_form_discussion(request, course_id):
try: try:
unsafethreads, query_params = get_threads(request, course_id) # This might process a search query unsafethreads, query_params = get_threads(request, course_id) # This might process a search query
threads = [utils.safe_content(thread) for thread in unsafethreads] threads = [utils.safe_content(thread) for thread in unsafethreads]
except (cc.utils.CommentClientMaintenanceError) as err:
log.warning("Forum is in maintenance mode")
return render_to_response('discussion/maintenance.html', {})
except (cc.utils.CommentClientError, cc.utils.CommentClientUnknownError) as err: except (cc.utils.CommentClientError, cc.utils.CommentClientUnknownError) as err:
log.error("Error loading forum discussion threads: %s" % str(err)) log.error("Error loading forum discussion threads: %s" % str(err))
raise Http404 raise Http404
......
...@@ -5,19 +5,20 @@ django-admin.py test --settings=lms.envs.test --pythonpath=. lms/djangoapps/open ...@@ -5,19 +5,20 @@ django-admin.py test --settings=lms.envs.test --pythonpath=. lms/djangoapps/open
""" """
import json import json
from mock import MagicMock from mock import MagicMock, patch, Mock
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.contrib.auth.models import Group from django.contrib.auth.models import Group
from django.http import HttpResponse
from mitxmako.shortcuts import render_to_string from mitxmako.shortcuts import render_to_string
from xmodule.open_ended_grading_classes import peer_grading_service from xmodule.open_ended_grading_classes import peer_grading_service, controller_query_service
from xmodule import peer_grading_module from xmodule import peer_grading_module
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
import xmodule.modulestore.django import xmodule.modulestore.django
from xmodule.x_module import ModuleSystem from xmodule.x_module import ModuleSystem
from open_ended_grading import staff_grading_service from open_ended_grading import staff_grading_service, views
from courseware.access import _course_staff_group_name from courseware.access import _course_staff_group_name
from courseware.tests.tests import LoginEnrollmentTestCase, TEST_DATA_XML_MODULESTORE, get_user from courseware.tests.tests import LoginEnrollmentTestCase, TEST_DATA_XML_MODULESTORE, get_user
...@@ -25,10 +26,11 @@ import logging ...@@ -25,10 +26,11 @@ import logging
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
from django.test.utils import override_settings from django.test.utils import override_settings
from django.http import QueryDict
from xmodule.tests import test_util_open_ended from xmodule.tests import test_util_open_ended
from courseware.tests import factories
@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE) @override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE)
class TestStaffGradingService(LoginEnrollmentTestCase): class TestStaffGradingService(LoginEnrollmentTestCase):
...@@ -55,8 +57,8 @@ class TestStaffGradingService(LoginEnrollmentTestCase): ...@@ -55,8 +57,8 @@ class TestStaffGradingService(LoginEnrollmentTestCase):
def make_instructor(course): def make_instructor(course):
group_name = _course_staff_group_name(course.location) group_name = _course_staff_group_name(course.location)
g = Group.objects.create(name=group_name) group = Group.objects.create(name=group_name)
g.user_set.add(get_user(self.instructor)) group.user_set.add(get_user(self.instructor))
make_instructor(self.toy) make_instructor(self.toy)
...@@ -76,30 +78,28 @@ class TestStaffGradingService(LoginEnrollmentTestCase): ...@@ -76,30 +78,28 @@ class TestStaffGradingService(LoginEnrollmentTestCase):
self.check_for_get_code(404, url) self.check_for_get_code(404, url)
self.check_for_post_code(404, url) self.check_for_post_code(404, url)
def test_get_next(self): def test_get_next(self):
self.login(self.instructor, self.password) self.login(self.instructor, self.password)
url = reverse('staff_grading_get_next', kwargs={'course_id': self.course_id}) url = reverse('staff_grading_get_next', kwargs={'course_id': self.course_id})
data = {'location': self.location} data = {'location': self.location}
r = self.check_for_post_code(200, url, data) response = self.check_for_post_code(200, url, data)
d = json.loads(r.content)
self.assertTrue(d['success']) content = json.loads(response.content)
self.assertEquals(d['submission_id'], self.mock_service.cnt)
self.assertIsNotNone(d['submission'])
self.assertIsNotNone(d['num_graded'])
self.assertIsNotNone(d['min_for_ml'])
self.assertIsNotNone(d['num_pending'])
self.assertIsNotNone(d['prompt'])
self.assertIsNotNone(d['ml_error_info'])
self.assertIsNotNone(d['max_score'])
self.assertIsNotNone(d['rubric'])
self.assertTrue(content['success'])
self.assertEquals(content['submission_id'], self.mock_service.cnt)
self.assertIsNotNone(content['submission'])
self.assertIsNotNone(content['num_graded'])
self.assertIsNotNone(content['min_for_ml'])
self.assertIsNotNone(content['num_pending'])
self.assertIsNotNone(content['prompt'])
self.assertIsNotNone(content['ml_error_info'])
self.assertIsNotNone(content['max_score'])
self.assertIsNotNone(content['rubric'])
def save_grade_base(self,skip=False): def save_grade_base(self, skip=False):
self.login(self.instructor, self.password) self.login(self.instructor, self.password)
url = reverse('staff_grading_save_grade', kwargs={'course_id': self.course_id}) url = reverse('staff_grading_save_grade', kwargs={'course_id': self.course_id})
...@@ -111,12 +111,12 @@ class TestStaffGradingService(LoginEnrollmentTestCase): ...@@ -111,12 +111,12 @@ class TestStaffGradingService(LoginEnrollmentTestCase):
'submission_flagged': "true", 'submission_flagged': "true",
'rubric_scores[]': ['1', '2']} 'rubric_scores[]': ['1', '2']}
if skip: if skip:
data.update({'skipped' : True}) data.update({'skipped': True})
r = self.check_for_post_code(200, url, data) response = self.check_for_post_code(200, url, data)
d = json.loads(r.content) content = json.loads(response.content)
self.assertTrue(d['success'], str(d)) self.assertTrue(content['success'], str(content))
self.assertEquals(d['submission_id'], self.mock_service.cnt) self.assertEquals(content['submission_id'], self.mock_service.cnt)
def test_save_grade(self): def test_save_grade(self):
self.save_grade_base(skip=False) self.save_grade_base(skip=False)
...@@ -130,11 +130,11 @@ class TestStaffGradingService(LoginEnrollmentTestCase): ...@@ -130,11 +130,11 @@ class TestStaffGradingService(LoginEnrollmentTestCase):
url = reverse('staff_grading_get_problem_list', kwargs={'course_id': self.course_id}) url = reverse('staff_grading_get_problem_list', kwargs={'course_id': self.course_id})
data = {} data = {}
r = self.check_for_post_code(200, url, data) response = self.check_for_post_code(200, url, data)
d = json.loads(r.content) content = json.loads(response.content)
self.assertTrue(d['success'], str(d)) self.assertTrue(content['success'], str(content))
self.assertIsNotNone(d['problem_list']) self.assertIsNotNone(content['problem_list'])
@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE) @override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE)
...@@ -181,14 +181,14 @@ class TestPeerGradingService(LoginEnrollmentTestCase): ...@@ -181,14 +181,14 @@ class TestPeerGradingService(LoginEnrollmentTestCase):
def test_get_next_submission_success(self): def test_get_next_submission_success(self):
data = {'location': self.location} data = {'location': self.location}
r = self.peer_module.get_next_submission(data) response = self.peer_module.get_next_submission(data)
d = r content = response
self.assertTrue(d['success']) self.assertTrue(content['success'])
self.assertIsNotNone(d['submission_id']) self.assertIsNotNone(content['submission_id'])
self.assertIsNotNone(d['prompt']) self.assertIsNotNone(content['prompt'])
self.assertIsNotNone(d['submission_key']) self.assertIsNotNone(content['submission_key'])
self.assertIsNotNone(d['max_score']) self.assertIsNotNone(content['max_score'])
def test_get_next_submission_missing_location(self): def test_get_next_submission_missing_location(self):
data = {} data = {}
...@@ -216,10 +216,9 @@ class TestPeerGradingService(LoginEnrollmentTestCase): ...@@ -216,10 +216,9 @@ class TestPeerGradingService(LoginEnrollmentTestCase):
qdict.getlist = fake_get_item qdict.getlist = fake_get_item
qdict.keys = data.keys qdict.keys = data.keys
r = self.peer_module.save_grade(qdict) response = self.peer_module.save_grade(qdict)
d = r
self.assertTrue(d['success']) self.assertTrue(response['success'])
def test_save_grade_missing_keys(self): def test_save_grade_missing_keys(self):
data = {} data = {}
...@@ -229,37 +228,35 @@ class TestPeerGradingService(LoginEnrollmentTestCase): ...@@ -229,37 +228,35 @@ class TestPeerGradingService(LoginEnrollmentTestCase):
def test_is_calibrated_success(self): def test_is_calibrated_success(self):
data = {'location': self.location} data = {'location': self.location}
r = self.peer_module.is_student_calibrated(data) response = self.peer_module.is_student_calibrated(data)
d = r
self.assertTrue(d['success']) self.assertTrue(response['success'])
self.assertTrue('calibrated' in d) self.assertTrue('calibrated' in response)
def test_is_calibrated_failure(self): def test_is_calibrated_failure(self):
data = {} data = {}
d = self.peer_module.is_student_calibrated(data) response = self.peer_module.is_student_calibrated(data)
self.assertFalse(d['success']) self.assertFalse(response['success'])
self.assertFalse('calibrated' in d) self.assertFalse('calibrated' in response)
def test_show_calibration_essay_success(self): def test_show_calibration_essay_success(self):
data = {'location': self.location} data = {'location': self.location}
r = self.peer_module.show_calibration_essay(data) response = self.peer_module.show_calibration_essay(data)
d = r
self.assertTrue(d['success']) self.assertTrue(response['success'])
self.assertIsNotNone(d['submission_id']) self.assertIsNotNone(response['submission_id'])
self.assertIsNotNone(d['prompt']) self.assertIsNotNone(response['prompt'])
self.assertIsNotNone(d['submission_key']) self.assertIsNotNone(response['submission_key'])
self.assertIsNotNone(d['max_score']) self.assertIsNotNone(response['max_score'])
def test_show_calibration_essay_missing_key(self): def test_show_calibration_essay_missing_key(self):
data = {} data = {}
d = self.peer_module.show_calibration_essay(data) response = self.peer_module.show_calibration_essay(data)
self.assertFalse(d['success']) self.assertFalse(response['success'])
self.assertEqual(d['error'], "Missing required keys: location") self.assertEqual(response['error'], "Missing required keys: location")
def test_save_calibration_essay_success(self): def test_save_calibration_essay_success(self):
data = { data = {
...@@ -281,13 +278,45 @@ class TestPeerGradingService(LoginEnrollmentTestCase): ...@@ -281,13 +278,45 @@ class TestPeerGradingService(LoginEnrollmentTestCase):
qdict.getlist = fake_get_item qdict.getlist = fake_get_item
qdict.keys = data.keys qdict.keys = data.keys
d = self.peer_module.save_calibration_essay(qdict) response = self.peer_module.save_calibration_essay(qdict)
self.assertTrue(d['success']) self.assertTrue(response['success'])
self.assertTrue('actual_score' in d) self.assertTrue('actual_score' in response)
def test_save_calibration_essay_missing_keys(self): def test_save_calibration_essay_missing_keys(self):
data = {} data = {}
d = self.peer_module.save_calibration_essay(data) response = self.peer_module.save_calibration_essay(data)
self.assertFalse(d['success']) self.assertFalse(response['success'])
self.assertTrue(d['error'].find('Missing required keys:') > -1) self.assertTrue(response['error'].find('Missing required keys:') > -1)
self.assertFalse('actual_score' in d) self.assertFalse('actual_score' in response)
@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE)
class TestPanel(LoginEnrollmentTestCase):
"""
Run tests on the open ended panel
"""
def setUp(self):
# Toy courses should be loaded
self.course_name = 'edX/open_ended/2012_Fall'
self.course = modulestore().get_course(self.course_name)
self.user = factories.UserFactory()
def test_open_ended_panel(self):
"""
Test to see if the peer grading module in the demo course is found
@return:
"""
found_module, peer_grading_module = views.find_peer_grading_module(self.course)
self.assertTrue(found_module)
@patch('xmodule.open_ended_grading_classes.controller_query_service.ControllerQueryService',
controller_query_service.MockControllerQueryService)
def test_problem_list(self):
"""
Ensure that the problem list from the grading controller server can be rendered properly locally
@return:
"""
request = Mock(user=self.user)
response = views.student_problem_list(request, self.course.id)
self.assertTrue(isinstance(response, HttpResponse))
...@@ -21,6 +21,7 @@ import open_ended_notifications ...@@ -21,6 +21,7 @@ import open_ended_notifications
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.modulestore import search from xmodule.modulestore import search
from xmodule.modulestore.exceptions import ItemNotFoundError
from django.http import HttpResponse, Http404, HttpResponseRedirect from django.http import HttpResponse, Http404, HttpResponseRedirect
from mitxmako.shortcuts import render_to_string from mitxmako.shortcuts import render_to_string
...@@ -30,10 +31,10 @@ log = logging.getLogger(__name__) ...@@ -30,10 +31,10 @@ log = logging.getLogger(__name__)
system = ModuleSystem( system = ModuleSystem(
ajax_url=None, ajax_url=None,
track_function=None, track_function=None,
get_module = None, get_module=None,
render_template=render_to_string, render_template=render_to_string,
replace_urls = None, replace_urls=None,
xblock_model_data= {} xblock_model_data={}
) )
controller_qs = ControllerQueryService(settings.OPEN_ENDED_GRADING_INTERFACE, system) controller_qs = ControllerQueryService(settings.OPEN_ENDED_GRADING_INTERFACE, system)
...@@ -90,40 +91,61 @@ def staff_grading(request, course_id): ...@@ -90,40 +91,61 @@ def staff_grading(request, course_id):
'staff_access': True, }) 'staff_access': True, })
def find_peer_grading_module(course):
"""
Given a course, finds the first peer grading module in it.
@param course: A course object.
@return: boolean found_module, string problem_url
"""
#Reverse the base course url
base_course_url = reverse('courses')
found_module = False
problem_url = ""
#Get the course id and split it
course_id_parts = course.id.split("/")
log.info("COURSE ID PARTS")
log.info(course_id_parts)
#Get the peer grading modules currently in the course. Explicitly specify the course id to avoid issues with different runs.
items = modulestore().get_items(['i4x', course_id_parts[0], course_id_parts[1], 'peergrading', None],
course_id=course.id)
#See if any of the modules are centralized modules (ie display info from multiple problems)
items = [i for i in items if not getattr(i, "use_for_single_location", True)]
#Get the first one
if len(items) > 0:
item_location = items[0].location
#Generate a url for the first module and redirect the user to it
problem_url_parts = search.path_to_location(modulestore(), course.id, item_location)
problem_url = generate_problem_url(problem_url_parts, base_course_url)
found_module = True
return found_module, problem_url
@cache_control(no_cache=True, no_store=True, must_revalidate=True) @cache_control(no_cache=True, no_store=True, must_revalidate=True)
def peer_grading(request, course_id): def peer_grading(request, course_id):
''' '''
Show a peer grading interface When a student clicks on the "peer grading" button in the open ended interface, link them to a peer grading
xmodule in the course.
''' '''
#Get the current course #Get the current course
course = get_course_with_access(request.user, course_id, 'load') course = get_course_with_access(request.user, course_id, 'load')
course_id_parts = course.id.split("/")
false_dict = [False, "False", "false", "FALSE"]
#Reverse the base course url
base_course_url = reverse('courses')
try:
#TODO: This will not work with multiple runs of a course. Make it work. The last key in the Location passed
#to get_items is called revision. Is this the same as run?
#Get the peer grading modules currently in the course
items = modulestore().get_items(['i4x', None, course_id_parts[1], 'peergrading', None])
#See if any of the modules are centralized modules (ie display info from multiple problems)
items = [i for i in items if getattr(i,"use_for_single_location", True) in false_dict]
#Get the first one
item_location = items[0].location
#Generate a url for the first module and redirect the user to it
problem_url_parts = search.path_to_location(modulestore(), course.id, item_location)
problem_url = generate_problem_url(problem_url_parts, base_course_url)
return HttpResponseRedirect(problem_url) found_module, problem_url = find_peer_grading_module(course)
except: if not found_module:
#This is a student_facing_error #This is a student_facing_error
error_message = "Error with initializing peer grading. Centralized module does not exist. Please contact course staff." error_message = """
Error with initializing peer grading.
There has not been a peer grading module created in the courseware that would allow you to grade others.
Please check back later for this.
"""
#This is a dev_facing_error #This is a dev_facing_error
log.exception(error_message + "Current course is: {0}".format(course_id)) log.exception(error_message + "Current course is: {0}".format(course_id))
return HttpResponse(error_message) return HttpResponse(error_message)
return HttpResponseRedirect(problem_url)
def generate_problem_url(problem_url_parts, base_course_url): def generate_problem_url(problem_url_parts, base_course_url):
""" """
...@@ -145,7 +167,8 @@ def generate_problem_url(problem_url_parts, base_course_url): ...@@ -145,7 +167,8 @@ def generate_problem_url(problem_url_parts, base_course_url):
@cache_control(no_cache=True, no_store=True, must_revalidate=True) @cache_control(no_cache=True, no_store=True, must_revalidate=True)
def student_problem_list(request, course_id): def student_problem_list(request, course_id):
''' '''
Show a student problem list Show a student problem list to a student. Fetch the list from the grading controller server, get some metadata,
and then show it to the student.
''' '''
course = get_course_with_access(request.user, course_id, 'load') course = get_course_with_access(request.user, course_id, 'load')
student_id = unique_id_for_user(request.user) student_id = unique_id_for_user(request.user)
...@@ -157,6 +180,7 @@ def student_problem_list(request, course_id): ...@@ -157,6 +180,7 @@ def student_problem_list(request, course_id):
base_course_url = reverse('courses') base_course_url = reverse('courses')
try: try:
#Get list of all open ended problems that the grading server knows about
problem_list_json = controller_qs.get_grading_status_list(course_id, unique_id_for_user(request.user)) problem_list_json = controller_qs.get_grading_status_list(course_id, unique_id_for_user(request.user))
problem_list_dict = json.loads(problem_list_json) problem_list_dict = json.loads(problem_list_json)
success = problem_list_dict['success'] success = problem_list_dict['success']
...@@ -166,8 +190,22 @@ def student_problem_list(request, course_id): ...@@ -166,8 +190,22 @@ def student_problem_list(request, course_id):
else: else:
problem_list = problem_list_dict['problem_list'] problem_list = problem_list_dict['problem_list']
#A list of problems to remove (problems that can't be found in the course)
list_to_remove = []
for i in xrange(0, len(problem_list)): for i in xrange(0, len(problem_list)):
problem_url_parts = search.path_to_location(modulestore(), course.id, problem_list[i]['location']) try:
#Try to load each problem in the courseware to get links to them
problem_url_parts = search.path_to_location(modulestore(), course.id, problem_list[i]['location'])
except ItemNotFoundError:
#If the problem cannot be found at the location received from the grading controller server, it has been deleted by the course author.
#Continue with the rest of the location to construct the list
error_message = "Could not find module for course {0} at location {1}".format(course.id,
problem_list[i][
'location'])
log.error(error_message)
#Mark the problem for removal from the list
list_to_remove.append(i)
continue
problem_url = generate_problem_url(problem_url_parts, base_course_url) problem_url = generate_problem_url(problem_url_parts, base_course_url)
problem_list[i].update({'actual_url': problem_url}) problem_list[i].update({'actual_url': problem_url})
eta_available = problem_list[i]['eta_available'] eta_available = problem_list[i]['eta_available']
...@@ -197,6 +235,8 @@ def student_problem_list(request, course_id): ...@@ -197,6 +235,8 @@ def student_problem_list(request, course_id):
log.error("Problem with results from external grading service for open ended.") log.error("Problem with results from external grading service for open ended.")
success = False success = False
#Remove problems that cannot be found in the courseware from the list
problem_list = [problem_list[i] for i in xrange(0, len(problem_list)) if i not in list_to_remove]
ajax_url = _reverse_with_slash('open_ended_problems', course_id) ajax_url = _reverse_with_slash('open_ended_problems', course_id)
return render_to_response('open_ended_problems/open_ended_problems.html', { return render_to_response('open_ended_problems/open_ended_problems.html', {
...@@ -300,7 +340,16 @@ def combined_notifications(request, course_id): ...@@ -300,7 +340,16 @@ def combined_notifications(request, course_id):
'description': description, 'description': description,
'alert_message': alert_message 'alert_message': alert_message
} }
notification_list.append(notification_item) #The open ended panel will need to link the "peer grading" button in the panel to a peer grading
#xmodule defined in the course. This checks to see if the human name of the server notification
#that we are currently processing is "peer grading". If it is, it looks for a peer grading
#module in the course. If none exists, it removes the peer grading item from the panel.
if human_name == "Peer Grading":
found_module, problem_url = find_peer_grading_module(course)
if found_module:
notification_list.append(notification_item)
else:
notification_list.append(notification_item)
ajax_url = _reverse_with_slash('open_ended_notifications', course_id) ajax_url = _reverse_with_slash('open_ended_notifications', course_id)
combined_dict = { combined_dict = {
...@@ -311,9 +360,7 @@ def combined_notifications(request, course_id): ...@@ -311,9 +360,7 @@ def combined_notifications(request, course_id):
'ajax_url': ajax_url, 'ajax_url': ajax_url,
} }
return render_to_response('open_ended_problems/combined_notifications.html', return render_to_response('open_ended_problems/combined_notifications.html', combined_dict)
combined_dict
)
@cache_control(no_cache=True, no_store=True, must_revalidate=True) @cache_control(no_cache=True, no_store=True, must_revalidate=True)
......
...@@ -2,6 +2,11 @@ ...@@ -2,6 +2,11 @@
This config file extends the test environment configuration This config file extends the test environment configuration
so that we can run the lettuce acceptance tests. so that we can run the lettuce acceptance tests.
""" """
# We intentionally define lots of variables that aren't used, and
# want to import all variables from base settings files
# pylint: disable=W0401, W0614
from .test import * from .test import *
# You need to start the server in debug mode, # You need to start the server in debug mode,
......
...@@ -6,6 +6,11 @@ Common traits: ...@@ -6,6 +6,11 @@ Common traits:
* Use memcached, and cache-backed sessions * Use memcached, and cache-backed sessions
* Use a MySQL 5.1 database * Use a MySQL 5.1 database
""" """
# We intentionally define lots of variables that aren't used, and
# want to import all variables from base settings files
# pylint: disable=W0401, W0614
import json import json
from .common import * from .common import *
...@@ -109,6 +114,11 @@ DEFAULT_FEEDBACK_EMAIL = ENV_TOKENS.get('DEFAULT_FEEDBACK_EMAIL', DEFAULT_FEEDBA ...@@ -109,6 +114,11 @@ DEFAULT_FEEDBACK_EMAIL = ENV_TOKENS.get('DEFAULT_FEEDBACK_EMAIL', DEFAULT_FEEDBA
ADMINS = ENV_TOKENS.get('ADMINS', ADMINS) ADMINS = ENV_TOKENS.get('ADMINS', ADMINS)
SERVER_EMAIL = ENV_TOKENS.get('SERVER_EMAIL', SERVER_EMAIL) SERVER_EMAIL = ENV_TOKENS.get('SERVER_EMAIL', SERVER_EMAIL)
#Theme overrides
THEME_NAME = ENV_TOKENS.get('THEME_NAME', None)
if not THEME_NAME is None:
enable_theme(THEME_NAME)
#Timezone overrides #Timezone overrides
TIME_ZONE = ENV_TOKENS.get('TIME_ZONE', TIME_ZONE) TIME_ZONE = ENV_TOKENS.get('TIME_ZONE', TIME_ZONE)
......
...@@ -3,6 +3,11 @@ This config file is a copy of dev environment without the Debug ...@@ -3,6 +3,11 @@ This config file is a copy of dev environment without the Debug
Toolbar. I it suitable to run against acceptance tests. Toolbar. I it suitable to run against acceptance tests.
""" """
# We intentionally define lots of variables that aren't used, and
# want to import all variables from base settings files
# pylint: disable=W0401, W0614
from .dev import * from .dev import *
# REMOVE DEBUG TOOLBAR # REMOVE DEBUG TOOLBAR
......
...@@ -2,6 +2,10 @@ ...@@ -2,6 +2,10 @@
Settings for the LMS that runs alongside the CMS on AWS Settings for the LMS that runs alongside the CMS on AWS
""" """
# We intentionally define lots of variables that aren't used, and
# want to import all variables from base settings files
# pylint: disable=W0401, W0614
from ..aws import * from ..aws import *
with open(ENV_ROOT / "cms.auth.json") as auth_file: with open(ENV_ROOT / "cms.auth.json") as auth_file:
......
...@@ -2,6 +2,10 @@ ...@@ -2,6 +2,10 @@
Settings for the LMS that runs alongside the CMS on AWS Settings for the LMS that runs alongside the CMS on AWS
""" """
# We intentionally define lots of variables that aren't used, and
# want to import all variables from base settings files
# pylint: disable=W0401, W0614
from ..dev import * from ..dev import *
MITX_FEATURES['AUTH_USE_MIT_CERTIFICATES'] = False MITX_FEATURES['AUTH_USE_MIT_CERTIFICATES'] = False
......
...@@ -2,6 +2,10 @@ ...@@ -2,6 +2,10 @@
Settings for the LMS that runs alongside the CMS on AWS Settings for the LMS that runs alongside the CMS on AWS
""" """
# We intentionally define lots of variables that aren't used, and
# want to import all variables from base settings files
# pylint: disable=W0401, W0614
from .dev import * from .dev import *
MODULESTORE = { MODULESTORE = {
......
...@@ -18,6 +18,11 @@ Longer TODO: ...@@ -18,6 +18,11 @@ Longer TODO:
3. We need to handle configuration for multiple courses. This could be as 3. We need to handle configuration for multiple courses. This could be as
multiple sites, but we do need a way to map their data assets. multiple sites, but we do need a way to map their data assets.
""" """
# We intentionally define lots of variables that aren't used, and
# want to import all variables from base settings files
# pylint: disable=W0401, W0614
import sys import sys
import os import os
...@@ -67,6 +72,7 @@ MITX_FEATURES = { ...@@ -67,6 +72,7 @@ MITX_FEATURES = {
'ENABLE_PSYCHOMETRICS': False, # real-time psychometrics (eg item response theory analysis in instructor dashboard) 'ENABLE_PSYCHOMETRICS': False, # real-time psychometrics (eg item response theory analysis in instructor dashboard)
'ENABLE_DJANGO_ADMIN_SITE': False, # set true to enable django's admin site, even on prod (e.g. for course ops)
'ENABLE_SQL_TRACKING_LOGS': False, 'ENABLE_SQL_TRACKING_LOGS': False,
'ENABLE_LMS_MIGRATION': False, 'ENABLE_LMS_MIGRATION': False,
'ENABLE_MANUAL_GIT_RELOAD': False, 'ENABLE_MANUAL_GIT_RELOAD': False,
...@@ -105,6 +111,9 @@ MITX_FEATURES = { ...@@ -105,6 +111,9 @@ MITX_FEATURES = {
# Enable URL that shows information about the status of variuous services # Enable URL that shows information about the status of variuous services
'ENABLE_SERVICE_STATUS': False, 'ENABLE_SERVICE_STATUS': False,
# Toggle to indicate use of a custom theme
'USE_CUSTOM_THEME': False
} }
# Used for A/B testing # Used for A/B testing
...@@ -162,12 +171,12 @@ MAKO_TEMPLATES['main'] = [PROJECT_ROOT / 'templates', ...@@ -162,12 +171,12 @@ MAKO_TEMPLATES['main'] = [PROJECT_ROOT / 'templates',
# This is where Django Template lookup is defined. There are a few of these # This is where Django Template lookup is defined. There are a few of these
# still left lying around. # still left lying around.
TEMPLATE_DIRS = ( TEMPLATE_DIRS = [
PROJECT_ROOT / "templates", PROJECT_ROOT / "templates",
COMMON_ROOT / 'templates', COMMON_ROOT / 'templates',
COMMON_ROOT / 'lib' / 'capa' / 'capa' / 'templates', COMMON_ROOT / 'lib' / 'capa' / 'capa' / 'templates',
COMMON_ROOT / 'djangoapps' / 'pipeline_mako' / 'templates', COMMON_ROOT / 'djangoapps' / 'pipeline_mako' / 'templates',
) ]
TEMPLATE_CONTEXT_PROCESSORS = ( TEMPLATE_CONTEXT_PROCESSORS = (
'django.core.context_processors.request', 'django.core.context_processors.request',
...@@ -709,3 +718,31 @@ MKTG_URL_LINK_MAP = { ...@@ -709,3 +718,31 @@ MKTG_URL_LINK_MAP = {
'HONOR': 'honor', 'HONOR': 'honor',
'PRIVACY': 'privacy_edx', 'PRIVACY': 'privacy_edx',
} }
############################### THEME ################################
def enable_theme(theme_name):
"""
Enable the settings for a custom theme, whose files should be stored
in ENV_ROOT/themes/THEME_NAME (e.g., edx_all/themes/stanford).
The THEME_NAME setting should be configured separately since it can't
be set here (this function closes too early). An idiom for doing this
is:
THEME_NAME = "stanford"
enable_theme(THEME_NAME)
"""
MITX_FEATURES['USE_CUSTOM_THEME'] = True
# Calculate the location of the theme's files
theme_root = ENV_ROOT / "themes" / theme_name
# Include the theme's templates in the template search paths
TEMPLATE_DIRS.append(theme_root / 'templates')
MAKO_TEMPLATES['main'].append(theme_root / 'templates')
# Namespace the theme's static files to 'themes/<theme_name>' to
# avoid collisions with default edX static files
STATICFILES_DIRS.append((u'themes/%s' % theme_name,
theme_root / 'static'))
...@@ -2,6 +2,11 @@ ...@@ -2,6 +2,11 @@
These are debug machines used for content creators, so they're kind of a cross These are debug machines used for content creators, so they're kind of a cross
between dev machines and AWS machines. between dev machines and AWS machines.
""" """
# We intentionally define lots of variables that aren't used, and
# want to import all variables from base settings files
# pylint: disable=W0401, W0614
from .aws import * from .aws import *
DEBUG = True DEBUG = True
......
...@@ -7,6 +7,11 @@ sessions. Assumes structure: ...@@ -7,6 +7,11 @@ sessions. Assumes structure:
/mitx # The location of this repo /mitx # The location of this repo
/log # Where we're going to write log files /log # Where we're going to write log files
""" """
# We intentionally define lots of variables that aren't used, and
# want to import all variables from base settings files
# pylint: disable=W0401, W0614
from .common import * from .common import *
from logsettings import get_logger_config from logsettings import get_logger_config
......
...@@ -8,6 +8,10 @@ sessions. Assumes structure: ...@@ -8,6 +8,10 @@ sessions. Assumes structure:
/log # Where we're going to write log files /log # Where we're going to write log files
""" """
# We intentionally define lots of variables that aren't used, and
# want to import all variables from base settings files
# pylint: disable=W0401, W0614
import socket import socket
if 'eecs1' in socket.gethostname(): if 'eecs1' in socket.gethostname():
......
...@@ -7,6 +7,11 @@ sessions. Assumes structure: ...@@ -7,6 +7,11 @@ sessions. Assumes structure:
/mitx # The location of this repo /mitx # The location of this repo
/log # Where we're going to write log files /log # Where we're going to write log files
""" """
# We intentionally define lots of variables that aren't used, and
# want to import all variables from base settings files
# pylint: disable=W0401, W0614
from .common import * from .common import *
from logsettings import get_logger_config from logsettings import get_logger_config
from .dev import * from .dev import *
......
...@@ -9,6 +9,11 @@ following domains to 127.0.0.1 in your /etc/hosts file: ...@@ -9,6 +9,11 @@ following domains to 127.0.0.1 in your /etc/hosts file:
Note that OS X has a bug where using *.local domains is excruciatingly slow, so Note that OS X has a bug where using *.local domains is excruciatingly slow, so
use *.dev domains instead for local testing. use *.dev domains instead for local testing.
""" """
# We intentionally define lots of variables that aren't used, and
# want to import all variables from base settings files
# pylint: disable=W0401, W0614
from .dev import * from .dev import *
MITX_FEATURES['SUBDOMAIN_COURSE_LISTINGS'] = True MITX_FEATURES['SUBDOMAIN_COURSE_LISTINGS'] = True
......
""" """
This config file runs the dev environment, but with mongo as the datastore This config file runs the dev environment, but with mongo as the datastore
""" """
# We intentionally define lots of variables that aren't used, and
# want to import all variables from base settings files
# pylint: disable=W0401, W0614
from .dev import * from .dev import *
GITHUB_REPO_ROOT = ENV_ROOT / "data" GITHUB_REPO_ROOT = ENV_ROOT / "data"
......
...@@ -8,6 +8,10 @@ The worker can be executed using: ...@@ -8,6 +8,10 @@ The worker can be executed using:
django_admin.py celery worker django_admin.py celery worker
""" """
# We intentionally define lots of variables that aren't used, and
# want to import all variables from base settings files
# pylint: disable=W0401, W0614
from dev import * from dev import *
################################# CELERY ###################################### ################################# CELERY ######################################
......
# We intentionally define lots of variables that aren't used, and
# want to import all variables from base settings files
# pylint: disable=W0401, W0614
from ..dev import * from ..dev import *
CLASSES_TO_DBS = { CLASSES_TO_DBS = {
......
# We intentionally define lots of variables that aren't used, and
# want to import all variables from base settings files
# pylint: disable=W0401, W0614
from .courses import * from .courses import *
DATABASES = course_db_for('HarvardX/CS50x/2012') DATABASES = course_db_for('HarvardX/CS50x/2012')
# We intentionally define lots of variables that aren't used, and
# want to import all variables from base settings files
# pylint: disable=W0401, W0614
from .courses import * from .courses import *
DATABASES = course_db_for('MITx/6.002x/2012_Fall') DATABASES = course_db_for('MITx/6.002x/2012_Fall')
...@@ -2,6 +2,11 @@ ...@@ -2,6 +2,11 @@
Note that for this to work at all, you must have memcached running (or you won't Note that for this to work at all, you must have memcached running (or you won't
get shared sessions) get shared sessions)
""" """
# We intentionally define lots of variables that aren't used, and
# want to import all variables from base settings files
# pylint: disable=W0401, W0614
from courses import * from courses import *
# Move this to a shared file later: # Move this to a shared file later:
......
...@@ -13,6 +13,11 @@ Dir structure: ...@@ -13,6 +13,11 @@ Dir structure:
/log # Where we're going to write log files /log # Where we're going to write log files
""" """
# We intentionally define lots of variables that aren't used, and
# want to import all variables from base settings files
# pylint: disable=W0401, W0614
from .dev import * from .dev import *
WIKI_ENABLED = True WIKI_ENABLED = True
......
# We intentionally define variables that aren't used
# pylint: disable=W0614
DISCUSSION_ALLOWED_UPLOAD_FILE_TYPES = ('.jpg', '.jpeg', '.gif', '.bmp', '.png', '.tiff') DISCUSSION_ALLOWED_UPLOAD_FILE_TYPES = ('.jpg', '.jpeg', '.gif', '.bmp', '.png', '.tiff')
# We intentionally define lots of variables that aren't used, and
# want to import all variables from base settings files
# pylint: disable=W0401, W0614
# Settings for edx4edx production instance # Settings for edx4edx production instance
from .aws import * from .aws import *
COURSE_NAME = "edx4edx" COURSE_NAME = "edx4edx"
......
...@@ -2,6 +2,10 @@ ...@@ -2,6 +2,10 @@
This configuration is used for running jasmine tests This configuration is used for running jasmine tests
""" """
# We intentionally define lots of variables that aren't used, and
# want to import all variables from base settings files
# pylint: disable=W0401, W0614
from .test import * from .test import *
from logsettings import get_logger_config from logsettings import get_logger_config
......
...@@ -7,6 +7,11 @@ sessions. Assumes structure: ...@@ -7,6 +7,11 @@ sessions. Assumes structure:
/mitx # The location of this repo /mitx # The location of this repo
/log # Where we're going to write log files /log # Where we're going to write log files
""" """
# We intentionally define lots of variables that aren't used, and
# want to import all variables from base settings files
# pylint: disable=W0401, W0614
from .common import * from .common import *
from logsettings import get_logger_config from logsettings import get_logger_config
......
...@@ -7,6 +7,11 @@ sessions. Assumes structure: ...@@ -7,6 +7,11 @@ sessions. Assumes structure:
/mitx # The location of this repo /mitx # The location of this repo
/log # Where we're going to write log files /log # Where we're going to write log files
""" """
# We intentionally define lots of variables that aren't used, and
# want to import all variables from base settings files
# pylint: disable=W0401, W0614
from .common import * from .common import *
import os import os
from path import path from path import path
......
...@@ -7,6 +7,11 @@ sessions. Assumes structure: ...@@ -7,6 +7,11 @@ sessions. Assumes structure:
/mitx # The location of this repo /mitx # The location of this repo
/log # Where we're going to write log files /log # Where we're going to write log files
""" """
# We intentionally define lots of variables that aren't used, and
# want to import all variables from base settings files
# pylint: disable=W0401, W0614
from .common import * from .common import *
from logsettings import get_logger_config from logsettings import get_logger_config
import os import os
......
...@@ -31,14 +31,9 @@ def merge_dict(dic1, dic2): ...@@ -31,14 +31,9 @@ def merge_dict(dic1, dic2):
def perform_request(method, url, data_or_params=None, *args, **kwargs): def perform_request(method, url, data_or_params=None, *args, **kwargs):
if data_or_params is None: if data_or_params is None:
data_or_params = {} data_or_params = {}
tags = [
"{k}:{v}".format(k=k, v=v)
for (k, v) in data_or_params.items() + [("method", method), ("url", url)]
if k != 'api_key'
]
data_or_params['api_key'] = settings.API_KEY data_or_params['api_key'] = settings.API_KEY
try: try:
with dog_stats_api.timer('comment_client.request.time', tags=tags): with dog_stats_api.timer('comment_client.request.time'):
if method in ['post', 'put', 'patch']: if method in ['post', 'put', 'patch']:
response = requests.request(method, url, data=data_or_params, timeout=5) response = requests.request(method, url, data=data_or_params, timeout=5)
else: else:
...@@ -55,6 +50,9 @@ def perform_request(method, url, data_or_params=None, *args, **kwargs): ...@@ -55,6 +50,9 @@ def perform_request(method, url, data_or_params=None, *args, **kwargs):
if 200 < response.status_code < 500: if 200 < response.status_code < 500:
raise CommentClientError(response.text) raise CommentClientError(response.text)
# Heroku returns a 503 when an application is in maintenance mode
elif response.status_code == 503:
raise CommentClientMaintenanceError(response.text)
elif response.status_code == 500: elif response.status_code == 500:
raise CommentClientUnknownError(response.text) raise CommentClientUnknownError(response.text)
else: else:
...@@ -72,5 +70,9 @@ class CommentClientError(Exception): ...@@ -72,5 +70,9 @@ class CommentClientError(Exception):
return repr(self.message) return repr(self.message)
class CommentClientMaintenanceError(CommentClientError):
pass
class CommentClientUnknownError(CommentClientError): class CommentClientUnknownError(CommentClientError):
pass pass
...@@ -36,3 +36,16 @@ ...@@ -36,3 +36,16 @@
@import 'news'; @import 'news';
@import 'shame'; @import 'shame';
## THEMING
## -------
## Set up this file to import an edX theme library if the environment
## indicates that a theme should be used. The assumption is that the
## theme resides outside of this main edX repository, in a directory
## called themes/<theme-name>/, with its base Sass file in
## themes/<theme-name>/static/sass/_<theme-name>.scss. That one entry
## point can be used to @import in as many other things as needed.
% if env.get('THEME_NAME') is not None:
// import theme's Sass overrides
@import '${env.get('THEME_NAME')}'
% endif
<%inherit file="../main.html" />
<h1>We're sorry</h1>
<p>The forums are currently undergoing maintenance. We'll have them back up shortly!</p>
...@@ -8,7 +8,7 @@ from . import one_time_startup ...@@ -8,7 +8,7 @@ from . import one_time_startup
import django.contrib.auth.views import django.contrib.auth.views
# Uncomment the next two lines to enable the admin: # Uncomment the next two lines to enable the admin:
if settings.DEBUG: if settings.DEBUG or settings.MITX_FEATURES.get('ENABLE_DJANGO_ADMIN_SITE'):
admin.autodiscover() admin.autodiscover()
urlpatterns = ('', # nopep8 urlpatterns = ('', # nopep8
...@@ -330,7 +330,7 @@ if settings.COURSEWARE_ENABLED: ...@@ -330,7 +330,7 @@ if settings.COURSEWARE_ENABLED:
if settings.ENABLE_JASMINE: if settings.ENABLE_JASMINE:
urlpatterns += (url(r'^_jasmine/', include('django_jasmine.urls')),) urlpatterns += (url(r'^_jasmine/', include('django_jasmine.urls')),)
if settings.DEBUG: if settings.DEBUG or settings.MITX_FEATURES.get('ENABLE_DJANGO_ADMIN_SITE'):
## Jasmine and admin ## Jasmine and admin
urlpatterns += (url(r'^admin/', include(admin.site.urls)),) urlpatterns += (url(r'^admin/', include(admin.site.urls)),)
......
require 'json'
require 'rake/clean' require 'rake/clean'
require './rakefiles/helpers.rb' require './rakefiles/helpers.rb'
...@@ -7,6 +8,13 @@ end ...@@ -7,6 +8,13 @@ end
# Build Constants # Build Constants
REPO_ROOT = File.dirname(__FILE__) REPO_ROOT = File.dirname(__FILE__)
ENV_ROOT = File.dirname(REPO_ROOT)
REPORT_DIR = File.join(REPO_ROOT, "reports") REPORT_DIR = File.join(REPO_ROOT, "reports")
# Environment constants
SERVICE_VARIANT = ENV['SERVICE_VARIANT']
CONFIG_PREFIX = SERVICE_VARIANT ? SERVICE_VARIANT + "." : ""
ENV_FILE = File.join(ENV_ROOT, CONFIG_PREFIX + "env.json")
ENV_TOKENS = File.exists?(ENV_FILE) ? JSON.parse(File.read(ENV_FILE)) : {}
task :default => [:test, :pep8, :pylint] task :default => [:test, :pep8, :pylint]
# Theming constants
THEME_NAME = ENV_TOKENS['THEME_NAME']
USE_CUSTOM_THEME = !(THEME_NAME.nil? || THEME_NAME.empty?)
if USE_CUSTOM_THEME
THEME_ROOT = File.join(ENV_ROOT, "themes", THEME_NAME)
THEME_SASS = File.join(THEME_ROOT, "static", "sass")
end
# Run the specified file through the Mako templating engine, providing
# the ENV_TOKENS to the templating context.
def preprocess_with_mako(filename)
# simple command-line invocation of Mako engine
mako = "from mako.template import Template;" +
"print Template(filename=\"#{filename}\")" +
# Total hack. It works because a Python dict literal has
# the same format as a JSON object.
".render(env=#{ENV_TOKENS.to_json});"
# strip off the .mako extension
output_filename = filename.chomp(File.extname(filename))
# just pipe from stdout into the new file, exiting on failure
File.open(output_filename, 'w') do |file|
file.write(`python -c '#{mako}'`)
exit_code = $?.to_i
abort "#{mako} failed with #{exit_code}" if exit_code.to_i != 0
end
end
def xmodule_cmd(watch=false, debug=false) def xmodule_cmd(watch=false, debug=false)
xmodule_cmd = 'xmodule_assets common/static/xmodule' xmodule_cmd = 'xmodule_assets common/static/xmodule'
...@@ -32,10 +60,17 @@ def coffee_cmd(watch=false, debug=false) ...@@ -32,10 +60,17 @@ def coffee_cmd(watch=false, debug=false)
end end
def sass_cmd(watch=false, debug=false) def sass_cmd(watch=false, debug=false)
sass_load_paths = ["./common/static/sass"]
sass_watch_paths = ["*/static"]
if USE_CUSTOM_THEME
sass_load_paths << THEME_SASS
sass_watch_paths << THEME_SASS
end
"sass #{debug ? '--debug-info' : '--style compressed'} " + "sass #{debug ? '--debug-info' : '--style compressed'} " +
"--load-path ./common/static/sass " + "--load-path #{sass_load_paths.join(' ')} " +
"--require ./common/static/sass/bourbon/lib/bourbon.rb " + "--require ./common/static/sass/bourbon/lib/bourbon.rb " +
"#{watch ? '--watch' : '--update'} */static" "#{watch ? '--watch' : '--update'} #{sass_watch_paths.join(' ')}"
end end
desc "Compile all assets" desc "Compile all assets"
...@@ -46,6 +81,13 @@ namespace :assets do ...@@ -46,6 +81,13 @@ namespace :assets do
desc "Compile all assets in debug mode" desc "Compile all assets in debug mode"
multitask :debug multitask :debug
desc "Preprocess all static assets that have the .mako extension"
task :preprocess do
# Run assets through the Mako templating engine. Right now we
# just hardcode the asset filenames.
preprocess_with_mako("lms/static/sass/application.scss.mako")
end
desc "Watch all assets for changes and automatically recompile" desc "Watch all assets for changes and automatically recompile"
task :watch => 'assets:_watch' do task :watch => 'assets:_watch' do
puts "Press ENTER to terminate".red puts "Press ENTER to terminate".red
...@@ -54,9 +96,9 @@ namespace :assets do ...@@ -54,9 +96,9 @@ namespace :assets do
{:xmodule => :install_python_prereqs, {:xmodule => :install_python_prereqs,
:coffee => :install_node_prereqs, :coffee => :install_node_prereqs,
:sass => :install_ruby_prereqs}.each_pair do |asset_type, prereq_task| :sass => [:install_ruby_prereqs, :preprocess]}.each_pair do |asset_type, prereq_tasks|
desc "Compile all #{asset_type} assets" desc "Compile all #{asset_type} assets"
task asset_type => prereq_task do task asset_type => prereq_tasks do
cmd = send(asset_type.to_s + "_cmd", watch=false, debug=false) cmd = send(asset_type.to_s + "_cmd", watch=false, debug=false)
if cmd.kind_of?(Array) if cmd.kind_of?(Array)
cmd.each {|c| sh(c)} cmd.each {|c| sh(c)}
...@@ -71,7 +113,7 @@ namespace :assets do ...@@ -71,7 +113,7 @@ namespace :assets do
namespace asset_type do namespace asset_type do
desc "Compile all #{asset_type} assets in debug mode" desc "Compile all #{asset_type} assets in debug mode"
task :debug => prereq_task do task :debug => prereq_tasks do
cmd = send(asset_type.to_s + "_cmd", watch=false, debug=true) cmd = send(asset_type.to_s + "_cmd", watch=false, debug=true)
sh(cmd) sh(cmd)
end end
...@@ -82,7 +124,7 @@ namespace :assets do ...@@ -82,7 +124,7 @@ namespace :assets do
$stdin.gets $stdin.gets
end end
task :_watch => prereq_task do task :_watch => prereq_tasks do
cmd = send(asset_type.to_s + "_cmd", watch=true, debug=true) cmd = send(asset_type.to_s + "_cmd", watch=true, debug=true)
if cmd.kind_of?(Array) if cmd.kind_of?(Array)
cmd.each {|c| background_process(c)} cmd.each {|c| background_process(c)}
......
...@@ -14,16 +14,31 @@ def report_dir_path(dir) ...@@ -14,16 +14,31 @@ def report_dir_path(dir)
return File.join(REPORT_DIR, dir.to_s) return File.join(REPORT_DIR, dir.to_s)
end end
def when_changed(unchanged_message, *files) def compute_fingerprint(files, dirs)
Rake::Task[PREREQS_MD5_DIR].invoke
cache_file = File.join(PREREQS_MD5_DIR, files.join('-').gsub(/\W+/, '-')) + '.md5'
digest = Digest::MD5.new() digest = Digest::MD5.new()
# Digest the contents of all the files.
Dir[*files].select{|file| File.file?(file)}.each do |file| Dir[*files].select{|file| File.file?(file)}.each do |file|
digest.file(file) digest.file(file)
end end
if !File.exists?(cache_file) or digest.hexdigest != File.read(cache_file)
# Digest the names of the files in all the dirs.
dirs.each do |dir|
file_names = Dir.entries(dir).sort.join(" ")
digest.update(file_names)
end
digest.hexdigest
end
# Hash the contents of all the files, and the names of files in the dirs.
# Run the block if they've changed.
def when_changed(unchanged_message, files, dirs=[])
Rake::Task[PREREQS_MD5_DIR].invoke
cache_file = File.join(PREREQS_MD5_DIR, files[0].gsub(/\W+/, '-').sub(/-+$/, '')) + '.md5'
if !File.exists?(cache_file) or compute_fingerprint(files, dirs) != File.read(cache_file)
yield yield
File.write(cache_file, digest.hexdigest) File.write(cache_file, compute_fingerprint(files, dirs))
elsif !unchanged_message.empty? elsif !unchanged_message.empty?
puts unchanged_message puts unchanged_message
end end
......
require './rakefiles/helpers.rb' require './rakefiles/helpers.rb'
PREREQS_MD5_DIR = ENV["PREREQ_CACHE_DIR"] || File.join(REPO_ROOT, '.prereqs_cache') PREREQS_MD5_DIR = ENV["PREREQ_CACHE_DIR"] || File.join(REPO_ROOT, '.prereqs_cache')
CLOBBER.include(PREREQS_MD5_DIR) CLOBBER.include(PREREQS_MD5_DIR)
...@@ -13,7 +12,7 @@ task :install_prereqs => [:install_node_prereqs, :install_ruby_prereqs, :install ...@@ -13,7 +12,7 @@ task :install_prereqs => [:install_node_prereqs, :install_ruby_prereqs, :install
desc "Install all node prerequisites for the lms and cms" desc "Install all node prerequisites for the lms and cms"
task :install_node_prereqs => "ws:migrate" do task :install_node_prereqs => "ws:migrate" do
unchanged = 'Node requirements unchanged, nothing to install' unchanged = 'Node requirements unchanged, nothing to install'
when_changed(unchanged, 'package.json') do when_changed(unchanged, ['package.json']) do
sh('npm install') sh('npm install')
end unless ENV['NO_PREREQ_INSTALL'] end unless ENV['NO_PREREQ_INSTALL']
end end
...@@ -21,20 +20,21 @@ end ...@@ -21,20 +20,21 @@ end
desc "Install all ruby prerequisites for the lms and cms" desc "Install all ruby prerequisites for the lms and cms"
task :install_ruby_prereqs => "ws:migrate" do task :install_ruby_prereqs => "ws:migrate" do
unchanged = 'Ruby requirements unchanged, nothing to install' unchanged = 'Ruby requirements unchanged, nothing to install'
when_changed(unchanged, 'Gemfile') do when_changed(unchanged, ['Gemfile']) do
sh('bundle install') sh('bundle install')
end unless ENV['NO_PREREQ_INSTALL'] end unless ENV['NO_PREREQ_INSTALL']
end end
desc "Install all python prerequisites for the lms and cms" desc "Install all python prerequisites for the lms and cms"
task :install_python_prereqs => "ws:migrate" do task :install_python_prereqs => "ws:migrate" do
site_packages_dir = `python -c 'import os; import distutils.sysconfig as dusc; print dusc.get_python_lib()'`.chomp
unchanged = 'Python requirements unchanged, nothing to install' unchanged = 'Python requirements unchanged, nothing to install'
when_changed(unchanged, 'requirements/**/*') do when_changed(unchanged, ['requirements/**/*'], [site_packages_dir]) do
ENV['PIP_DOWNLOAD_CACHE'] ||= '.pip_download_cache' ENV['PIP_DOWNLOAD_CACHE'] ||= '.pip_download_cache'
sh('pip install --exists-action w -r requirements/edx/base.txt') sh('pip install --exists-action w -r requirements/edx/base.txt')
sh('pip install --exists-action w -r requirements/edx/post.txt') sh('pip install --exists-action w -r requirements/edx/post.txt')
# Check for private-requirements.txt: used to install our libs as working dirs, # requirements/private.txt is used to install our libs as
# or personal-use tools. # working dirs, or for personal-use tools.
if File.file?("requirements/private.txt") if File.file?("requirements/private.txt")
sh('pip install -r requirements/private.txt') sh('pip install -r requirements/private.txt')
end end
......
def run_pylint(system, report_dir, flags='')
apps = Dir["#{system}", "#{system}/djangoapps/*", "#{system}/lib/*"].map do |app|
File.basename(app)
end.select do |app|
app !=~ /.pyc$/
end.map do |app|
if app =~ /.py$/
app.gsub('.py', '')
else
app
end
end
pythonpath_prefix = "PYTHONPATH=#{system}:#{system}/djangoapps:#{system}/lib:common/djangoapps:common/lib"
sh("#{pythonpath_prefix} pylint #{flags} -f parseable #{apps.join(' ')} | tee #{report_dir}/pylint.report")
end
[:lms, :cms, :common].each do |system| [:lms, :cms, :common].each do |system|
report_dir = report_dir_path(system) report_dir = report_dir_path(system)
...@@ -11,21 +28,18 @@ ...@@ -11,21 +28,18 @@
desc "Run pylint on all #{system} code" desc "Run pylint on all #{system} code"
task "pylint_#{system}" => [report_dir, :install_python_prereqs] do task "pylint_#{system}" => [report_dir, :install_python_prereqs] do
apps = Dir["#{system}/*.py", "#{system}/djangoapps/*", "#{system}/lib/*"].map do |app| run_pylint(system, report_dir)
File.basename(app) end
end.select do |app| namespace "pylint_#{system}" do
app !=~ /.pyc$/ desc "Run pylint checking for errors only, and aborting if there are any"
end.map do |app| task :errors do
if app =~ /.py$/ run_pylint(system, report_dir, '-E')
app.gsub('.py', '')
else
app
end
end end
pythonpath_prefix = "PYTHONPATH=#{system}:#{system}/djangoapps:#{system}/lib:common/djangoapps:common/lib"
sh("#{pythonpath_prefix} pylint --rcfile=.pylintrc -f parseable #{apps.join(' ')} | tee #{report_dir}/pylint.report")
end end
namespace :pylint do
task :errors => "pylint_#{system}:errors"
end
task :pylint => "pylint_#{system}" task :pylint => "pylint_#{system}"
end end
\ No newline at end of file
# Install these packages from the edx-platform working tree
# NOTE: if you change code in these packages, you MUST change the version
# number in its setup.py or the code WILL NOT be installed during deploy.
common/lib/calc
common/lib/chem
common/lib/sandbox-packages
# Packages to install in the Python sandbox for secured execution. # Packages to install in the Python sandbox for secured execution.
scipy==0.11.0 scipy==0.11.0
lxml==3.0.1 lxml==3.0.1
-e common/lib/calc
-e common/lib/chem
-e common/lib/sandbox-packages
...@@ -74,6 +74,7 @@ lettuce==0.2.16 ...@@ -74,6 +74,7 @@ lettuce==0.2.16
mock==0.8.0 mock==0.8.0
nosexcover==1.0.7 nosexcover==1.0.7
pep8==1.4.5 pep8==1.4.5
pylint==0.28
rednose==0.3 rednose==0.3
selenium==2.31.0 selenium==2.31.0
splinter==0.5.0 splinter==0.5.0
...@@ -81,7 +82,3 @@ django_nose==1.1 ...@@ -81,7 +82,3 @@ django_nose==1.1
django-jasmine==0.3.2 django-jasmine==0.3.2
django_debug_toolbar django_debug_toolbar
django-debug-toolbar-mongo django-debug-toolbar-mongo
# Install pylint from a specific commit on trunk
# to get the fix for this issue: http://www.logilab.org/ticket/122793
https://bitbucket.org/logilab/pylint/get/e828cb5.zip
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