Commit dcb95291 by Will Daly

Merge branch 'master' into feature/will/capa-template-tests

parents a28aed58 256a3677
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
:2e# :2e#
.AppleDouble .AppleDouble
database.sqlite database.sqlite
private-requirements.txt requirements/private.txt
courseware/static/js/mathjax/* courseware/static/js/mathjax/*
flushdb.sh flushdb.sh
build build
...@@ -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
Piotr Mitros <pmitros@edx.org>
Kyle Fiedler <kyle@kylefiedler.com>
Ernie Park <eipark@mit.edu>
Bridger Maxwell <bridger@mit.edu>
Lyla Fischer <lyla@edx.org>
David Ormsbee <dave@edx.org>
Chris Terman <cjt@edx.org>
Reda Lemeden <reda@thoughtbot.com>
Anant Agarwal <agarwal@edx.org>
Jean-Michel Claus <jmc@edx.org>
Calen Pennington <calen.pennington@gmail.com>
JM Van Thong <jm@edx.org>
Prem Sichanugrist <psichanugrist@thoughtbot.com>
Isaac Chuang <ichuang@mit.edu>
Galen Frechette <galen@thoughtbot.com>
Edward Loveall <edward@edwardloveall.com>
Matt Jankowski <mjankowski@thoughtbot.com>
John Jarvis <jarv@edx.org>
Victor Shnayder <victor@edx.org>
Matthew Mongeau <halogenandtoast@gmail.com>
Tony Kim <kimth@edx.org>
Arjun Singh <arjun810@gmail.com>
John Hess <mgojohn@gmail.com>
Carlos Andrés Rocha <rocha@edx.org>
Mike Chen <ccp0101@gmail.com>
Rocky Duan <dementrock@gmail.com>
Sidhanth Rao <sidhanth@mitx.mit.edu>
Brittany Cheng <bcheng42@gmail.com>
Dhaval Adjodah <dhaval@mit.edu>
Tom Giannattasio <tom@mitx.mit.edu>
Ibrahim Awwal <ibrahim.awwal@gmail.com>
Sarina Canelake <sarina@edx.org>
Mark L. Chang <mark.chang@gmail.com>
Dean Dieker <ddieker@gmail.com>
Tommy MacWilliam <tmacwilliam@cs.harvard.edu>
Nate Hardison <natehardison@gmail.com>
Chris Dodge <cdodge@edx.org>
Kevin Chugh <kevinchugh@edx.org>
Ned Batchelder <ned@nedbatchelder.com>
Alexander Kryklia <kryklia@gmail.com>
Vik Paruchuri <vik@edx.org>
Louis Sobel <sobel@edx.org>
Brian Wilson <brian@edx.org>
Ashley Penney <apenney@edx.org>
Don Mitchell <dmitchell@edx.org>
Aaron Culich <aculich@edx.org>
Brian Talbot <btalbot@edx.org>
Jay Zoldak <jzoldak@edx.org>
Valera Rozuvan <valera.rozuvan@gmail.com>
Diana Huang <dkh@edx.org>
Marco Morales <marcotuts@gmail.com>
Christina Roberts <christina@edx.org>
Robert Chirwa <robert@edx.org>
Ed Zarecor <ed@edx.org>
Deena Wang <thedeenawang@gmail.com>
Jean Manuel-Nater <jnater@edx.org>
Emily Zhang <1800.ehz.hang@gmail.com>
Jennifer Akana <jaakana@gmail.com>
Peter Baratta <peter.baratta@gmail.com>
Julian Arni <julian@edx.org>
Arthur Barrett <abarrett@edx.org>
Vasyl Nakvasiuk <vaxxxa@gmail.com>
Will Daly <will@edx.org>
James Tauber <jtauber@jtauber.com>
Greg Price <gprice@edx.org>
Joe Blaylock <jrbl@stanford.edu>
Sef Kloninger <sef@kloninger.com>
Anto Stupak <s2pak.anton@gmail.com>
David Adams <dcadams@stanford.edu>
Steve Strassmann <straz@edx.org>
Giulio Gratta <giulio@giuliogratta.com>
David Baumgold <david@davidbaumgold.com>
Jason Bau <jbau@stanford.edu>
Frances Botsford <frances@edx.org>
...@@ -77,8 +77,8 @@ environment), and Node has a library installer called ...@@ -77,8 +77,8 @@ environment), and Node has a library installer called
Once you've got your languages and virtual environments set up, install Once you've got your languages and virtual environments set up, install
the libraries like so: the libraries like so:
$ pip install -r requirements/base.txt $ pip install -r requirements/edx/base.txt
$ pip install -r requirements/post.txt $ pip install -r requirements/edx/post.txt
$ bundle install $ bundle install
$ npm install $ npm install
......
...@@ -11,7 +11,8 @@ Feature: Advanced (manual) course policy ...@@ -11,7 +11,8 @@ Feature: Advanced (manual) course policy
Given I am on the Advanced Course Settings page in Studio Given I am on the Advanced Course Settings page in Studio
Then the settings are alphabetized Then the settings are alphabetized
@skip-phantom # Skipped because Ubuntu ChromeDriver cannot click notification "Cancel"
@skip
Scenario: Test cancel editing key value Scenario: Test cancel editing key value
Given I am on the Advanced Course Settings page in Studio Given I am on the Advanced Course Settings page in Studio
When I edit the value of a policy key When I edit the value of a policy key
...@@ -20,7 +21,8 @@ Feature: Advanced (manual) course policy ...@@ -20,7 +21,8 @@ Feature: Advanced (manual) course policy
And I reload the page And I reload the page
Then the policy key value is unchanged Then the policy key value is unchanged
@skip-phantom # Skipped because Ubuntu ChromeDriver cannot click notification "Save"
@skip
Scenario: Test editing key value Scenario: Test editing key value
Given I am on the Advanced Course Settings page in Studio Given I am on the Advanced Course Settings page in Studio
When I edit the value of a policy key and save When I edit the value of a policy key and save
...@@ -28,7 +30,8 @@ Feature: Advanced (manual) course policy ...@@ -28,7 +30,8 @@ Feature: Advanced (manual) course policy
And I reload the page And I reload the page
Then the policy key value is changed Then the policy key value is changed
@skip-phantom # Skipped because Ubuntu ChromeDriver cannot edit CodeMirror input
@skip
Scenario: Test how multi-line input appears Scenario: Test how multi-line input appears
Given I am on the Advanced Course Settings page in Studio Given I am on the Advanced Course Settings page in Studio
When I create a JSON object as a value When I create a JSON object as a value
...@@ -36,7 +39,8 @@ Feature: Advanced (manual) course policy ...@@ -36,7 +39,8 @@ Feature: Advanced (manual) course policy
And I reload the page And I reload the page
Then it is displayed as formatted Then it is displayed as formatted
@skip-phantom # Skipped because Ubuntu ChromeDriver cannot edit CodeMirror input
@skip
Scenario: Test automatic quoting of non-JSON values Scenario: Test automatic quoting of non-JSON values
Given I am on the Advanced Course Settings page in Studio Given I am on the Advanced Course Settings page in Studio
When I create a non-JSON value not in quotes When I create a non-JSON value not in quotes
......
...@@ -19,9 +19,7 @@ DISPLAY_NAME_VALUE = '"Robot Super Course"' ...@@ -19,9 +19,7 @@ DISPLAY_NAME_VALUE = '"Robot Super Course"'
############### ACTIONS #################### ############### ACTIONS ####################
@step('I select the Advanced Settings$') @step('I select the Advanced Settings$')
def i_select_advanced_settings(step): def i_select_advanced_settings(step):
expand_icon_css = 'li.nav-course-settings i.icon-expand' world.click_course_settings()
if world.browser.is_element_present_by_css(expand_icon_css):
world.css_click(expand_icon_css)
link_css = 'li.nav-course-settings-advanced a' link_css = 'li.nav-course-settings-advanced a'
world.css_click(link_css) world.css_click(link_css)
......
...@@ -10,8 +10,6 @@ Feature: Course checklists ...@@ -10,8 +10,6 @@ Feature: Course checklists
Then I can check and uncheck tasks in a checklist Then I can check and uncheck tasks in a checklist
And They are correctly selected after I reload the page And They are correctly selected after I reload the page
@skip-phantom
@skip-firefox
Scenario: A task can link to a location within Studio Scenario: A task can link to a location within Studio
Given I have opened Checklists Given I have opened Checklists
When I select a link to the course outline When I select a link to the course outline
...@@ -19,8 +17,6 @@ Feature: Course checklists ...@@ -19,8 +17,6 @@ Feature: Course checklists
And I press the browser back button And I press the browser back button
Then I am brought back to the course outline in the correct state Then I am brought back to the course outline in the correct state
@skip-phantom
@skip-firefox
Scenario: A task can link to a location outside Studio Scenario: A task can link to a location outside Studio
Given I have opened Checklists Given I have opened Checklists
When I select a link to help page When I select a link to help page
......
...@@ -10,9 +10,7 @@ from selenium.common.exceptions import StaleElementReferenceException ...@@ -10,9 +10,7 @@ from selenium.common.exceptions import StaleElementReferenceException
############### ACTIONS #################### ############### ACTIONS ####################
@step('I select Checklists from the Tools menu$') @step('I select Checklists from the Tools menu$')
def i_select_checklists(step): def i_select_checklists(step):
expand_icon_css = 'li.nav-course-tools i.icon-expand' world.click_tools()
if world.browser.is_element_present_by_css(expand_icon_css):
world.css_click(expand_icon_css)
link_css = 'li.nav-course-tools-checklists a' link_css = 'li.nav-course-tools-checklists a'
world.css_click(link_css) world.css_click(link_css)
......
Feature: Course Settings Feature: Course Settings
As a course author, I want to be able to configure my course settings. As a course author, I want to be able to configure my course settings.
@skip-phantom
Scenario: User can set course dates Scenario: User can set course dates
Given I have opened a new course in Studio Given I have opened a new course in Studio
When I select Schedule and Details When I select Schedule and Details
And I set course dates And I set course dates
Then I see the set dates on refresh Then I see the set dates on refresh
@skip-phantom
Scenario: User can clear previously set course dates (except start date) Scenario: User can clear previously set course dates (except start date)
Given I have set course dates Given I have set course dates
And I clear all the dates except start And I clear all the dates except start
Then I see cleared dates on refresh Then I see cleared dates on refresh
@skip-phantom
Scenario: User cannot clear the course start date Scenario: User cannot clear the course start date
Given I have set course dates Given I have set course dates
And I clear the course start date And I clear the course start date
......
...@@ -25,9 +25,7 @@ DEFAULT_TIME = "00:00" ...@@ -25,9 +25,7 @@ DEFAULT_TIME = "00:00"
############### ACTIONS #################### ############### ACTIONS ####################
@step('I select Schedule and Details$') @step('I select Schedule and Details$')
def test_i_select_schedule_and_details(step): def test_i_select_schedule_and_details(step):
expand_icon_css = 'li.nav-course-settings i.icon-expand' world.click_course_settings()
if world.browser.is_element_present_by_css(expand_icon_css):
world.css_click(expand_icon_css)
link_css = 'li.nav-course-settings-schedule a' link_css = 'li.nav-course-settings-schedule a'
world.css_click(link_css) world.css_click(link_css)
......
...@@ -62,4 +62,4 @@ def i_am_on_tab(step, tab_name): ...@@ -62,4 +62,4 @@ def i_am_on_tab(step, tab_name):
@step('I see a link for adding a new section$') @step('I see a link for adding a new section$')
def i_see_new_section_link(step): def i_see_new_section_link(step):
link_css = 'a.new-courseware-section-button' link_css = 'a.new-courseware-section-button'
assert world.css_has_text(link_css, '+ New Section') assert world.css_has_text(link_css, 'New Section')
...@@ -3,7 +3,6 @@ Feature: Create Section ...@@ -3,7 +3,6 @@ Feature: Create Section
As a course author As a course author
I want to create and edit sections I want to create and edit sections
@skip-phantom
Scenario: Add a new section to a course Scenario: Add a new section to a course
Given I have opened a new course in Studio Given I have opened a new course in Studio
When I click the New Section link When I click the New Section link
...@@ -27,7 +26,8 @@ Feature: Create Section ...@@ -27,7 +26,8 @@ Feature: Create Section
And I save a new section release date And I save a new section release date
Then the section release date is updated Then the section release date is updated
@skip-phantom # Skipped because Ubuntu ChromeDriver hangs on alert
@skip
Scenario: Delete section Scenario: Delete section
Given I have opened a new course in Studio Given I have opened a new course in Studio
And I have added a new section And I have added a new section
......
...@@ -62,7 +62,7 @@ def i_click_to_edit_section_name(step): ...@@ -62,7 +62,7 @@ def i_click_to_edit_section_name(step):
@step('I see the complete section name with a quote in the editor$') @step('I see the complete section name with a quote in the editor$')
def i_see_complete_section_name_with_quote_in_editor(step): def i_see_complete_section_name_with_quote_in_editor(step):
css = '.edit-section-name' css = '.section-name-edit input[type=text]'
assert world.is_css_present(css) assert world.is_css_present(css)
assert_equal(world.browser.find_by_css(css).value, 'Section with "Quote"') assert_equal(world.browser.find_by_css(css).value, 'Section with "Quote"')
......
...@@ -18,10 +18,7 @@ def i_fill_in_the_registration_form(step): ...@@ -18,10 +18,7 @@ def i_fill_in_the_registration_form(step):
@step('I press the Create My Account button on the registration form$') @step('I press the Create My Account button on the registration form$')
def i_press_the_button_on_the_registration_form(step): def i_press_the_button_on_the_registration_form(step):
submit_css = 'form#register_form button#submit' submit_css = 'form#register_form button#submit'
# Workaround for click not working on ubuntu world.css_click(submit_css)
# for some unknown reason.
e = world.css_find(submit_css)
e.type(' ')
@step('I should see be on the studio home page$') @step('I should see be on the studio home page$')
......
...@@ -14,7 +14,6 @@ Feature: Overview Toggle Section ...@@ -14,7 +14,6 @@ Feature: Overview Toggle Section
When I navigate to the course overview page When I navigate to the course overview page
Then I do not see the "Collapse All Sections" link Then I do not see the "Collapse All Sections" link
@skip-phantom
Scenario: Collapse link appears after creating first section of a course Scenario: Collapse link appears after creating first section of a course
Given I have a course with no sections Given I have a course with no sections
When I navigate to the course overview page When I navigate to the course overview page
...@@ -22,7 +21,8 @@ Feature: Overview Toggle Section ...@@ -22,7 +21,8 @@ Feature: Overview Toggle Section
Then I see the "Collapse All Sections" link Then I see the "Collapse All Sections" link
And all sections are expanded And all sections are expanded
@skip-phantom # Skipped because Ubuntu ChromeDriver hangs on alert
@skip
Scenario: Collapse link is not removed after last section of a course is deleted Scenario: Collapse link is not removed after last section of a course is deleted
Given I have a course with 1 section Given I have a course with 1 section
And I navigate to the course overview page And I navigate to the course overview page
......
...@@ -3,14 +3,12 @@ Feature: Create Subsection ...@@ -3,14 +3,12 @@ Feature: Create Subsection
As a course author As a course author
I want to create and edit subsections I want to create and edit subsections
@skip-phantom
Scenario: Add a new subsection to a section Scenario: Add a new subsection to a section
Given I have opened a new course section in Studio Given I have opened a new course section in Studio
When I click the New Subsection link When I click the New Subsection link
And I enter the subsection name and click save And I enter the subsection name and click save
Then I see my subsection on the Courseware page Then I see my subsection on the Courseware page
@skip-phantom
Scenario: Add a new subsection (with a name containing a quote) to a section (bug #216) Scenario: Add a new subsection (with a name containing a quote) to a section (bug #216)
Given I have opened a new course section in Studio Given I have opened a new course section in Studio
When I click the New Subsection link When I click the New Subsection link
...@@ -27,7 +25,6 @@ Feature: Create Subsection ...@@ -27,7 +25,6 @@ Feature: Create Subsection
And I reload the page And I reload the page
Then I see it marked as Homework Then I see it marked as Homework
@skip-phantom
Scenario: Set a due date in a different year (bug #256) Scenario: Set a due date in a different year (bug #256)
Given I have opened a new subsection in Studio Given I have opened a new subsection in Studio
And I have set a release date and due date in different years And I have set a release date and due date in different years
...@@ -35,7 +32,8 @@ Feature: Create Subsection ...@@ -35,7 +32,8 @@ Feature: Create Subsection
And I reload the page And I reload the page
Then I see the correct dates Then I see the correct dates
@skip-phantom # Skipped because Ubuntu ChromeDriver hangs on alert
@skip
Scenario: Delete a subsection Scenario: Delete a subsection
Given I have opened a new course section in Studio Given I have opened a new course section in Studio
And I have added a new subsection And I have added a new subsection
......
...@@ -63,14 +63,6 @@ def test_have_set_dates_in_different_years(step): ...@@ -63,14 +63,6 @@ def test_have_set_dates_in_different_years(step):
set_date_and_time('input#due_date', '01/02/2012', 'input#due_time', '04:00') set_date_and_time('input#due_date', '01/02/2012', 'input#due_time', '04:00')
@step('I see the correct dates$')
def i_see_the_correct_dates(step):
assert_equal('12/25/2011', world.css_find('input#start_date').first.value)
assert_equal('03:00', world.css_find('input#start_time').first.value)
assert_equal('01/02/2012', world.css_find('input#due_date').first.value)
assert_equal('04:00', world.css_find('input#due_time').first.value)
@step('I mark it as Homework$') @step('I mark it as Homework$')
def i_mark_it_as_homework(step): def i_mark_it_as_homework(step):
world.css_click('a.menu-toggle') world.css_click('a.menu-toggle')
...@@ -101,8 +93,20 @@ def the_subsection_does_not_exist(step): ...@@ -101,8 +93,20 @@ def the_subsection_does_not_exist(step):
assert world.browser.is_element_not_present_by_css(css) assert world.browser.is_element_not_present_by_css(css)
@step('I see the correct dates$')
def i_see_the_correct_dates(step):
assert_equal('12/25/2011', get_date('input#start_date'))
assert_equal('03:00', get_date('input#start_time'))
assert_equal('01/02/2012', get_date('input#due_date'))
assert_equal('04:00', get_date('input#due_time'))
############ HELPER METHODS ################### ############ HELPER METHODS ###################
def get_date(css):
return world.css_find(css).first.value.strip()
def save_subsection_name(name): def save_subsection_name(name):
name_css = 'input.new-subsection-name-input' name_css = 'input.new-subsection-name-input'
save_css = 'input.new-subsection-name-save' save_css = 'input.new-subsection-name-save'
......
...@@ -17,7 +17,6 @@ from xmodule.modulestore.tests.factories import CourseFactory ...@@ -17,7 +17,6 @@ from xmodule.modulestore.tests.factories import CourseFactory
from models.settings.course_metadata import CourseMetadata from models.settings.course_metadata import CourseMetadata
from xmodule.modulestore.xml_importer import import_from_xml from xmodule.modulestore.xml_importer import import_from_xml
from xmodule.modulestore.django import modulestore
from xmodule.fields import Date from xmodule.fields import Date
...@@ -256,7 +255,7 @@ class CourseMetadataEditingTest(CourseTestCase): ...@@ -256,7 +255,7 @@ class CourseMetadataEditingTest(CourseTestCase):
def setUp(self): def setUp(self):
CourseTestCase.setUp(self) CourseTestCase.setUp(self)
# add in the full class too # add in the full class too
import_from_xml(modulestore(), 'common/test/data/', ['full']) import_from_xml(get_modulestore(self.course_location), 'common/test/data/', ['full'])
self.fullcourse_location = Location(['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None]) self.fullcourse_location = Location(['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None])
def test_fetch_initial_fields(self): def test_fetch_initial_fields(self):
......
from contentstore.utils import get_modulestore, get_url_reverse
from contentstore.tests.test_course_settings import CourseTestCase
from xmodule.modulestore.tests.factories import CourseFactory
from django.core.urlresolvers import reverse
class DeleteItem(CourseTestCase):
def setUp(self):
""" Creates the test course with a static page in it. """
super(DeleteItem, self).setUp()
self.course = CourseFactory.create(org='mitX', number='333', display_name='Dummy Course')
def testDeleteStaticPage(self):
# Add static tab
data = {
'parent_location': 'i4x://mitX/333/course/Dummy_Course',
'template': 'i4x://edx/templates/static_tab/Empty'
}
resp = self.client.post(reverse('clone_item'), data)
self.assertEqual(resp.status_code, 200)
# Now delete it. There was a bug that the delete was failing (static tabs do not exist in draft modulestore).
resp = self.client.post(reverse('delete_item'), resp.content, "application/json")
self.assertEqual(resp.status_code, 200)
""" Tests for utils. """ """ Tests for utils. """
from contentstore import utils from contentstore import utils
import mock import mock
import collections
import copy
from django.test import TestCase from django.test import TestCase
from xmodule.modulestore.tests.factories import CourseFactory from xmodule.modulestore.tests.factories import CourseFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
...@@ -70,3 +72,79 @@ class UrlReverseTestCase(ModuleStoreTestCase): ...@@ -70,3 +72,79 @@ class UrlReverseTestCase(ModuleStoreTestCase):
'https://edge.edx.org/courses/edX/edX101/How_to_Create_an_edX_Course/about', 'https://edge.edx.org/courses/edX/edX101/How_to_Create_an_edX_Course/about',
utils.get_url_reverse('https://edge.edx.org/courses/edX/edX101/How_to_Create_an_edX_Course/about', course) utils.get_url_reverse('https://edge.edx.org/courses/edX/edX101/How_to_Create_an_edX_Course/about', course)
) )
class ExtraPanelTabTestCase(TestCase):
""" Tests adding and removing extra course tabs. """
def get_tab_type_dicts(self, tab_types):
""" Returns an array of tab dictionaries. """
if tab_types:
return [{'tab_type': tab_type} for tab_type in tab_types.split(',')]
else:
return []
def get_course_with_tabs(self, tabs=[]):
""" Returns a mock course object with a tabs attribute. """
course = collections.namedtuple('MockCourse', ['tabs'])
if isinstance(tabs, basestring):
course.tabs = self.get_tab_type_dicts(tabs)
else:
course.tabs = tabs
return course
def test_add_extra_panel_tab(self):
""" Tests if a tab can be added to a course tab list. """
for tab_type in utils.EXTRA_TAB_PANELS.keys():
tab = utils.EXTRA_TAB_PANELS.get(tab_type)
# test adding with changed = True
for tab_setup in ['', 'x', 'x,y,z']:
course = self.get_course_with_tabs(tab_setup)
expected_tabs = copy.copy(course.tabs)
expected_tabs.append(tab)
changed, actual_tabs = utils.add_extra_panel_tab(tab_type, course)
self.assertTrue(changed)
self.assertEqual(actual_tabs, expected_tabs)
# test adding with changed = False
tab_test_setup = [
[tab],
[tab, self.get_tab_type_dicts('x,y,z')],
[self.get_tab_type_dicts('x,y'), tab, self.get_tab_type_dicts('z')],
[self.get_tab_type_dicts('x,y,z'), tab]]
for tab_setup in tab_test_setup:
course = self.get_course_with_tabs(tab_setup)
expected_tabs = copy.copy(course.tabs)
changed, actual_tabs = utils.add_extra_panel_tab(tab_type, course)
self.assertFalse(changed)
self.assertEqual(actual_tabs, expected_tabs)
def test_remove_extra_panel_tab(self):
""" Tests if a tab can be removed from a course tab list. """
for tab_type in utils.EXTRA_TAB_PANELS.keys():
tab = utils.EXTRA_TAB_PANELS.get(tab_type)
# test removing with changed = True
tab_test_setup = [
[tab],
[tab, self.get_tab_type_dicts('x,y,z')],
[self.get_tab_type_dicts('x,y'), tab, self.get_tab_type_dicts('z')],
[self.get_tab_type_dicts('x,y,z'), tab]]
for tab_setup in tab_test_setup:
course = self.get_course_with_tabs(tab_setup)
expected_tabs = [t for t in course.tabs if t != utils.EXTRA_TAB_PANELS.get(tab_type)]
changed, actual_tabs = utils.remove_extra_panel_tab(tab_type, course)
self.assertTrue(changed)
self.assertEqual(actual_tabs, expected_tabs)
# test removing with changed = False
for tab_setup in ['', 'x', 'x,y,z']:
course = self.get_course_with_tabs(tab_setup)
expected_tabs = copy.copy(course.tabs)
changed, actual_tabs = utils.remove_extra_panel_tab(tab_type, course)
self.assertFalse(changed)
self.assertEqual(actual_tabs, expected_tabs)
...@@ -9,6 +9,8 @@ DIRECT_ONLY_CATEGORIES = ['course', 'chapter', 'sequential', 'about', 'static_ta ...@@ -9,6 +9,8 @@ DIRECT_ONLY_CATEGORIES = ['course', 'chapter', 'sequential', 'about', 'static_ta
#In order to instantiate an open ended tab automatically, need to have this data #In order to instantiate an open ended tab automatically, need to have this data
OPEN_ENDED_PANEL = {"name": "Open Ended Panel", "type": "open_ended"} OPEN_ENDED_PANEL = {"name": "Open Ended Panel", "type": "open_ended"}
NOTES_PANEL = {"name": "My Notes", "type": "notes"}
EXTRA_TAB_PANELS = dict([(p['type'], p) for p in [OPEN_ENDED_PANEL, NOTES_PANEL]])
def get_modulestore(location): def get_modulestore(location):
...@@ -192,9 +194,10 @@ class CoursePageNames: ...@@ -192,9 +194,10 @@ class CoursePageNames:
Checklists = "checklists" Checklists = "checklists"
def add_open_ended_panel_tab(course): def add_extra_panel_tab(tab_type, course):
""" """
Used to add the open ended panel tab to a course if it does not exist. Used to add the panel tab to a course if it does not exist.
@param tab_type: A string representing the tab type.
@param course: A course object from the modulestore. @param course: A course object from the modulestore.
@return: Boolean indicating whether or not a tab was added and a list of tabs for the course. @return: Boolean indicating whether or not a tab was added and a list of tabs for the course.
""" """
...@@ -202,16 +205,19 @@ def add_open_ended_panel_tab(course): ...@@ -202,16 +205,19 @@ def add_open_ended_panel_tab(course):
course_tabs = copy.copy(course.tabs) course_tabs = copy.copy(course.tabs)
changed = False changed = False
#Check to see if open ended panel is defined in the course #Check to see if open ended panel is defined in the course
if OPEN_ENDED_PANEL not in course_tabs:
tab_panel = EXTRA_TAB_PANELS.get(tab_type)
if tab_panel not in course_tabs:
#Add panel to the tabs if it is not defined #Add panel to the tabs if it is not defined
course_tabs.append(OPEN_ENDED_PANEL) course_tabs.append(tab_panel)
changed = True changed = True
return changed, course_tabs return changed, course_tabs
def remove_open_ended_panel_tab(course): def remove_extra_panel_tab(tab_type, course):
""" """
Used to remove the open ended panel tab from a course if it exists. Used to remove the panel tab from a course if it exists.
@param tab_type: A string representing the tab type.
@param course: A course object from the modulestore. @param course: A course object from the modulestore.
@return: Boolean indicating whether or not a tab was added and a list of tabs for the course. @return: Boolean indicating whether or not a tab was added and a list of tabs for the course.
""" """
...@@ -219,8 +225,10 @@ def remove_open_ended_panel_tab(course): ...@@ -219,8 +225,10 @@ def remove_open_ended_panel_tab(course):
course_tabs = copy.copy(course.tabs) course_tabs = copy.copy(course.tabs)
changed = False changed = False
#Check to see if open ended panel is defined in the course #Check to see if open ended panel is defined in the course
if OPEN_ENDED_PANEL in course_tabs:
tab_panel = EXTRA_TAB_PANELS.get(tab_type)
if tab_panel in course_tabs:
#Add panel to the tabs if it is not defined #Add panel to the tabs if it is not defined
course_tabs = [ct for ct in course_tabs if ct != OPEN_ENDED_PANEL] course_tabs = [ct for ct in course_tabs if ct != tab_panel]
changed = True changed = True
return changed, course_tabs return changed, course_tabs
...@@ -41,7 +41,8 @@ log = logging.getLogger(__name__) ...@@ -41,7 +41,8 @@ log = logging.getLogger(__name__)
COMPONENT_TYPES = ['customtag', 'discussion', 'html', 'problem', 'video'] COMPONENT_TYPES = ['customtag', 'discussion', 'html', 'problem', 'video']
OPEN_ENDED_COMPONENT_TYPES = ["combinedopenended", "peergrading"] OPEN_ENDED_COMPONENT_TYPES = ["combinedopenended", "peergrading"]
ADVANCED_COMPONENT_TYPES = ['annotatable', 'word_cloud'] + OPEN_ENDED_COMPONENT_TYPES NOTE_COMPONENT_TYPES = ['notes']
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'
......
...@@ -13,17 +13,13 @@ from django.core.urlresolvers import reverse ...@@ -13,17 +13,13 @@ from django.core.urlresolvers import reverse
from mitxmako.shortcuts import render_to_response from mitxmako.shortcuts import render_to_response
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.modulestore.exceptions \
import ItemNotFoundError, InvalidLocationError from xmodule.modulestore.exceptions import ItemNotFoundError, InvalidLocationError
from xmodule.modulestore import Location from xmodule.modulestore import Location
from contentstore.course_info_model \ from contentstore.course_info_model import get_course_updates, update_course_updates, delete_course_update
import get_course_updates, update_course_updates, delete_course_update from contentstore.utils import get_lms_link_for_item, add_extra_panel_tab, remove_extra_panel_tab
from contentstore.utils \ from models.settings.course_details import CourseDetails, CourseSettingsEncoder
import get_lms_link_for_item, add_open_ended_panel_tab, \
remove_open_ended_panel_tab
from models.settings.course_details \
import CourseDetails, CourseSettingsEncoder
from models.settings.course_grading import CourseGradingModel from models.settings.course_grading import CourseGradingModel
from models.settings.course_metadata import CourseMetadata from models.settings.course_metadata import CourseMetadata
from auth.authz import create_all_course_groups from auth.authz import create_all_course_groups
...@@ -32,7 +28,12 @@ from util.json_request import expect_json ...@@ -32,7 +28,12 @@ from util.json_request import expect_json
from .access import has_access, get_location_and_verify_access from .access import has_access, get_location_and_verify_access
from .requests import get_request_method from .requests import get_request_method
from .tabs import initialize_course_tabs from .tabs import initialize_course_tabs
from .component import OPEN_ENDED_COMPONENT_TYPES, ADVANCED_COMPONENT_POLICY_KEY from .component import OPEN_ENDED_COMPONENT_TYPES, \
NOTE_COMPONENT_TYPES, ADVANCED_COMPONENT_POLICY_KEY
from django_comment_common.utils import seed_permissions_roles
# TODO: should explicitly enumerate exports with __all__
__all__ = ['course_index', 'create_new_course', 'course_info', __all__ = ['course_index', 'create_new_course', 'course_info',
'course_info_updates', 'get_course_settings', 'course_info_updates', 'get_course_settings',
...@@ -135,6 +136,9 @@ def create_new_course(request): ...@@ -135,6 +136,9 @@ def create_new_course(request):
create_all_course_groups(request.user, new_course.location) create_all_course_groups(request.user, new_course.location)
# seed the forums
seed_permissions_roles(new_course.location.course_id)
return HttpResponse(json.dumps({'id': new_course.location.url()})) return HttpResponse(json.dumps({'id': new_course.location.url()}))
...@@ -352,38 +356,52 @@ def course_advanced_updates(request, org, course, name): ...@@ -352,38 +356,52 @@ def course_advanced_updates(request, org, course, name):
request_body = json.loads(request.body) request_body = json.loads(request.body)
# Whether or not to filter the tabs key out of the settings metadata # Whether or not to filter the tabs key out of the settings metadata
filter_tabs = True filter_tabs = True
# Check to see if the user instantiated any advanced components.
# This is a hack to add the open ended panel tab #Check to see if the user instantiated any advanced components. This is a hack
# to a course automatically if the user has indicated that they want #that does the following :
# to edit the combinedopenended or peergrading # 1) adds/removes the open ended panel tab to a course automatically if the user
# module, and to remove it if they have removed the open ended elements. # has indicated that they want to edit the combinedopendended or peergrading module
# 2) adds/removes the notes panel tab to a course automatically if the user has
# indicated that they want the notes module enabled in their course
# TODO refactor the above into distinct advanced policy settings
if ADVANCED_COMPONENT_POLICY_KEY in request_body: if ADVANCED_COMPONENT_POLICY_KEY in request_body:
# Check to see if the user instantiated any open ended components #Get the course so that we can scrape current tabs
found_oe_type = False
# Get the course so that we can scrape current tabs
course_module = modulestore().get_item(location) course_module = modulestore().get_item(location)
for oe_type in OPEN_ENDED_COMPONENT_TYPES:
if oe_type in request_body[ADVANCED_COMPONENT_POLICY_KEY]: #Maps tab types to components
# Add an open ended tab to the course if needed tab_component_map = {
changed, new_tabs = add_open_ended_panel_tab(course_module) 'open_ended': OPEN_ENDED_COMPONENT_TYPES,
# If a tab has been added to the course, then send the 'notes': NOTE_COMPONENT_TYPES,
# metadata along to CourseMetadata.update_from_json }
#Check to see if the user instantiated any notes or open ended components
for tab_type in tab_component_map.keys():
component_types = tab_component_map.get(tab_type)
found_ac_type = False
for ac_type in component_types:
if ac_type in request_body[ADVANCED_COMPONENT_POLICY_KEY]:
#Add tab to the course if needed
changed, new_tabs = add_extra_panel_tab(tab_type, course_module)
#If a tab has been added to the course, then send the metadata along to CourseMetadata.update_from_json
if changed:
course_module.tabs = new_tabs
request_body.update({'tabs': new_tabs})
#Indicate that tabs should not be filtered out of the metadata
filter_tabs = False
#Set this flag to avoid the tab removal code below.
found_ac_type = True
break
#If we did not find a module type in the advanced settings,
# we may need to remove the tab from the course.
if not found_ac_type:
#Remove tab from the course if needed
changed, new_tabs = remove_extra_panel_tab(tab_type, course_module)
if changed: if changed:
course_module.tabs = new_tabs
request_body.update({'tabs': new_tabs}) request_body.update({'tabs': new_tabs})
# Indicate that tabs should not be filtered out of the metadata #Indicate that tabs should *not* be filtered out of the metadata
filter_tabs = False filter_tabs = False
# Set this flag to avoid the open ended tab removal code below.
found_oe_type = True
break
# If we did not find an open ended module type in the advanced settings,
# we may need to remove the open ended tab from the course.
if not found_oe_type:
# Remove open ended tab to the course if needed
changed, new_tabs = remove_open_ended_panel_tab(course_module)
if changed:
request_body.update({'tabs': new_tabs})
# Indicate that tabs should not be filtered out of the metadata
filter_tabs = False
response_json = json.dumps(CourseMetadata.update_from_json(location, response_json = json.dumps(CourseMetadata.update_from_json(location,
request_body, request_body,
filter_tabs=filter_tabs)) filter_tabs=filter_tabs))
......
...@@ -113,7 +113,7 @@ def delete_item(request): ...@@ -113,7 +113,7 @@ def delete_item(request):
delete_children = request.POST.get('delete_children', False) delete_children = request.POST.get('delete_children', False)
delete_all_versions = request.POST.get('delete_all_versions', False) delete_all_versions = request.POST.get('delete_all_versions', False)
store = modulestore() store = get_modulestore(item_location)
item = store.get_item(item_location) item = store.get_item(item_location)
......
...@@ -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,33 +2,52 @@ ...@@ -2,33 +2,52 @@
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,
# otherwise the browser will not render the pages correctly # otherwise the browser will not render the pages correctly
DEBUG = True DEBUG = True
# Show the courses that are in the data directory # Disable warnings for acceptance tests, to make the logs readable
COURSES_ROOT = ENV_ROOT / "data" import logging
DATA_DIR = COURSES_ROOT logging.disable(logging.ERROR)
# MODULESTORE = {
# 'default': { MODULESTORE_OPTIONS = {
# 'ENGINE': 'xmodule.modulestore.xml.XMLModuleStore', 'default_class': 'xmodule.raw_module.RawDescriptor',
# 'OPTIONS': { 'host': 'localhost',
# 'data_dir': DATA_DIR, 'db': 'test_xmodule',
# 'default_class': 'xmodule.hidden_module.HiddenDescriptor', 'collection': 'acceptance_modulestore',
# } 'fs_root': TEST_ROOT / "data",
# } 'render_template': 'mitxmako.shortcuts.render_to_string',
# } }
MODULESTORE = {
'default': {
'ENGINE': 'xmodule.modulestore.mongo.DraftMongoModuleStore',
'OPTIONS': MODULESTORE_OPTIONS
},
'direct': {
'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore',
'OPTIONS': MODULESTORE_OPTIONS
},
'draft': {
'ENGINE': 'xmodule.modulestore.mongo.DraftMongoModuleStore',
'OPTIONS': MODULESTORE_OPTIONS
}
}
# Set this up so that rake lms[acceptance] and running the # Set this up so that rake lms[acceptance] and running the
# harvest command both use the same (test) database # harvest command both use the same (test) database
# which they can flush without messing up your dev db # which they can flush without messing up your dev db
DATABASES = { DATABASES = {
'default': { 'default': {
'ENGINE': 'django.db.backends.sqlite3', 'ENGINE': 'django.db.backends.sqlite3',
'NAME': ENV_ROOT / "db" / "test_mitx.db", 'NAME': TEST_ROOT / "db" / "test_mitx.db",
'TEST_NAME': ENV_ROOT / "db" / "test_mitx.db", 'TEST_NAME': TEST_ROOT / "db" / "test_mitx.db",
} }
} }
......
""" """
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 *
...@@ -28,6 +33,48 @@ EMAIL_BACKEND = 'django_ses.SESBackend' ...@@ -28,6 +33,48 @@ EMAIL_BACKEND = 'django_ses.SESBackend'
SESSION_ENGINE = 'django.contrib.sessions.backends.cache' SESSION_ENGINE = 'django.contrib.sessions.backends.cache'
DEFAULT_FILE_STORAGE = 'storages.backends.s3boto.S3BotoStorage' DEFAULT_FILE_STORAGE = 'storages.backends.s3boto.S3BotoStorage'
###################################### CELERY ################################
# Don't use a connection pool, since connections are dropped by ELB.
BROKER_POOL_LIMIT = 0
BROKER_CONNECTION_TIMEOUT = 1
# For the Result Store, use the django cache named 'celery'
CELERY_RESULT_BACKEND = 'cache'
CELERY_CACHE_BACKEND = 'celery'
# When the broker is behind an ELB, use a heartbeat to refresh the
# connection and to detect if it has been dropped.
BROKER_HEARTBEAT = 10.0
BROKER_HEARTBEAT_CHECKRATE = 2
# Each worker should only fetch one message at a time
CELERYD_PREFETCH_MULTIPLIER = 1
# Skip djcelery migrations, since we don't use the database as the broker
SOUTH_MIGRATION_MODULES = {
'djcelery': 'ignore',
}
# Rename the exchange and queues for each variant
QUEUE_VARIANT = CONFIG_PREFIX.lower()
CELERY_DEFAULT_EXCHANGE = 'edx.{0}core'.format(QUEUE_VARIANT)
HIGH_PRIORITY_QUEUE = 'edx.{0}core.high'.format(QUEUE_VARIANT)
DEFAULT_PRIORITY_QUEUE = 'edx.{0}core.default'.format(QUEUE_VARIANT)
LOW_PRIORITY_QUEUE = 'edx.{0}core.low'.format(QUEUE_VARIANT)
CELERY_DEFAULT_QUEUE = DEFAULT_PRIORITY_QUEUE
CELERY_DEFAULT_ROUTING_KEY = DEFAULT_PRIORITY_QUEUE
CELERY_QUEUES = {
HIGH_PRIORITY_QUEUE: {},
LOW_PRIORITY_QUEUE: {},
DEFAULT_PRIORITY_QUEUE: {}
}
############# NON-SECURE ENV CONFIG ############################## ############# NON-SECURE ENV CONFIG ##############################
# Things like server locations, ports, etc. # Things like server locations, ports, etc.
with open(ENV_ROOT / CONFIG_PREFIX + "env.json") as env_file: with open(ENV_ROOT / CONFIG_PREFIX + "env.json") as env_file:
...@@ -78,3 +125,14 @@ CONTENTSTORE = AUTH_TOKENS['CONTENTSTORE'] ...@@ -78,3 +125,14 @@ CONTENTSTORE = AUTH_TOKENS['CONTENTSTORE']
# Datadog for events! # Datadog for events!
DATADOG_API = AUTH_TOKENS.get("DATADOG_API") DATADOG_API = AUTH_TOKENS.get("DATADOG_API")
# Celery Broker
CELERY_BROKER_TRANSPORT = ENV_TOKENS.get("CELERY_BROKER_TRANSPORT", "")
CELERY_BROKER_HOSTNAME = ENV_TOKENS.get("CELERY_BROKER_HOSTNAME", "")
CELERY_BROKER_USER = AUTH_TOKENS.get("CELERY_BROKER_USER", "")
CELERY_BROKER_PASSWORD = AUTH_TOKENS.get("CELERY_BROKER_PASSWORD", "")
BROKER_URL = "{0}://{1}:{2}@{3}".format(CELERY_BROKER_TRANSPORT,
CELERY_BROKER_USER,
CELERY_BROKER_PASSWORD,
CELERY_BROKER_HOSTNAME)
...@@ -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
...@@ -34,6 +38,9 @@ MITX_FEATURES = { ...@@ -34,6 +38,9 @@ MITX_FEATURES = {
'STAFF_EMAIL': '', # email address for staff (eg to request course creation) 'STAFF_EMAIL': '', # email address for staff (eg to request course creation)
'STUDIO_NPS_SURVEY': True, 'STUDIO_NPS_SURVEY': True,
'SEGMENT_IO': True, 'SEGMENT_IO': True,
# Enable URL that shows information about the status of variuous services
'ENABLE_SERVICE_STATUS': False,
} }
ENABLE_JASMINE = False ENABLE_JASMINE = False
...@@ -214,7 +221,9 @@ PIPELINE_JS = { ...@@ -214,7 +221,9 @@ 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/hesitate.js', 'js/base.js',
'js/models/feedback.js', 'js/views/feedback.js',
'js/models/section.js', 'js/views/section.js'],
'output_filename': 'js/cms-application.js', 'output_filename': 'js/cms-application.js',
'test_order': 0 'test_order': 0
}, },
...@@ -240,6 +249,51 @@ STATICFILES_IGNORE_PATTERNS = ( ...@@ -240,6 +249,51 @@ STATICFILES_IGNORE_PATTERNS = (
PIPELINE_YUI_BINARY = 'yui-compressor' PIPELINE_YUI_BINARY = 'yui-compressor'
################################# CELERY ######################################
# Message configuration
CELERY_TASK_SERIALIZER = 'json'
CELERY_RESULT_SERIALIZER = 'json'
CELERY_MESSAGE_COMPRESSION = 'gzip'
# Results configuration
CELERY_IGNORE_RESULT = False
CELERY_STORE_ERRORS_EVEN_IF_IGNORED = True
# Events configuration
CELERY_TRACK_STARTED = True
CELERY_SEND_EVENTS = True
CELERY_SEND_TASK_SENT_EVENT = True
# Exchange configuration
CELERY_DEFAULT_EXCHANGE = 'edx.core'
CELERY_DEFAULT_EXCHANGE_TYPE = 'direct'
# Queues configuration
HIGH_PRIORITY_QUEUE = 'edx.core.high'
DEFAULT_PRIORITY_QUEUE = 'edx.core.default'
LOW_PRIORITY_QUEUE = 'edx.core.low'
CELERY_QUEUE_HA_POLICY = 'all'
CELERY_CREATE_MISSING_QUEUES = True
CELERY_DEFAULT_QUEUE = DEFAULT_PRIORITY_QUEUE
CELERY_DEFAULT_ROUTING_KEY = DEFAULT_PRIORITY_QUEUE
CELERY_QUEUES = {
HIGH_PRIORITY_QUEUE: {},
LOW_PRIORITY_QUEUE: {},
DEFAULT_PRIORITY_QUEUE: {}
}
############################ APPS ##################################### ############################ APPS #####################################
INSTALLED_APPS = ( INSTALLED_APPS = (
...@@ -249,8 +303,12 @@ INSTALLED_APPS = ( ...@@ -249,8 +303,12 @@ INSTALLED_APPS = (
'django.contrib.sessions', 'django.contrib.sessions',
'django.contrib.sites', 'django.contrib.sites',
'django.contrib.messages', 'django.contrib.messages',
'djcelery',
'south', 'south',
# Monitor the status of services
'service_status',
# For CMS # For CMS
'contentstore', 'contentstore',
'auth', 'auth',
...@@ -264,4 +322,11 @@ INSTALLED_APPS = ( ...@@ -264,4 +322,11 @@ INSTALLED_APPS = (
'pipeline', 'pipeline',
'staticfiles', 'staticfiles',
'static_replace', 'static_replace',
# comment common
'django_comment_common',
) )
################# EDX MARKETING SITE ##################################
EDXMKTG_COOKIE_NAME = 'edxloggedin'
""" """
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
...@@ -116,10 +120,14 @@ SECRET_KEY = '85920908f28904ed733fe576320db18cabd7b6cd' ...@@ -116,10 +120,14 @@ SECRET_KEY = '85920908f28904ed733fe576320db18cabd7b6cd'
PIPELINE_SASS_ARGUMENTS = '--debug-info --require {proj_dir}/static/sass/bourbon/lib/bourbon.rb'.format(proj_dir=PROJECT_ROOT) PIPELINE_SASS_ARGUMENTS = '--debug-info --require {proj_dir}/static/sass/bourbon/lib/bourbon.rb'.format(proj_dir=PROJECT_ROOT)
################################# CELERY ######################################
# By default don't use a worker, execute tasks as if they were local functions
CELERY_ALWAYS_EAGER = True
################################ DEBUG TOOLBAR ################################# ################################ DEBUG TOOLBAR #################################
INSTALLED_APPS += ('debug_toolbar', 'debug_toolbar_mongo') INSTALLED_APPS += ('debug_toolbar', 'debug_toolbar_mongo')
MIDDLEWARE_CLASSES += ('django_comment_client.utils.QueryCountDebugMiddleware', MIDDLEWARE_CLASSES += ('debug_toolbar.middleware.DebugToolbarMiddleware',)
'debug_toolbar.middleware.DebugToolbarMiddleware',)
INTERNAL_IPS = ('127.0.0.1',) INTERNAL_IPS = ('127.0.0.1',)
DEBUG_TOOLBAR_PANELS = ( DEBUG_TOOLBAR_PANELS = (
...@@ -151,5 +159,8 @@ DEBUG_TOOLBAR_MONGO_STACKTRACES = True ...@@ -151,5 +159,8 @@ DEBUG_TOOLBAR_MONGO_STACKTRACES = True
# disable NPS survey in dev mode # disable NPS survey in dev mode
MITX_FEATURES['STUDIO_NPS_SURVEY'] = False MITX_FEATURES['STUDIO_NPS_SURVEY'] = False
# Enable URL that shows information about the status of variuous services
MITX_FEATURES['ENABLE_SERVICE_STATUS'] = True
# segment-io key for dev # segment-io key for dev
SEGMENT_IO_KEY = 'mty8edrrsg' SEGMENT_IO_KEY = 'mty8edrrsg'
# 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'
......
"""
This config file follows the dev enviroment, but adds the
requirement of a celery worker running in the background to process
celery tasks.
The worker can be executed using:
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 *
################################# CELERY ######################################
# Requires a separate celery worker
CELERY_ALWAYS_EAGER = False
# Use django db as the broker and result store
BROKER_URL = 'django://'
INSTALLED_APPS += ('djcelery.transport', )
CELERY_RESULT_BACKEND = 'database'
DJKOMBU_POLLING_INTERVAL = 1.0
# Disable transaction management because we are using a worker. Views
# that request a task and wait for the result will deadlock otherwise.
MIDDLEWARE_CLASSES = tuple(
c for c in MIDDLEWARE_CLASSES
if c != 'django.middleware.transaction.TransactionMiddleware')
# Note: other alternatives for disabling transactions don't work in 1.4
# https://code.djangoproject.com/ticket/2304
# https://code.djangoproject.com/ticket/16039
...@@ -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
...@@ -32,8 +36,13 @@ PIPELINE_JS['spec'] = { ...@@ -32,8 +36,13 @@ PIPELINE_JS['spec'] = {
} }
JASMINE_TEST_DIRECTORY = PROJECT_ROOT + '/static/coffee' JASMINE_TEST_DIRECTORY = PROJECT_ROOT + '/static/coffee'
JASMINE_REPORT_DIR = os.environ.get('JASMINE_REPORT_DIR', 'reports/cms/jasmine')
TEMPLATE_CONTEXT_PROCESSORS += ('settings_context_processor.context_processors.settings',)
TEMPLATE_VISIBLE_SETTINGS = ('JASMINE_REPORT_DIR', )
STATICFILES_DIRS.append(REPO_ROOT/'node_modules/phantom-jasmine/lib') STATICFILES_DIRS.append(REPO_ROOT/'node_modules/phantom-jasmine/lib')
STATICFILES_DIRS.append(REPO_ROOT/'node_modules/jasmine-reporters/src')
# Remove the localization middleware class because it requires the test database # Remove the localization middleware class because it requires the test database
# to be sync'd and migrated in order to run the jasmine tests interactively # to be sync'd and migrated in order to run the jasmine tests interactively
...@@ -41,4 +50,4 @@ STATICFILES_DIRS.append(REPO_ROOT/'node_modules/phantom-jasmine/lib') ...@@ -41,4 +50,4 @@ STATICFILES_DIRS.append(REPO_ROOT/'node_modules/phantom-jasmine/lib')
MIDDLEWARE_CLASSES = tuple(e for e in MIDDLEWARE_CLASSES \ MIDDLEWARE_CLASSES = tuple(e for e in MIDDLEWARE_CLASSES \
if e != 'django.middleware.locale.LocaleMiddleware') if e != 'django.middleware.locale.LocaleMiddleware')
INSTALLED_APPS += ('django_jasmine', ) INSTALLED_APPS += ('django_jasmine', 'settings_context_processor')
...@@ -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
...@@ -41,14 +46,14 @@ MODULESTORE_OPTIONS = { ...@@ -41,14 +46,14 @@ MODULESTORE_OPTIONS = {
'default_class': 'xmodule.raw_module.RawDescriptor', 'default_class': 'xmodule.raw_module.RawDescriptor',
'host': 'localhost', 'host': 'localhost',
'db': 'test_xmodule', 'db': 'test_xmodule',
'collection': 'modulestore', 'collection': 'test_modulestore',
'fs_root': GITHUB_REPO_ROOT, 'fs_root': TEST_ROOT / "data",
'render_template': 'mitxmako.shortcuts.render_to_string', 'render_template': 'mitxmako.shortcuts.render_to_string',
} }
MODULESTORE = { MODULESTORE = {
'default': { 'default': {
'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore', 'ENGINE': 'xmodule.modulestore.mongo.DraftMongoModuleStore',
'OPTIONS': MODULESTORE_OPTIONS 'OPTIONS': MODULESTORE_OPTIONS
}, },
'direct': { 'direct': {
...@@ -108,6 +113,12 @@ CACHES = { ...@@ -108,6 +113,12 @@ CACHES = {
} }
} }
################################# CELERY ######################################
CELERY_ALWAYS_EAGER = True
CELERY_RESULT_BACKEND = 'cache'
BROKER_TRANSPORT = 'memory'
################### Make tests faster ################### Make tests faster
#http://slacy.com/blog/2012/04/make-your-tests-faster-in-django-1-4/ #http://slacy.com/blog/2012/04/make-your-tests-faster-in-django-1-4/
PASSWORD_HASHERS = ( PASSWORD_HASHERS = (
...@@ -121,3 +132,4 @@ SEGMENT_IO_KEY = '***REMOVED***' ...@@ -121,3 +132,4 @@ SEGMENT_IO_KEY = '***REMOVED***'
# disable NPS survey in test mode # disable NPS survey in test mode
MITX_FEATURES['STUDIO_NPS_SURVEY'] = False MITX_FEATURES['STUDIO_NPS_SURVEY'] = False
MITX_FEATURES['ENABLE_SERVICE_STATUS'] = True
...@@ -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;
} }
} }
...@@ -801,7 +801,8 @@ hr.divide { ...@@ -801,7 +801,8 @@ hr.divide {
} }
.tooltip { .tooltip {
@extend .t-copy-sub2; @include font-size(12);
@include transition(opacity 0.1s ease-out);
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
...@@ -811,10 +812,9 @@ hr.divide { ...@@ -811,10 +812,9 @@ hr.divide {
background: rgba(0, 0, 0, 0.85); background: rgba(0, 0, 0, 0.85);
font-weight: normal; font-weight: normal;
line-height: 26px; line-height: 26px;
color: #fff; color: $white;
pointer-events: none; pointer-events: none;
opacity: 0; opacity: 0;
@include transition(opacity 0.1s ease-out);
&:after { &:after {
content: '▾'; content: '▾';
...@@ -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% {
......
// studio - assets - fonts // studio - assets - fonts
// NOTE: Sass currently can't process the standard Google Web Font import method, so a @font-face with src declaration of the .woff file that the Google @import method uses is needed :/
// ==================== // ====================
// import from google fonts - Open Sans // Open Sans - http://www.google.com/fonts/specimen/Open+Sans
@import url(http://fonts.googleapis.com/css?family=Open+Sans:300italic,400italic,700italic,400,700,300); @font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 700;
src: local('Open Sans Bold'), local('OpenSans-Bold'), url(http://themes.googleusercontent.com/static/fonts/opensans/v6/k3k702ZOKiLJc3WVjuplzKRDOzjiPcYnFooOUGCOsRk.woff) format('woff');
}
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 300;
src: local('Open Sans Light'), local('OpenSans-Light'), url(http://themes.googleusercontent.com/static/fonts/opensans/v6/DXI1ORHCpsQm3Vp6mXoaTaRDOzjiPcYnFooOUGCOsRk.woff) format('woff');
}
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 700;
src: local('Open Sans Bold Italic'), local('OpenSans-BoldItalic'), url(http://themes.googleusercontent.com/static/fonts/opensans/v6/PRmiXeptR36kaC0GEAetxhbnBKKEOwRKgsHDreGcocg.woff) format('woff');
}
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 300;
src: local('Open Sans Light Italic'), local('OpenSansLight-Italic'), url(http://themes.googleusercontent.com/static/fonts/opensans/v6/PRmiXeptR36kaC0GEAetxvR_54zmj3SbGZQh3vCOwvY.woff) format('woff');
}
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 400;
src: local('Open Sans Italic'), local('OpenSans-Italic'), url(http://themes.googleusercontent.com/static/fonts/opensans/v6/xjAJXh38I15wypJXxuGMBrrIa-7acMAeDBVuclsi6Gc.woff) format('woff');
}
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 400;
src: local('Open Sans'), local('OpenSans'), url(http://themes.googleusercontent.com/static/fonts/opensans/v6/cJZKeOuBrn4kERxqtaUH3bO3LdcAZYWl9Si6vvxL-qU.woff) format('woff');
}
// import from google fonts - Bree // Bree Serif - http://www.google.com/fonts/specimen/Bree+Serif
@import url(http://fonts.googleapis.com/css?family=Bree+Serif); @font-face {
font-family: 'Bree Serif';
font-style: normal;
font-weight: 400;
src: local('Bree Serif'), local('BreeSerif'), url(http://themes.googleusercontent.com/static/fonts/breeserif/v2/LQ7WLTaITDg4OSRuOZCps73hpw3pgy2gAi-Ip7WPMi0.woff) format('woff');
}
...@@ -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;
} }
} }
......
...@@ -106,13 +106,19 @@ ...@@ -106,13 +106,19 @@
width: 1px; width: 1px;
} }
.course-org {
margin-right: ($baseline/4);
}
.course-number, .course-org { .course-number, .course-org {
@include font-size(12); @include font-size(12);
display: inline-block; display: inline-block;
max-width: 70px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
line-height: 1.3;
}
.course-org {
margin-right: ($baseline/4);
max-width: 140px;
} }
.course-title { .course-title {
...@@ -132,9 +138,9 @@ ...@@ -132,9 +138,9 @@
// specific elements - course nav // specific elements - course nav
.nav-course { .nav-course {
width: 285px; width: 290px;
@extend .t-copy-sub1;
margin-top: -($baseline/4); margin-top: -($baseline/4);
@include font-size(14);
> ol > .nav-item { > ol > .nav-item {
vertical-align: bottom; vertical-align: bottom;
...@@ -152,8 +158,8 @@ ...@@ -152,8 +158,8 @@
color: $gray-d3; color: $gray-d3;
.label-prefix { .label-prefix {
display: block;
@include font-size(11); @include font-size(11);
display: block;
font-weight: 400; font-weight: 400;
} }
} }
...@@ -213,10 +219,11 @@ ...@@ -213,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;
...@@ -224,7 +231,7 @@ ...@@ -224,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);
} }
...@@ -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>
......
...@@ -20,8 +20,6 @@ ...@@ -20,8 +20,6 @@
<%static:css group='base-style'/> <%static:css group='base-style'/>
<link rel="stylesheet" type="text/css" href="${static.url('js/vendor/markitup/skins/simple/style.css')}" /> <link rel="stylesheet" type="text/css" href="${static.url('js/vendor/markitup/skins/simple/style.css')}" />
<link rel="stylesheet" type="text/css" href="${static.url('js/vendor/markitup/sets/wiki/style.css')}" /> <link rel="stylesheet" type="text/css" href="${static.url('js/vendor/markitup/sets/wiki/style.css')}" />
<link rel="stylesheet" type="text/css" href="${static.url('css/vendor/symbolset.ss-symbolicons-block.css')}" />
<link rel="stylesheet" type="text/css" href="${static.url('css/vendor/symbolset.ss-standard.css')}" />
<%include file="widgets/segment-io.html" /> <%include file="widgets/segment-io.html" />
...@@ -30,14 +28,19 @@ ...@@ -30,14 +28,19 @@
<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>
<script type="text/javascript" src="${static.url('js/vendor/backbone-min.js')}"></script> <script type="text/javascript" src="${static.url('js/vendor/backbone-min.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/markitup/jquery.markitup.js')}"></script> <script type="text/javascript" src="${static.url('js/vendor/markitup/jquery.markitup.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/markitup/sets/wiki/set.js')}"></script> <script type="text/javascript" src="${static.url('js/vendor/markitup/sets/wiki/set.js')}"></script>
<script src="${static.url('js/vendor/symbolset.ss-standard.js')}"></script>
<script src="${static.url('js/vendor/symbolset.ss-symbolicons.js')}"></script>
<%static:js group='main'/> <%static:js group='main'/>
<%static:js group='module-js'/> <%static:js group='module-js'/>
...@@ -49,17 +52,16 @@ ...@@ -49,17 +52,16 @@
<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"> <script type="text/javascript" src="//www.youtube.com/player_api"></script>
document.write('\x3Cscript type="text/javascript" src="' +
document.location.protocol + '//www.youtube.com/player_api">\x3C/script>'); <script src="${static.url('js/models/feedback.js')}"></script>
</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>
...@@ -70,10 +72,10 @@ ...@@ -70,10 +72,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>
......
...@@ -9,7 +9,6 @@ ...@@ -9,7 +9,6 @@
<script type="text/javascript" src="${static.url('js/template_loader.js')}"></script> <script type="text/javascript" src="${static.url('js/template_loader.js')}"></script>
<script type="text/javascript" src="${static.url('js/views/checklists_view.js')}"></script> <script type="text/javascript" src="${static.url('js/views/checklists_view.js')}"></script>
<script type="text/javascript" src="${static.url('js/models/checklists.js')}"></script> <script type="text/javascript" src="${static.url('js/models/checklists.js')}"></script>
<script type="text/javascript" src="${static.url('js/views/server_error.js')}"></script>
<script type="text/javascript"> <script type="text/javascript">
$(document).ready(function () { $(document).ready(function () {
......
...@@ -11,7 +11,6 @@ ...@@ -11,7 +11,6 @@
<script type="text/javascript" src="${static.url('js/models/course_info.js')}"></script> <script type="text/javascript" src="${static.url('js/models/course_info.js')}"></script>
<script type="text/javascript" src="${static.url('js/models/module_info.js')}"></script> <script type="text/javascript" src="${static.url('js/models/module_info.js')}"></script>
<script type="text/javascript" src="${static.url('js/views/course_info_edit.js')}"></script> <script type="text/javascript" src="${static.url('js/views/course_info_edit.js')}"></script>
<script type="text/javascript" src="${static.url('js/views/server_error.js')}"></script>
<link rel="stylesheet" type="text/css" href="${static.url('js/vendor/timepicker/jquery.timepicker.css')}" /> <link rel="stylesheet" type="text/css" href="${static.url('js/vendor/timepicker/jquery.timepicker.css')}" />
<script src="${static.url('js/vendor/timepicker/jquery.timepicker.js')}"></script> <script src="${static.url('js/vendor/timepicker/jquery.timepicker.js')}"></script>
<script src="${static.url('js/vendor/timepicker/datepair.js')}"></script> <script src="${static.url('js/vendor/timepicker/datepair.js')}"></script>
...@@ -53,7 +52,7 @@ ...@@ -53,7 +52,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')
......
...@@ -15,7 +15,6 @@ from contentstore import utils ...@@ -15,7 +15,6 @@ from contentstore import utils
<script src="${static.url('js/vendor/date.js')}"></script> <script src="${static.url('js/vendor/date.js')}"></script>
<script type="text/javascript" src="${static.url('js/template_loader.js')}"></script> <script type="text/javascript" src="${static.url('js/template_loader.js')}"></script>
<script type="text/javascript" src="${static.url('js/views/server_error.js')}"></script>
<script type="text/javascript" src="${static.url('js/models/course_relative.js')}"></script> <script type="text/javascript" src="${static.url('js/models/course_relative.js')}"></script>
<script type="text/javascript" src="${static.url('js/views/validating_view.js')}"></script> <script type="text/javascript" src="${static.url('js/views/validating_view.js')}"></script>
<script type="text/javascript" src="${static.url('js/views/settings/main_settings_view.js')}"></script> <script type="text/javascript" src="${static.url('js/views/settings/main_settings_view.js')}"></script>
...@@ -92,7 +91,7 @@ from contentstore import utils ...@@ -92,7 +91,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>
......
...@@ -11,7 +11,6 @@ from contentstore import utils ...@@ -11,7 +11,6 @@ from contentstore import utils
<%block name="jsextra"> <%block name="jsextra">
<script type="text/javascript" src="${static.url('js/template_loader.js')}"></script> <script type="text/javascript" src="${static.url('js/template_loader.js')}"></script>
<script type="text/javascript" src="${static.url('js/views/server_error.js')}"></script>
<script type="text/javascript" src="${static.url('js/views/validating_view.js')}"></script> <script type="text/javascript" src="${static.url('js/views/validating_view.js')}"></script>
<script type="text/javascript" src="${static.url('js/models/settings/advanced.js')}"></script> <script type="text/javascript" src="${static.url('js/models/settings/advanced.js')}"></script>
<script type="text/javascript" src="${static.url('js/views/settings/advanced_view.js')}"></script> <script type="text/javascript" src="${static.url('js/views/settings/advanced_view.js')}"></script>
...@@ -110,7 +109,7 @@ editor.render(); ...@@ -110,7 +109,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 +135,7 @@ editor.render(); ...@@ -136,7 +135,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 +143,7 @@ editor.render(); ...@@ -144,7 +143,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 +152,7 @@ editor.render(); ...@@ -153,7 +152,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>
......
...@@ -6,27 +6,26 @@ ...@@ -6,27 +6,26 @@
<%namespace name='static' file='static_content.html'/> <%namespace name='static' file='static_content.html'/>
<%! <%!
from contentstore import utils from contentstore import utils
%> %>
<%block name="jsextra"> <%block name="jsextra">
<script type="text/javascript" src="${static.url('js/template_loader.js')}"></script> <script type="text/javascript" src="${static.url('js/template_loader.js')}"></script>
<script type="text/javascript" src="${static.url('js/views/server_error.js')}"></script>
<script type="text/javascript" src="${static.url('js/models/course_relative.js')}"></script> <script type="text/javascript" src="${static.url('js/models/course_relative.js')}"></script>
<script type="text/javascript" src="${static.url('js/views/validating_view.js')}"></script> <script type="text/javascript" src="${static.url('js/views/validating_view.js')}"></script>
<script type="text/javascript"> <script type="text/javascript">
$(document).ready(function(){ $(document).ready(function(){
}); });
</script> </script>
</%block> </%block>
<%block name="content"> <%block name="content">
<!-- --> <!-- -->
<div class="main-wrapper"> <div class="main-wrapper">
<div class="inner-wrapper"> <div class="inner-wrapper">
<h1>Settings</h1> <h1>Settings</h1>
<article class="settings-overview"> <article class="settings-overview">
<div class="settings-page-section main-column"> <div class="settings-page-section main-column">
...@@ -74,7 +73,7 @@ from contentstore import utils ...@@ -74,7 +73,7 @@ from contentstore import utils
<div class="field"> <div class="field">
<textarea class="long tall edit-box tinymce" id="course-faculty-1-bio"></textarea> <textarea class="long tall edit-box tinymce" id="course-faculty-1-bio"></textarea>
<span class="tip tip-stacked">A brief description of your education, experience, and expertise</span> <span class="tip tip-stacked">A brief description of your education, experience, and expertise</span>
</div> </div>
</div> </div>
<a href="#" class="remove-item remove-faculty-data"><span class="delete-icon"></span> Delete Faculty Member</a> <a href="#" class="remove-item remove-faculty-data"><span class="delete-icon"></span> Delete Faculty Member</a>
...@@ -102,7 +101,7 @@ from contentstore import utils ...@@ -102,7 +101,7 @@ from contentstore import utils
<a href="#" class="new-item new-faculty-photo add-faculty-photo-data" id="course-faculty-2-photo"> <a href="#" class="new-item new-faculty-photo add-faculty-photo-data" id="course-faculty-2-photo">
<span class="upload-icon"></span>Upload Faculty Photo <span class="upload-icon"></span>Upload Faculty Photo
</a> </a>
<span class="tip tip-inline">Max size: 30KB</span> <span class="tip tip-inline">Max size: 30KB</span>
</div> </div>
</div> </div>
</div> </div>
...@@ -114,7 +113,7 @@ from contentstore import utils ...@@ -114,7 +113,7 @@ from contentstore import utils
<textarea class="long tall edit-box tinymce" id="course-faculty-2-bio"></textarea> <textarea class="long tall edit-box tinymce" id="course-faculty-2-bio"></textarea>
<span class="tip tip-stacked">A brief description of your education, experience, and expertise</span> <span class="tip tip-stacked">A brief description of your education, experience, and expertise</span>
</div> </div>
</div> </div>
</div> </div>
</li> </li>
</ul> </ul>
...@@ -143,7 +142,7 @@ from contentstore import utils ...@@ -143,7 +142,7 @@ from contentstore import utils
<div class="field"> <div class="field">
<div class="input input-radio"> <div class="input input-radio">
<input checked="checked" type="radio" name="course-problems-general-randomization" id="course-problems-general-randomization-always" value="Always"> <input checked="checked" type="radio" name="course-problems-general-randomization" id="course-problems-general-randomization-always" value="Always">
<div class="copy"> <div class="copy">
<label for="course-problems-general-randomization-always">Always</label> <label for="course-problems-general-randomization-always">Always</label>
<span class="tip tip-stacked"><strong>randomize all</strong> problems</span> <span class="tip tip-stacked"><strong>randomize all</strong> problems</span>
...@@ -217,7 +216,7 @@ from contentstore import utils ...@@ -217,7 +216,7 @@ from contentstore import utils
<div class="field"> <div class="field">
<div class="input input-radio"> <div class="input input-radio">
<input checked="checked" type="radio" name="course-problems-assignment-1-randomization" id="course-problems-assignment-1-randomization-always" value="Always"> <input checked="checked" type="radio" name="course-problems-assignment-1-randomization" id="course-problems-assignment-1-randomization-always" value="Always">
<div class="copy"> <div class="copy">
<label for="course-problems-assignment-1-randomization-always">Always</label> <label for="course-problems-assignment-1-randomization-always">Always</label>
<span class="tip tip-stacked"><strong>randomize all</strong> problems</span> <span class="tip tip-stacked"><strong>randomize all</strong> problems</span>
...@@ -283,7 +282,7 @@ from contentstore import utils ...@@ -283,7 +282,7 @@ from contentstore import utils
<section class="settings-discussions"> <section class="settings-discussions">
<h2 class="title">Discussions</h2> <h2 class="title">Discussions</h2>
<section class="settings-discussions-general"> <section class="settings-discussions-general">
<header> <header>
<h3>General Settings</h3> <h3>General Settings</h3>
...@@ -296,7 +295,7 @@ from contentstore import utils ...@@ -296,7 +295,7 @@ from contentstore import utils
<div class="field"> <div class="field">
<div class="input input-radio"> <div class="input input-radio">
<input type="radio" name="course-discussions-anonymous" id="course-discussions-anonymous-allow" value="Allow"> <input type="radio" name="course-discussions-anonymous" id="course-discussions-anonymous-allow" value="Allow">
<div class="copy"> <div class="copy">
<label for="course-discussions-anonymous-allow">Allow</label> <label for="course-discussions-anonymous-allow">Allow</label>
<span class="tip tip-stacked">Students and faculty <strong>will be able to post anonymously</strong></span> <span class="tip tip-stacked">Students and faculty <strong>will be able to post anonymously</strong></span>
...@@ -320,7 +319,7 @@ from contentstore import utils ...@@ -320,7 +319,7 @@ from contentstore import utils
<div class="field"> <div class="field">
<div class="input input-radio"> <div class="input input-radio">
<input checked="checked" type="radio" name="course-discussions-anonymous" id="course-discussions-anonymous-allow" value="Allow"> <input checked="checked" type="radio" name="course-discussions-anonymous" id="course-discussions-anonymous-allow" value="Allow">
<div class="copy"> <div class="copy">
<label for="course-discussions-anonymous-allow">Allow</label> <label for="course-discussions-anonymous-allow">Allow</label>
<span class="tip tip-stacked">Students and faculty <strong>will be able to post anonymously</strong></span> <span class="tip tip-stacked">Students and faculty <strong>will be able to post anonymously</strong></span>
...@@ -329,7 +328,7 @@ from contentstore import utils ...@@ -329,7 +328,7 @@ from contentstore import utils
<div class="input input-radio"> <div class="input input-radio">
<input disabled="disabled" type="radio" name="course-discussions-anonymous" id="course-discussions-anonymous-dontallow" value="Do Not Allow"> <input disabled="disabled" type="radio" name="course-discussions-anonymous" id="course-discussions-anonymous-dontallow" value="Do Not Allow">
<div class="copy"> <div class="copy">
<label for="course-discussions-anonymous-dontallow">Do not allow</label> <label for="course-discussions-anonymous-dontallow">Do not allow</label>
<span class="tip tip-stacked">This option is disabled since there are previous discussions that are anonymous.</span> <span class="tip tip-stacked">This option is disabled since there are previous discussions that are anonymous.</span>
...@@ -351,7 +350,7 @@ from contentstore import utils ...@@ -351,7 +350,7 @@ from contentstore import utils
<a href="#" class="drag-handle"></a> <a href="#" class="drag-handle"></a>
</li> </li>
<li class="input input-existing input-default course-discussions-categories-list-item sortable-item"> <li class="input input-existing input-default course-discussions-categories-list-item sortable-item">
<div class="group"> <div class="group">
<label for="course-discussions-categories-2-name">Category Name: </label> <label for="course-discussions-categories-2-name">Category Name: </label>
......
...@@ -12,7 +12,6 @@ from contentstore import utils ...@@ -12,7 +12,6 @@ from contentstore import utils
<script src="${static.url('js/vendor/timepicker/jquery.timepicker.js')}"></script> <script src="${static.url('js/vendor/timepicker/jquery.timepicker.js')}"></script>
<script type="text/javascript" src="${static.url('js/template_loader.js')}"></script> <script type="text/javascript" src="${static.url('js/template_loader.js')}"></script>
<script type="text/javascript" src="${static.url('js/views/server_error.js')}"></script>
<script type="text/javascript" src="${static.url('js/models/course_relative.js')}"></script> <script type="text/javascript" src="${static.url('js/models/course_relative.js')}"></script>
<script type="text/javascript" src="${static.url('js/views/validating_view.js')}"></script> <script type="text/javascript" src="${static.url('js/views/validating_view.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>
...@@ -117,7 +116,7 @@ from contentstore import utils ...@@ -117,7 +116,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>
......
This source diff could not be displayed because it is too large. You can view the blob instead.
phantom-jasmine @ a54d435b
This diff is collapsed. Click to expand it.
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