Commit e9472182 by Ned Batchelder

Merge master to here.

parents bbd1d8d0 fca63f06
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
*.swp *.swp
*.orig *.orig
*.DS_Store *.DS_Store
*.mo
:2e_* :2e_*
:2e# :2e#
.AppleDouble .AppleDouble
...@@ -22,6 +23,8 @@ reports/ ...@@ -22,6 +23,8 @@ reports/
*.egg-info *.egg-info
Gemfile.lock Gemfile.lock
.env/ .env/
conf/locale/en/LC_MESSAGES/*.po
!messages.po
lms/static/sass/*.css lms/static/sass/*.css
cms/static/sass/*.css cms/static/sass/*.css
lms/lib/comment_client/python lms/lib/comment_client/python
...@@ -33,3 +36,7 @@ chromedriver.log ...@@ -33,3 +36,7 @@ chromedriver.log
/nbproject /nbproject
ghostdriver.log ghostdriver.log
node_modules node_modules
.pip_download_cache/
.prereqs_cache
autodeploy.properties
.ws_migrations_complete
[submodule "common/test/phantom-jasmine"]
path = common/test/phantom-jasmine
url = https://github.com/jcarver989/phantom-jasmine.git
\ No newline at end of file
[main]
host = https://www.transifex.com
[edx-studio.django-partial]
file_filter = conf/locale/<lang>/LC_MESSAGES/django-partial.po
source_file = conf/locale/en/LC_MESSAGES/django-partial.po
source_lang = en
type = PO
[edx-studio.djangojs]
file_filter = conf/locale/<lang>/LC_MESSAGES/djangojs.po
source_file = conf/locale/en/LC_MESSAGES/djangojs.po
source_lang = en
type = PO
[edx-studio.mako]
file_filter = conf/locale/<lang>/LC_MESSAGES/mako.po
source_file = conf/locale/en/LC_MESSAGES/mako.po
source_lang = en
type = PO
[edx-studio.messages]
file_filter = conf/locale/<lang>/LC_MESSAGES/messages.po
source_file = conf/locale/en/LC_MESSAGES/messages.po
source_lang = en
type = PO
See doc/ for documentation.
This is edX, a platform for online course delivery. The project is primarily
written in [Python](http://python.org/), using the
[Django](https://www.djangoproject.com/) framework. We also use some
[Ruby](http://www.ruby-lang.org/) and some [NodeJS](http://nodejs.org/).
Installation
============
The installation process is a bit messy at the moment. Here's a high-level
overview of what you should do to get started.
**TLDR:** There is a `scripts/create-dev-env.sh` script that will attempt to set all
of this up for you. If you're in a hurry, run that script. Otherwise, I suggest
that you understand what the script is doing, and why, by reading this document.
Directory Hierarchy
-------------------
This code assumes that it is checked out in a directory that has three sibling
directories: `data` (used for XML course data), `db` (used to hold a
[sqlite](https://sqlite.org/) database), and `log` (used to hold logs). If you
clone the repository into a directory called `edx` inside of a directory
called `dev`, here's an example of how the directory hierarchy should look:
* dev
\
* data
* db
* log
* edx
\
README.md
Language Runtimes
-----------------
You'll need to be sure that you have Python 2.7, Ruby 1.9.3, and NodeJS
(latest stable) installed on your system. Some of these you can install
using your system's package manager: [homebrew](http://mxcl.github.io/homebrew/)
for Mac, [apt](http://wiki.debian.org/Apt) for Debian-based systems
(including Ubuntu), [rpm](http://www.rpm.org/) or [yum](http://yum.baseurl.org/)
for Red Hat based systems (including CentOS).
If your system's package manager gives you the wrong version of a language
runtime, then you'll need to use a versioning tool to install the correct version.
Usually, you'll need to do this for Ruby: you can use
[`rbenv`](https://github.com/sstephenson/rbenv) or [`rvm`](https://rvm.io/), but
typically `rbenv` is simpler. For Python, you can use
[`pythonz`](http://saghul.github.io/pythonz/),
and for Node, you can use [`nvm`](https://github.com/creationix/nvm).
Virtual Environments
--------------------
Often, different projects will have conflicting dependencies: for example, two
projects depending on two different, incompatible versions of a library. Clearly,
you can't have both versions installed and used on your machine simultaneously.
Virtual environments were created to solve this problem: by installing libraries
into an isolated environment, only projects that live inside the environment
will be able to see and use those libraries. Got incompatible dependencies? Use
different virtual environments, and your problem is solved.
Remember, each language has a different implementation. Python has
[`virtualenv`](http://www.virtualenv.org/), Ruby has
[`bundler`](http://gembundler.com/), and Node's virtual environment support
is built into [`npm`](https://npmjs.org/), its library management tool.
For each language, decide if you want to use a virtual environment, or if you
want to install all the language dependencies globally (and risk conflicts).
I suggest you start with installing things globally until and unless things
break; you can always switch over to a virtual environment later on.
Language Packages
-----------------
The Python libraries we use are listed in `requirements.txt`. The Ruby libraries
we use are listed in `Gemfile`. The Node libraries we use are listed in
`packages.json`. Python has a library installer called
[`pip`](http://www.pip-installer.org/), Ruby has a library installer called
[`gem`](https://rubygems.org/) (or `bundle` if you're using a virtual
environment), and Node has a library installer called
[`npm`](https://npmjs.org/).
Once you've got your languages and virtual environments set up, install
the libraries like so:
$ pip install -r requirements/base.txt
$ pip install -r requirements/post.txt
$ bundle install
$ npm install
You can also use [`rake`](http://rake.rubyforge.org/) to get all of the prerequisites (or to update)
them if they've changed
$ rake install_prereqs
Other Dependencies
------------------
You'll also need to install [MongoDB](http://www.mongodb.org/), since our
application uses it in addition to sqlite. You can install it through your
system package manager, and I suggest that you configure it to start
automatically when you boot up your system, so that you never have to worry
about it again. For Mac, use
[`launchd`](https://developer.apple.com/library/mac/documentation/Darwin/Reference/ManPages/man8/launchd.8.html)
(running `brew info mongodb` will give you some commands you can copy-paste.)
For Linux, you can use [`upstart`](http://upstart.ubuntu.com/), `chkconfig`,
or any other process management tool.
Configuring Your Project
------------------------
We use [`rake`](http://rake.rubyforge.org/) to execute common tasks in our
project. The `rake` tasks are defined in the `rakefile`, or you can run `rake -T`
to view a summary.
Before you run your project, you need to create a sqlite database, create
tables in that database, run database migrations, and populate templates for
CMS templates. Fortunately, `rake` will do all of this for you! Just run:
$ rake django-admin[syncdb]
$ rake django-admin[migrate]
$ rake django-admin[update_templates]
If you are running these commands using the [`zsh`](http://www.zsh.org/) shell,
zsh will assume that you are doing
[shell globbing](https://en.wikipedia.org/wiki/Glob_(programming)), search for
a file in your directory named `django-adminsyncdb` or `django-adminmigrate`,
and fail. To fix this, just surround the argument with quotation marks, so that
you're running `rake "django-admin[syncdb]"`.
Run Your Project
----------------
edX has two components: Studio, the course authoring system; and the LMS
(learning management system) used by students. These two systems communicate
through the MongoDB database, which stores course information.
To run Studio, run:
$ rake cms
To run the LMS, run:
$ rake lms[cms.dev]
Studio runs on port 8001, while LMS runs on port 8000, so you can run both of
these commands simultaneously, using two different terminal windows. To view
Studio, visit `127.0.0.1:8001` in your web browser; to view the LMS, visit
`127.0.0.1:8000`.
There's also an older version of the LMS that saves its information in XML files
in the `data` directory, instead of in Mongo. To run this older version, run:
$ rake lms
Further Documentation
=====================
Once you've got your project up and running, you can check out the `docs`
directory to see more documentation about how edX is structured.
...@@ -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
......
...@@ -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
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
#pylint: disable=W0621 #pylint: disable=W0621
from lettuce import world, step from lettuce import world, step
from nose.tools import assert_true, assert_equal from nose.tools import assert_true, assert_equal, assert_in
from terrain.steps import reload_the_page from terrain.steps import reload_the_page
from selenium.common.exceptions import StaleElementReferenceException from selenium.common.exceptions import StaleElementReferenceException
...@@ -63,7 +63,7 @@ def i_select_a_link_to_the_course_outline(step): ...@@ -63,7 +63,7 @@ def i_select_a_link_to_the_course_outline(step):
@step('I am brought to the course outline page$') @step('I am brought to the course outline page$')
def i_am_brought_to_course_outline(step): def i_am_brought_to_course_outline(step):
assert_equal('Course Outline', world.css_find('.outline .title-1')[0].text) assert_in('Course Outline', world.css_find('.outline .page-header')[0].text)
assert_equal(1, len(world.browser.windows)) assert_equal(1, len(world.browser.windows))
......
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
......
...@@ -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
......
...@@ -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'
......
from xmodule.templates import update_templates from xmodule.templates import update_templates
from xmodule.modulestore.django import modulestore
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
...@@ -6,4 +7,4 @@ class Command(BaseCommand): ...@@ -6,4 +7,4 @@ class Command(BaseCommand):
help = 'Imports and updates the Studio component templates from the code pack and put in the DB' help = 'Imports and updates the Studio component templates from the code pack and put in the DB'
def handle(self, *args, **options): def handle(self, *args, **options):
update_templates() update_templates(modulestore('direct'))
...@@ -75,11 +75,7 @@ def set_module_info(store, location, post_data): ...@@ -75,11 +75,7 @@ def set_module_info(store, location, post_data):
# IMPORTANT NOTE: if the client passed pack 'null' (None) for a piece of metadata that means 'remove it' # IMPORTANT NOTE: if the client passed pack 'null' (None) for a piece of metadata that means 'remove it'
for metadata_key, value in posted_metadata.items(): for metadata_key, value in posted_metadata.items():
# let's strip out any metadata fields from the postback which have been identified as system metadata if posted_metadata[metadata_key] is None:
# and therefore should not be user-editable, so we should accept them back from the client
if metadata_key in module.system_metadata_fields:
del posted_metadata[metadata_key]
elif posted_metadata[metadata_key] is None:
# remove both from passed in collection as well as the collection read in from the modulestore # remove both from passed in collection as well as the collection read in from the modulestore
if metadata_key in module._model_data: if metadata_key in module._model_data:
del module._model_data[metadata_key] del module._model_data[metadata_key]
......
...@@ -220,6 +220,14 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -220,6 +220,14 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
num_drafts = self._get_draft_counts(course) num_drafts = self._get_draft_counts(course)
self.assertEqual(num_drafts, 1) self.assertEqual(num_drafts, 1)
def test_import_textbook_as_content_element(self):
import_from_xml(modulestore(), 'common/test/data/', ['full'])
module_store = modulestore('direct')
course = module_store.get_item(Location(['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None]))
self.assertGreater(len(course.textbooks), 0)
def test_static_tab_reordering(self): def test_static_tab_reordering(self):
import_from_xml(modulestore(), 'common/test/data/', ['full']) import_from_xml(modulestore(), 'common/test/data/', ['full'])
...@@ -293,7 +301,6 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -293,7 +301,6 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
# make sure the parent no longer points to the child object which was deleted # make sure the parent no longer points to the child object which was deleted
self.assertFalse(sequential.location.url() in chapter.children) self.assertFalse(sequential.location.url() in chapter.children)
def test_about_overrides(self): def test_about_overrides(self):
''' '''
This test case verifies that a course can use specialized override for about data, e.g. /about/Fall_2012/effort.html This test case verifies that a course can use specialized override for about data, e.g. /about/Fall_2012/effort.html
...@@ -490,6 +497,11 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -490,6 +497,11 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
self.assertTrue(getattr(test_private_vertical, 'is_draft', False)) self.assertTrue(getattr(test_private_vertical, 'is_draft', False))
# make sure the textbook survived the export/import
course = module_store.get_item(Location(['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None]))
self.assertGreater(len(course.textbooks), 0)
shutil.rmtree(root_dir) shutil.rmtree(root_dir)
def test_course_handouts_rewrites(self): def test_course_handouts_rewrites(self):
...@@ -634,7 +646,7 @@ class ContentStoreTest(ModuleStoreTestCase): ...@@ -634,7 +646,7 @@ class ContentStoreTest(ModuleStoreTestCase):
resp = self.client.get(reverse('index')) resp = self.client.get(reverse('index'))
self.assertContains( self.assertContains(
resp, resp,
'<h1 class="title-1">My Courses</h1>', '<h1 class="page-header">My Courses</h1>',
status_code=200, status_code=200,
html=True html=True
) )
...@@ -937,7 +949,7 @@ class TemplateTestCase(ModuleStoreTestCase): ...@@ -937,7 +949,7 @@ class TemplateTestCase(ModuleStoreTestCase):
self.assertIsNotNone(verify_create) self.assertIsNotNone(verify_create)
# now run cleanup # now run cleanup
update_templates() update_templates(modulestore('direct'))
# now try to find dangling template, it should not be in DB any longer # now try to find dangling template, it should not be in DB any longer
asserted = False asserted = False
......
...@@ -10,9 +10,9 @@ class CourseUpdateTest(CourseTestCase): ...@@ -10,9 +10,9 @@ class CourseUpdateTest(CourseTestCase):
'''Go through each interface and ensure it works.''' '''Go through each interface and ensure it works.'''
# first get the update to force the creation # first get the update to force the creation
url = reverse('course_info', url = reverse('course_info',
kwargs={'org': self.course_location.org, kwargs={'org': self.course_location.org,
'course': self.course_location.course, 'course': self.course_location.course,
'name': self.course_location.name}) 'name': self.course_location.name})
self.client.get(url) self.client.get(url)
init_content = '<iframe width="560" height="315" src="http://www.youtube.com/embed/RocY-Jd93XU" frameborder="0">' init_content = '<iframe width="560" height="315" src="http://www.youtube.com/embed/RocY-Jd93XU" frameborder="0">'
...@@ -20,9 +20,9 @@ class CourseUpdateTest(CourseTestCase): ...@@ -20,9 +20,9 @@ class CourseUpdateTest(CourseTestCase):
payload = {'content': content, payload = {'content': content,
'date': 'January 8, 2013'} 'date': 'January 8, 2013'}
url = reverse('course_info_json', url = reverse('course_info_json',
kwargs={'org': self.course_location.org, kwargs={'org': self.course_location.org,
'course': self.course_location.course, 'course': self.course_location.course,
'provided_id': ''}) 'provided_id': ''})
resp = self.client.post(url, json.dumps(payload), "application/json") resp = self.client.post(url, json.dumps(payload), "application/json")
...@@ -31,25 +31,25 @@ class CourseUpdateTest(CourseTestCase): ...@@ -31,25 +31,25 @@ class CourseUpdateTest(CourseTestCase):
self.assertHTMLEqual(payload['content'], content) self.assertHTMLEqual(payload['content'], content)
first_update_url = reverse('course_info_json', first_update_url = reverse('course_info_json',
kwargs={'org': self.course_location.org, kwargs={'org': self.course_location.org,
'course': self.course_location.course, 'course': self.course_location.course,
'provided_id': payload['id']}) 'provided_id': payload['id']})
content += '<div>div <p>p<br/></p></div>' content += '<div>div <p>p<br/></p></div>'
payload['content'] = content payload['content'] = content
resp = self.client.post(first_update_url, json.dumps(payload), resp = self.client.post(first_update_url, json.dumps(payload),
"application/json") "application/json")
self.assertHTMLEqual(content, json.loads(resp.content)['content'], self.assertHTMLEqual(content, json.loads(resp.content)['content'],
"iframe w/ div") "iframe w/ div")
# now put in an evil update # now put in an evil update
content = '<ol/>' content = '<ol/>'
payload = {'content': content, payload = {'content': content,
'date': 'January 11, 2013'} 'date': 'January 11, 2013'}
url = reverse('course_info_json', url = reverse('course_info_json',
kwargs={'org': self.course_location.org, kwargs={'org': self.course_location.org,
'course': self.course_location.course, 'course': self.course_location.course,
'provided_id': ''}) 'provided_id': ''})
resp = self.client.post(url, json.dumps(payload), "application/json") resp = self.client.post(url, json.dumps(payload), "application/json")
...@@ -58,25 +58,24 @@ class CourseUpdateTest(CourseTestCase): ...@@ -58,25 +58,24 @@ class CourseUpdateTest(CourseTestCase):
self.assertHTMLEqual(content, payload['content'], "self closing ol") self.assertHTMLEqual(content, payload['content'], "self closing ol")
url = reverse('course_info_json', url = reverse('course_info_json',
kwargs={'org': self.course_location.org, kwargs={'org': self.course_location.org,
'course': self.course_location.course, 'course': self.course_location.course,
'provided_id': ''}) 'provided_id': ''})
resp = self.client.get(url) resp = self.client.get(url)
payload = json.loads(resp.content) payload = json.loads(resp.content)
self.assertTrue(len(payload) == 2) self.assertTrue(len(payload) == 2)
# can't test non-json paylod b/c expect_json throws error # can't test non-json paylod b/c expect_json throws error
# try json w/o required fields # try json w/o required fields
self.assertContains( self.assertContains(self.client.post(url, json.dumps({'garbage': 1}),
self.client.post(url, json.dumps({'garbage': 1}), "application/json"),
"application/json"), 'Failed to save', status_code=400)
'Failed to save', status_code=400)
# now try to update a non-existent update # now try to update a non-existent update
url = reverse('course_info_json', url = reverse('course_info_json',
kwargs={'org': self.course_location.org, kwargs={'org': self.course_location.org,
'course': self.course_location.course, 'course': self.course_location.course,
'provided_id': '9'}) 'provided_id': '9'})
content = 'blah blah' content = 'blah blah'
payload = {'content': content, payload = {'content': content,
'date': 'January 21, 2013'} 'date': 'January 21, 2013'}
...@@ -89,8 +88,8 @@ class CourseUpdateTest(CourseTestCase): ...@@ -89,8 +88,8 @@ class CourseUpdateTest(CourseTestCase):
payload = {'content': content, payload = {'content': content,
'date': 'January 11, 2013'} 'date': 'January 11, 2013'}
url = reverse('course_info_json', kwargs={'org': self.course_location.org, url = reverse('course_info_json', kwargs={'org': self.course_location.org,
'course': self.course_location.course, 'course': self.course_location.course,
'provided_id': ''}) 'provided_id': ''})
self.assertContains( self.assertContains(
self.client.post(url, json.dumps(payload), "application/json"), self.client.post(url, json.dumps(payload), "application/json"),
...@@ -101,8 +100,8 @@ class CourseUpdateTest(CourseTestCase): ...@@ -101,8 +100,8 @@ class CourseUpdateTest(CourseTestCase):
payload = {'content': content, payload = {'content': content,
'date': 'January 11, 2013'} 'date': 'January 11, 2013'}
url = reverse('course_info_json', kwargs={'org': self.course_location.org, url = reverse('course_info_json', kwargs={'org': self.course_location.org,
'course': self.course_location.course, 'course': self.course_location.course,
'provided_id': ''}) 'provided_id': ''})
resp = self.client.post(url, json.dumps(payload), "application/json") resp = self.client.post(url, json.dumps(payload), "application/json")
payload = json.loads(resp.content) payload = json.loads(resp.content)
...@@ -110,8 +109,8 @@ class CourseUpdateTest(CourseTestCase): ...@@ -110,8 +109,8 @@ class CourseUpdateTest(CourseTestCase):
# now try to delete a non-existent update # now try to delete a non-existent update
url = reverse('course_info_json', kwargs={'org': self.course_location.org, url = reverse('course_info_json', kwargs={'org': self.course_location.org,
'course': self.course_location.course, 'course': self.course_location.course,
'provided_id': '19'}) 'provided_id': '19'})
payload = {'content': content, payload = {'content': content,
'date': 'January 21, 2013'} 'date': 'January 21, 2013'}
self.assertContains(self.client.delete(url), "delete", status_code=400) self.assertContains(self.client.delete(url), "delete", status_code=400)
...@@ -121,25 +120,25 @@ class CourseUpdateTest(CourseTestCase): ...@@ -121,25 +120,25 @@ class CourseUpdateTest(CourseTestCase):
payload = {'content': content, payload = {'content': content,
'date': 'January 28, 2013'} 'date': 'January 28, 2013'}
url = reverse('course_info_json', kwargs={'org': self.course_location.org, url = reverse('course_info_json', kwargs={'org': self.course_location.org,
'course': self.course_location.course, 'course': self.course_location.course,
'provided_id': ''}) 'provided_id': ''})
resp = self.client.post(url, json.dumps(payload), "application/json") resp = self.client.post(url, json.dumps(payload), "application/json")
payload = json.loads(resp.content) payload = json.loads(resp.content)
this_id = payload['id'] this_id = payload['id']
self.assertHTMLEqual(content, payload['content'], "single iframe") self.assertHTMLEqual(content, payload['content'], "single iframe")
# first count the entries # first count the entries
url = reverse('course_info_json', url = reverse('course_info_json',
kwargs={'org': self.course_location.org, kwargs={'org': self.course_location.org,
'course': self.course_location.course, 'course': self.course_location.course,
'provided_id': ''}) 'provided_id': ''})
resp = self.client.get(url) resp = self.client.get(url)
payload = json.loads(resp.content) payload = json.loads(resp.content)
before_delete = len(payload) before_delete = len(payload)
url = reverse('course_info_json', url = reverse('course_info_json',
kwargs={'org': self.course_location.org, kwargs={'org': self.course_location.org,
'course': self.course_location.course, 'course': self.course_location.course,
'provided_id': this_id}) 'provided_id': this_id})
resp = self.client.delete(url) resp = self.client.delete(url)
payload = json.loads(resp.content) payload = json.loads(resp.content)
self.assertTrue(len(payload) == before_delete - 1) self.assertTrue(len(payload) == before_delete - 1)
...@@ -6,6 +6,7 @@ from django.test.client import Client ...@@ -6,6 +6,7 @@ from django.test.client import Client
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
class InternationalizationTest(ModuleStoreTestCase): class InternationalizationTest(ModuleStoreTestCase):
""" """
Tests to validate Internationalization. Tests to validate Internationalization.
...@@ -38,34 +39,33 @@ class InternationalizationTest(ModuleStoreTestCase): ...@@ -38,34 +39,33 @@ class InternationalizationTest(ModuleStoreTestCase):
'org': 'MITx', 'org': 'MITx',
'number': '999', 'number': '999',
'display_name': 'Robot Super Course', 'display_name': 'Robot Super Course',
} }
def test_course_plain_english(self): def test_course_plain_english(self):
"""Test viewing the index page with no courses""" """Test viewing the index page with no courses"""
self.client = Client() self.client = Client()
self.client.login(username=self.uname, password=self.password) self.client.login(username=self.uname, password=self.password)
resp = self.client.get(reverse('index')) resp = self.client.get(reverse('index'))
self.assertContains(resp, self.assertContains(resp,
'<h1 class="title-1">My Courses</h1>', '<h1 class="page-header">My Courses</h1>',
status_code=200, status_code=200,
html=True) html=True)
def test_course_explicit_english(self): def test_course_explicit_english(self):
"""Test viewing the index page with no courses""" """Test viewing the index page with no courses"""
self.client = Client() self.client = Client()
self.client.login(username=self.uname, password=self.password) self.client.login(username=self.uname, password=self.password)
resp = self.client.get(reverse('index'), resp = self.client.get(reverse('index'),
{}, {},
HTTP_ACCEPT_LANGUAGE='en' HTTP_ACCEPT_LANGUAGE='en'
) )
self.assertContains(resp, self.assertContains(resp,
'<h1 class="title-1">My Courses</h1>', '<h1 class="page-header">My Courses</h1>',
status_code=200, status_code=200,
html=True) html=True)
# **** # ****
# NOTE: # NOTE:
...@@ -74,14 +74,13 @@ class InternationalizationTest(ModuleStoreTestCase): ...@@ -74,14 +74,13 @@ class InternationalizationTest(ModuleStoreTestCase):
# This test will break when we replace this fake 'test' language # This test will break when we replace this fake 'test' language
# with actual French. This test will need to be updated with # with actual French. This test will need to be updated with
# actual French at that time. # actual French at that time.
# Test temporarily disable since it depends on creation of dummy strings # Test temporarily disable since it depends on creation of dummy strings
@skip @skip
def test_course_with_accents (self): def test_course_with_accents(self):
"""Test viewing the index page with no courses""" """Test viewing the index page with no courses"""
self.client = Client() self.client = Client()
self.client.login(username=self.uname, password=self.password) self.client.login(username=self.uname, password=self.password)
resp = self.client.get(reverse('index'), resp = self.client.get(reverse('index'),
{}, {},
HTTP_ACCEPT_LANGUAGE='fr' HTTP_ACCEPT_LANGUAGE='fr'
...@@ -90,8 +89,8 @@ class InternationalizationTest(ModuleStoreTestCase): ...@@ -90,8 +89,8 @@ class InternationalizationTest(ModuleStoreTestCase):
TEST_STRING = u'<h1 class="title-1">' \ TEST_STRING = u'<h1 class="title-1">' \
+ u'My \xc7\xf6\xfcrs\xe9s L#' \ + u'My \xc7\xf6\xfcrs\xe9s L#' \
+ u'</h1>' + u'</h1>'
self.assertContains(resp, self.assertContains(resp,
TEST_STRING, TEST_STRING,
status_code=200, status_code=200,
html=True) html=True)
# pylint: disable=W0401, W0511
# Disable warnings about import from wildcard
# All files below declare exports with __all__
from .assets import *
from .checklist import *
from .component import *
from .course import *
from .error import *
from .item import *
from .preview import *
from .public import *
from .user import *
from .tabs import *
from .requests import *
from auth.authz import STAFF_ROLE_NAME, INSTRUCTOR_ROLE_NAME
from auth.authz import is_user_in_course_group_role
from django.core.exceptions import PermissionDenied
from ..utils import get_course_location_for_item
def get_location_and_verify_access(request, org, course, name):
"""
Create the location tuple verify that the user has permissions
to view the location. Returns the location.
"""
location = ['i4x', org, course, 'course', name]
# check that logged in user has permissions to this item
if not has_access(request.user, location):
raise PermissionDenied()
return location
def has_access(user, location, role=STAFF_ROLE_NAME):
'''
Return True if user allowed to access this piece of data
Note that the CMS permissions model is with respect to courses
There is a super-admin permissions if user.is_staff is set
Also, since we're unifying the user database between LMS and CAS,
I'm presuming that the course instructor (formally known as admin)
will not be in both INSTRUCTOR and STAFF groups, so we have to cascade our queries here as INSTRUCTOR
has all the rights that STAFF do
'''
course_location = get_course_location_for_item(location)
_has_access = is_user_in_course_group_role(user, course_location, role)
# if we're not in STAFF, perhaps we're in INSTRUCTOR groups
if not _has_access and role == STAFF_ROLE_NAME:
_has_access = is_user_in_course_group_role(user, course_location, INSTRUCTOR_ROLE_NAME)
return _has_access
import json
from django.http import HttpResponse, HttpResponseBadRequest
from django.contrib.auth.decorators import login_required
from django_future.csrf import ensure_csrf_cookie
from mitxmako.shortcuts import render_to_response
from xmodule.modulestore import Location
from xmodule.modulestore.inheritance import own_metadata
from ..utils import get_modulestore, get_url_reverse
from .requests import get_request_method
from .access import get_location_and_verify_access
__all__ = ['get_checklists', 'update_checklist']
@ensure_csrf_cookie
@login_required
def get_checklists(request, org, course, name):
"""
Send models, views, and html for displaying the course checklists.
org, course, name: Attributes of the Location for the item to edit
"""
location = get_location_and_verify_access(request, org, course, name)
modulestore = get_modulestore(location)
course_module = modulestore.get_item(location)
new_course_template = Location('i4x', 'edx', 'templates', 'course', 'Empty')
template_module = modulestore.get_item(new_course_template)
# If course was created before checklists were introduced, copy them over from the template.
copied = False
if not course_module.checklists:
course_module.checklists = template_module.checklists
copied = True
checklists, modified = expand_checklist_action_urls(course_module)
if copied or modified:
modulestore.update_metadata(location, own_metadata(course_module))
return render_to_response('checklists.html',
{
'context_course': course_module,
'checklists': checklists
})
@ensure_csrf_cookie
@login_required
def update_checklist(request, org, course, name, checklist_index=None):
"""
restful CRUD operations on course checklists. The payload is a json rep of
the modified checklist. For PUT or POST requests, the index of the
checklist being modified must be included; the returned payload will
be just that one checklist. For GET requests, the returned payload
is a json representation of the list of all checklists.
org, course, name: Attributes of the Location for the item to edit
"""
location = get_location_and_verify_access(request, org, course, name)
modulestore = get_modulestore(location)
course_module = modulestore.get_item(location)
real_method = get_request_method(request)
if real_method == 'POST' or real_method == 'PUT':
if checklist_index is not None and 0 <= int(checklist_index) < len(course_module.checklists):
index = int(checklist_index)
course_module.checklists[index] = json.loads(request.body)
checklists, modified = expand_checklist_action_urls(course_module)
modulestore.update_metadata(location, own_metadata(course_module))
return HttpResponse(json.dumps(checklists[index]), mimetype="application/json")
else:
return HttpResponseBadRequest(
"Could not save checklist state because the checklist index was out of range or unspecified.",
content_type="text/plain")
elif request.method == 'GET':
# In the JavaScript view initialize method, we do a fetch to get all the checklists.
checklists, modified = expand_checklist_action_urls(course_module)
if modified:
modulestore.update_metadata(location, own_metadata(course_module))
return HttpResponse(json.dumps(checklists), mimetype="application/json")
else:
return HttpResponseBadRequest("Unsupported request.", content_type="text/plain")
def expand_checklist_action_urls(course_module):
"""
Gets the checklists out of the course module and expands their action urls
if they have not yet been expanded.
Returns the checklists with modified urls, as well as a boolean
indicating whether or not the checklists were modified.
"""
checklists = course_module.checklists
modified = False
for checklist in checklists:
if not checklist.get('action_urls_expanded', False):
for item in checklist.get('items'):
item['action_url'] = get_url_reverse(item.get('action_url'), course_module)
checklist['action_urls_expanded'] = True
modified = True
return checklists, modified
from django.http import HttpResponseServerError, HttpResponseNotFound
from mitxmako.shortcuts import render_to_string, render_to_response
__all__ = ['not_found', 'server_error', 'render_404', 'render_500']
def not_found(request):
return render_to_response('error.html', {'error': '404'})
def server_error(request):
return render_to_response('error.html', {'error': '500'})
def render_404(request):
return HttpResponseNotFound(render_to_string('404.html', {}))
def render_500(request):
return HttpResponseServerError(render_to_string('500.html', {}))
import json
from uuid import uuid4
from django.core.exceptions import PermissionDenied
from django.http import HttpResponse
from django.contrib.auth.decorators import login_required
from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.inheritance import own_metadata
from util.json_request import expect_json
from ..utils import get_modulestore
from .access import has_access
from .requests import _xmodule_recurse
__all__ = ['save_item', 'clone_item', 'delete_item']
# cdodge: these are categories which should not be parented, they are detached from the hierarchy
DETACHED_CATEGORIES = ['about', 'static_tab', 'course_info']
@login_required
@expect_json
def save_item(request):
item_location = request.POST['id']
# check permissions for this user within this course
if not has_access(request.user, item_location):
raise PermissionDenied()
store = get_modulestore(Location(item_location))
if request.POST.get('data') is not None:
data = request.POST['data']
store.update_item(item_location, data)
# cdodge: note calling request.POST.get('children') will return None if children is an empty array
# so it lead to a bug whereby the last component to be deleted in the UI was not actually
# deleting the children object from the children collection
if 'children' in request.POST and request.POST['children'] is not None:
children = request.POST['children']
store.update_children(item_location, children)
# cdodge: also commit any metadata which might have been passed along in the
# POST from the client, if it is there
# NOTE, that the postback is not the complete metadata, as there's system metadata which is
# not presented to the end-user for editing. So let's fetch the original and
# 'apply' the submitted metadata, so we don't end up deleting system metadata
if request.POST.get('metadata') is not None:
posted_metadata = request.POST['metadata']
# fetch original
existing_item = modulestore().get_item(item_location)
# update existing metadata with submitted metadata (which can be partial)
# IMPORTANT NOTE: if the client passed pack 'null' (None) for a piece of metadata that means 'remove it'
for metadata_key, value in posted_metadata.items():
if posted_metadata[metadata_key] is None:
# remove both from passed in collection as well as the collection read in from the modulestore
if metadata_key in existing_item._model_data:
del existing_item._model_data[metadata_key]
del posted_metadata[metadata_key]
else:
existing_item._model_data[metadata_key] = value
# commit to datastore
# TODO (cpennington): This really shouldn't have to do this much reaching in to get the metadata
store.update_metadata(item_location, own_metadata(existing_item))
return HttpResponse()
@login_required
@expect_json
def clone_item(request):
parent_location = Location(request.POST['parent_location'])
template = Location(request.POST['template'])
display_name = request.POST.get('display_name')
if not has_access(request.user, parent_location):
raise PermissionDenied()
parent = get_modulestore(template).get_item(parent_location)
dest_location = parent_location._replace(category=template.category, name=uuid4().hex)
new_item = get_modulestore(template).clone_item(template, dest_location)
# replace the display name with an optional parameter passed in from the caller
if display_name is not None:
new_item.display_name = display_name
get_modulestore(template).update_metadata(new_item.location.url(), own_metadata(new_item))
if new_item.location.category not in DETACHED_CATEGORIES:
get_modulestore(parent.location).update_children(parent_location, parent.children + [new_item.location.url()])
return HttpResponse(json.dumps({'id': dest_location.url()}))
@login_required
@expect_json
def delete_item(request):
item_location = request.POST['id']
item_loc = Location(item_location)
# check permissions for this user within this course
if not has_access(request.user, item_location):
raise PermissionDenied()
# optional parameter to delete all children (default False)
delete_children = request.POST.get('delete_children', False)
delete_all_versions = request.POST.get('delete_all_versions', False)
store = modulestore()
item = store.get_item(item_location)
if delete_children:
_xmodule_recurse(item, lambda i: store.delete_item(i.location, delete_all_versions))
else:
store.delete_item(item.location, delete_all_versions)
# cdodge: we need to remove our parent's pointer to us so that it is no longer dangling
if delete_all_versions:
parent_locs = modulestore('direct').get_parent_locations(item_loc, None)
for parent_loc in parent_locs:
parent = modulestore('direct').get_item(parent_loc)
item_url = item_loc.url()
if item_url in parent.children:
children = parent.children
children.remove(item_url)
parent.children = children
modulestore('direct').update_children(parent.location, parent.children)
return HttpResponse()
import logging
import sys
from functools import partial
from django.http import HttpResponse, Http404, HttpResponseBadRequest, HttpResponseForbidden
from django.core.urlresolvers import reverse
from django.contrib.auth.decorators import login_required
from mitxmako.shortcuts import render_to_response
from xmodule_modifiers import replace_static_urls, wrap_xmodule
from xmodule.error_module import ErrorDescriptor
from xmodule.errortracker import exc_info_to_str
from xmodule.exceptions import NotFoundError, ProcessingError
from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.mongo import MongoUsage
from xmodule.x_module import ModuleSystem
from xblock.runtime import DbModel
import static_replace
from .session_kv_store import SessionKeyValueStore
from .requests import render_from_lms
from .access import has_access
__all__ = ['preview_dispatch', 'preview_component']
log = logging.getLogger(__name__)
@login_required
def preview_dispatch(request, preview_id, location, dispatch=None):
"""
Dispatch an AJAX action to a preview XModule
Expects a POST request, and passes the arguments to the module
preview_id (str): An identifier specifying which preview this module is used for
location: The Location of the module to dispatch to
dispatch: The action to execute
"""
descriptor = modulestore().get_item(location)
instance = load_preview_module(request, preview_id, descriptor)
# Let the module handle the AJAX
try:
ajax_return = instance.handle_ajax(dispatch, request.POST)
except NotFoundError:
log.exception("Module indicating to user that request doesn't exist")
raise Http404
except ProcessingError:
log.warning("Module raised an error while processing AJAX request",
exc_info=True)
return HttpResponseBadRequest()
except:
log.exception("error processing ajax call")
raise
return HttpResponse(ajax_return)
@login_required
def preview_component(request, location):
# TODO (vshnayder): change name from id to location in coffee+html as well.
if not has_access(request.user, location):
raise HttpResponseForbidden()
component = modulestore().get_item(location)
return render_to_response('component.html', {
'preview': get_module_previews(request, component)[0],
'editor': wrap_xmodule(component.get_html, component, 'xmodule_edit.html')(),
})
def preview_module_system(request, preview_id, descriptor):
"""
Returns a ModuleSystem for the specified descriptor that is specialized for
rendering module previews.
request: The active django request
preview_id (str): An identifier specifying which preview this module is used for
descriptor: An XModuleDescriptor
"""
def preview_model_data(descriptor):
return DbModel(
SessionKeyValueStore(request, descriptor._model_data),
descriptor.module_class,
preview_id,
MongoUsage(preview_id, descriptor.location.url()),
)
return ModuleSystem(
ajax_url=reverse('preview_dispatch', args=[preview_id, descriptor.location.url(), '']).rstrip('/'),
# TODO (cpennington): Do we want to track how instructors are using the preview problems?
track_function=lambda event_type, event: None,
filestore=descriptor.system.resources_fs,
get_module=partial(get_preview_module, request, preview_id),
render_template=render_from_lms,
debug=True,
replace_urls=partial(static_replace.replace_static_urls, data_directory=None, course_namespace=descriptor.location),
user=request.user,
xblock_model_data=preview_model_data,
)
def get_preview_module(request, preview_id, descriptor):
"""
Returns a preview XModule at the specified location. The preview_data is chosen arbitrarily
from the set of preview data for the descriptor specified by Location
request: The active django request
preview_id (str): An identifier specifying which preview this module is used for
location: A Location
"""
return load_preview_module(request, preview_id, descriptor)
def load_preview_module(request, preview_id, descriptor):
"""
Return a preview XModule instantiated from the supplied descriptor, instance_state, and shared_state
request: The active django request
preview_id (str): An identifier specifying which preview this module is used for
descriptor: An XModuleDescriptor
instance_state: An instance state string
shared_state: A shared state string
"""
system = preview_module_system(request, preview_id, descriptor)
try:
module = descriptor.xmodule(system)
except:
log.debug("Unable to load preview module", exc_info=True)
module = ErrorDescriptor.from_descriptor(
descriptor,
error_msg=exc_info_to_str(sys.exc_info())
).xmodule(system)
# cdodge: Special case
if module.location.category == 'static_tab':
module.get_html = wrap_xmodule(
module.get_html,
module,
"xmodule_tab_display.html",
)
else:
module.get_html = wrap_xmodule(
module.get_html,
module,
"xmodule_display.html",
)
module.get_html = replace_static_urls(
module.get_html,
getattr(module, 'data_dir', module.location.course),
course_namespace=Location([module.location.tag, module.location.org, module.location.course, None, None])
)
return module
def get_module_previews(request, descriptor):
"""
Returns a list of preview XModule html contents. One preview is returned for each
pair of states returned by get_sample_state() for the supplied descriptor.
descriptor: An XModuleDescriptor
"""
preview_html = []
for idx, (_instance_state, _shared_state) in enumerate(descriptor.get_sample_state()):
module = load_preview_module(request, str(idx), descriptor)
preview_html.append(module.get_html())
return preview_html
from django_future.csrf import ensure_csrf_cookie
from django.core.context_processors import csrf
from django.shortcuts import redirect
from django.conf import settings
from mitxmako.shortcuts import render_to_response
from external_auth.views import ssl_login_shortcut
from .user import index
__all__ = ['signup', 'old_login_redirect', 'login_page', 'howitworks', 'ux_alerts']
"""
Public views
"""
@ensure_csrf_cookie
def signup(request):
"""
Display the signup form.
"""
csrf_token = csrf(request)['csrf_token']
return render_to_response('signup.html', {'csrf': csrf_token})
def old_login_redirect(request):
'''
Redirect to the active login url.
'''
return redirect('login', permanent=True)
@ssl_login_shortcut
@ensure_csrf_cookie
def login_page(request):
"""
Display the login form.
"""
csrf_token = csrf(request)['csrf_token']
return render_to_response('login.html', {
'csrf': csrf_token,
'forgot_password_link': "//{base}/#forgot-password-modal".format(base=settings.LMS_BASE),
})
def howitworks(request):
if request.user.is_authenticated():
return index(request)
else:
return render_to_response('howitworks.html', {})
def ux_alerts(request):
"""
static/proof-of-concept views
"""
return render_to_response('ux-alerts.html', {})
import json
from django.http import HttpResponse
from mitxmako.shortcuts import render_to_string, render_to_response
__all__ = ['edge', 'event', 'landing']
# points to the temporary course landing page with log in and sign up
def landing(request, org, course, coursename):
return render_to_response('temp-course-landing.html', {})
# points to the temporary edge page
def edge(request):
return render_to_response('university_profiles/edge.html', {})
def event(request):
'''
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 :-)
'''
return HttpResponse(True)
def get_request_method(request):
"""
Using HTTP_X_HTTP_METHOD_OVERRIDE, in the request metadata, determine
what type of request came from the client, and return it.
"""
# NB: we're setting Backbone.emulateHTTP to true on the client so everything comes as a post!!!
if request.method == 'POST' and 'HTTP_X_HTTP_METHOD_OVERRIDE' in request.META:
real_method = request.META['HTTP_X_HTTP_METHOD_OVERRIDE']
else:
real_method = request.method
return real_method
def create_json_response(errmsg=None):
if errmsg is not None:
resp = HttpResponse(json.dumps({'Status': 'Failed', 'ErrMsg': errmsg}))
else:
resp = HttpResponse(json.dumps({'Status': 'OK'}))
return resp
def render_from_lms(template_name, dictionary, context=None, namespace='main'):
"""
Render a template using the LMS MAKO_TEMPLATES
"""
return render_to_string(template_name, dictionary, context, namespace="lms." + namespace)
def _xmodule_recurse(item, action):
for child in item.get_children():
_xmodule_recurse(child, action)
action(item)
from xblock.runtime import KeyValueStore, InvalidScopeError
class SessionKeyValueStore(KeyValueStore):
def __init__(self, request, model_data):
self._model_data = model_data
self._session = request.session
def get(self, key):
try:
return self._model_data[key.field_name]
except (KeyError, InvalidScopeError):
return self._session[tuple(key)]
def set(self, key, value):
try:
self._model_data[key.field_name] = value
except (KeyError, InvalidScopeError):
self._session[tuple(key)] = value
def delete(self, key):
try:
del self._model_data[key.field_name]
except (KeyError, InvalidScopeError):
del self._session[tuple(key)]
def has(self, key):
return key in self._model_data or key in self._session
from access import has_access
from util.json_request import expect_json
from django.http import HttpResponse, HttpResponseBadRequest
from django.contrib.auth.decorators import login_required
from django.core.exceptions import PermissionDenied
from django_future.csrf import ensure_csrf_cookie
from mitxmako.shortcuts import render_to_response
from xmodule.modulestore import Location
from xmodule.modulestore.inheritance import own_metadata
from xmodule.modulestore.django import modulestore
from ..utils import get_course_for_item
from .access import get_location_and_verify_access
__all__ = ['edit_tabs', 'reorder_static_tabs', 'static_pages', 'edit_static']
def initialize_course_tabs(course):
# set up the default tabs
# I've added this because when we add static tabs, the LMS either expects a None for the tabs list or
# at least a list populated with the minimal times
# @TODO: I don't like the fact that the presentation tier is away of these data related constraints, let's find a better
# place for this. Also rather than using a simple list of dictionaries a nice class model would be helpful here
# This logic is repeated in xmodule/modulestore/tests/factories.py
# so if you change anything here, you need to also change it there.
course.tabs = [{"type": "courseware"},
{"type": "course_info", "name": "Course Info"},
{"type": "discussion", "name": "Discussion"},
{"type": "wiki", "name": "Wiki"},
{"type": "progress", "name": "Progress"}]
modulestore('direct').update_metadata(course.location.url(), own_metadata(course))
@login_required
@expect_json
def reorder_static_tabs(request):
tabs = request.POST['tabs']
course = get_course_for_item(tabs[0])
if not has_access(request.user, course.location):
raise PermissionDenied()
# get list of existing static tabs in course
# make sure they are the same lengths (i.e. the number of passed in tabs equals the number
# that we know about) otherwise we can drop some!
existing_static_tabs = [t for t in course.tabs if t['type'] == 'static_tab']
if len(existing_static_tabs) != len(tabs):
return HttpResponseBadRequest()
# load all reference tabs, return BadRequest if we can't find any of them
tab_items = []
for tab in tabs:
item = modulestore('direct').get_item(Location(tab))
if item is None:
return HttpResponseBadRequest()
tab_items.append(item)
# now just go through the existing course_tabs and re-order the static tabs
reordered_tabs = []
static_tab_idx = 0
for tab in course.tabs:
if tab['type'] == 'static_tab':
reordered_tabs.append({'type': 'static_tab',
'name': tab_items[static_tab_idx].display_name,
'url_slug': tab_items[static_tab_idx].location.name})
static_tab_idx += 1
else:
reordered_tabs.append(tab)
# OK, re-assemble the static tabs in the new order
course.tabs = reordered_tabs
modulestore('direct').update_metadata(course.location, own_metadata(course))
return HttpResponse()
@login_required
@ensure_csrf_cookie
def edit_tabs(request, org, course, coursename):
location = ['i4x', org, course, 'course', coursename]
course_item = modulestore().get_item(location)
# check that logged in user has permissions to this item
if not has_access(request.user, location):
raise PermissionDenied()
# see tabs have been uninitialized (e.g. supporing courses created before tab support in studio)
if course_item.tabs is None or len(course_item.tabs) == 0:
initialize_course_tabs(course_item)
# first get all static tabs from the tabs list
# we do this because this is also the order in which items are displayed in the LMS
static_tabs_refs = [t for t in course_item.tabs if t['type'] == 'static_tab']
static_tabs = []
for static_tab_ref in static_tabs_refs:
static_tab_loc = Location(location)._replace(category='static_tab', name=static_tab_ref['url_slug'])
static_tabs.append(modulestore('direct').get_item(static_tab_loc))
components = [
static_tab.location.url()
for static_tab
in static_tabs
]
return render_to_response('edit-tabs.html', {
'active_tab': 'pages',
'context_course': course_item,
'components': components
})
@login_required
@ensure_csrf_cookie
def static_pages(request, org, course, coursename):
location = get_location_and_verify_access(request, org, course, coursename)
course = modulestore().get_item(location)
return render_to_response('static-pages.html', {
'active_tab': 'pages',
'context_course': course,
})
def edit_static(request, org, course, coursename):
return render_to_response('edit-static-page.html', {})
from django.conf import settings
from django.core.exceptions import PermissionDenied
from django.core.urlresolvers import reverse
from django.contrib.auth.decorators import login_required
from django_future.csrf import ensure_csrf_cookie
from mitxmako.shortcuts import render_to_response
from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore
from contentstore.utils import get_url_reverse, get_lms_link_for_item
from util.json_request import expect_json
from auth.authz import STAFF_ROLE_NAME, INSTRUCTOR_ROLE_NAME, get_users_in_course_group_by_role
from auth.authz import get_user_by_email, add_user_to_course_group, remove_user_from_course_group
from .access import has_access
from .requests import create_json_response
def user_author_string(user):
'''Get an author string for commits by this user. Format:
first last <email@email.com>.
If the first and last names are blank, uses the username instead.
Assumes that the email is not blank.
'''
f = user.first_name
l = user.last_name
if f == '' and l == '':
f = user.username
return '{first} {last} <{email}>'.format(first=f,
last=l,
email=user.email)
@login_required
@ensure_csrf_cookie
def index(request):
"""
List all courses available to the logged in user
"""
courses = modulestore('direct').get_items(['i4x', None, None, 'course', None])
# filter out courses that we don't have access too
def course_filter(course):
return (has_access(request.user, course.location)
and course.location.course != 'templates'
and course.location.org != ''
and course.location.course != ''
and course.location.name != '')
courses = filter(course_filter, courses)
return render_to_response('index.html', {
'new_course_template': Location('i4x', 'edx', 'templates', 'course', 'Empty'),
'courses': [(course.display_name,
get_url_reverse('CourseOutline', course),
get_lms_link_for_item(course.location, course_id=course.location.course_id))
for course in courses],
'user': request.user,
'disable_course_creation': settings.MITX_FEATURES.get('DISABLE_COURSE_CREATION', False) and not request.user.is_staff
})
@login_required
@ensure_csrf_cookie
def manage_users(request, location):
'''
This view will return all CMS users who are editors for the specified course
'''
# check that logged in user has permissions to this item
if not has_access(request.user, location, role=INSTRUCTOR_ROLE_NAME) and not has_access(request.user, location, role=STAFF_ROLE_NAME):
raise PermissionDenied()
course_module = modulestore().get_item(location)
return render_to_response('manage_users.html', {
'active_tab': 'users',
'context_course': course_module,
'staff': get_users_in_course_group_by_role(location, STAFF_ROLE_NAME),
'add_user_postback_url': reverse('add_user', args=[location]).rstrip('/'),
'remove_user_postback_url': reverse('remove_user', args=[location]).rstrip('/'),
'allow_actions': has_access(request.user, location, role=INSTRUCTOR_ROLE_NAME),
'request_user_id': request.user.id
})
@expect_json
@login_required
@ensure_csrf_cookie
def add_user(request, location):
'''
This POST-back view will add a user - specified by email - to the list of editors for
the specified course
'''
email = request.POST["email"]
if email == '':
return create_json_response('Please specify an email address.')
# check that logged in user has admin permissions to this course
if not has_access(request.user, location, role=INSTRUCTOR_ROLE_NAME):
raise PermissionDenied()
user = get_user_by_email(email)
# user doesn't exist?!? Return error.
if user is None:
return create_json_response('Could not find user by email address \'{0}\'.'.format(email))
# user exists, but hasn't activated account?!?
if not user.is_active:
return create_json_response('User {0} has registered but has not yet activated his/her account.'.format(email))
# ok, we're cool to add to the course group
add_user_to_course_group(request.user, user, location, STAFF_ROLE_NAME)
return create_json_response()
@expect_json
@login_required
@ensure_csrf_cookie
def remove_user(request, location):
'''
This POST-back view will remove a user - specified by email - from the list of editors for
the specified course
'''
email = request.POST["email"]
# check that logged in user has admin permissions on this course
if not has_access(request.user, location, role=INSTRUCTOR_ROLE_NAME):
raise PermissionDenied()
user = get_user_by_email(email)
if user is None:
return create_json_response('Could not find user by email address \'{0}\'.'.format(email))
# make sure we're not removing ourselves
if user.id == request.user.id:
raise PermissionDenied()
remove_user_from_course_group(request.user, user, location, STAFF_ROLE_NAME)
return create_json_response()
...@@ -14,13 +14,14 @@ class CourseMetadata(object): ...@@ -14,13 +14,14 @@ class CourseMetadata(object):
The objects have no predefined attrs but instead are obj encodings of the The objects have no predefined attrs but instead are obj encodings of the
editable metadata. editable metadata.
''' '''
FILTERED_LIST = XModuleDescriptor.system_metadata_fields + ['start', FILTERED_LIST = ['xml_attributes',
'end', 'start',
'enrollment_start', 'end',
'enrollment_end', 'enrollment_start',
'tabs', 'enrollment_end',
'graceperiod', 'tabs',
'checklists'] 'graceperiod',
'checklists']
@classmethod @classmethod
def fetch(cls, course_location): def fetch(cls, course_location):
......
...@@ -8,27 +8,41 @@ from .test import * ...@@ -8,27 +8,41 @@ from .test import *
# 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': {
# 'ENGINE': 'xmodule.modulestore.xml.XMLModuleStore',
# 'OPTIONS': {
# 'data_dir': DATA_DIR,
# 'default_class': 'xmodule.hidden_module.HiddenDescriptor',
# }
# }
# }
MODULESTORE_OPTIONS = {
'default_class': 'xmodule.raw_module.RawDescriptor',
'host': 'localhost',
'db': 'test_xmodule',
'collection': 'acceptance_modulestore',
'fs_root': TEST_ROOT / "data",
'render_template': 'mitxmako.shortcuts.render_to_string',
}
MODULESTORE = {
'default': {
'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore',
'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",
} }
} }
......
...@@ -43,6 +43,16 @@ CACHES = ENV_TOKENS['CACHES'] ...@@ -43,6 +43,16 @@ CACHES = ENV_TOKENS['CACHES']
SESSION_COOKIE_DOMAIN = ENV_TOKENS.get('SESSION_COOKIE_DOMAIN') SESSION_COOKIE_DOMAIN = ENV_TOKENS.get('SESSION_COOKIE_DOMAIN')
#Email overrides
DEFAULT_FROM_EMAIL = ENV_TOKENS.get('DEFAULT_FROM_EMAIL', DEFAULT_FROM_EMAIL)
DEFAULT_FEEDBACK_EMAIL = ENV_TOKENS.get('DEFAULT_FEEDBACK_EMAIL', DEFAULT_FEEDBACK_EMAIL)
ADMINS = ENV_TOKENS.get('ADMINS', ADMINS)
SERVER_EMAIL = ENV_TOKENS.get('SERVER_EMAIL', SERVER_EMAIL)
#Timezone overrides
TIME_ZONE = ENV_TOKENS.get('TIME_ZONE', TIME_ZONE)
for feature, value in ENV_TOKENS.get('MITX_FEATURES', {}).items(): for feature, value in ENV_TOKENS.get('MITX_FEATURES', {}).items():
MITX_FEATURES[feature] = value MITX_FEATURES[feature] = value
......
...@@ -152,6 +152,7 @@ IGNORABLE_404_ENDS = ('favicon.ico') ...@@ -152,6 +152,7 @@ IGNORABLE_404_ENDS = ('favicon.ico')
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
DEFAULT_FROM_EMAIL = 'registration@edx.org' DEFAULT_FROM_EMAIL = 'registration@edx.org'
DEFAULT_FEEDBACK_EMAIL = 'feedback@edx.org' DEFAULT_FEEDBACK_EMAIL = 'feedback@edx.org'
SERVER_EMAIL = 'devops@edx.org'
ADMINS = ( ADMINS = (
('edX Admins', 'admin@edx.org'), ('edX Admins', 'admin@edx.org'),
) )
......
...@@ -33,7 +33,7 @@ PIPELINE_JS['spec'] = { ...@@ -33,7 +33,7 @@ PIPELINE_JS['spec'] = {
JASMINE_TEST_DIRECTORY = PROJECT_ROOT + '/static/coffee' JASMINE_TEST_DIRECTORY = PROJECT_ROOT + '/static/coffee'
STATICFILES_DIRS.append(COMMON_ROOT / 'test' / 'phantom-jasmine' / 'lib') STATICFILES_DIRS.append(REPO_ROOT/'node_modules/phantom-jasmine/lib')
# 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,8 +41,8 @@ MODULESTORE_OPTIONS = { ...@@ -41,8 +41,8 @@ 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',
} }
......
...@@ -72,3 +72,14 @@ describe "CMS.Views.ModuleEdit", -> ...@@ -72,3 +72,14 @@ describe "CMS.Views.ModuleEdit", ->
it "loads the .xmodule-display inside the module editor", -> it "loads the .xmodule-display inside the module editor", ->
expect(XModule.loadModule).toHaveBeenCalled() expect(XModule.loadModule).toHaveBeenCalled()
expect(XModule.loadModule.mostRecentCall.args[0]).toBe($('.xmodule_display')) expect(XModule.loadModule.mostRecentCall.args[0]).toBe($('.xmodule_display'))
describe "changedMetadata", ->
it "returns empty if no metadata loaded", ->
expect(@moduleEdit.changedMetadata()).toEqual({})
it "returns only changed values", ->
@moduleEdit.originalMetadata = {'foo', 'bar'}
spyOn(@moduleEdit, 'metadata').andReturn({'a': '', 'b': 'before', 'c': ''})
@moduleEdit.loadEdit()
@moduleEdit.metadata.andReturn({'a': '', 'b': 'after', 'd': 'only_after'})
expect(@moduleEdit.changedMetadata()).toEqual({'b' : 'after', 'd' : 'only_after'})
...@@ -20,6 +20,7 @@ class CMS.Views.ModuleEdit extends Backbone.View ...@@ -20,6 +20,7 @@ class CMS.Views.ModuleEdit extends Backbone.View
loadEdit: -> loadEdit: ->
if not @module if not @module
@module = XModule.loadModule(@$el.find('.xmodule_edit')) @module = XModule.loadModule(@$el.find('.xmodule_edit'))
@originalMetadata = @metadata()
metadata: -> metadata: ->
# cdodge: package up metadata which is separated into a number of input fields # cdodge: package up metadata which is separated into a number of input fields
...@@ -35,6 +36,14 @@ class CMS.Views.ModuleEdit extends Backbone.View ...@@ -35,6 +36,14 @@ class CMS.Views.ModuleEdit extends Backbone.View
return _metadata return _metadata
changedMetadata: ->
currentMetadata = @metadata()
changedMetadata = {}
for key of currentMetadata
if currentMetadata[key] != @originalMetadata[key]
changedMetadata[key] = currentMetadata[key]
return changedMetadata
cloneTemplate: (parent, template) -> cloneTemplate: (parent, template) ->
$.post("/clone_item", { $.post("/clone_item", {
parent_location: parent parent_location: parent
...@@ -60,7 +69,7 @@ class CMS.Views.ModuleEdit extends Backbone.View ...@@ -60,7 +69,7 @@ class CMS.Views.ModuleEdit extends Backbone.View
course: course_location_analytics course: course_location_analytics
id: _this.model.id id: _this.model.id
data.metadata = _.extend(data.metadata || {}, @metadata()) data.metadata = _.extend(data.metadata || {}, @changedMetadata())
@hideModal() @hideModal()
@model.save(data).done( => @model.save(data).done( =>
# # showToastMessage("Your changes have been saved.", null, 3) # # showToastMessage("Your changes have been saved.", null, 3)
......
...@@ -895,4 +895,4 @@ function saveSetSectionScheduleDate(e) { ...@@ -895,4 +895,4 @@ function saveSetSectionScheduleDate(e) {
hideModal(); hideModal();
}); });
} }
\ No newline at end of file
...@@ -2,13 +2,13 @@ ...@@ -2,13 +2,13 @@
* Create a HesitateEvent and assign it as the event to execute: * Create a HesitateEvent and assign it as the event to execute:
* $(el).on('mouseEnter', CMS.HesitateEvent( expand, 'mouseLeave').trigger); * $(el).on('mouseEnter', CMS.HesitateEvent( expand, 'mouseLeave').trigger);
* It calls the executeOnTimeOut function with the event.currentTarget after the configurable timeout IFF the cancelSelector event * It calls the executeOnTimeOut function with the event.currentTarget after the configurable timeout IFF the cancelSelector event
* did not occur on the event.currentTarget. * did not occur on the event.currentTarget.
* *
* More specifically, when trigger is called (triggered by the event you bound it to), it starts a timer * More specifically, when trigger is called (triggered by the event you bound it to), it starts a timer
* which the cancelSelector event will cancel or if the timer finished, it executes the executeOnTimeOut function * which the cancelSelector event will cancel or if the timer finished, it executes the executeOnTimeOut function
* passing it the original event (whose currentTarget s/b the specific ele). It never accumulates events; however, it doesn't hurt for your * passing it the original event (whose currentTarget s/b the specific ele). It never accumulates events; however, it doesn't hurt for your
* code to minimize invocations of trigger by binding to mouseEnter v mouseOver and such. * code to minimize invocations of trigger by binding to mouseEnter v mouseOver and such.
* *
* NOTE: if something outside of this wants to cancel the event, invoke cachedhesitation.untrigger(null | anything); * NOTE: if something outside of this wants to cancel the event, invoke cachedhesitation.untrigger(null | anything);
*/ */
...@@ -25,7 +25,7 @@ CMS.HesitateEvent.DURATION = 800; ...@@ -25,7 +25,7 @@ CMS.HesitateEvent.DURATION = 800;
CMS.HesitateEvent.prototype.trigger = function(event) { CMS.HesitateEvent.prototype.trigger = function(event) {
if (event.data.timeoutEventId == null) { if (event.data.timeoutEventId == null) {
event.data.timeoutEventId = window.setTimeout( event.data.timeoutEventId = window.setTimeout(
function() { event.data.fireEvent(event); }, function() { event.data.fireEvent(event); },
CMS.HesitateEvent.DURATION); CMS.HesitateEvent.DURATION);
event.data.originalEvent = event; event.data.originalEvent = event;
$(event.data.originalEvent.delegateTarget).on(event.data.cancelSelector, event.data, event.data.untrigger); $(event.data.originalEvent.delegateTarget).on(event.data.cancelSelector, event.data, event.data.untrigger);
...@@ -45,4 +45,4 @@ CMS.HesitateEvent.prototype.untrigger = function(event) { ...@@ -45,4 +45,4 @@ CMS.HesitateEvent.prototype.untrigger = function(event) {
$(event.data.originalEvent.delegateTarget).off(event.data.cancelSelector, event.data.untrigger); $(event.data.originalEvent.delegateTarget).off(event.data.cancelSelector, event.data.untrigger);
} }
event.data.timeoutEventId = null; event.data.timeoutEventId = null;
}; };
\ No newline at end of file
...@@ -80,6 +80,6 @@ $(document).ready(function(){ ...@@ -80,6 +80,6 @@ $(document).ready(function(){
$('section.problem-edit').show(); $('section.problem-edit').show();
return false; return false;
}); });
}); });
// single per course holds the updates and handouts // single per course holds the updates and handouts
CMS.Models.CourseInfo = Backbone.Model.extend({ CMS.Models.CourseInfo = Backbone.Model.extend({
// This model class is not suited for restful operations and is considered just a server side initialized container // This model class is not suited for restful operations and is considered just a server side initialized container
url: '', url: '',
defaults: { defaults: {
"courseId": "", // the location url "courseId": "", // the location url
"updates" : null, // UpdateCollection "updates" : null, // UpdateCollection
"handouts": null // HandoutCollection "handouts": null // HandoutCollection
}, },
idAttribute : "courseId" idAttribute : "courseId"
}); });
// course update -- biggest kludge here is the lack of a real id to map updates to originals // course update -- biggest kludge here is the lack of a real id to map updates to originals
CMS.Models.CourseUpdate = Backbone.Model.extend({ CMS.Models.CourseUpdate = Backbone.Model.extend({
defaults: { defaults: {
...@@ -26,11 +26,11 @@ CMS.Models.CourseUpdate = Backbone.Model.extend({ ...@@ -26,11 +26,11 @@ CMS.Models.CourseUpdate = Backbone.Model.extend({
*/ */
CMS.Models.CourseUpdateCollection = Backbone.Collection.extend({ CMS.Models.CourseUpdateCollection = Backbone.Collection.extend({
url : function() {return this.urlbase + "course_info/updates/";}, url : function() {return this.urlbase + "course_info/updates/";},
model : CMS.Models.CourseUpdate model : CMS.Models.CourseUpdate
}); });
\ No newline at end of file
...@@ -16,7 +16,7 @@ CMS.Models.Location = Backbone.Model.extend({ ...@@ -16,7 +16,7 @@ CMS.Models.Location = Backbone.Model.extend({
}, },
_tagPattern : /[^:]+/g, _tagPattern : /[^:]+/g,
_fieldPattern : new RegExp('[^/]+','g'), _fieldPattern : new RegExp('[^/]+','g'),
parse: function(payload) { parse: function(payload) {
if (_.isArray(payload)) { if (_.isArray(payload)) {
return { return {
...@@ -25,7 +25,7 @@ CMS.Models.Location = Backbone.Model.extend({ ...@@ -25,7 +25,7 @@ CMS.Models.Location = Backbone.Model.extend({
course: payload[2], course: payload[2],
category: payload[3], category: payload[3],
name: payload[4] name: payload[4]
} };
} }
else if (_.isString(payload)) { else if (_.isString(payload)) {
this._tagPattern.lastIndex = 0; // odd regex behavior requires this to be reset sometimes this._tagPattern.lastIndex = 0; // odd regex behavior requires this to be reset sometimes
...@@ -65,4 +65,4 @@ CMS.Models.CourseRelative = Backbone.Model.extend({ ...@@ -65,4 +65,4 @@ CMS.Models.CourseRelative = Backbone.Model.extend({
CMS.Models.CourseRelativeCollection = Backbone.Collection.extend({ CMS.Models.CourseRelativeCollection = Backbone.Collection.extend({
model : CMS.Models.CourseRelative model : CMS.Models.CourseRelative
}); });
\ No newline at end of file
...@@ -6,5 +6,5 @@ CMS.Models.ModuleInfo = Backbone.Model.extend({ ...@@ -6,5 +6,5 @@ CMS.Models.ModuleInfo = Backbone.Model.extend({
"data": null, "data": null,
"metadata" : null, "metadata" : null,
"children" : null "children" : null
}, }
}); });
\ No newline at end of file
...@@ -11,7 +11,7 @@ CMS.Models.Settings.Advanced = Backbone.Model.extend({ ...@@ -11,7 +11,7 @@ CMS.Models.Settings.Advanced = Backbone.Model.extend({
validate: function (attrs) { validate: function (attrs) {
// Keys can no longer be edited. We are currently not validating values. // Keys can no longer be edited. We are currently not validating values.
}, },
save : function (attrs, options) { save : function (attrs, options) {
// wraps the save call w/ the deletion of the removed keys after we know the saved ones worked // wraps the save call w/ the deletion of the removed keys after we know the saved ones worked
options = options ? _.clone(options) : {}; options = options ? _.clone(options) : {};
...@@ -23,7 +23,7 @@ CMS.Models.Settings.Advanced = Backbone.Model.extend({ ...@@ -23,7 +23,7 @@ CMS.Models.Settings.Advanced = Backbone.Model.extend({
}; };
Backbone.Model.prototype.save.call(this, attrs, options); Backbone.Model.prototype.save.call(this, attrs, options);
}, },
afterSave : function(self) { afterSave : function(self) {
// remove deleted attrs // remove deleted attrs
if (!_.isEmpty(self.deleteKeys)) { if (!_.isEmpty(self.deleteKeys)) {
......
...@@ -66,7 +66,7 @@ CMS.Models.Settings.CourseDetails = Backbone.Model.extend({ ...@@ -66,7 +66,7 @@ CMS.Models.Settings.CourseDetails = Backbone.Model.extend({
save_videosource: function(newsource) { save_videosource: function(newsource) {
// newsource either is <video youtube="speed:key, *"/> or just the "speed:key, *" string // newsource either is <video youtube="speed:key, *"/> or just the "speed:key, *" string
// returns the videosource for the preview which iss the key whose speed is closest to 1 // returns the videosource for the preview which iss the key whose speed is closest to 1
if (_.isEmpty(newsource) && !_.isEmpty(this.get('intro_video'))) this.save({'intro_video': null}); if (_.isEmpty(newsource) && !_.isEmpty(this.get('intro_video'))) this.save({'intro_video': null});
// TODO remove all whitespace w/in string // TODO remove all whitespace w/in string
else { else {
if (this.get('intro_video') !== newsource) this.save('intro_video', newsource); if (this.get('intro_video') !== newsource) this.save('intro_video', newsource);
......
if (!CMS.Models['Settings']) CMS.Models.Settings = new Object(); if (!CMS.Models['Settings']) CMS.Models.Settings = new Object();
CMS.Models.Settings.CourseGradingPolicy = Backbone.Model.extend({ CMS.Models.Settings.CourseGradingPolicy = Backbone.Model.extend({
defaults : { defaults : {
course_location : null, course_location : null,
graders : null, // CourseGraderCollection graders : null, // CourseGraderCollection
grade_cutoffs : null, // CourseGradeCutoff model grade_cutoffs : null, // CourseGradeCutoff model
grace_period : null // either null or { hours: n, minutes: m, ...} grace_period : null // either null or { hours: n, minutes: m, ...}
}, },
...@@ -54,7 +54,7 @@ CMS.Models.Settings.CourseGrader = Backbone.Model.extend({ ...@@ -54,7 +54,7 @@ CMS.Models.Settings.CourseGrader = Backbone.Model.extend({
"type" : "", // must be unique w/in collection (ie. w/in course) "type" : "", // must be unique w/in collection (ie. w/in course)
"min_count" : 1, "min_count" : 1,
"drop_count" : 0, "drop_count" : 0,
"short_label" : "", // what to use in place of type if space is an issue "short_label" : "", // what to use in place of type if space is an issue
"weight" : 0 // int 0..100 "weight" : 0 // int 0..100
}, },
parse : function(attrs) { parse : function(attrs) {
...@@ -125,4 +125,4 @@ CMS.Models.Settings.CourseGraderCollection = Backbone.Collection.extend({ ...@@ -125,4 +125,4 @@ CMS.Models.Settings.CourseGraderCollection = Backbone.Collection.extend({
sumWeights : function() { sumWeights : function() {
return this.reduce(function(subtotal, grader) { return subtotal + grader.get('weight'); }, 0); return this.reduce(function(subtotal, grader) { return subtotal + grader.get('weight'); }, 0);
} }
}); });
\ No newline at end of file
...@@ -22,6 +22,7 @@ CMS.Views.Checklists = Backbone.View.extend({ ...@@ -22,6 +22,7 @@ CMS.Views.Checklists = Backbone.View.extend({
} }
); );
}, },
reset: true,
error: CMS.ServerError error: CMS.ServerError
} }
); );
...@@ -93,4 +94,4 @@ CMS.Views.Checklists = Backbone.View.extend({ ...@@ -93,4 +94,4 @@ CMS.Views.Checklists = Backbone.View.extend({
error : CMS.ServerError error : CMS.ServerError
}); });
} }
}); });
\ No newline at end of file
...@@ -32,7 +32,7 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({ ...@@ -32,7 +32,7 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
"click .post-actions > .edit-button" : "onEdit", "click .post-actions > .edit-button" : "onEdit",
"click .post-actions > .delete-button" : "onDelete" "click .post-actions > .delete-button" : "onDelete"
}, },
initialize: function() { initialize: function() {
var self = this; var self = this;
// instantiates an editor template for each update in the collection // instantiates an editor template for each update in the collection
...@@ -41,13 +41,13 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({ ...@@ -41,13 +41,13 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
"/static/client_templates/course_info_update.html", "/static/client_templates/course_info_update.html",
function (raw_template) { function (raw_template) {
self.template = _.template(raw_template); self.template = _.template(raw_template);
self.render(); self.render();
} }
); );
// when the client refetches the updates as a whole, re-render them // when the client refetches the updates as a whole, re-render them
this.listenTo(this.collection, 'reset', this.render); this.listenTo(this.collection, 'reset', this.render);
}, },
render: function () { render: function () {
// iterate over updates and create views for each using the template // iterate over updates and create views for each using the template
var updateEle = this.$el.find("#course-update-list"); var updateEle = this.$el.find("#course-update-list");
...@@ -66,14 +66,14 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({ ...@@ -66,14 +66,14 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
this.$el.find('.date').datepicker({ 'dateFormat': 'MM d, yy' }); this.$el.find('.date').datepicker({ 'dateFormat': 'MM d, yy' });
return this; return this;
}, },
onNew: function(event) { onNew: function(event) {
event.preventDefault(); event.preventDefault();
var self = this; var self = this;
// create new obj, insert into collection, and render this one ele overriding the hidden attr // create new obj, insert into collection, and render this one ele overriding the hidden attr
var newModel = new CMS.Models.CourseUpdate(); var newModel = new CMS.Models.CourseUpdate();
this.collection.add(newModel, {at : 0}); this.collection.add(newModel, {at : 0});
var $newForm = $(this.template({ updateModel : newModel })); var $newForm = $(this.template({ updateModel : newModel }));
var updateEle = this.$el.find("#course-update-list"); var updateEle = this.$el.find("#course-update-list");
...@@ -87,7 +87,7 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({ ...@@ -87,7 +87,7 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
lineWrapping: true, lineWrapping: true,
}); });
} }
$newForm.addClass('editing'); $newForm.addClass('editing');
this.$currentPost = $newForm.closest('li'); this.$currentPost = $newForm.closest('li');
...@@ -99,21 +99,21 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({ ...@@ -99,21 +99,21 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
$('.date').datepicker('destroy'); $('.date').datepicker('destroy');
$('.date').datepicker({ 'dateFormat': 'MM d, yy' }); $('.date').datepicker({ 'dateFormat': 'MM d, yy' });
}, },
onSave: function(event) { onSave: function(event) {
event.preventDefault(); event.preventDefault();
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({}, {error : CMS.ServerError});
this.closeEditor(this); this.closeEditor(this);
analytics.track('Saved Course Update', { analytics.track('Saved Course Update', {
'course': course_location_analytics, 'course': course_location_analytics,
'date': this.dateEntry(event).val() 'date': this.dateEntry(event).val()
}); });
}, },
onCancel: function(event) { onCancel: function(event) {
event.preventDefault(); event.preventDefault();
// change editor contents back to model values and hide the editor // change editor contents back to model values and hide the editor
...@@ -121,13 +121,13 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({ ...@@ -121,13 +121,13 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
var targetModel = this.eventModel(event); var targetModel = this.eventModel(event);
this.closeEditor(this, !targetModel.id); this.closeEditor(this, !targetModel.id);
}, },
onEdit: function(event) { onEdit: function(event) {
event.preventDefault(); event.preventDefault();
var self = this; var self = this;
this.$currentPost = $(event.target).closest('li'); this.$currentPost = $(event.target).closest('li');
this.$currentPost.addClass('editing'); this.$currentPost.addClass('editing');
$(this.editor(event)).show(); $(this.editor(event)).show();
var $textArea = this.$currentPost.find(".new-update-content").first(); var $textArea = this.$currentPost.find(".new-update-content").first();
if (this.$codeMirror == null ) { if (this.$codeMirror == null ) {
...@@ -154,17 +154,23 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({ ...@@ -154,17 +154,23 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
analytics.track('Deleted Course Update', { analytics.track('Deleted Course Update', {
'course': course_location_analytics, 'course': course_location_analytics,
'date': this.dateEntry(event).val() 'date': this.dateEntry(event).val()
}); });
var targetModel = this.eventModel(event); var targetModel = this.eventModel(event);
this.modelDom(event).remove(); this.modelDom(event).remove();
var cacheThis = this; var cacheThis = this;
targetModel.destroy({success : function (model, response) { targetModel.destroy({
cacheThis.collection.fetch({success : function() {cacheThis.render();}, success: function (model, response) {
error : CMS.ServerError}); cacheThis.collection.fetch({
}, success: function() {
error : CMS.ServerError cacheThis.render();
},
reset: true,
error: CMS.ServerError
});
},
error : CMS.ServerError
}); });
}, },
...@@ -192,17 +198,17 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({ ...@@ -192,17 +198,17 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
this.$codeMirror = null; this.$codeMirror = null;
self.$currentPost.find('.CodeMirror').remove(); self.$currentPost.find('.CodeMirror').remove();
}, },
// Dereferencing from events to screen elements // Dereferencing from events to screen elements
eventModel: function(event) { eventModel: function(event) {
// not sure if it should be currentTarget or delegateTarget // not sure if it should be currentTarget or delegateTarget
return this.collection.get($(event.currentTarget).attr("name")); return this.collection.get($(event.currentTarget).attr("name"));
}, },
modelDom: function(event) { modelDom: function(event) {
return $(event.currentTarget).closest("li"); return $(event.currentTarget).closest("li");
}, },
editor: function(event) { editor: function(event) {
var li = $(event.currentTarget).closest("li"); var li = $(event.currentTarget).closest("li");
if (li) return $(li).find("form").first(); if (li) return $(li).find("form").first();
...@@ -216,7 +222,7 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({ ...@@ -216,7 +222,7 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
contentEntry: function(event) { contentEntry: function(event) {
return $(event.currentTarget).closest("li").find(".new-update-content").first(); return $(event.currentTarget).closest("li").find(".new-update-content").first();
}, },
dateDisplay: function(event) { dateDisplay: function(event) {
return $(event.currentTarget).closest("li").find("#date-display").first(); return $(event.currentTarget).closest("li").find("#date-display").first();
}, },
...@@ -224,7 +230,7 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({ ...@@ -224,7 +230,7 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
contentDisplay: function(event) { contentDisplay: function(event) {
return $(event.currentTarget).closest("li").find(".update-contents").first(); return $(event.currentTarget).closest("li").find(".update-contents").first();
} }
}); });
// the handouts view is dumb right now; it needs tied to a model and all that jazz // the handouts view is dumb right now; it needs tied to a model and all that jazz
...@@ -238,23 +244,22 @@ CMS.Views.ClassInfoHandoutsView = Backbone.View.extend({ ...@@ -238,23 +244,22 @@ CMS.Views.ClassInfoHandoutsView = Backbone.View.extend({
initialize: function() { initialize: function() {
var self = this; var self = this;
this.model.fetch( this.model.fetch({
{ complete: function() {
complete: function() { window.templateLoader.loadRemoteTemplate("course_info_handouts",
window.templateLoader.loadRemoteTemplate("course_info_handouts", "/static/client_templates/course_info_handouts.html",
"/static/client_templates/course_info_handouts.html", function (raw_template) {
function (raw_template) { self.template = _.template(raw_template);
self.template = _.template(raw_template); self.render();
self.render(); }
} );
); },
}, reset: true,
error : CMS.ServerError error: CMS.ServerError
} });
);
}, },
render: function () { render: function () {
var updateEle = this.$el; var updateEle = this.$el;
var self = this; var self = this;
this.$el.html( this.$el.html(
...@@ -313,4 +318,4 @@ CMS.Views.ClassInfoHandoutsView = Backbone.View.extend({ ...@@ -313,4 +318,4 @@ CMS.Views.ClassInfoHandoutsView = Backbone.View.extend({
self.$form.find('.CodeMirror').remove(); self.$form.find('.CodeMirror').remove();
this.$codeMirror = null; this.$codeMirror = null;
} }
}); });
\ No newline at end of file
...@@ -16,7 +16,7 @@ CMS.Models.AssignmentGrade = Backbone.Model.extend({ ...@@ -16,7 +16,7 @@ CMS.Models.AssignmentGrade = Backbone.Model.extend({
urlRoot : function() { urlRoot : function() {
if (this.has('location')) { if (this.has('location')) {
var location = this.get('location'); var location = this.get('location');
return '/' + location.get('org') + "/" + location.get('course') + '/' + location.get('category') + '/' return '/' + location.get('org') + "/" + location.get('course') + '/' + location.get('category') + '/'
+ location.get('name') + '/gradeas/'; + location.get('name') + '/gradeas/';
} }
else return ""; else return "";
...@@ -37,14 +37,14 @@ CMS.Views.OverviewAssignmentGrader = Backbone.View.extend({ ...@@ -37,14 +37,14 @@ CMS.Views.OverviewAssignmentGrader = Backbone.View.extend({
'<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) {%><span class="ss-icon ss-standard">&#x2713;</span><%};%>' +
'</a>' + '</a>' +
'<ul class="menu">' + '<ul class="menu">' +
'<% graders.each(function(option) { %>' + '<% graders.each(function(option) { %>' +
'<li><a <% if (option.get("type") == assignmentType) {%>class="is-selected" <%}%> href="#"><%= option.get("type") %></a></li>' + '<li><a <% if (option.get("type") == assignmentType) {%>class="is-selected" <%}%> href="#"><%= option.get("type") %></a></li>' +
'<% }) %>' + '<% }) %>' +
'<li><a class="gradable-status-notgraded" href="#">Not Graded</a></li>' + '<li><a class="gradable-status-notgraded" href="#">Not Graded</a></li>' +
'</ul>'); '</ul>');
this.assignmentGrade = new CMS.Models.AssignmentGrade({ this.assignmentGrade = new CMS.Models.AssignmentGrade({
assignmentUrl : this.$el.closest('.id-holder').data('id'), assignmentUrl : this.$el.closest('.id-holder').data('id'),
graderType : this.$el.data('initial-status')}); graderType : this.$el.data('initial-status')});
// TODO throw exception if graders is null // TODO throw exception if graders is null
this.graders = this.options['graders']; this.graders = this.options['graders'];
...@@ -78,13 +78,13 @@ CMS.Views.OverviewAssignmentGrader = Backbone.View.extend({ ...@@ -78,13 +78,13 @@ CMS.Views.OverviewAssignmentGrader = Backbone.View.extend({
}, },
selectGradeType : function(e) { selectGradeType : function(e) {
e.preventDefault(); e.preventDefault();
this.removeMenu(e); this.removeMenu(e);
// TODO I'm not happy with this string fetch via the html for what should be an id. I'd rather use the id attr // TODO I'm not happy with this string fetch via the html for what should be an id. I'd rather use the id attr
// of the CourseGradingPolicy model or null for Not Graded (NOTE, change template's if check for is-selected accordingly) // of the CourseGradingPolicy model or null for Not Graded (NOTE, change template's if check for is-selected accordingly)
this.assignmentGrade.save('graderType', $(e.target).text()); this.assignmentGrade.save('graderType', $(e.target).text());
this.render(); this.render();
} }
}) })
\ No newline at end of file
...@@ -6,26 +6,26 @@ $(document).ready(function() { ...@@ -6,26 +6,26 @@ $(document).ready(function() {
$('.unit').draggable({ $('.unit').draggable({
axis: 'y', axis: 'y',
handle: '.drag-handle', handle: '.drag-handle',
zIndex: 999, zIndex: 999,
start: initiateHesitate, start: initiateHesitate,
// left 2nd arg in as inert selector b/c i was uncertain whether we'd try to get the shove up/down // left 2nd arg in as inert selector b/c i was uncertain whether we'd try to get the shove up/down
// to work in the future // to work in the future
drag: generateCheckHoverState('.collapsed', ''), drag: generateCheckHoverState('.collapsed', ''),
stop: removeHesitate, stop: removeHesitate,
revert: "invalid" revert: "invalid"
}); });
// Subsection reordering // Subsection reordering
$('.id-holder').draggable({ $('.id-holder').draggable({
axis: 'y', axis: 'y',
handle: '.section-item .drag-handle', handle: '.section-item .drag-handle',
zIndex: 999, zIndex: 999,
start: initiateHesitate, start: initiateHesitate,
drag: generateCheckHoverState('.courseware-section.collapsed', ''), drag: generateCheckHoverState('.courseware-section.collapsed', ''),
stop: removeHesitate, stop: removeHesitate,
revert: "invalid" revert: "invalid"
}); });
// Section reordering // Section reordering
$('.courseware-section').draggable({ $('.courseware-section').draggable({
axis: 'y', axis: 'y',
...@@ -33,8 +33,8 @@ $(document).ready(function() { ...@@ -33,8 +33,8 @@ $(document).ready(function() {
stack: '.courseware-section', stack: '.courseware-section',
revert: "invalid" revert: "invalid"
}); });
$('.sortable-unit-list').droppable({ $('.sortable-unit-list').droppable({
accept : '.unit', accept : '.unit',
greedy: true, greedy: true,
...@@ -50,7 +50,7 @@ $(document).ready(function() { ...@@ -50,7 +50,7 @@ $(document).ready(function() {
drop: onSubsectionReordered, drop: onSubsectionReordered,
greedy: true greedy: true
}); });
// Section reordering // Section reordering
$('.courseware-overview').droppable({ $('.courseware-overview').droppable({
accept : '.courseware-section', accept : '.courseware-section',
...@@ -58,7 +58,7 @@ $(document).ready(function() { ...@@ -58,7 +58,7 @@ $(document).ready(function() {
drop: onSectionReordered, drop: onSectionReordered,
greedy: true greedy: true
}); });
// stop clicks on drag bars from doing their thing w/o stopping drag // stop clicks on drag bars from doing their thing w/o stopping drag
$('.drag-handle').click(function(e) {e.preventDefault(); }); $('.drag-handle').click(function(e) {e.preventDefault(); });
...@@ -87,7 +87,7 @@ function computeIntersection(droppable, uiHelper, y) { ...@@ -87,7 +87,7 @@ function computeIntersection(droppable, uiHelper, y) {
$.extend(droppable, {offset : $(droppable).offset()}); $.extend(droppable, {offset : $(droppable).offset()});
var t = droppable.offset.top, var t = droppable.offset.top,
b = t + droppable.proportions.height; b = t + droppable.proportions.height;
if (t === b) { if (t === b) {
...@@ -118,10 +118,10 @@ function generateCheckHoverState(selectorsToOpen, selectorsToShove) { ...@@ -118,10 +118,10 @@ function generateCheckHoverState(selectorsToOpen, selectorsToShove) {
this[c === "isout" ? "isover" : "isout"] = false; this[c === "isout" ? "isover" : "isout"] = false;
$(this).trigger(c === "isover" ? "dragEnter" : "dragLeave"); $(this).trigger(c === "isover" ? "dragEnter" : "dragLeave");
}); });
$(selectorsToShove).each(function() { $(selectorsToShove).each(function() {
var intersectsBottom = computeIntersection(this, ui.helper, (draggable.positionAbs || draggable.position.absolute).top); var intersectsBottom = computeIntersection(this, ui.helper, (draggable.positionAbs || draggable.position.absolute).top);
if ($(this).hasClass('ui-dragging-pushup')) { if ($(this).hasClass('ui-dragging-pushup')) {
if (!intersectsBottom) { if (!intersectsBottom) {
console.log('not up', $(this).data('id')); console.log('not up', $(this).data('id'));
...@@ -132,10 +132,10 @@ function generateCheckHoverState(selectorsToOpen, selectorsToShove) { ...@@ -132,10 +132,10 @@ function generateCheckHoverState(selectorsToOpen, selectorsToShove) {
console.log('up', $(this).data('id')); console.log('up', $(this).data('id'));
$(this).addClass('ui-dragging-pushup'); $(this).addClass('ui-dragging-pushup');
} }
var intersectsTop = computeIntersection(this, ui.helper, var intersectsTop = computeIntersection(this, ui.helper,
(draggable.positionAbs || draggable.position.absolute).top + draggable.helperProportions.height); (draggable.positionAbs || draggable.position.absolute).top + draggable.helperProportions.height);
if ($(this).hasClass('ui-dragging-pushdown')) { if ($(this).hasClass('ui-dragging-pushdown')) {
if (!intersectsTop) { if (!intersectsTop) {
console.log('not down', $(this).data('id')); console.log('not down', $(this).data('id'));
...@@ -146,7 +146,7 @@ function generateCheckHoverState(selectorsToOpen, selectorsToShove) { ...@@ -146,7 +146,7 @@ function generateCheckHoverState(selectorsToOpen, selectorsToShove) {
console.log('down', $(this).data('id')); console.log('down', $(this).data('id'));
$(this).addClass('ui-dragging-pushdown'); $(this).addClass('ui-dragging-pushdown');
} }
}); });
} }
} }
...@@ -159,20 +159,20 @@ function removeHesitate(event, ui) { ...@@ -159,20 +159,20 @@ function removeHesitate(event, ui) {
} }
function expandSection(event) { function expandSection(event) {
$(event.delegateTarget).removeClass('collapsed', 400); $(event.delegateTarget).removeClass('collapsed', 400);
// don't descend to icon's on children (which aren't under first child) only to this element's icon // don't descend to icon's on children (which aren't under first child) only to this element's icon
$(event.delegateTarget).children().first().find('.expand-collapse-icon').removeClass('expand', 400).addClass('collapse'); $(event.delegateTarget).children().first().find('.expand-collapse-icon').removeClass('expand', 400).addClass('collapse');
} }
function onUnitReordered(event, ui) { function onUnitReordered(event, ui) {
// a unit's been dropped on this subsection, // a unit's been dropped on this subsection,
// figure out where it came from and where it slots in. // figure out where it came from and where it slots in.
_handleReorder(event, ui, 'subsection-id', 'li:.leaf'); _handleReorder(event, ui, 'subsection-id', 'li:.leaf');
} }
function onSubsectionReordered(event, ui) { function onSubsectionReordered(event, ui) {
// a subsection has been dropped on this section, // a subsection has been dropped on this section,
// figure out where it came from and where it slots in. // figure out where it came from and where it slots in.
_handleReorder(event, ui, 'section-id', 'li:.branch'); _handleReorder(event, ui, 'section-id', 'li:.branch');
} }
...@@ -182,7 +182,7 @@ function onSectionReordered(event, ui) { ...@@ -182,7 +182,7 @@ function onSectionReordered(event, ui) {
} }
function _handleReorder(event, ui, parentIdField, childrenSelector) { function _handleReorder(event, ui, parentIdField, childrenSelector) {
// figure out where it came from and where it slots in. // figure out where it came from and where it slots in.
var subsection_id = $(event.target).data(parentIdField); var subsection_id = $(event.target).data(parentIdField);
var _els = $(event.target).children(childrenSelector); var _els = $(event.target).children(childrenSelector);
var children = _els.map(function(idx, el) { return $(el).data('id'); }).get(); var children = _els.map(function(idx, el) { return $(el).data('id'); }).get();
......
CMS.ServerError = function(model, error) { CMS.ServerError = function(model, error) {
// this handler is for the client:server communication not the validation errors which handleValidationError catches // this handler is for the client:server communication not the validation errors which handleValidationError catches
window.alert("Server Error: " + error.responseText); window.alert("Server Error: " + error.responseText);
}; };
\ No newline at end of file
...@@ -155,6 +155,7 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({ ...@@ -155,6 +155,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,
error : CMS.ServerError error : CMS.ServerError
}); });
}, },
......
CMS.Views.ValidatingView = Backbone.View.extend({ CMS.Views.ValidatingView = Backbone.View.extend({
// Intended as an abstract class which catches validation errors on the model and // Intended as an abstract class which catches validation errors on the model and
// 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, 'error', CMS.ServerError);
...@@ -15,7 +15,7 @@ CMS.Views.ValidatingView = Backbone.View.extend({ ...@@ -15,7 +15,7 @@ CMS.Views.ValidatingView = Backbone.View.extend({
"change textarea" : "clearValidationErrors" "change textarea" : "clearValidationErrors"
}, },
fieldToSelectorMap : { fieldToSelectorMap : {
// Your subclass must populate this w/ all of the model keys and dom selectors // Your subclass must populate this w/ all of the model keys and dom selectors
// which may be the subjects of validation errors // which may be the subjects of validation errors
}, },
_cacheValidationErrors : [], _cacheValidationErrors : [],
......
...@@ -8,11 +8,10 @@ html { ...@@ -8,11 +8,10 @@ html {
} }
body { body {
@include font-size(16); @extend .t-copy-base;
min-width: $fg-min-width; min-width: $fg-min-width;
background: $gray-l5; background: $gray-l5;
line-height: 1.6; color: $gray-d2;
color: $baseFontColor;
} }
body, input { body, input {
...@@ -30,7 +29,7 @@ a { ...@@ -30,7 +29,7 @@ a {
} }
h1 { h1 {
@include font-size(28); @extend .t-title4;
font-weight: 300; font-weight: 300;
} }
...@@ -51,43 +50,183 @@ h1 { ...@@ -51,43 +50,183 @@ h1 {
// ==================== // ====================
// typography - basic // typography - basic
.title-1, .title-2, .title-3, .title-4, .title-5, .title-6 { .page-header {
@extend .t-title3;
display: block;
font-weight: 600; font-weight: 600;
color: $gray-d3; color: $gray-d3;
margin: 0;
padding: 0; .subtitle {
@extend .t-title7;
position: relative;
top: ($baseline/4);
display: block;
color: $gray-l2;
font-weight: 400;
}
}
.section-header {
@extend .t-title4;
font-weight: 600;
.subtitle {
@extend .t-title7;
}
}
.area-header {
@extend .t-title6;
font-weight: 600;
.subtitle {
@extend .t-title8;
}
}
.area-subheader {
@extend .t-title7;
font-weight: 600;
.subtitle {
@extend .t-title9;
}
}
// ====================
// typography - primary content
.content-primary {
.section-header {
color: $gray-d3;
.subtitle {
color: $gray-l2;
}
}
.content-header {
color: $gray-d3;
.subtitle {
color: $gray-l2;
}
}
.area-header {
color: $gray-d3;
.subtitle {
color: $gray-l2;
}
}
.area-subheader {
color: $gray-d3;
.subtitle {
color: $gray-l2;
}
}
}
// typography - primary content
.content-secondary {
.section-header {
color: $gray-d3;
.subtitle {
color: $gray-l2;
}
}
.content-header {
color: $gray-d3;
.subtitle {
color: $gray-l2;
}
}
.content-header {
color: $gray-d3;
.subtitle {
color: $gray-l2;
}
}
}
// ====================
// typography - loose headings (BT: needs to be removed once html is clean)
.title-1, .title-2, .title-3, .title-4, .title-5, .title-6 {
font-weight: 600;
}
// typography - primary content
.content-secondary {
.section-header {
color: $gray-d3;
.subtitle {
color: $gray-l2;
}
}
.content-header {
color: $gray-d3;
.subtitle {
color: $gray-l2;
}
}
.content-header {
color: $gray-d3;
.subtitle {
color: $gray-l2;
}
}
} }
// ====================
// typography - loose headings (BT: needs to be removed once html is clean)
.title-1 { .title-1 {
@include font-size(32); @extend .t-title3;
margin-bottom: ($baseline*1.5); margin-bottom: ($baseline*1.5);
} }
.title-2 { .title-2 {
@include font-size(24); @extend .t-title4;
margin-bottom: $baseline; margin-bottom: $baseline;
} }
.title-3 { .title-3 {
@include font-size(18); @extend .t-title5;
margin-bottom: ($baseline/2); margin-bottom: ($baseline/2);
} }
.title-4 { .title-4 {
@include font-size(14); @extend .t-title7;
margin-bottom: $baseline; margin-bottom: $baseline;
font-weight: 500 font-weight: 500
} }
.title-5 { .title-5 {
@include font-size(14); @extend .t-title7;
color: $gray-l1; color: $gray-l1;
margin-bottom: $baseline; margin-bottom: $baseline;
font-weight: 500 font-weight: 500
} }
.title-6 { .title-6 {
@include font-size(14); @extend .t-title7;
color: $gray-l2; color: $gray-l2;
margin-bottom: $baseline; margin-bottom: $baseline;
font-weight: 500 font-weight: 500
...@@ -118,7 +257,6 @@ p, ul, ol, dl { ...@@ -118,7 +257,6 @@ p, ul, ol, dl {
.mast, .metadata { .mast, .metadata {
@include clearfix(); @include clearfix();
@include font-size(16);
position: relative; position: relative;
max-width: $fg-max-width; max-width: $fg-max-width;
min-width: $fg-min-width; min-width: $fg-min-width;
...@@ -131,53 +269,8 @@ p, ul, ol, dl { ...@@ -131,53 +269,8 @@ p, ul, ol, dl {
border-bottom: 1px solid $gray-l4; border-bottom: 1px solid $gray-l4;
padding-bottom: ($baseline/2); padding-bottom: ($baseline/2);
.title-sub {
@include font-size(14);
position: relative;
top: ($baseline/4);
display: block;
margin: 0;
color: $gray-l2;
font-weight: 400;
}
.title, .title-1 {
@include font-size(32);
margin: 0;
padding: 0;
font-weight: 600;
color: $gray-d3;
}
.nav-hierarchy {
@include font-size(14);
display: block;
margin: 0;
color: $gray-l2;
font-weight: 400;
.nav-item {
display: inline;
vertical-align: middle;
margin-right: ($baseline/4);
&:after {
content: ">>";
margin-left: ($baseline/4);
}
&:last-child {
margin-right: 0;
&:after {
content: none;
}
}
}
}
// layout with actions // layout with actions
.title { .page-header {
width: flex-grid(12); width: flex-grid(12);
} }
...@@ -185,7 +278,7 @@ p, ul, ol, dl { ...@@ -185,7 +278,7 @@ p, ul, ol, dl {
&.has-actions { &.has-actions {
@include clearfix(); @include clearfix();
.title { .page-header {
float: left; float: left;
width: flex-grid(6,12); width: flex-grid(6,12);
margin-right: flex-gutter(); margin-right: flex-gutter();
...@@ -210,22 +303,20 @@ p, ul, ol, dl { ...@@ -210,22 +303,20 @@ p, ul, ol, dl {
// buttons // buttons
.button { .button {
padding: ($baseline/4) ($baseline/2) ($baseline/3) ($baseline/2) !important; padding: ($baseline/4) ($baseline/2) ($baseline/3) ($baseline/2);
font-weight: 400 !important;
} }
.new-button { .new-button {
font-weight: 700 !important;
} }
.view-button { .view-button {
font-weight: 700 !important;
} }
.upload-button .icon-create { .upload-button .icon-create {
@include font-size(18); @extend .t-action2;
margin-top: ($baseline/4); line-height: 0 !important;
} }
} }
} }
...@@ -254,7 +345,7 @@ p, ul, ol, dl { ...@@ -254,7 +345,7 @@ p, ul, ol, dl {
.content { .content {
@include clearfix(); @include clearfix();
@include font-size(16); @extend .t-copy-base;
max-width: $fg-max-width; max-width: $fg-max-width;
min-width: $fg-min-width; min-width: $fg-min-width;
width: flex-grid(12); width: flex-grid(12);
...@@ -268,14 +359,14 @@ p, ul, ol, dl { ...@@ -268,14 +359,14 @@ p, ul, ol, dl {
padding-bottom: ($baseline/2); padding-bottom: ($baseline/2);
.title-sub { .title-sub {
@include font-size(14); @extend .t-copy-sub1;
display: block; display: block;
margin: 0; margin: 0;
color: $gray-l2; color: $gray-l2;
} }
.title-1 { .title-1 {
@include font-size(32); @extend .t-title3;
margin: 0; margin: 0;
padding: 0; padding: 0;
font-weight: 600; font-weight: 600;
...@@ -285,7 +376,7 @@ p, ul, ol, dl { ...@@ -285,7 +376,7 @@ p, ul, ol, dl {
.introduction { .introduction {
@include box-sizing(border-box); @include box-sizing(border-box);
@include font-size(14); @extend .t-copy-sub1;
width: flex-grid(12); width: flex-grid(12);
margin: 0 0 $baseline 0; margin: 0 0 $baseline 0;
...@@ -303,14 +394,14 @@ p, ul, ol, dl { ...@@ -303,14 +394,14 @@ p, ul, ol, dl {
} }
.nav-introduction-supplementary { .nav-introduction-supplementary {
@include font-size(13); @extend .t-copy-sub2;
float: right; float: right;
width: flex-grid(4,12); width: flex-grid(4,12);
display: block; display: block;
text-align: right; text-align: right;
.icon { .icon {
@include font-size(14); @extend .t-action3;
display: inline-block; display: inline-block;
vertical-align: middle; vertical-align: middle;
margin-right: ($baseline/4); margin-right: ($baseline/4);
...@@ -327,21 +418,17 @@ p, ul, ol, dl { ...@@ -327,21 +418,17 @@ p, ul, ol, dl {
// layout - primary content // layout - primary content
.content-primary { .content-primary {
.title-1, .title-2, .title-3, .title-4, .title-5, .title-5 {
color: $gray-d3;
}
.title-1 { .title-1 {
@extend .t-title-1; @extend .t-title3;
} }
.title-2 { .title-2 {
@extend .t-title-2; @extend .t-title4;
margin: 0 0 ($baseline/2) 0; margin: 0 0 ($baseline/2) 0;
} }
.title-3 { .title-3 {
@extend .t-title-3; @extend .t-title6;
margin: 0 0 ($baseline/2) 0; margin: 0 0 ($baseline/2) 0;
} }
...@@ -355,7 +442,7 @@ p, ul, ol, dl { ...@@ -355,7 +442,7 @@ p, ul, ol, dl {
} }
.tip { .tip {
@include font-size(13); @extend .t-copy-sub2;
width: flex-grid(7, 12); width: flex-grid(7, 12);
float: right; float: right;
margin-top: ($baseline/2); margin-top: ($baseline/2);
...@@ -373,7 +460,7 @@ p, ul, ol, dl { ...@@ -373,7 +460,7 @@ p, ul, ol, dl {
} }
.bit { .bit {
@include font-size(13); @extend .t-copy-sub1;
margin: 0 0 $baseline 0; margin: 0 0 $baseline 0;
border-bottom: 1px solid $gray-l4; border-bottom: 1px solid $gray-l4;
padding: 0 0 $baseline 0; padding: 0 0 $baseline 0;
...@@ -386,7 +473,7 @@ p, ul, ol, dl { ...@@ -386,7 +473,7 @@ p, ul, ol, dl {
} }
h3 { h3 {
@include font-size(14); @extend .t-title7;
margin: 0 0 ($baseline/4) 0; margin: 0 0 ($baseline/4) 0;
color: $gray-d2; color: $gray-d2;
font-weight: 600; font-weight: 600;
...@@ -494,7 +581,7 @@ p, ul, ol, dl { ...@@ -494,7 +581,7 @@ p, ul, ol, dl {
// misc // misc
hr.divide { hr.divide {
@include text-sr(); @extend .text-sr;
} }
.item-details { .item-details {
...@@ -655,7 +742,7 @@ hr.divide { ...@@ -655,7 +742,7 @@ hr.divide {
.new-button { .new-button {
@include green-button; @include green-button;
@include font-size(13); @extend .t-action4;
padding: 8px 20px 10px; padding: 8px 20px 10px;
text-align: center; text-align: center;
...@@ -674,7 +761,7 @@ hr.divide { ...@@ -674,7 +761,7 @@ hr.divide {
.view-button { .view-button {
@include blue-button; @include blue-button;
@include font-size(13); @extend .t-copy-base;
text-align: center; text-align: center;
&.big { &.big {
...@@ -693,7 +780,7 @@ hr.divide { ...@@ -693,7 +780,7 @@ hr.divide {
.edit-button.standard, .edit-button.standard,
.delete-button.standard { .delete-button.standard {
@include font-size(12); @extend .t-action4;
@include white-button; @include white-button;
float: left; float: left;
padding: 3px 10px 4px; padding: 3px 10px 4px;
...@@ -714,6 +801,7 @@ hr.divide { ...@@ -714,6 +801,7 @@ hr.divide {
} }
.tooltip { .tooltip {
@extend .t-copy-sub2;
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
...@@ -721,7 +809,6 @@ hr.divide { ...@@ -721,7 +809,6 @@ hr.divide {
padding: 0 10px; padding: 0 10px;
border-radius: 3px; border-radius: 3px;
background: rgba(0, 0, 0, 0.85); background: rgba(0, 0, 0, 0.85);
font-size: 11px;
font-weight: normal; font-weight: normal;
line-height: 26px; line-height: 26px;
color: #fff; color: #fff;
...@@ -745,7 +832,7 @@ hr.divide { ...@@ -745,7 +832,7 @@ hr.divide {
// basic utility // basic utility
.sr { .sr {
@include text-sr(); @extend .text-sr;
} }
.fake-link { .fake-link {
...@@ -798,7 +885,7 @@ body.js { ...@@ -798,7 +885,7 @@ body.js {
text-align: center; text-align: center;
.label { .label {
@include text-sr(); @extend .text-sr;
} }
.ss-icon { .ss-icon {
...@@ -821,14 +908,14 @@ body.js { ...@@ -821,14 +908,14 @@ body.js {
} }
.title { .title {
@include font-size(18); @extend .t-title5;
margin: 0 0 ($baseline/2) 0; margin: 0 0 ($baseline/2) 0;
font-weight: 600; font-weight: 600;
color: $gray-d3; color: $gray-d3;
} }
.description { .description {
@include font-size(13); @extend .t-copy-sub2;
margin-top: ($baseline/2); margin-top: ($baseline/2);
color: $gray-l1; color: $gray-l1;
} }
......
...@@ -6,6 +6,11 @@ ...@@ -6,6 +6,11 @@
// @include box-sizing(border-box); // @include box-sizing(border-box);
// } // }
// better text rendering/kerning through SVG
* {
text-rendering: optimizeLegibility;
}
html, body, div, span, applet, object, iframe, html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre, h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code, a, abbr, acronym, address, big, cite, code,
...@@ -15,7 +20,7 @@ b, u, i, center, ...@@ -15,7 +20,7 @@ b, u, i, center,
dl, dt, dd, ol, ul, li, dl, dt, dd, ol, ul, li,
fieldset, form, label, legend, fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td, table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, figcaption, figure, article, aside, canvas, details, figcaption, figure,
footer, header, hgroup, menu, nav, section, summary, footer, header, hgroup, menu, nav, section, summary,
time, mark, audio, video { time, mark, audio, video {
margin: 0; margin: 0;
...@@ -31,7 +36,7 @@ html,body { ...@@ -31,7 +36,7 @@ html,body {
height: 100%; height: 100%;
} }
article, aside, details, figcaption, figure, article, aside, details, figcaption, figure,
footer, header, hgroup, menu, nav, section { footer, header, hgroup, menu, nav, section {
display: block; display: block;
} }
...@@ -110,7 +115,7 @@ abbr[title] { ...@@ -110,7 +115,7 @@ abbr[title] {
border: none; border: none;
top: 0; top: 0;
margin: 0; margin: 0;
float: none; float: none;
border-top-left-radius: 0; border-top-left-radius: 0;
border-top-right-radius: 0; border-top-right-radius: 0;
} }
...@@ -131,7 +136,7 @@ abbr[title] { ...@@ -131,7 +136,7 @@ abbr[title] {
padding: 0; padding: 0;
} }
// reapplying the tab styles from unit.scss after removing jquery ui ui-lightness styling // reapplying the tab styles from unit.scss after removing jquery ui ui-lightness styling
.problem-type-tabs { .problem-type-tabs {
border:none; border:none;
list-style-type: none; list-style-type: none;
...@@ -139,7 +144,7 @@ abbr[title] { ...@@ -139,7 +144,7 @@ abbr[title] {
@include linear-gradient(top, rgba(255, 255, 255, .4), rgba(255, 255, 255, 0)); @include linear-gradient(top, rgba(255, 255, 255, .4), rgba(255, 255, 255, 0));
//background-color: $lightBluishGrey; //background-color: $lightBluishGrey;
@include box-shadow(0 1px 0 rgba(255, 255, 255, 0.2) inset, 0 -1px 0 rgba(0, 0, 0, 0.2) inset); @include box-shadow(0 1px 0 rgba(255, 255, 255, 0.2) inset, 0 -1px 0 rgba(0, 0, 0, 0.2) inset);
li:first-child { li:first-child {
margin-left: 20px; margin-left: 20px;
} }
...@@ -156,4 +161,4 @@ abbr[title] { ...@@ -156,4 +161,4 @@ abbr[title] {
border: 0px; border: 0px;
} }
} }
} }
\ No newline at end of file
// studio - shame
// // shame file - used for any bad-form/orphaned scss that knowingly violate edX FED architecture/standards (see - http://csswizardry.com/2013/04/shame-css/)
// ====================
...@@ -12,9 +12,15 @@ $fg-max-columns: 12; ...@@ -12,9 +12,15 @@ $fg-max-columns: 12;
$fg-max-width: 1280px; $fg-max-width: 1280px;
$fg-min-width: 900px; $fg-min-width: 900px;
// type // ====================
$sans-serif: 'Open Sans', $verdana;
$body-line-height: golden-ratio(.875em, 1); // fonts
$f-serif: 'Bree Serif', Georgia, Cambria, 'Times New Roman', Times, serif;
$f-sans-serif: 'Open Sans','Helvetica Neue', Helvetica, Arial, sans-serif;
$f-decorative: '';
$f-monospace: 'Bitstream Vera Sans Mono', Consolas, Courier, monospace;
// ====================
// colors - new for re-org // colors - new for re-org
$black: rgb(0,0,0); $black: rgb(0,0,0);
...@@ -152,11 +158,14 @@ $shadow-l1: rgba(0,0,0,0.1); ...@@ -152,11 +158,14 @@ $shadow-l1: rgba(0,0,0,0.1);
$shadow-l2: rgba(0,0,0,0.05); $shadow-l2: rgba(0,0,0,0.05);
$shadow-d1: rgba(0,0,0,0.4); $shadow-d1: rgba(0,0,0,0.4);
// ====================
// specific UI // specific UI
$notification-height: ($baseline*10); $notification-height: ($baseline*10);
// colors - inherited // ====================
// inherited
$baseFontColor: $gray-d2; $baseFontColor: $gray-d2;
$offBlack: #3c3c3c; $offBlack: #3c3c3c;
$green: #108614; $green: #108614;
...@@ -173,3 +182,8 @@ $darkGreen: rgb(52, 133, 76); ...@@ -173,3 +182,8 @@ $darkGreen: rgb(52, 133, 76);
$lightBluishGrey: rgb(197, 207, 223); $lightBluishGrey: rgb(197, 207, 223);
$lightBluishGrey2: rgb(213, 220, 228); $lightBluishGrey2: rgb(213, 220, 228);
$error-red: rgb(253, 87, 87); $error-red: rgb(253, 87, 87);
// type
$sans-serif: $f-sans-serif;
$body-line-height: golden-ratio(.875em, 1);
...@@ -94,7 +94,7 @@ ...@@ -94,7 +94,7 @@
// notifications slide up then down // notifications slide up then down
@mixin notificationsSlideUpDown { @mixin notificationsSlideUpDown {
0%, 100% { 0%, 100% {
@include transform(translateY(0)); @include transform(translateY(0));
} }
...@@ -199,4 +199,4 @@ ...@@ -199,4 +199,4 @@
@include animation-timing-function($timing); @include animation-timing-function($timing);
@include animation-iteration-count($count); @include animation-iteration-count($count);
@include animation-fill-mode(both); @include animation-fill-mode(both);
} }
\ No newline at end of file
@font-face { // studio - assets - fonts
font-family: 'Open Sans'; // ====================
font-style: normal;
font-weight: 700; // import from google fonts - Open Sans
src: local('Open Sans Bold'), local('OpenSans-Bold'), url(http://themes.googleusercontent.com/static/fonts/opensans/v6/k3k702ZOKiLJc3WVjuplzKRDOzjiPcYnFooOUGCOsRk.woff) format('woff'); @import url(http://fonts.googleapis.com/css?family=Open+Sans:300italic,400italic,700italic,400,700,300);
}
@font-face { // import from google fonts - Bree
font-family: 'Open Sans'; @import url(http://fonts.googleapis.com/css?family=Bree+Serif);
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');
}
// studio - css architecture // studio - css architecture
// ==================== // ====================
// bourbon libs and resets // libs and resets *do not edit*
@import 'bourbon/bourbon'; @import 'bourbon/bourbon'; // lib - bourbon
@import 'bourbon/addons/button'; @import 'bourbon/addons/button'; // lib bourbon - button add-on
@import "variables";
// VENDOR + REBASE *referenced/used vendor presentation and reset*
// ====================
@import 'vendor/normalize'; @import 'vendor/normalize';
@import 'reset'; @import 'reset';
// utilities
// BASE *default edX offerings*
// ====================
// base - utilities
@import 'variables'; @import 'variables';
@import 'mixins'; @import 'mixins';
@import 'cms_mixins'; @import 'mixins-inherited';
// assets // base - assets
@import 'assets/fonts'; @import 'assets/fonts';
@import 'assets/graphics'; @import 'assets/graphics'; // sprites, basic img/figure/svg styling
@import 'assets/keyframes'; @import 'assets/anims'; // animations
// base // base - starter
@import 'base'; @import 'base';
// elements // base - elements
@import 'elements/typography'; @import 'elements/typography';
@import 'elements/icons'; @import 'elements/icons'; // references to icons used
@import 'elements/controls'; @import 'elements/controls'; // buttons, link styles, sliders, etc.
@import 'elements/navigation'; @import 'elements/navigation'; // all archetypes of navigation
@import 'elements/forms';
@import 'elements/header'; @import 'elements/header';
@import 'elements/footer'; @import 'elements/footer';
@import 'elements/sock'; @import 'elements/sock';
@import 'elements/forms';
@import 'elements/modal';
@import 'elements/alerts';
@import 'elements/vendor';
@import 'elements/tender-widget'; @import 'elements/tender-widget';
@import 'elements/system-feedback'; // alerts, notifications, states
@import 'elements/system-help'; // help UI
@import 'elements/modal'; // interstitial UI, dialogs, modal windows
@import 'elements/vendor'; // overrides to vendor-provided styling
// specific views // base - specific views
@import 'views/account'; @import 'views/account';
@import 'views/assets'; @import 'views/assets';
@import 'views/updates'; @import 'views/updates';
...@@ -51,8 +58,12 @@ ...@@ -51,8 +58,12 @@
@import 'views/users'; @import 'views/users';
@import 'views/checklists'; @import 'views/checklists';
// temp - inherited
@import 'assets/content-types'; @import 'assets/content-types';
// xblock-related // xmodule
@import 'xmodule/modules/css/module-styles.scss'; @import 'xmodule/modules/css/module-styles.scss';
@import 'xmodule/descriptors/css/module-styles.scss'; @import 'xmodule/descriptors/css/module-styles.scss';
@import 'shame'; // shame file - used for any bad-form/orphaned scss that knowingly violate edX FED architecture/standards (see - http://csswizardry.com/2013/04/shame-css/)
...@@ -141,3 +141,9 @@ ...@@ -141,3 +141,9 @@
// calls-to-action // calls-to-action
// ====================
// specific buttons - view live
.view-live-button {
@extend .t-action4;
}
...@@ -8,8 +8,8 @@ ...@@ -8,8 +8,8 @@
padding: $baseline; padding: $baseline;
footer.primary { footer.primary {
@extend .t-copy-sub2;
@include clearfix(); @include clearfix();
@include font-size(13);
max-width: $fg-max-width; max-width: $fg-max-width;
min-width: $fg-min-width; min-width: $fg-min-width;
width: flex-grid(12); width: flex-grid(12);
...@@ -72,4 +72,4 @@ ...@@ -72,4 +72,4 @@
} }
} }
} }
} }
\ No newline at end of file
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
// ==================== // ====================
.wrapper-header { .wrapper-header {
@extend .depth3;
margin: 0; margin: 0;
padding: $baseline; padding: $baseline;
border-bottom: 1px solid $gray; border-bottom: 1px solid $gray;
...@@ -10,7 +11,6 @@ ...@@ -10,7 +11,6 @@
height: 76px; height: 76px;
position: relative; position: relative;
width: 100%; width: 100%;
z-index: 1000;
a { a {
color: $baseFontColor; color: $baseFontColor;
...@@ -67,7 +67,7 @@ ...@@ -67,7 +67,7 @@
padding-right: ($baseline*0.75); padding-right: ($baseline*0.75);
a { a {
@include text-hide(); @extend .text-hide;
display: block; display: block;
width: 164px; width: 164px;
height: 32px; height: 32px;
...@@ -146,7 +146,7 @@ ...@@ -146,7 +146,7 @@
.title { .title {
display: block; display: block;
padding: 5px; padding: 0 ($baseline/4);
text-transform: uppercase; text-transform: uppercase;
font-weight: 600; font-weight: 600;
color: $gray-d3; color: $gray-d3;
...@@ -516,21 +516,21 @@ body.signup .nav-not-signedin-signin { ...@@ -516,21 +516,21 @@ body.signup .nav-not-signedin-signin {
a { a {
background-color: #d9e3ee; background-color: #d9e3ee;
color: #6d788b; color: #6d788b;
} }
} }
body.signin .nav-not-signedin-signup { body.signin .nav-not-signedin-signup {
a { a {
background-color: #62aaf5; background-color: #62aaf5;
color: #fff; color: #fff;
} }
} }
// ==================== // ====================
// STATE: js enabled // STATE: js enabled
.js { .js {
.nav-dropdown { .nav-dropdown {
...@@ -559,4 +559,4 @@ body.signin .nav-not-signedin-signup { ...@@ -559,4 +559,4 @@ body.signin .nav-not-signedin-signup {
pointer-events: auto; pointer-events: auto;
} }
} }
} }
\ No newline at end of file
...@@ -2,6 +2,19 @@ ...@@ -2,6 +2,19 @@
// ==================== // ====================
// common // common
nav {
ol, ul {
@extend .no-list;
}
.nav-item {
a {
}
}
}
// ==================== // ====================
...@@ -21,4 +34,4 @@ ...@@ -21,4 +34,4 @@
// ==================== // ====================
// //
\ No newline at end of file
...@@ -28,13 +28,13 @@ ...@@ -28,13 +28,13 @@
.cta-show-sock { .cta-show-sock {
@extend .btn-pill; @extend .btn-pill;
@extend .t-action3; @extend .t-action4;
background: $gray-l5; background: $gray-l5;
padding: ($baseline/2) $baseline; padding: ($baseline/2) $baseline;
color: $gray; color: $gray;
.icon { .icon {
@include font-size(16); @include font-size(14);
} }
&:hover { &:hover {
...@@ -59,7 +59,7 @@ ...@@ -59,7 +59,7 @@
header { header {
.title { .title {
@extend .t-title-2; @extend .t-title4;
} }
.ss-icon { .ss-icon {
...@@ -73,12 +73,13 @@ ...@@ -73,12 +73,13 @@
@include box-sizing(border-box); @include box-sizing(border-box);
.title { .title {
@extend .t-title-3; @extend .t-title6;
color: $white; color: $white;
margin-bottom: ($baseline/2); margin-bottom: ($baseline/2);
} }
.copy { .copy {
@extend .t-copy-sub2;
margin: 0 0 $baseline 0; margin: 0 0 $baseline 0;
} }
...@@ -94,6 +95,7 @@ ...@@ -94,6 +95,7 @@
} }
.action { .action {
@extend .t-action4;
display: block; display: block;
.icon { .icon {
...@@ -149,4 +151,4 @@ ...@@ -149,4 +151,4 @@
color: $white; color: $white;
} }
} }
} }
\ No newline at end of file
// studio alerts, prompts and notifications // studio - elements - system feedback
// alerts, notifications, prompts, and status communication
// ==================== // ====================
// shared // shared
...@@ -6,7 +7,7 @@ ...@@ -6,7 +7,7 @@
@include box-sizing(border-box); @include box-sizing(border-box);
.copy { .copy {
@include font-size(13); @extend .t-copy-sub1;
} }
} }
...@@ -36,6 +37,7 @@ ...@@ -36,6 +37,7 @@
.nav-actions .action-primary { .nav-actions .action-primary {
@include blue-button(); @include blue-button();
@include font-size(12); // needed due to bad button mixins for now
border-color: $blue-d2; border-color: $blue-d2;
} }
...@@ -53,6 +55,7 @@ ...@@ -53,6 +55,7 @@
.nav-actions .action-primary { .nav-actions .action-primary {
@include orange-button(); @include orange-button();
@include font-size(12); // needed due to bad button mixins for now
border-color: $orange-d2; border-color: $orange-d2;
color: $gray-d4; color: $gray-d4;
} }
...@@ -71,6 +74,7 @@ ...@@ -71,6 +74,7 @@
.nav-actions .action-primary { .nav-actions .action-primary {
@include red-button(); @include red-button();
@include font-size(12); // needed due to bad button mixins for now
border-color: $red-d2; border-color: $red-d2;
} }
...@@ -88,6 +92,7 @@ ...@@ -88,6 +92,7 @@
.nav-actions .action-primary { .nav-actions .action-primary {
@include blue-button(); @include blue-button();
@include font-size(12); // needed due to bad button mixins for now
border-color: $blue-d2; border-color: $blue-d2;
} }
...@@ -105,6 +110,7 @@ ...@@ -105,6 +110,7 @@
.nav-actions .action-primary { .nav-actions .action-primary {
@include green-button(); @include green-button();
@include font-size(12); // needed due to bad button mixins for now
border-color: $green-d2; border-color: $green-d2;
} }
...@@ -121,8 +127,9 @@ ...@@ -121,8 +127,9 @@
&.step-required { &.step-required {
.nav-actions .action-primary { .nav-actions .action-primary {
border-color: $pink-d2;
@include pink-button(); @include pink-button();
@include font-size(12); // needed due to bad button mixins for now
border-color: $pink-d2;
} }
a { a {
...@@ -137,6 +144,7 @@ ...@@ -137,6 +144,7 @@
// prompts // prompts
.wrapper-prompt { .wrapper-prompt {
@extend .depth4;
@include transition(all 0.05s ease-in-out); @include transition(all 0.05s ease-in-out);
position: fixed; position: fixed;
top: 0; top: 0;
...@@ -144,7 +152,6 @@ ...@@ -144,7 +152,6 @@
width: 100%; width: 100%;
height: 100%; height: 100%;
text-align: center; text-align: center;
z-index: 10000;
&:before { &:before {
content: ''; content: '';
...@@ -184,12 +191,12 @@ ...@@ -184,12 +191,12 @@
} }
.action-primary { .action-primary {
@include font-size(13); @extend .t-action4;
font-weight: 600; font-weight: 600;
} }
.action-secondary { .action-secondary {
@include font-size(13); @extend .t-action4;
} }
} }
} }
...@@ -235,11 +242,11 @@ ...@@ -235,11 +242,11 @@
// notifications // notifications
.wrapper-notification { .wrapper-notification {
@extend .depth3;
@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;
bottom: 0; bottom: 0;
z-index: 1000;
width: 100%; width: 100%;
padding: $baseline ($baseline*2); padding: $baseline ($baseline*2);
...@@ -361,18 +368,19 @@ ...@@ -361,18 +368,19 @@
@include font-size(22); @include font-size(22);
width: flex-grid(1, 12); width: flex-grid(1, 12);
height: ($baseline*1.25); height: ($baseline*1.25);
margin-top: ($baseline/4);
margin-right: flex-gutter(); margin-right: flex-gutter();
text-align: right; text-align: right;
color: $white; color: $white;
} }
.copy { .copy {
@include font-size(13); @extend .t-copy-sub1;
width: flex-grid(10, 12); width: flex-grid(10, 12);
color: $gray-l2; color: $gray-l2;
.title { .title {
@include font-size(14); @extend .t-title7;
margin-bottom: 0; margin-bottom: 0;
color: $white; color: $white;
} }
...@@ -409,13 +417,13 @@ ...@@ -409,13 +417,13 @@
.action-primary { .action-primary {
@include blue-button(); @include blue-button();
@include font-size(13);
border-color: $blue-d2; border-color: $blue-d2;
font-weight: 600; font-weight: 600;
} }
.action-secondary { .action-secondary {
@include font-size(13);
@extend .t-action4;
} }
} }
...@@ -434,7 +442,7 @@ ...@@ -434,7 +442,7 @@
} }
.copy p { .copy p {
@include text-sr(); @extend .text-sr;
} }
} }
} }
...@@ -443,10 +451,10 @@ ...@@ -443,10 +451,10 @@
// alerts // alerts
.wrapper-alert { .wrapper-alert {
@extend .depth2;
@include box-sizing(border-box); @include box-sizing(border-box);
@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);
position: relative; position: relative;
z-index: 100;
overflow: hidden; overflow: hidden;
width: 100%; width: 100%;
border-top: 1px solid $black; border-top: 1px solid $black;
...@@ -504,7 +512,6 @@ ...@@ -504,7 +512,6 @@
// adopted alerts // adopted alerts
.alert { .alert {
@include font-size(14);
@include box-sizing(border-box); @include box-sizing(border-box);
@include clearfix(); @include clearfix();
margin: 0 auto; margin: 0 auto;
...@@ -530,11 +537,11 @@ ...@@ -530,11 +537,11 @@
} }
.copy { .copy {
@include font-size(13);
width: flex-grid(10, 12); width: flex-grid(10, 12);
color: $gray-l2; color: $gray-l2;
.title { .title {
@extend .t-title7;
margin-bottom: 0; margin-bottom: 0;
color: $white; color: $white;
} }
...@@ -568,12 +575,12 @@ ...@@ -568,12 +575,12 @@
} }
.action-primary { .action-primary {
@include font-size(13); @extend .t-action4;
font-weight: 600; font-weight: 600;
} }
.action-secondary { .action-secondary {
@include font-size(13); @extend .t-action4;
} }
} }
} }
...@@ -590,7 +597,7 @@ ...@@ -590,7 +597,7 @@
text-align: center; text-align: center;
.label { .label {
@include text-sr(); @extend .text-sr;
} }
.icon { .icon {
......
// studio - elements - system help
// ====================
// studio - elements - typography // studio - elements - typography
// ==================== // ====================
// Scale - (6, 7, 8, 9, 10, 11, 12, 14, 16, 18, 21, 24, 36, 48, 60, 72)
// headings/titles // headings/titles
.t-title-1, .t-title-2, .t-title-3, .t-title-4, .t-title-5, .t-title-5 { .t-title {
color: $gray-d3; font-family: $f-sans-serif;
}
.t-title1 {
@extend .t-title;
@include font-size(60);
@include lh(60);
} }
.t-title-1 { .t-title2 {
@extend .t-title;
@include font-size(48);
@include lh(48);
}
.t-title3 {
@include font-size(36); @include font-size(36);
@include lh(36);
} }
.t-title-2 { .t-title4 {
@extend .t-title;
@include font-size(24); @include font-size(24);
font-weight: 600; @include lh(24);
} }
.t-title-3 { .t-title5 {
@include font-size(16); @extend .t-title;
font-weight: 600; @include font-size(18);
@include lh(18);
} }
.t-title-4 { .t-title6 {
@extend .t-title;
@include font-size(16);
@include lh(16);
}
.t-title7 {
@extend .t-title;
@include font-size(14);
@include lh(14);
} }
.t-title-5 { .t-title8 {
@extend .t-title;
@include font-size(12);
@include lh(12);
}
.t-title9 {
@extend .t-title;
@include font-size(11);
@include lh(11);
} }
// ==================== // ====================
// copy // copy
.t-copy {
font-family: $f-sans-serif;
}
.t-copy-base { .t-copy-base {
@extend .t-copy;
@include font-size(16); @include font-size(16);
@include lh(16);
} }
.t-copy-lead1 { .t-copy-lead1 {
@extend .t-copy;
@include font-size(18); @include font-size(18);
@include lh(18);
} }
.t-copy-lead2 { .t-copy-lead2 {
@include font-size(20); @extend .t-copy;
@include font-size(24);
@include lh(24);
} }
.t-copy-sub1 { .t-copy-sub1 {
@extend .t-copy;
@include font-size(14); @include font-size(14);
@include lh(14);
} }
.t-copy-sub2 { .t-copy-sub2 {
@include font-size(13); @extend .t-copy;
}
.t-copy-sub3 {
@include font-size(12); @include font-size(12);
@include lh(12);
} }
// ==================== // ====================
// actions/labels // actions/labels
.t-action1 { .t-action1 {
@include font-size(14); @include font-size(18);
font-weight: 600; @include lh(18);
} }
.t-action2 { .t-action2 {
@include font-size(13); @include font-size(16);
font-weight: 600; @include lh(16);
text-transform: uppercase;
} }
.t-action3 { .t-action3 {
@include font-size(13); @include font-size(14);
@include lh(14);
} }
.t-action4 { .t-action4 {
@include font-size(12); @include font-size(12);
@include lh(12);
}
// ====================
// code
.t-code {
font-family: $f-monospace;
} }
// ==================== // ====================
...@@ -82,4 +134,4 @@ ...@@ -82,4 +134,4 @@
// misc // misc
.t-icon { .t-icon {
line-height: 0; line-height: 0;
} }
\ No newline at end of file
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
border-color: $darkGrey; border-color: $darkGrey;
border-radius: 2px; border-radius: 2px;
background: #fff; background: #fff;
font-family: $sans-serif; font-family: $f-sans-serif;
font-size: 12px; font-size: 12px;
@include box-shadow(0 5px 10px rgba(0, 0, 0, 0.1)); @include box-shadow(0 5px 10px rgba(0, 0, 0, 0.1));
z-index: 100000 !important; z-index: 100000 !important;
...@@ -62,4 +62,4 @@ ...@@ -62,4 +62,4 @@
// JQUI timepicker // JQUI timepicker
.ui-timepicker-list { .ui-timepicker-list {
z-index: 100000 !important; z-index: 100000 !important;
} }
\ No newline at end of file
...@@ -12,7 +12,7 @@ body.signup, body.signin { ...@@ -12,7 +12,7 @@ body.signup, body.signin {
.content { .content {
@include clearfix(); @include clearfix();
@include font-size(16); @extend .t-copy-base;
max-width: $fg-max-width; max-width: $fg-max-width;
min-width: $fg-min-width; min-width: $fg-min-width;
width: flex-grid(12); width: flex-grid(12);
...@@ -26,14 +26,14 @@ body.signup, body.signin { ...@@ -26,14 +26,14 @@ body.signup, body.signin {
padding-bottom: ($baseline/2); padding-bottom: ($baseline/2);
h1 { h1 {
@include font-size(32); @extend .t-title3;
margin: 0; margin: 0;
padding: 0; padding: 0;
font-weight: 600; font-weight: 600;
} }
.action { .action {
@include font-size(13); @extend .t-action3;
position: absolute; position: absolute;
right: 0; right: 0;
top: 40%; top: 40%;
...@@ -41,7 +41,7 @@ body.signup, body.signin { ...@@ -41,7 +41,7 @@ body.signup, body.signin {
} }
.introduction { .introduction {
@include font-size(14); @extend .t-copy-sub1;
margin: 0 0 $baseline 0; margin: 0 0 $baseline 0;
} }
} }
...@@ -69,8 +69,8 @@ body.signup, body.signin { ...@@ -69,8 +69,8 @@ body.signup, body.signin {
.action-primary { .action-primary {
@include blue-button; @include blue-button;
@extend .t-action2;
@include transition(all .15s); @include transition(all .15s);
@include font-size(15);
display: block; display: block;
width: 100%; width: 100%;
padding: ($baseline*0.75) ($baseline/2); padding: ($baseline*0.75) ($baseline/2);
...@@ -108,7 +108,7 @@ body.signup, body.signin { ...@@ -108,7 +108,7 @@ body.signup, body.signin {
} }
label { label {
@include font-size(14); @extend .t-copy-sub1;
@include transition(color, 0.15s, ease-in-out); @include transition(color, 0.15s, ease-in-out);
margin: 0 0 ($baseline/4) 0; margin: 0 0 ($baseline/4) 0;
...@@ -118,7 +118,7 @@ body.signup, body.signin { ...@@ -118,7 +118,7 @@ body.signup, body.signin {
} }
input, textarea { input, textarea {
@include font-size(16); @extend .t-copy-base;
height: 100%; height: 100%;
width: 100%; width: 100%;
padding: ($baseline/2); padding: ($baseline/2);
...@@ -171,8 +171,8 @@ body.signup, body.signin { ...@@ -171,8 +171,8 @@ body.signup, body.signin {
} }
.tip { .tip {
@extend .t-copy-sub2;
@include transition(color, 0.15s, ease-in-out); @include transition(color, 0.15s, ease-in-out);
@include font-size(13);
display: block; display: block;
margin-top: ($baseline/4); margin-top: ($baseline/4);
color: $gray-l3; color: $gray-l3;
...@@ -212,7 +212,7 @@ body.signup, body.signin { ...@@ -212,7 +212,7 @@ body.signup, body.signin {
width: flex-grid(4, 12); width: flex-grid(4, 12);
.bit { .bit {
@include font-size(13); @extend .t-copy-sub1;
margin: 0 0 $baseline 0; margin: 0 0 $baseline 0;
border-bottom: 1px solid $gray-l4; border-bottom: 1px solid $gray-l4;
padding: 0 0 $baseline 0; padding: 0 0 $baseline 0;
...@@ -225,7 +225,7 @@ body.signup, body.signin { ...@@ -225,7 +225,7 @@ body.signup, body.signin {
} }
h3 { h3 {
@include font-size(14); @extend .t-title7;
margin: 0 0 ($baseline/4) 0; margin: 0 0 ($baseline/4) 0;
color: $gray-d2; color: $gray-d2;
font-weight: 600; font-weight: 600;
...@@ -245,7 +245,7 @@ body.signup, body.signin { ...@@ -245,7 +245,7 @@ body.signup, body.signin {
position: relative; position: relative;
.action-forgotpassword { .action-forgotpassword {
@include font-size(13); @extend .t-action3;
position: absolute; position: absolute;
top: 0; top: 0;
right: 0; right: 0;
...@@ -257,7 +257,7 @@ body.signup, body.signin { ...@@ -257,7 +257,7 @@ body.signup, body.signin {
// messages // messages
.message { .message {
@include font-size(14); @extend .t-copy-sub1;
display: block; display: block;
} }
......
...@@ -23,7 +23,7 @@ body.course.checklists { ...@@ -23,7 +23,7 @@ body.course.checklists {
// visual status // visual status
.viz-checklist-status { .viz-checklist-status {
@include text-hide(); @extend .text-hide;
@include size(100%,($baseline/4)); @include size(100%,($baseline/4));
position: relative; position: relative;
display: block; display: block;
...@@ -40,7 +40,7 @@ body.course.checklists { ...@@ -40,7 +40,7 @@ body.course.checklists {
background: $green; background: $green;
.int { .int {
@include text-sr(); @extend .text-sr;
} }
} }
} }
...@@ -83,13 +83,13 @@ body.course.checklists { ...@@ -83,13 +83,13 @@ body.course.checklists {
} }
.checklist-status { .checklist-status {
@include font-size(13); @extend .t-copy-sub1;
width: flex-grid(3, 9); width: flex-grid(3, 9);
float: right; float: right;
margin-top: ($baseline/2); margin-top: ($baseline/2);
text-align: right; text-align: right;
color: $gray-l2; color: $gray-l2;
.icon-confirm { .icon-confirm {
@include font-size(20); @include font-size(20);
...@@ -100,7 +100,7 @@ body.course.checklists { ...@@ -100,7 +100,7 @@ body.course.checklists {
} }
.status-count { .status-count {
@include font-size(16); @extend .t-copy-base;
margin-left: ($baseline/4); margin-left: ($baseline/4);
margin-right: ($baseline/4); margin-right: ($baseline/4);
color: $gray-d3; color: $gray-d3;
...@@ -108,7 +108,7 @@ body.course.checklists { ...@@ -108,7 +108,7 @@ body.course.checklists {
} }
.status-amount { .status-amount {
@include font-size(16); @extend .t-copy-base;
margin-left: ($baseline/4); margin-left: ($baseline/4);
color: $gray-d3; color: $gray-d3;
font-weight: 600; font-weight: 600;
...@@ -138,8 +138,8 @@ body.course.checklists { ...@@ -138,8 +138,8 @@ body.course.checklists {
} }
.action-secondary { .action-secondary {
@include font-size(14);
@include grey-button(); @include grey-button();
@extend .t-action3;
font-weight: 400; font-weight: 400;
float: right; float: right;
...@@ -174,7 +174,7 @@ body.course.checklists { ...@@ -174,7 +174,7 @@ body.course.checklists {
// state - completed // state - completed
&.is-completed { &.is-completed {
.viz-checklist-status { .viz-checklist-status {
.viz-checklist-status-value { .viz-checklist-status-value {
...@@ -189,9 +189,9 @@ body.course.checklists { ...@@ -189,9 +189,9 @@ body.course.checklists {
} }
.checklist-status { .checklist-status {
.status-count, .status-amount, .icon-confirm { .status-count, .status-amount, .icon-confirm {
color: $green; color: $green;
} }
} }
} }
...@@ -244,17 +244,17 @@ body.course.checklists { ...@@ -244,17 +244,17 @@ body.course.checklists {
vertical-align: baseline; vertical-align: baseline;
cursor: pointer; cursor: pointer;
margin-bottom: 0; margin-bottom: 0;
} }
.task-description { .task-description {
@extend .t-copy-sub1;
@include transition(color .15s .25s ease-in-out); @include transition(color .15s .25s ease-in-out);
@include font-size(14);
color: $gray-l2; color: $gray-l2;
} }
.task-support { .task-support {
@extend .t-copy-sub2;
@include transition(opacity .15s .25s ease-in-out); @include transition(opacity .15s .25s ease-in-out);
@include font-size(12);
opacity: 0; opacity: 0;
pointer-events: none; pointer-events: none;
} }
...@@ -274,14 +274,14 @@ body.course.checklists { ...@@ -274,14 +274,14 @@ body.course.checklists {
.action-primary { .action-primary {
@include blue-button; @include blue-button;
@extend .t-action4;
@include transition(all .15s); @include transition(all .15s);
@include font-size(12);
font-weight: 600; font-weight: 600;
text-align: center; text-align: center;
} }
.action-secondary { .action-secondary {
@include font-size(13); @extend .t-action4;
margin-top: ($baseline/2); margin-top: ($baseline/2);
} }
} }
...@@ -322,7 +322,7 @@ body.course.checklists { ...@@ -322,7 +322,7 @@ body.course.checklists {
.action-primary { .action-primary {
@include grey-button; @include grey-button;
@include font-size(12); @extend .t-action4;
font-weight: 600; font-weight: 600;
text-align: center; text-align: center;
} }
...@@ -344,4 +344,4 @@ body.course.checklists { ...@@ -344,4 +344,4 @@ body.course.checklists {
.content-supplementary { .content-supplementary {
width: flex-grid(3, 12); width: flex-grid(3, 12);
} }
} }
\ No newline at end of file
...@@ -18,14 +18,14 @@ body.index { ...@@ -18,14 +18,14 @@ body.index {
} }
.content { .content {
@extend .t-copy-base;
@include clearfix(); @include clearfix();
@include font-size(16);
max-width: $fg-max-width; max-width: $fg-max-width;
min-width: $fg-min-width; min-width: $fg-min-width;
width: flex-grid(12); width: flex-grid(12);
margin: 0 auto; margin: 0 auto;
color: $gray-d2; color: $gray-d2;
header { header {
border: none; border: none;
padding-bottom: 0; padding-bottom: 0;
...@@ -62,7 +62,7 @@ body.index { ...@@ -62,7 +62,7 @@ body.index {
color: $white; color: $white;
h1 { h1 {
@include font-size(52); @extend .t-title2;
float: none; float: none;
margin: 0 0 ($baseline/2) 0; margin: 0 0 ($baseline/2) 0;
border-bottom: 1px solid $blue-l1; border-bottom: 1px solid $blue-l1;
...@@ -72,7 +72,7 @@ body.index { ...@@ -72,7 +72,7 @@ body.index {
} }
.logo { .logo {
@include text-hide(); @extend .text-hide;
position: relative; position: relative;
top: 3px; top: 3px;
display: inline-block; display: inline-block;
...@@ -83,7 +83,7 @@ body.index { ...@@ -83,7 +83,7 @@ body.index {
} }
.tagline { .tagline {
@include font-size(24); @extend .t-title4;
margin: 0; margin: 0;
color: $blue-l3; color: $blue-l3;
} }
...@@ -198,13 +198,13 @@ body.index { ...@@ -198,13 +198,13 @@ body.index {
margin-top: -($baseline/4); margin-top: -($baseline/4);
h3 { h3 {
@extend .t-title4;
margin: 0 0 ($baseline/2) 0; margin: 0 0 ($baseline/2) 0;
@include font-size(24);
font-weight: 600; font-weight: 600;
} }
> p { > p {
@include font-size(18); @extend .t-copy-lead1;
color: $gray-d1; color: $gray-d1;
} }
...@@ -214,8 +214,8 @@ body.index { ...@@ -214,8 +214,8 @@ body.index {
} }
.list-proofpoints { .list-proofpoints {
@extend .t-copy-sub1;
@include clearfix(); @include clearfix();
@include font-size(14);
width: flex-grid(9, 9); width: flex-grid(9, 9);
margin: ($baseline*1.5) 0 0 0; margin: ($baseline*1.5) 0 0 0;
...@@ -233,7 +233,7 @@ body.index { ...@@ -233,7 +233,7 @@ body.index {
color: $gray-l1; color: $gray-l1;
.title { .title {
@include font-size(16); @extend .t-copy-base;
margin: 0 0 ($baseline/4) 0; margin: 0 0 ($baseline/4) 0;
font-weight: 500; font-weight: 500;
color: $gray-d3; color: $gray-d3;
...@@ -314,7 +314,7 @@ body.index { ...@@ -314,7 +314,7 @@ body.index {
margin-top: -($baseline*1.5); margin-top: -($baseline*1.5);
li { li {
width: flex-grid(6, 12); width: flex-grid(6, 12);
margin: 0 auto; margin: 0 auto;
} }
...@@ -322,23 +322,23 @@ body.index { ...@@ -322,23 +322,23 @@ body.index {
display: block; display: block;
width: 100%; width: 100%;
text-align: center; text-align: center;
}
.action-primary { &.action-primary {
@include blue-button; @extend .t-action1;
@include transition(all .15s); @include blue-button;
@include font-size(18); @include transition(all .15s);
padding: ($baseline*0.75) ($baseline/2); padding: ($baseline*0.75) ($baseline/2);
font-weight: 600; font-weight: 600;
text-align: center; text-align: center;
text-transform: uppercase; text-transform: uppercase;
} }
.action-secondary { &.action-secondary {
@include font-size(14); @extend .t-action3;
margin-top: ($baseline/2); margin-top: ($baseline/2);
}
} }
} }
} }
} }
} }
\ No newline at end of file
...@@ -29,6 +29,7 @@ body.course.outline { ...@@ -29,6 +29,7 @@ body.course.outline {
width: 100px; width: 100px;
.status-label { .status-label {
@include font-size(12);
position: absolute; position: absolute;
top: 2px; top: 2px;
right: -5px; right: -5px;
...@@ -38,7 +39,6 @@ body.course.outline { ...@@ -38,7 +39,6 @@ body.course.outline {
@include border-radius(3px); @include border-radius(3px);
color: $lightGrey; color: $lightGrey;
text-align: right; text-align: right;
font-size: 12px;
font-weight: bold; font-weight: bold;
line-height: 16px; line-height: 16px;
} }
...@@ -57,6 +57,10 @@ body.course.outline { ...@@ -57,6 +57,10 @@ body.course.outline {
} }
.menu { .menu {
@include font-size(12);
@include border-radius(4px);
@include box-shadow(0 1px 2px rgba(0, 0, 0, .2));
@include transition(opacity .15s);
z-index: 1; z-index: 1;
display: none; display: none;
opacity: 0.0; opacity: 0.0;
...@@ -67,10 +71,6 @@ body.course.outline { ...@@ -67,10 +71,6 @@ body.course.outline {
padding: 8px 12px; padding: 8px 12px;
background: $white; background: $white;
border: 1px solid $mediumGrey; border: 1px solid $mediumGrey;
font-size: 12px;
@include border-radius(4px);
@include box-shadow(0 1px 2px rgba(0, 0, 0, .2));
@include transition(opacity .15s);
li { li {
...@@ -167,7 +167,7 @@ body.course.outline { ...@@ -167,7 +167,7 @@ body.course.outline {
text-align: right; text-align: right;
.published-status { .published-status {
font-size: 12px; @include font-size(12);
margin-right: 15px; margin-right: 15px;
strong { strong {
...@@ -185,19 +185,19 @@ body.course.outline { ...@@ -185,19 +185,19 @@ body.course.outline {
.schedule-button, .schedule-button,
.edit-button { .edit-button {
font-size: 11px; @include font-size(11);
padding: 3px 15px 5px; padding: 3px 15px 5px;
} }
} }
.datepair .date, .datepair .date,
.datepair .time { .datepair .time {
@include font-size(13);
@include box-shadow(none);
padding-left: 0; padding-left: 0;
padding-right: 0; padding-right: 0;
border: none; border: none;
background: none; background: none;
@include box-shadow(none);
font-size: 13px;
font-weight: bold; font-weight: bold;
color: $blue; color: $blue;
cursor: pointer; cursor: pointer;
...@@ -231,10 +231,10 @@ body.course.outline { ...@@ -231,10 +231,10 @@ body.course.outline {
@include clearfix(); @include clearfix();
.section-name { .section-name {
@include font-size(19);
float: left; float: left;
margin-right: 10px; margin-right: 10px;
width: 350px; width: 350px;
font-size: 19px;
font-weight: bold; font-weight: bold;
color: $blue; color: $blue;
} }
...@@ -254,9 +254,9 @@ body.course.outline { ...@@ -254,9 +254,9 @@ body.course.outline {
background: $white; background: $white;
input { input {
font-size: 16px; @include font-size(16);
} }
.save-button { .save-button {
@include blue-button; @include blue-button;
padding: 7px 20px 7px; padding: 7px 20px 7px;
...@@ -275,7 +275,7 @@ body.course.outline { ...@@ -275,7 +275,7 @@ body.course.outline {
background: $lightGrey; background: $lightGrey;
.published-status { .published-status {
font-size: 12px; @include font-size(12);
margin-right: 15px; margin-right: 15px;
strong { strong {
...@@ -293,9 +293,8 @@ body.course.outline { ...@@ -293,9 +293,8 @@ body.course.outline {
.schedule-button, .schedule-button,
.edit-button { .edit-button {
font-size: 11px; @include font-size(11);
padding: 3px 15px 5px; padding: 0 15px 2px 15px;
} }
} }
...@@ -306,17 +305,17 @@ body.course.outline { ...@@ -306,17 +305,17 @@ body.course.outline {
width: 145px; width: 145px;
.status-label { .status-label {
@include font-size(12);
@include border-radius(3px);
position: absolute; position: absolute;
top: 0; top: 0;
right: 2px; right: 2px;
display: none; display: none;
width: 100px; width: 100px;
padding: 10px 35px 10px 10px; padding: 10px 35px 10px 10px;
@include border-radius(3px);
background: $lightGrey; background: $lightGrey;
color: $lightGrey; color: $lightGrey;
text-align: right; text-align: right;
font-size: 12px;
font-weight: bold; font-weight: bold;
line-height: 16px; line-height: 16px;
} }
...@@ -335,6 +334,11 @@ body.course.outline { ...@@ -335,6 +334,11 @@ body.course.outline {
} }
.menu { .menu {
@include font-size(12);
@include border-radius(4px);
@include box-shadow(0 1px 2px rgba(0, 0, 0, .2));
@include transition(opacity .15s);
@include transition(display .15s);
z-index: 1; z-index: 1;
display: none; display: none;
opacity: 0.0; opacity: 0.0;
...@@ -345,11 +349,7 @@ body.course.outline { ...@@ -345,11 +349,7 @@ body.course.outline {
padding: 8px 12px; padding: 8px 12px;
background: $white; background: $white;
border: 1px solid $mediumGrey; border: 1px solid $mediumGrey;
font-size: 12px;
@include border-radius(4px);
@include box-shadow(0 1px 2px rgba(0, 0, 0, .2));
@include transition(opacity .15s);
@include transition(display .15s);
li { li {
...@@ -410,7 +410,7 @@ body.course.outline { ...@@ -410,7 +410,7 @@ body.course.outline {
} }
} }
.item-actions { .item-actions {
margin-top: 21px; margin-top: 21px;
margin-right: 12px; margin-right: 12px;
...@@ -422,7 +422,7 @@ body.course.outline { ...@@ -422,7 +422,7 @@ body.course.outline {
.expand-collapse-icon { .expand-collapse-icon {
float: left; float: left;
margin: 29px 6px 16px 16px; margin: 25px 6px 16px 16px;
@include transition(none); @include transition(none);
&.expand { &.expand {
...@@ -430,7 +430,7 @@ body.course.outline { ...@@ -430,7 +430,7 @@ body.course.outline {
} }
&.collapsed { &.collapsed {
} }
} }
...@@ -440,7 +440,7 @@ body.course.outline { ...@@ -440,7 +440,7 @@ body.course.outline {
} }
h3 { h3 {
font-size: 19px; @include font-size(19);
font-weight: 700; font-weight: 700;
color: $blue; color: $blue;
} }
...@@ -460,9 +460,9 @@ body.course.outline { ...@@ -460,9 +460,9 @@ body.course.outline {
.section-name-edit { .section-name-edit {
input { input {
font-size: 16px; @include font-size(16);
} }
.save-button { .save-button {
@include blue-button; @include blue-button;
padding: 7px 20px 7px; padding: 7px 20px 7px;
...@@ -476,7 +476,7 @@ body.course.outline { ...@@ -476,7 +476,7 @@ body.course.outline {
} }
h4 { h4 {
font-size: 12px; @include font-size(12);
color: #878e9d; color: #878e9d;
strong { strong {
...@@ -502,8 +502,8 @@ body.course.outline { ...@@ -502,8 +502,8 @@ body.course.outline {
&.new-section { &.new-section {
header { header {
height: auto;
@include clearfix(); @include clearfix();
height: auto;
} }
.expand-collapse-icon { .expand-collapse-icon {
...@@ -522,12 +522,11 @@ body.course.outline { ...@@ -522,12 +522,11 @@ body.course.outline {
} }
.toggle-button-sections { .toggle-button-sections {
@include font-size(12);
display: none; display: none;
position: relative; position: relative;
float: right; float: right;
margin-top: 10px; margin-top: ($baseline/4);
font-size: 13px;
color: $darkGrey; color: $darkGrey;
&.is-shown { &.is-shown {
...@@ -535,13 +534,13 @@ body.course.outline { ...@@ -535,13 +534,13 @@ body.course.outline {
} }
.ss-icon { .ss-icon {
@include font-size(11);
@include border-radius(20px); @include border-radius(20px);
position: relative; position: relative;
top: -1px; top: -1px;
display: inline-block; display: inline-block;
margin-right: 2px; margin-right: 2px;
line-height: 5px; line-height: 5px;
font-size: 11px;
} }
.label { .label {
...@@ -599,7 +598,7 @@ body.course.outline { ...@@ -599,7 +598,7 @@ body.course.outline {
} }
h3 { h3 {
font-size: 34px; @include font-size(34);
font-weight: 300; font-weight: 300;
} }
...@@ -625,16 +624,16 @@ body.course.outline { ...@@ -625,16 +624,16 @@ body.course.outline {
} }
label { label {
@include font-size(14); @extend .t-copy-sub1;
margin-bottom: ($baseline/4); margin-bottom: ($baseline/4);
} }
} }
} }
.description { .description {
@include font-size(14);
float: left; float: left;
margin-top: 30px; margin-top: 30px;
font-size: 14px;
line-height: 20px; line-height: 20px;
width: 100%; width: 100%;
} }
...@@ -645,7 +644,7 @@ body.course.outline { ...@@ -645,7 +644,7 @@ body.course.outline {
.start-date, .start-date,
.start-time { .start-time {
font-size: 19px; @include font-size(19);
} }
.save-button { .save-button {
...@@ -659,14 +658,14 @@ body.course.outline { ...@@ -659,14 +658,14 @@ body.course.outline {
.save-button, .save-button,
.cancel-button { .cancel-button {
font-size: 16px; @include font-size(16);
} }
} }
.collapse-all-button { .collapse-all-button {
@include font-size(13);
float: right; float: right;
margin-top: 10px; margin-top: 10px;
font-size: 13px;
color: $darkGrey; color: $darkGrey;
} }
...@@ -686,7 +685,7 @@ body.course.outline { ...@@ -686,7 +685,7 @@ body.course.outline {
border: 1px solid $darkGrey; border: 1px solid $darkGrey;
opacity : 0.2; opacity : 0.2;
&:hover { &:hover {
opacity : 1.0; opacity : 1.0;
.section-item { .section-item {
background: $yellow !important; background: $yellow !important;
} }
...@@ -701,4 +700,4 @@ body.course.outline { ...@@ -701,4 +700,4 @@ body.course.outline {
ol.ui-droppable .branch:first-child .section-item { ol.ui-droppable .branch:first-child .section-item {
border-top: none; border-top: none;
} }
} }
\ No newline at end of file
...@@ -66,7 +66,7 @@ body.course.settings { ...@@ -66,7 +66,7 @@ body.course.settings {
} }
.tip { .tip {
@include font-size(13); @extend .t-copy-sub2;
width: flex-grid(5, 9); width: flex-grid(5, 9);
float: right; float: right;
margin-top: ($baseline/2); margin-top: ($baseline/2);
...@@ -86,20 +86,20 @@ body.course.settings { ...@@ -86,20 +86,20 @@ body.course.settings {
// in form -UI hints/tips/messages // in form -UI hints/tips/messages
.instructions { .instructions {
@include font-size(14); @extend .t-copy-sub1;
margin: 0 0 $baseline 0; margin: 0 0 $baseline 0;
} }
.tip { .tip {
@extend .t-copy-sub2;
@include transition(color, 0.15s, ease-in-out); @include transition(color, 0.15s, ease-in-out);
@include font-size(13);
display: block; display: block;
margin-top: ($baseline/4); margin-top: ($baseline/4);
color: $gray-l3; color: $gray-l3;
} }
.message-error { .message-error {
@include font-size(13); @extend .t-copy-sub1;
display: block; display: block;
margin-top: ($baseline/4); margin-top: ($baseline/4);
margin-bottom: ($baseline/2); margin-bottom: ($baseline/2);
...@@ -109,12 +109,13 @@ body.course.settings { ...@@ -109,12 +109,13 @@ body.course.settings {
// buttons // buttons
.remove-item { .remove-item {
@include white-button; @include white-button;
@include font-size(13); @extend .t-action-3;
font-weight: 400; font-weight: 400;
} }
.new-button { .new-button {
@include font-size(13); // @extend .t-action-3; - bad buttons won't render this properly
@include font-size(14);
} }
// form basics // form basics
...@@ -158,8 +159,8 @@ body.course.settings { ...@@ -158,8 +159,8 @@ body.course.settings {
} }
input, textarea { input, textarea {
@extend .t-copy-base;
@include placeholder($gray-l4); @include placeholder($gray-l4);
@include font-size(16);
@include size(100%,100%); @include size(100%,100%);
padding: ($baseline/2); padding: ($baseline/2);
...@@ -324,7 +325,7 @@ body.course.settings { ...@@ -324,7 +325,7 @@ body.course.settings {
.action-primary { .action-primary {
@include blue-button(); @include blue-button();
@include font-size(13); @extend .t-action-3;
font-weight: 600; font-weight: 600;
.icon { .icon {
...@@ -747,7 +748,7 @@ body.course.settings { ...@@ -747,7 +748,7 @@ body.course.settings {
// specific to code mirror instance in JSON policy editing, need to sync up with other similar code mirror UIs // specific to code mirror instance in JSON policy editing, need to sync up with other similar code mirror UIs
.CodeMirror { .CodeMirror {
@include font-size(16); @extend .t-copy-base;
@include box-sizing(border-box); @include box-sizing(border-box);
@include box-shadow(0 1px 2px rgba(0, 0, 0, .1) inset); @include box-shadow(0 1px 2px rgba(0, 0, 0, .1) inset);
@include linear-gradient($lightGrey, tint($lightGrey, 90%)); @include linear-gradient($lightGrey, tint($lightGrey, 90%));
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
// ==================== // ====================
body.course.static-pages { body.course.static-pages {
.new-static-page-button { .new-static-page-button {
@include grey-button; @include grey-button;
display: block; display: block;
...@@ -44,7 +44,7 @@ body.course.static-pages { ...@@ -44,7 +44,7 @@ body.course.static-pages {
label { label {
display: inline-block; display: inline-block;
margin-right: 10px; margin-right: 10px;
} }
} }
...@@ -123,7 +123,7 @@ body.course.static-pages { ...@@ -123,7 +123,7 @@ body.course.static-pages {
.component-actions { .component-actions {
position: absolute; position: absolute;
top: 26px; top: 20px;
right: 44px; right: 44px;
} }
} }
...@@ -150,12 +150,12 @@ body.course.static-pages { ...@@ -150,12 +150,12 @@ body.course.static-pages {
} }
.static-page-item { .static-page-item {
position: relative; position: relative;
margin: 10px 0; margin: 10px 0;
padding: 22px 20px; padding: 22px 20px;
border: 1px solid $darkGrey; border: 1px solid $darkGrey;
border-radius: 3px; border-radius: 3px;
background: #fff; background: #fff;
@include box-shadow(0 1px 2px rgba(0, 0, 0, .1)); @include box-shadow(0 1px 2px rgba(0, 0, 0, .1));
.page-name { .page-name {
...@@ -204,4 +204,4 @@ body.course.static-pages { ...@@ -204,4 +204,4 @@ body.course.static-pages {
color: #3c3c3c; color: #3c3c3c;
outline: 0; outline: 0;
} }
} }
\ No newline at end of file
This source diff could not be displayed because it is too large. You can view the blob instead.
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