Commit 04e75485 by Arthur Barrett

merged master branch into feature/abarrett/lms-notes-app. resolved conflicts in…

merged master branch into feature/abarrett/lms-notes-app. resolved conflicts in lms/envs/common.py and lms/templates/static_htmlbook.html.
parents 7784a29e fe12c645
...@@ -8,6 +8,7 @@ ...@@ -8,6 +8,7 @@
:2e# :2e#
.AppleDouble .AppleDouble
database.sqlite database.sqlite
private-requirements.txt
courseware/static/js/mathjax/* courseware/static/js/mathjax/*
flushdb.sh flushdb.sh
build build
...@@ -31,3 +32,4 @@ cover_html/ ...@@ -31,3 +32,4 @@ cover_html/
chromedriver.log chromedriver.log
/nbproject /nbproject
ghostdriver.log ghostdriver.log
node_modules
...@@ -34,15 +34,23 @@ load-plugins= ...@@ -34,15 +34,23 @@ load-plugins=
# multiple time (only on the command line, not in the configuration file where # multiple time (only on the command line, not in the configuration file where
# it should appear only once). # it should appear only once).
disable= disable=
# W0141: Used builtin function 'map' # Never going to use these
# C0301: Line too long
# W0142: Used * or ** magic # W0142: Used * or ** magic
# W0141: Used builtin function 'map'
# Might use these when the code is in better shape
# C0302: Too many lines in module
# R0201: Method could be a function # R0201: Method could be a function
# R0901: Too many ancestors # R0901: Too many ancestors
# R0902: Too many instance attributes # R0902: Too many instance attributes
# R0903: Too few public methods (1/2) # R0903: Too few public methods (1/2)
# R0904: Too many public methods # R0904: Too many public methods
# R0911: Too many return statements
# R0912: Too many branches
# R0913: Too many arguments # R0913: Too many arguments
W0141,W0142,R0201,R0901,R0902,R0903,R0904,R0913 # R0914: Too many local variables
C0301,C0302,W0141,W0142,R0201,R0901,R0902,R0903,R0904,R0911,R0912,R0913,R0914
[REPORTS] [REPORTS]
...@@ -91,7 +99,18 @@ zope=no ...@@ -91,7 +99,18 @@ zope=no
# List of members which are set dynamically and missed by pylint inference # List of members which are set dynamically and missed by pylint inference
# system, and so shouldn't trigger E0201 when accessed. Python regular # system, and so shouldn't trigger E0201 when accessed. Python regular
# expressions are accepted. # expressions are accepted.
generated-members=REQUEST,acl_users,aq_parent,objects,DoesNotExist,can_read,can_write,get_url,size generated-members=
REQUEST,
acl_users,
aq_parent,
objects,
DoesNotExist,
can_read,
can_write,
get_url,
size,
content,
status_code
[BASIC] [BASIC]
......
...@@ -22,5 +22,4 @@ libreadline6 ...@@ -22,5 +22,4 @@ libreadline6
libreadline6-dev libreadline6-dev
mongodb mongodb
nodejs nodejs
npm
coffeescript coffeescript
import logging
import sys
from django.contrib.auth.models import User, Group from django.contrib.auth.models import User, Group
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
...@@ -131,7 +128,7 @@ def remove_user_from_course_group(caller, user, location, role): ...@@ -131,7 +128,7 @@ def remove_user_from_course_group(caller, user, location, role):
raise PermissionDenied raise PermissionDenied
# see if the user is actually in that role, if not then we don't have to do anything # see if the user is actually in that role, if not then we don't have to do anything
if is_user_in_course_group_role(user, location, role) == True: if is_user_in_course_group_role(user, location, role):
groupname = get_course_groupname_for_role(location, role) groupname = get_course_groupname_for_role(location, role)
group = Group.objects.get(name=groupname) group = Group.objects.get(name=groupname)
......
...@@ -97,8 +97,7 @@ def update_course_updates(location, update, passed_id=None): ...@@ -97,8 +97,7 @@ def update_course_updates(location, update, passed_id=None):
if (len(new_html_parsed) == 1): if (len(new_html_parsed) == 1):
content = new_html_parsed[0].tail content = new_html_parsed[0].tail
else: else:
content = "\n".join([html.tostring(ele) content = "\n".join([html.tostring(ele) for ele in new_html_parsed[1:]])
for ele in new_html_parsed[1:]])
return {"id": passed_id, return {"id": passed_id,
"date": update['date'], "date": update['date'],
......
Feature: Advanced (manual) course policy Feature: Advanced (manual) course policy
In order to specify course policy settings for which no custom user interface exists In order to specify course policy settings for which no custom user interface exists
I want to be able to manually enter JSON key/value pairs I want to be able to manually enter JSON key /value pairs
Scenario: A course author sees default advanced settings Scenario: A course author sees default advanced settings
Given I have opened a new course in Studio Given I have opened a new course in Studio
...@@ -11,6 +11,7 @@ Feature: Advanced (manual) course policy ...@@ -11,6 +11,7 @@ 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
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
...@@ -19,14 +20,15 @@ Feature: Advanced (manual) course policy ...@@ -19,14 +20,15 @@ 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
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 When I edit the value of a policy key and save
And I press the "Save" notification button
Then the policy key value is changed Then the policy key value is changed
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
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
...@@ -34,6 +36,7 @@ Feature: Advanced (manual) course policy ...@@ -34,6 +36,7 @@ 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
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
......
#pylint: disable=C0111
#pylint: disable=W0621
from lettuce import world, step from lettuce import world, step
from common import * from common import *
import time from nose.tools import assert_false, assert_equal
from terrain.steps import reload_the_page
from selenium.common.exceptions import WebDriverException
from selenium.webdriver.support import expected_conditions as EC
from nose.tools import assert_true, assert_false, assert_equal
""" """
http://selenium.googlecode.com/svn/trunk/docs/api/py/webdriver/selenium.webdriver.common.keys.html http://selenium.googlecode.com/svn/trunk/docs/api/py/webdriver/selenium.webdriver.common.keys.html
...@@ -17,14 +15,15 @@ VALUE_CSS = 'textarea.json' ...@@ -17,14 +15,15 @@ VALUE_CSS = 'textarea.json'
DISPLAY_NAME_KEY = "display_name" DISPLAY_NAME_KEY = "display_name"
DISPLAY_NAME_VALUE = '"Robot Super Course"' DISPLAY_NAME_VALUE = '"Robot Super Course"'
############### ACTIONS #################### ############### ACTIONS ####################
@step('I select the Advanced Settings$') @step('I select the Advanced Settings$')
def i_select_advanced_settings(step): def i_select_advanced_settings(step):
expand_icon_css = 'li.nav-course-settings i.icon-expand' expand_icon_css = 'li.nav-course-settings i.icon-expand'
if world.browser.is_element_present_by_css(expand_icon_css): if world.browser.is_element_present_by_css(expand_icon_css):
css_click(expand_icon_css) world.css_click(expand_icon_css)
link_css = 'li.nav-course-settings-advanced a' link_css = 'li.nav-course-settings-advanced a'
css_click(link_css) world.css_click(link_css)
@step('I am on the Advanced Course Settings page in Studio$') @step('I am on the Advanced Course Settings page in Studio$')
...@@ -35,24 +34,8 @@ def i_am_on_advanced_course_settings(step): ...@@ -35,24 +34,8 @@ def i_am_on_advanced_course_settings(step):
@step(u'I press the "([^"]*)" notification button$') @step(u'I press the "([^"]*)" notification button$')
def press_the_notification_button(step, name): def press_the_notification_button(step, name):
def is_visible(driver):
return EC.visibility_of_element_located((By.CSS_SELECTOR, css,))
# def is_invisible(driver):
# return EC.invisibility_of_element_located((By.CSS_SELECTOR,css,))
css = 'a.%s-button' % name.lower() css = 'a.%s-button' % name.lower()
wait_for(is_visible) world.css_click(css)
time.sleep(float(1))
css_click_at(css)
# is_invisible is not returning a boolean, not working
# try:
# css_click_at(css)
# wait_for(is_invisible)
# except WebDriverException, e:
# css_click_at(css)
# wait_for(is_invisible)
@step(u'I edit the value of a policy key$') @step(u'I edit the value of a policy key$')
...@@ -61,10 +44,15 @@ def edit_the_value_of_a_policy_key(step): ...@@ -61,10 +44,15 @@ def edit_the_value_of_a_policy_key(step):
It is hard to figure out how to get into the CodeMirror It is hard to figure out how to get into the CodeMirror
area, so cheat and do it from the policy key field :) area, so cheat and do it from the policy key field :)
""" """
e = css_find(KEY_CSS)[get_index_of(DISPLAY_NAME_KEY)] e = world.css_find(KEY_CSS)[get_index_of(DISPLAY_NAME_KEY)]
e._element.send_keys(Keys.TAB, Keys.END, Keys.ARROW_LEFT, ' ', 'X') e._element.send_keys(Keys.TAB, Keys.END, Keys.ARROW_LEFT, ' ', 'X')
@step(u'I edit the value of a policy key and save$')
def edit_the_value_of_a_policy_key_and_save(step):
change_display_name_value(step, '"foo"')
@step('I create a JSON object as a value$') @step('I create a JSON object as a value$')
def create_JSON_object(step): def create_JSON_object(step):
change_display_name_value(step, '{"key": "value", "key_2": "value_2"}') change_display_name_value(step, '{"key": "value", "key_2": "value_2"}')
...@@ -85,7 +73,7 @@ def i_see_default_advanced_settings(step): ...@@ -85,7 +73,7 @@ def i_see_default_advanced_settings(step):
@step('the settings are alphabetized$') @step('the settings are alphabetized$')
def they_are_alphabetized(step): def they_are_alphabetized(step):
key_elements = css_find(KEY_CSS) key_elements = world.css_find(KEY_CSS)
all_keys = [] all_keys = []
for key in key_elements: for key in key_elements:
all_keys.append(key.value) all_keys.append(key.value)
...@@ -99,7 +87,7 @@ def it_is_formatted(step): ...@@ -99,7 +87,7 @@ def it_is_formatted(step):
@step('it is displayed as a string') @step('it is displayed as a string')
def it_is_formatted(step): def it_is_displayed_as_string(step):
assert_policy_entries([DISPLAY_NAME_KEY], ['"quote me"']) assert_policy_entries([DISPLAY_NAME_KEY], ['"quote me"'])
...@@ -110,7 +98,7 @@ def the_policy_key_value_is_unchanged(step): ...@@ -110,7 +98,7 @@ def the_policy_key_value_is_unchanged(step):
@step(u'the policy key value is changed$') @step(u'the policy key value is changed$')
def the_policy_key_value_is_changed(step): def the_policy_key_value_is_changed(step):
assert_equal(get_display_name_value(), '"Robot Super Course X"') assert_equal(get_display_name_value(), '"foo"')
############# HELPERS ############### ############# HELPERS ###############
...@@ -118,13 +106,13 @@ def assert_policy_entries(expected_keys, expected_values): ...@@ -118,13 +106,13 @@ def assert_policy_entries(expected_keys, expected_values):
for counter in range(len(expected_keys)): for counter in range(len(expected_keys)):
index = get_index_of(expected_keys[counter]) index = get_index_of(expected_keys[counter])
assert_false(index == -1, "Could not find key: " + expected_keys[counter]) assert_false(index == -1, "Could not find key: " + expected_keys[counter])
assert_equal(expected_values[counter], css_find(VALUE_CSS)[index].value, "value is incorrect") assert_equal(expected_values[counter], world.css_find(VALUE_CSS)[index].value, "value is incorrect")
def get_index_of(expected_key): def get_index_of(expected_key):
for counter in range(len(css_find(KEY_CSS))): for counter in range(len(world.css_find(KEY_CSS))):
# Sometimes get stale reference if I hold on to the array of elements # Sometimes get stale reference if I hold on to the array of elements
key = css_find(KEY_CSS)[counter].value key = world.css_find(KEY_CSS)[counter].value
if key == expected_key: if key == expected_key:
return counter return counter
...@@ -133,14 +121,14 @@ def get_index_of(expected_key): ...@@ -133,14 +121,14 @@ def get_index_of(expected_key):
def get_display_name_value(): def get_display_name_value():
index = get_index_of(DISPLAY_NAME_KEY) index = get_index_of(DISPLAY_NAME_KEY)
return css_find(VALUE_CSS)[index].value return world.css_find(VALUE_CSS)[index].value
def change_display_name_value(step, new_value): def change_display_name_value(step, new_value):
e = css_find(KEY_CSS)[get_index_of(DISPLAY_NAME_KEY)] e = world.css_find(KEY_CSS)[get_index_of(DISPLAY_NAME_KEY)]
display_name = get_display_name_value() display_name = get_display_name_value()
for count in range(len(display_name)): for count in range(len(display_name)):
e._element.send_keys(Keys.TAB, Keys.END, Keys.BACK_SPACE) e._element.send_keys(Keys.TAB, Keys.END, Keys.BACK_SPACE)
# Must delete "" before typing the JSON value # Must delete "" before typing the JSON value
e._element.send_keys(Keys.TAB, Keys.END, Keys.BACK_SPACE, Keys.BACK_SPACE, new_value) e._element.send_keys(Keys.TAB, Keys.END, Keys.BACK_SPACE, Keys.BACK_SPACE, new_value)
press_the_notification_button(step, "Save") press_the_notification_button(step, "Save")
\ No newline at end of file
...@@ -10,6 +10,8 @@ Feature: Course checklists ...@@ -10,6 +10,8 @@ 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
...@@ -17,8 +19,9 @@ Feature: Course checklists ...@@ -17,8 +19,9 @@ 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
Then I am brought to the help page in a new window Then I am brought to the help page in a new window
#pylint: disable=C0111
#pylint: disable=W0621
from lettuce import world, step from lettuce import world, step
from common import * from nose.tools import assert_true, assert_equal
from terrain.steps import reload_the_page from terrain.steps import reload_the_page
from selenium.common.exceptions import StaleElementReferenceException
############### ACTIONS #################### ############### ACTIONS ####################
@step('I select Checklists from the Tools menu$') @step('I select Checklists from the Tools menu$')
def i_select_checklists(step): def i_select_checklists(step):
expand_icon_css = 'li.nav-course-tools i.icon-expand' expand_icon_css = 'li.nav-course-tools i.icon-expand'
if world.browser.is_element_present_by_css(expand_icon_css): if world.browser.is_element_present_by_css(expand_icon_css):
css_click(expand_icon_css) world.css_click(expand_icon_css)
link_css = 'li.nav-course-tools-checklists a' link_css = 'li.nav-course-tools-checklists a'
css_click(link_css) world.css_click(link_css)
@step('I have opened Checklists$') @step('I have opened Checklists$')
...@@ -20,7 +25,7 @@ def i_have_opened_checklists(step): ...@@ -20,7 +25,7 @@ def i_have_opened_checklists(step):
@step('I see the four default edX checklists$') @step('I see the four default edX checklists$')
def i_see_default_checklists(step): def i_see_default_checklists(step):
checklists = css_find('.checklist-title') checklists = world.css_find('.checklist-title')
assert_equal(4, len(checklists)) assert_equal(4, len(checklists))
assert_true(checklists[0].text.endswith('Getting Started With Studio')) assert_true(checklists[0].text.endswith('Getting Started With Studio'))
assert_true(checklists[1].text.endswith('Draft a Rough Course Outline')) assert_true(checklists[1].text.endswith('Draft a Rough Course Outline'))
...@@ -58,7 +63,7 @@ def i_select_a_link_to_the_course_outline(step): ...@@ -58,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', css_find('.outline .title-1')[0].text) assert_equal('Course Outline', world.css_find('.outline .title-1')[0].text)
assert_equal(1, len(world.browser.windows)) assert_equal(1, len(world.browser.windows))
...@@ -84,36 +89,35 @@ def i_am_brought_to_help_page_in_new_window(step): ...@@ -84,36 +89,35 @@ def i_am_brought_to_help_page_in_new_window(step):
assert_equal('http://help.edge.edx.org/', world.browser.url) assert_equal('http://help.edge.edx.org/', world.browser.url)
############### HELPER METHODS #################### ############### HELPER METHODS ####################
def verifyChecklist2Status(completed, total, percentage): def verifyChecklist2Status(completed, total, percentage):
def verify_count(driver): def verify_count(driver):
try: try:
statusCount = css_find('#course-checklist1 .status-count').first statusCount = world.css_find('#course-checklist1 .status-count').first
return statusCount.text == str(completed) return statusCount.text == str(completed)
except StaleElementReferenceException: except StaleElementReferenceException:
return False return False
wait_for(verify_count) world.wait_for(verify_count)
assert_equal(str(total), css_find('#course-checklist1 .status-amount').first.text) assert_equal(str(total), world.css_find('#course-checklist1 .status-amount').first.text)
# Would like to check the CSS width, but not sure how to do that. # Would like to check the CSS width, but not sure how to do that.
assert_equal(str(percentage), css_find('#course-checklist1 .viz-checklist-status-value .int').first.text) assert_equal(str(percentage), world.css_find('#course-checklist1 .viz-checklist-status-value .int').first.text)
def toggleTask(checklist, task): def toggleTask(checklist, task):
css_click('#course-checklist' + str(checklist) +'-task' + str(task)) world.css_click('#course-checklist' + str(checklist) + '-task' + str(task))
# TODO: figure out a way to do this in phantom and firefox
# For now we will mark the scenerios that use this method as skipped
def clickActionLink(checklist, task, actionText): def clickActionLink(checklist, task, actionText):
# toggle checklist item to make sure that the link button is showing # toggle checklist item to make sure that the link button is showing
toggleTask(checklist, task) toggleTask(checklist, task)
action_link = css_find('#course-checklist' + str(checklist) + ' a')[task] action_link = world.css_find('#course-checklist' + str(checklist) + ' a')[task]
# text will be empty initially, wait for it to populate # text will be empty initially, wait for it to populate
def verify_action_link_text(driver): def verify_action_link_text(driver):
return action_link.text == actionText return action_link.text == actionText
wait_for(verify_action_link_text) world.wait_for(verify_action_link_text)
action_link.click() action_link.click()
#pylint: disable=C0111
#pylint: disable=W0621
from lettuce import world, step from lettuce import world, step
from lettuce.django import django_url
from nose.tools import assert_true from nose.tools import assert_true
from nose.tools import assert_equal from nose.tools import assert_equal
from selenium.webdriver.support.ui import WebDriverWait
from selenium.common.exceptions import WebDriverException, StaleElementReferenceException
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
from xmodule.modulestore.django import _MODULESTORES, modulestore from xmodule.modulestore.django import _MODULESTORES, modulestore
from xmodule.templates import update_templates from xmodule.templates import update_templates
from auth.authz import get_user_by_email from auth.authz import get_user_by_email
from selenium.webdriver.common.keys import Keys
import time
from logging import getLogger from logging import getLogger
logger = getLogger(__name__) logger = getLogger(__name__)
########### STEP HELPERS ############## ########### STEP HELPERS ##############
@step('I (?:visit|access|open) the Studio homepage$') @step('I (?:visit|access|open) the Studio homepage$')
def i_visit_the_studio_homepage(step): def i_visit_the_studio_homepage(step):
# To make this go to port 8001, put # To make this go to port 8001, put
# LETTUCE_SERVER_PORT = 8001 # LETTUCE_SERVER_PORT = 8001
# in your settings.py file. # in your settings.py file.
world.browser.visit(django_url('/')) world.visit('/')
signin_css = 'a.action-signin' signin_css = 'a.action-signin'
assert world.browser.is_element_present_by_css(signin_css, 10) assert world.is_css_present(signin_css)
@step('I am logged into Studio$') @step('I am logged into Studio$')
...@@ -43,12 +45,12 @@ def i_press_the_category_delete_icon(step, category): ...@@ -43,12 +45,12 @@ def i_press_the_category_delete_icon(step, category):
css = 'a.delete-button.delete-subsection-button span.delete-icon' css = 'a.delete-button.delete-subsection-button span.delete-icon'
else: else:
assert False, 'Invalid category: %s' % category assert False, 'Invalid category: %s' % category
css_click(css) world.css_click(css)
@step('I have opened a new course in Studio$') @step('I have opened a new course in Studio$')
def i_have_opened_a_new_course(step): def i_have_opened_a_new_course(step):
clear_courses() world.clear_courses()
log_into_studio() log_into_studio()
create_a_course() create_a_course()
...@@ -74,80 +76,13 @@ def create_studio_user( ...@@ -74,80 +76,13 @@ def create_studio_user(
user_profile = world.UserProfileFactory(user=studio_user) user_profile = world.UserProfileFactory(user=studio_user)
def flush_xmodule_store():
# Flush and initialize the module store
# It needs the templates because it creates new records
# by cloning from the template.
# Note that if your test module gets in some weird state
# (though it shouldn't), do this manually
# from the bash shell to drop it:
# $ mongo test_xmodule --eval "db.dropDatabase()"
_MODULESTORES = {}
modulestore().collection.drop()
update_templates()
def assert_css_with_text(css, text):
assert_true(world.browser.is_element_present_by_css(css, 5))
assert_equal(world.browser.find_by_css(css).text, text)
def css_click(css):
'''
First try to use the regular click method,
but if clicking in the middle of an element
doesn't work it might be that it thinks some other
element is on top of it there so click in the upper left
'''
try:
css_find(css).first.click()
except WebDriverException, e:
css_click_at(css)
def css_click_at(css, x=10, y=10):
'''
A method to click at x,y coordinates of the element
rather than in the center of the element
'''
e = css_find(css).first
e.action_chains.move_to_element_with_offset(e._element, x, y)
e.action_chains.click()
e.action_chains.perform()
def css_fill(css, value):
world.browser.find_by_css(css).first.fill(value)
def css_find(css):
def is_visible(driver):
return EC.visibility_of_element_located((By.CSS_SELECTOR,css,))
world.browser.is_element_present_by_css(css, 5)
wait_for(is_visible)
return world.browser.find_by_css(css)
def wait_for(func):
WebDriverWait(world.browser.driver, 5).until(func)
def id_find(id):
return world.browser.find_by_id(id)
def clear_courses():
flush_xmodule_store()
def fill_in_course_info( def fill_in_course_info(
name='Robot Super Course', name='Robot Super Course',
org='MITx', org='MITx',
num='101'): num='101'):
css_fill('.new-course-name', name) world.css_fill('.new-course-name', name)
css_fill('.new-course-org', org) world.css_fill('.new-course-org', org)
css_fill('.new-course-number', num) world.css_fill('.new-course-number', num)
def log_into_studio( def log_into_studio(
...@@ -155,21 +90,22 @@ def log_into_studio( ...@@ -155,21 +90,22 @@ def log_into_studio(
email='robot+studio@edx.org', email='robot+studio@edx.org',
password='test', password='test',
is_staff=False): is_staff=False):
create_studio_user(uname=uname, email=email, is_staff=is_staff) create_studio_user(uname=uname, email=email, is_staff=is_staff)
world.browser.cookies.delete() world.browser.cookies.delete()
world.browser.visit(django_url('/')) world.visit('/')
signin_css = 'a.action-signin'
world.browser.is_element_present_by_css(signin_css, 10)
# click the signin button signin_css = 'a.action-signin'
css_click(signin_css) world.is_css_present(signin_css)
world.css_click(signin_css)
login_form = world.browser.find_by_css('form#login_form') login_form = world.browser.find_by_css('form#login_form')
login_form.find_by_name('email').fill(email) login_form.find_by_name('email').fill(email)
login_form.find_by_name('password').fill(password) login_form.find_by_name('password').fill(password)
login_form.find_by_name('submit').click() login_form.find_by_name('submit').click()
assert_true(world.browser.is_element_present_by_css('.new-course-button', 5)) assert_true(world.is_css_present('.new-course-button'))
def create_a_course(): def create_a_course():
...@@ -184,26 +120,37 @@ def create_a_course(): ...@@ -184,26 +120,37 @@ def create_a_course():
world.browser.reload() world.browser.reload()
course_link_css = 'span.class-name' course_link_css = 'span.class-name'
css_click(course_link_css) world.css_click(course_link_css)
course_title_css = 'span.course-title' course_title_css = 'span.course-title'
assert_true(world.browser.is_element_present_by_css(course_title_css, 5)) assert_true(world.is_css_present(course_title_css))
def add_section(name='My Section'): def add_section(name='My Section'):
link_css = 'a.new-courseware-section-button' link_css = 'a.new-courseware-section-button'
css_click(link_css) world.css_click(link_css)
name_css = 'input.new-section-name' name_css = 'input.new-section-name'
save_css = 'input.new-section-name-save' save_css = 'input.new-section-name-save'
css_fill(name_css, name) world.css_fill(name_css, name)
css_click(save_css) world.css_click(save_css)
span_css = 'span.section-name-span' span_css = 'span.section-name-span'
assert_true(world.browser.is_element_present_by_css(span_css, 5)) assert_true(world.is_css_present(span_css))
def add_subsection(name='Subsection One'): def add_subsection(name='Subsection One'):
css = 'a.new-subsection-item' css = 'a.new-subsection-item'
css_click(css) world.css_click(css)
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'
css_fill(name_css, name) world.css_fill(name_css, name)
css_click(save_css) world.css_click(save_css)
def set_date_and_time(date_css, desired_date, time_css, desired_time):
world.css_fill(date_css, desired_date)
# hit TAB to get to the time field
e = world.css_find(date_css).first
e._element.send_keys(Keys.TAB)
world.css_fill(time_css, desired_time)
e = world.css_find(time_css).first
e._element.send_keys(Keys.TAB)
time.sleep(float(1))
Feature: Course Settings
As a course author, I want to be able to configure my course settings.
@skip-phantom
Scenario: User can set course dates
Given I have opened a new course in Studio
When I select Schedule and Details
And I set course dates
Then I see the set dates on refresh
@skip-phantom
Scenario: User can clear previously set course dates (except start date)
Given I have set course dates
And I clear all the dates except start
Then I see cleared dates on refresh
@skip-phantom
Scenario: User cannot clear the course start date
Given I have set course dates
And I clear the course start date
Then I receive a warning about course start date
And The previously set start date is shown on refresh
Scenario: User can correct the course start date warning
Given I have tried to clear the course start
And I have entered a new course start date
Then The warning about course start date goes away
And My new course start date is shown on refresh
#pylint: disable=C0111
#pylint: disable=W0621
from lettuce import world, step
from terrain.steps import reload_the_page
from selenium.webdriver.common.keys import Keys
import time
from nose.tools import assert_true, assert_false, assert_equal
COURSE_START_DATE_CSS = "#course-start-date"
COURSE_END_DATE_CSS = "#course-end-date"
ENROLLMENT_START_DATE_CSS = "#course-enrollment-start-date"
ENROLLMENT_END_DATE_CSS = "#course-enrollment-end-date"
COURSE_START_TIME_CSS = "#course-start-time"
COURSE_END_TIME_CSS = "#course-end-time"
ENROLLMENT_START_TIME_CSS = "#course-enrollment-start-time"
ENROLLMENT_END_TIME_CSS = "#course-enrollment-end-time"
DUMMY_TIME = "15:30"
DEFAULT_TIME = "00:00"
############### ACTIONS ####################
@step('I select Schedule and Details$')
def test_i_select_schedule_and_details(step):
expand_icon_css = 'li.nav-course-settings i.icon-expand'
if world.browser.is_element_present_by_css(expand_icon_css):
world.css_click(expand_icon_css)
link_css = 'li.nav-course-settings-schedule a'
world.css_click(link_css)
@step('I have set course dates$')
def test_i_have_set_course_dates(step):
step.given('I have opened a new course in Studio')
step.given('I select Schedule and Details')
step.given('And I set course dates')
@step('And I set course dates$')
def test_and_i_set_course_dates(step):
set_date_or_time(COURSE_START_DATE_CSS, '12/20/2013')
set_date_or_time(COURSE_END_DATE_CSS, '12/26/2013')
set_date_or_time(ENROLLMENT_START_DATE_CSS, '12/1/2013')
set_date_or_time(ENROLLMENT_END_DATE_CSS, '12/10/2013')
set_date_or_time(COURSE_START_TIME_CSS, DUMMY_TIME)
set_date_or_time(ENROLLMENT_END_TIME_CSS, DUMMY_TIME)
pause()
@step('Then I see the set dates on refresh$')
def test_then_i_see_the_set_dates_on_refresh(step):
reload_the_page(step)
verify_date_or_time(COURSE_START_DATE_CSS, '12/20/2013')
verify_date_or_time(COURSE_END_DATE_CSS, '12/26/2013')
verify_date_or_time(ENROLLMENT_START_DATE_CSS, '12/01/2013')
verify_date_or_time(ENROLLMENT_END_DATE_CSS, '12/10/2013')
verify_date_or_time(COURSE_START_TIME_CSS, DUMMY_TIME)
# Unset times get set to 12 AM once the corresponding date has been set.
verify_date_or_time(COURSE_END_TIME_CSS, DEFAULT_TIME)
verify_date_or_time(ENROLLMENT_START_TIME_CSS, DEFAULT_TIME)
verify_date_or_time(ENROLLMENT_END_TIME_CSS, DUMMY_TIME)
@step('And I clear all the dates except start$')
def test_and_i_clear_all_the_dates_except_start(step):
set_date_or_time(COURSE_END_DATE_CSS, '')
set_date_or_time(ENROLLMENT_START_DATE_CSS, '')
set_date_or_time(ENROLLMENT_END_DATE_CSS, '')
pause()
@step('Then I see cleared dates on refresh$')
def test_then_i_see_cleared_dates_on_refresh(step):
reload_the_page(step)
verify_date_or_time(COURSE_END_DATE_CSS, '')
verify_date_or_time(ENROLLMENT_START_DATE_CSS, '')
verify_date_or_time(ENROLLMENT_END_DATE_CSS, '')
verify_date_or_time(COURSE_END_TIME_CSS, '')
verify_date_or_time(ENROLLMENT_START_TIME_CSS, '')
verify_date_or_time(ENROLLMENT_END_TIME_CSS, '')
# Verify course start date (required) and time still there
verify_date_or_time(COURSE_START_DATE_CSS, '12/20/2013')
verify_date_or_time(COURSE_START_TIME_CSS, DUMMY_TIME)
@step('I clear the course start date$')
def test_i_clear_the_course_start_date(step):
set_date_or_time(COURSE_START_DATE_CSS, '')
@step('I receive a warning about course start date$')
def test_i_receive_a_warning_about_course_start_date(step):
assert_true(world.css_has_text('.message-error', 'The course must have an assigned start date.'))
assert_true('error' in world.css_find(COURSE_START_DATE_CSS).first._element.get_attribute('class'))
assert_true('error' in world.css_find(COURSE_START_TIME_CSS).first._element.get_attribute('class'))
@step('The previously set start date is shown on refresh$')
def test_the_previously_set_start_date_is_shown_on_refresh(step):
reload_the_page(step)
verify_date_or_time(COURSE_START_DATE_CSS, '12/20/2013')
verify_date_or_time(COURSE_START_TIME_CSS, DUMMY_TIME)
@step('Given I have tried to clear the course start$')
def test_i_have_tried_to_clear_the_course_start(step):
step.given("I have set course dates")
step.given("I clear the course start date")
step.given("I receive a warning about course start date")
@step('I have entered a new course start date$')
def test_i_have_entered_a_new_course_start_date(step):
set_date_or_time(COURSE_START_DATE_CSS, '12/22/2013')
pause()
@step('The warning about course start date goes away$')
def test_the_warning_about_course_start_date_goes_away(step):
assert_equal(0, len(world.css_find('.message-error')))
assert_false('error' in world.css_find(COURSE_START_DATE_CSS).first._element.get_attribute('class'))
assert_false('error' in world.css_find(COURSE_START_TIME_CSS).first._element.get_attribute('class'))
@step('My new course start date is shown on refresh$')
def test_my_new_course_start_date_is_shown_on_refresh(step):
reload_the_page(step)
verify_date_or_time(COURSE_START_DATE_CSS, '12/22/2013')
# Time should have stayed from before attempt to clear date.
verify_date_or_time(COURSE_START_TIME_CSS, DUMMY_TIME)
############### HELPER METHODS ####################
def set_date_or_time(css, date_or_time):
"""
Sets date or time field.
"""
world.css_fill(css, date_or_time)
e = world.css_find(css).first
# hit Enter to apply the changes
e._element.send_keys(Keys.ENTER)
def verify_date_or_time(css, date_or_time):
"""
Verifies date or time field.
"""
assert_equal(date_or_time, world.css_find(css).first.value)
def pause():
"""
Must sleep briefly to allow last time save to finish,
else refresh of browser will fail.
"""
time.sleep(float(1))
...@@ -10,4 +10,4 @@ Feature: Create Course ...@@ -10,4 +10,4 @@ Feature: Create Course
And I fill in the new course information And I fill in the new course information
And I press the "Save" button And I press the "Save" button
Then the Courseware page has loaded in Studio Then the Courseware page has loaded in Studio
And I see a link for adding a new section And I see a link for adding a new section
\ No newline at end of file
#pylint: disable=C0111
#pylint: disable=W0621
from lettuce import world, step from lettuce import world, step
from common import * from common import *
...@@ -6,12 +9,12 @@ from common import * ...@@ -6,12 +9,12 @@ from common import *
@step('There are no courses$') @step('There are no courses$')
def no_courses(step): def no_courses(step):
clear_courses() world.clear_courses()
@step('I click the New Course button$') @step('I click the New Course button$')
def i_click_new_course(step): def i_click_new_course(step):
css_click('.new-course-button') world.css_click('.new-course-button')
@step('I fill in the new course information$') @step('I fill in the new course information$')
...@@ -27,7 +30,7 @@ def i_create_a_course(step): ...@@ -27,7 +30,7 @@ def i_create_a_course(step):
@step('I click the course link in My Courses$') @step('I click the course link in My Courses$')
def i_click_the_course_link_in_my_courses(step): def i_click_the_course_link_in_my_courses(step):
course_css = 'span.class-name' course_css = 'span.class-name'
css_click(course_css) world.css_click(course_css)
############ ASSERTIONS ################### ############ ASSERTIONS ###################
...@@ -35,28 +38,28 @@ def i_click_the_course_link_in_my_courses(step): ...@@ -35,28 +38,28 @@ def i_click_the_course_link_in_my_courses(step):
@step('the Courseware page has loaded in Studio$') @step('the Courseware page has loaded in Studio$')
def courseware_page_has_loaded_in_studio(step): def courseware_page_has_loaded_in_studio(step):
course_title_css = 'span.course-title' course_title_css = 'span.course-title'
assert world.browser.is_element_present_by_css(course_title_css) assert world.is_css_present(course_title_css)
@step('I see the course listed in My Courses$') @step('I see the course listed in My Courses$')
def i_see_the_course_in_my_courses(step): def i_see_the_course_in_my_courses(step):
course_css = 'span.class-name' course_css = 'span.class-name'
assert_css_with_text(course_css, 'Robot Super Course') assert world.css_has_text(course_css, 'Robot Super Course')
@step('the course is loaded$') @step('the course is loaded$')
def course_is_loaded(step): def course_is_loaded(step):
class_css = 'a.class-name' class_css = 'a.class-name'
assert_css_with_text(class_css, 'Robot Super Course') assert world.css_has_text(course_css, 'Robot Super Cousre')
@step('I am on the "([^"]*)" tab$') @step('I am on the "([^"]*)" tab$')
def i_am_on_tab(step, tab_name): def i_am_on_tab(step, tab_name):
header_css = 'div.inner-wrapper h1' header_css = 'div.inner-wrapper h1'
assert_css_with_text(header_css, tab_name) assert world.css_has_text(header_css, tab_name)
@step('I see a link for adding a new section$') @step('I see a link for adding a new section$')
def i_see_new_section_link(step): def i_see_new_section_link(step):
link_css = 'a.new-courseware-section-button' link_css = 'a.new-courseware-section-button'
assert_css_with_text(link_css, '+ New Section') assert world.css_has_text(link_css, '+ New Section')
...@@ -3,6 +3,7 @@ Feature: Create Section ...@@ -3,6 +3,7 @@ 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
......
#pylint: disable=C0111
#pylint: disable=W0621
from lettuce import world, step from lettuce import world, step
from common import * from common import *
from nose.tools import assert_equal from nose.tools import assert_equal
from selenium.webdriver.common.keys import Keys
import time
############### ACTIONS #################### ############### ACTIONS ####################
...@@ -10,7 +11,7 @@ import time ...@@ -10,7 +11,7 @@ import time
@step('I click the new section link$') @step('I click the new section link$')
def i_click_new_section_link(step): def i_click_new_section_link(step):
link_css = 'a.new-courseware-section-button' link_css = 'a.new-courseware-section-button'
css_click(link_css) world.css_click(link_css)
@step('I enter the section name and click save$') @step('I enter the section name and click save$')
...@@ -31,21 +32,13 @@ def i_have_added_new_section(step): ...@@ -31,21 +32,13 @@ def i_have_added_new_section(step):
@step('I click the Edit link for the release date$') @step('I click the Edit link for the release date$')
def i_click_the_edit_link_for_the_release_date(step): def i_click_the_edit_link_for_the_release_date(step):
button_css = 'div.section-published-date a.edit-button' button_css = 'div.section-published-date a.edit-button'
css_click(button_css) world.css_click(button_css)
@step('I save a new section release date$') @step('I save a new section release date$')
def i_save_a_new_section_release_date(step): def i_save_a_new_section_release_date(step):
date_css = 'input.start-date.date.hasDatepicker' set_date_and_time('input.start-date.date.hasDatepicker', '12/25/2013',
time_css = 'input.start-time.time.ui-timepicker-input' 'input.start-time.time.ui-timepicker-input', '00:00')
css_fill(date_css, '12/25/2013')
# hit TAB to get to the time field
e = css_find(date_css).first
e._element.send_keys(Keys.TAB)
css_fill(time_css, '12:00am')
e = css_find(time_css).first
e._element.send_keys(Keys.TAB)
time.sleep(float(1))
world.browser.click_link_by_text('Save') world.browser.click_link_by_text('Save')
...@@ -64,13 +57,13 @@ def i_see_my_section_name_with_quote_on_the_courseware_page(step): ...@@ -64,13 +57,13 @@ def i_see_my_section_name_with_quote_on_the_courseware_page(step):
@step('I click to edit the section name$') @step('I click to edit the section name$')
def i_click_to_edit_section_name(step): def i_click_to_edit_section_name(step):
css_click('span.section-name-span') world.css_click('span.section-name-span')
@step('I see the complete section name with a quote in the editor$') @step('I see the complete section name with a quote in the editor$')
def i_see_complete_section_name_with_quote_in_editor(step): def i_see_complete_section_name_with_quote_in_editor(step):
css = '.edit-section-name' css = '.edit-section-name'
assert world.browser.is_element_present_by_css(css, 5) assert world.is_css_present(css)
assert_equal(world.browser.find_by_css(css).value, 'Section with "Quote"') assert_equal(world.browser.find_by_css(css).value, 'Section with "Quote"')
...@@ -85,7 +78,7 @@ def i_see_a_release_date_for_my_section(step): ...@@ -85,7 +78,7 @@ def i_see_a_release_date_for_my_section(step):
import re import re
css = 'span.published-status' css = 'span.published-status'
assert world.browser.is_element_present_by_css(css) assert world.is_css_present(css)
status_text = world.browser.find_by_css(css).text status_text = world.browser.find_by_css(css).text
# e.g. 11/06/2012 at 16:25 # e.g. 11/06/2012 at 16:25
...@@ -99,20 +92,20 @@ def i_see_a_release_date_for_my_section(step): ...@@ -99,20 +92,20 @@ def i_see_a_release_date_for_my_section(step):
@step('I see a link to create a new subsection$') @step('I see a link to create a new subsection$')
def i_see_a_link_to_create_a_new_subsection(step): def i_see_a_link_to_create_a_new_subsection(step):
css = 'a.new-subsection-item' css = 'a.new-subsection-item'
assert world.browser.is_element_present_by_css(css) assert world.is_css_present(css)
@step('the section release date picker is not visible$') @step('the section release date picker is not visible$')
def the_section_release_date_picker_not_visible(step): def the_section_release_date_picker_not_visible(step):
css = 'div.edit-subsection-publish-settings' css = 'div.edit-subsection-publish-settings'
assert False, world.browser.find_by_css(css).visible assert not world.css_visible(css)
@step('the section release date is updated$') @step('the section release date is updated$')
def the_section_release_date_is_updated(step): def the_section_release_date_is_updated(step):
css = 'span.published-status' css = 'span.published-status'
status_text = world.browser.find_by_css(css).text status_text = world.css_text(css)
assert_equal(status_text,'Will Release: 12/25/2013 at 12:00am') assert_equal(status_text, 'Will Release: 12/25/2013 at 00:00 UTC')
############ HELPER METHODS ################### ############ HELPER METHODS ###################
...@@ -120,10 +113,10 @@ def the_section_release_date_is_updated(step): ...@@ -120,10 +113,10 @@ def the_section_release_date_is_updated(step):
def save_section_name(name): def save_section_name(name):
name_css = '.new-section-name' name_css = '.new-section-name'
save_css = '.new-section-name-save' save_css = '.new-section-name-save'
css_fill(name_css, name) world.css_fill(name_css, name)
css_click(save_css) world.css_click(save_css)
def see_my_section_on_the_courseware_page(name): def see_my_section_on_the_courseware_page(name):
section_css = 'span.section-name-span' section_css = 'span.section-name-span'
assert_css_with_text(section_css, name) assert world.css_has_text(section_css, name)
#pylint: disable=C0111
#pylint: disable=W0621
from lettuce import world, step from lettuce import world, step
from common import * from common import *
...@@ -17,9 +20,10 @@ def i_press_the_button_on_the_registration_form(step): ...@@ -17,9 +20,10 @@ 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 # Workaround for click not working on ubuntu
# for some unknown reason. # for some unknown reason.
e = css_find(submit_css) e = world.css_find(submit_css)
e.type(' ') e.type(' ')
@step('I should see be on the studio home page$') @step('I should see be on the studio home page$')
def i_should_see_be_on_the_studio_home_page(step): def i_should_see_be_on_the_studio_home_page(step):
assert world.browser.find_by_css('div.inner-wrapper') assert world.browser.find_by_css('div.inner-wrapper')
......
Feature: Overview Toggle Section Feature: Overview Toggle Section
In order to quickly view the details of a course's section or to scan the inventory of sections In order to quickly view the details of a course's section or to scan the inventory of sections
As a course author As a course author
I want to toggle the visibility of each section's subsection details in the overview listing I want to toggle the visibility of each section's subsection details in the overview listing
Scenario: The default layout for the overview page is to show sections in expanded view Scenario: The default layout for the overview page is to show sections in expanded view
Given I have a course with multiple sections Given I have a course with multiple sections
When I navigate to the course overview page When I navigate to the course overview page
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
Scenario: Expand/collapse for a course with no sections Scenario: Expand /collapse for a course with no sections
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
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
And I add a section And I add a 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 @skip-phantom
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
When I press the "section" delete icon When I press the "section" delete icon
And I confirm the alert And I confirm the alert
Then I see the "Collapse All Sections" link Then I see the "Collapse All Sections" link
Scenario: Collapsing all sections when all sections are expanded Scenario: Collapsing all sections when all sections are expanded
...@@ -57,4 +58,4 @@ Feature: Overview Toggle Section ...@@ -57,4 +58,4 @@ Feature: Overview Toggle Section
When I expand the first section When I expand the first section
And I click the "Expand All Sections" link And I click the "Expand All Sections" link
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
\ No newline at end of file
#pylint: disable=C0111
#pylint: disable=W0621
from lettuce import world, step from lettuce import world, step
from common import * from common import *
from nose.tools import assert_true, assert_false, assert_equal from nose.tools import assert_true, assert_false, assert_equal
...@@ -8,13 +11,13 @@ logger = getLogger(__name__) ...@@ -8,13 +11,13 @@ logger = getLogger(__name__)
@step(u'I have a course with no sections$') @step(u'I have a course with no sections$')
def have_a_course(step): def have_a_course(step):
clear_courses() world.clear_courses()
course = world.CourseFactory.create() course = world.CourseFactory.create()
@step(u'I have a course with 1 section$') @step(u'I have a course with 1 section$')
def have_a_course_with_1_section(step): def have_a_course_with_1_section(step):
clear_courses() world.clear_courses()
course = world.CourseFactory.create() course = world.CourseFactory.create()
section = world.ItemFactory.create(parent_location=course.location) section = world.ItemFactory.create(parent_location=course.location)
subsection1 = world.ItemFactory.create( subsection1 = world.ItemFactory.create(
...@@ -25,7 +28,7 @@ def have_a_course_with_1_section(step): ...@@ -25,7 +28,7 @@ def have_a_course_with_1_section(step):
@step(u'I have a course with multiple sections$') @step(u'I have a course with multiple sections$')
def have_a_course_with_two_sections(step): def have_a_course_with_two_sections(step):
clear_courses() world.clear_courses()
course = world.CourseFactory.create() course = world.CourseFactory.create()
section = world.ItemFactory.create(parent_location=course.location) section = world.ItemFactory.create(parent_location=course.location)
subsection1 = world.ItemFactory.create( subsection1 = world.ItemFactory.create(
...@@ -49,7 +52,7 @@ def have_a_course_with_two_sections(step): ...@@ -49,7 +52,7 @@ def have_a_course_with_two_sections(step):
def navigate_to_the_course_overview_page(step): def navigate_to_the_course_overview_page(step):
log_into_studio(is_staff=True) log_into_studio(is_staff=True)
course_locator = '.class-name' course_locator = '.class-name'
css_click(course_locator) world.css_click(course_locator)
@step(u'I navigate to the courseware page of a course with multiple sections') @step(u'I navigate to the courseware page of a course with multiple sections')
...@@ -66,44 +69,44 @@ def i_add_a_section(step): ...@@ -66,44 +69,44 @@ def i_add_a_section(step):
@step(u'I click the "([^"]*)" link$') @step(u'I click the "([^"]*)" link$')
def i_click_the_text_span(step, text): def i_click_the_text_span(step, text):
span_locator = '.toggle-button-sections span' span_locator = '.toggle-button-sections span'
assert_true(world.browser.is_element_present_by_css(span_locator, 5)) assert_true(world.browser.is_element_present_by_css(span_locator))
# first make sure that the expand/collapse text is the one you expected # first make sure that the expand/collapse text is the one you expected
assert_equal(world.browser.find_by_css(span_locator).value, text) assert_equal(world.browser.find_by_css(span_locator).value, text)
css_click(span_locator) world.css_click(span_locator)
@step(u'I collapse the first section$') @step(u'I collapse the first section$')
def i_collapse_a_section(step): def i_collapse_a_section(step):
collapse_locator = 'section.courseware-section a.collapse' collapse_locator = 'section.courseware-section a.collapse'
css_click(collapse_locator) world.css_click(collapse_locator)
@step(u'I expand the first section$') @step(u'I expand the first section$')
def i_expand_a_section(step): def i_expand_a_section(step):
expand_locator = 'section.courseware-section a.expand' expand_locator = 'section.courseware-section a.expand'
css_click(expand_locator) world.css_click(expand_locator)
@step(u'I see the "([^"]*)" link$') @step(u'I see the "([^"]*)" link$')
def i_see_the_span_with_text(step, text): def i_see_the_span_with_text(step, text):
span_locator = '.toggle-button-sections span' span_locator = '.toggle-button-sections span'
assert_true(world.browser.is_element_present_by_css(span_locator, 5)) assert_true(world.is_css_present(span_locator))
assert_equal(world.browser.find_by_css(span_locator).value, text) assert_equal(world.css_find(span_locator).value, text)
assert_true(world.browser.find_by_css(span_locator).visible) assert_true(world.css_visible(span_locator))
@step(u'I do not see the "([^"]*)" link$') @step(u'I do not see the "([^"]*)" link$')
def i_do_not_see_the_span_with_text(step, text): def i_do_not_see_the_span_with_text(step, text):
# Note that the span will exist on the page but not be visible # Note that the span will exist on the page but not be visible
span_locator = '.toggle-button-sections span' span_locator = '.toggle-button-sections span'
assert_true(world.browser.is_element_present_by_css(span_locator)) assert_true(world.is_css_present(span_locator))
assert_false(world.browser.find_by_css(span_locator).visible) assert_false(world.css_visible(span_locator))
@step(u'all sections are expanded$') @step(u'all sections are expanded$')
def all_sections_are_expanded(step): def all_sections_are_expanded(step):
subsection_locator = 'div.subsection-list' subsection_locator = 'div.subsection-list'
subsections = world.browser.find_by_css(subsection_locator) subsections = world.css_find(subsection_locator)
for s in subsections: for s in subsections:
assert_true(s.visible) assert_true(s.visible)
...@@ -111,6 +114,6 @@ def all_sections_are_expanded(step): ...@@ -111,6 +114,6 @@ def all_sections_are_expanded(step):
@step(u'all sections are collapsed$') @step(u'all sections are collapsed$')
def all_sections_are_expanded(step): def all_sections_are_expanded(step):
subsection_locator = 'div.subsection-list' subsection_locator = 'div.subsection-list'
subsections = world.browser.find_by_css(subsection_locator) subsections = world.css_find(subsection_locator)
for s in subsections: for s in subsections:
assert_false(s.visible) assert_false(s.visible)
...@@ -3,13 +3,15 @@ Feature: Create Subsection ...@@ -3,13 +3,15 @@ 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
Scenario: Add a new subsection to a section @skip-phantom
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
Scenario: Add a new subsection (with a name containing a quote) to a section (bug #216) @skip-phantom
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
And I enter a subsection name with a quote and click save And I enter a subsection name with a quote and click save
...@@ -17,8 +19,24 @@ Feature: Create Subsection ...@@ -17,8 +19,24 @@ Feature: Create Subsection
And I click to edit the subsection name And I click to edit the subsection name
Then I see the complete subsection name with a quote in the editor Then I see the complete subsection name with a quote in the editor
@skip-phantom Scenario: Assign grading type to a subsection and verify it is still shown after refresh (bug #258)
Scenario: Delete a subsection Given I have opened a new course section in Studio
And I have added a new subsection
And I mark it as Homework
Then I see it marked as Homework
And I reload the page
Then I see it marked as Homework
@skip-phantom
Scenario: Set a due date in a different year (bug #256)
Given I have opened a new subsection in Studio
And I have set a release date and due date in different years
Then I see the correct dates
And I reload the page
Then I see the correct dates
@skip-phantom
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
And I see my subsection on the Courseware page And I see my subsection on the Courseware page
......
#pylint: disable=C0111
#pylint: disable=W0621
from lettuce import world, step from lettuce import world, step
from common import * from common import *
from nose.tools import assert_equal from nose.tools import assert_equal
...@@ -7,16 +10,27 @@ from nose.tools import assert_equal ...@@ -7,16 +10,27 @@ from nose.tools import assert_equal
@step('I have opened a new course section in Studio$') @step('I have opened a new course section in Studio$')
def i_have_opened_a_new_course_section(step): def i_have_opened_a_new_course_section(step):
clear_courses() world.clear_courses()
log_into_studio() log_into_studio()
create_a_course() create_a_course()
add_section() add_section()
@step('I have added a new subsection$')
def i_have_added_a_new_subsection(step):
add_subsection()
@step('I have opened a new subsection in Studio$')
def i_have_opened_a_new_subsection(step):
step.given('I have opened a new course section in Studio')
step.given('I have added a new subsection')
world.css_click('span.subsection-name-value')
@step('I click the New Subsection link') @step('I click the New Subsection link')
def i_click_the_new_subsection_link(step): def i_click_the_new_subsection_link(step):
css = 'a.new-subsection-item' world.css_click('a.new-subsection-item')
css_click(css)
@step('I enter the subsection name and click save$') @step('I enter the subsection name and click save$')
...@@ -31,19 +45,41 @@ def i_save_subsection_name_with_quote(step): ...@@ -31,19 +45,41 @@ def i_save_subsection_name_with_quote(step):
@step('I click to edit the subsection name$') @step('I click to edit the subsection name$')
def i_click_to_edit_subsection_name(step): def i_click_to_edit_subsection_name(step):
css_click('span.subsection-name-value') world.css_click('span.subsection-name-value')
@step('I see the complete subsection name with a quote in the editor$') @step('I see the complete subsection name with a quote in the editor$')
def i_see_complete_subsection_name_with_quote_in_editor(step): def i_see_complete_subsection_name_with_quote_in_editor(step):
css = '.subsection-display-name-input' css = '.subsection-display-name-input'
assert world.browser.is_element_present_by_css(css, 5) assert world.is_css_present(css)
assert_equal(world.browser.find_by_css(css).value, 'Subsection With "Quote"') assert_equal(world.css_find(css).value, 'Subsection With "Quote"')
@step('I have added a new subsection$') @step('I have set a release date and due date in different years$')
def i_have_added_a_new_subsection(step): def test_have_set_dates_in_different_years(step):
add_subsection() set_date_and_time('input#start_date', '12/25/2011', 'input#start_time', '03:00')
world.css_click('.set-date')
# Use a year in the past so that current year will always be different.
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$')
def i_mark_it_as_homework(step):
world.css_click('a.menu-toggle')
world.browser.click_link_by_text('Homework')
@step('I see it marked as Homework$')
def i_see_it_marked__as_homework(step):
assert_equal(world.css_find(".status-label").value, 'Homework')
############ ASSERTIONS ################### ############ ASSERTIONS ###################
...@@ -70,11 +106,12 @@ def the_subsection_does_not_exist(step): ...@@ -70,11 +106,12 @@ def the_subsection_does_not_exist(step):
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'
css_fill(name_css, name) world.css_fill(name_css, name)
css_click(save_css) world.css_click(save_css)
def see_subsection_name(name): def see_subsection_name(name):
css = 'span.subsection-name' css = 'span.subsection-name'
assert world.browser.is_element_present_by_css(css) assert world.is_css_present(css)
css = 'span.subsection-name-value' css = 'span.subsection-name-value'
assert_css_with_text(css, name) assert world.css_has_text(css, name)
from django.core.management.base import BaseCommand, CommandError
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.xml_importer import check_module_metadata_editability
from xmodule.course_module import CourseDescriptor
from request_cache.middleware import RequestCache
class Command(BaseCommand):
help = '''Enumerates through the course and find common errors'''
def handle(self, *args, **options):
if len(args) != 1:
raise CommandError("check_course requires one argument: <location>")
loc_str = args[0]
loc = CourseDescriptor.id_to_location(loc_str)
store = modulestore()
# setup a request cache so we don't throttle the DB with all the metadata inheritance requests
store.request_cache = RequestCache.get_request_cache()
course = store.get_item(loc, depth=3)
err_cnt = 0
def _xlint_metadata(module):
err_cnt = check_module_metadata_editability(module)
for child in module.get_children():
err_cnt = err_cnt + _xlint_metadata(child)
return err_cnt
err_cnt = err_cnt + _xlint_metadata(course)
# we've had a bug where the xml_attributes field can we rewritten as a string rather than a dict
def _check_xml_attributes_field(module):
err_cnt = 0
if hasattr(module, 'xml_attributes') and isinstance(module.xml_attributes, basestring):
print 'module = {0} has xml_attributes as a string. It should be a dict'.format(module.location.url())
err_cnt = err_cnt + 1
for child in module.get_children():
err_cnt = err_cnt + _check_xml_attributes_field(child)
return err_cnt
err_cnt = err_cnt + _check_xml_attributes_field(course)
# check for dangling discussion items, this can cause errors in the forums
def _get_discussion_items(module):
discussion_items = []
if module.location.category == 'discussion':
discussion_items = discussion_items + [module.location.url()]
for child in module.get_children():
discussion_items = discussion_items + _get_discussion_items(child)
return discussion_items
discussion_items = _get_discussion_items(course)
# now query all discussion items via get_items() and compare with the tree-traversal
queried_discussion_items = store.get_items(['i4x', course.location.org, course.location.course,
'discussion', None, None])
for item in queried_discussion_items:
if item.location.url() not in discussion_items:
print 'Found dangling discussion module = {0}'.format(item.location.url())
...@@ -5,7 +5,6 @@ from django.core.management.base import BaseCommand, CommandError ...@@ -5,7 +5,6 @@ from django.core.management.base import BaseCommand, CommandError
from xmodule.modulestore.store_utilities import clone_course from xmodule.modulestore.store_utilities import clone_course
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.contentstore.django import contentstore from xmodule.contentstore.django import contentstore
from xmodule.modulestore import Location
from xmodule.course_module import CourseDescriptor from xmodule.course_module import CourseDescriptor
from auth.authz import _copy_course_group from auth.authz import _copy_course_group
...@@ -16,8 +15,7 @@ from auth.authz import _copy_course_group ...@@ -16,8 +15,7 @@ from auth.authz import _copy_course_group
class Command(BaseCommand): class Command(BaseCommand):
help = \ help = 'Clone a MongoDB backed course to another location'
'''Clone a MongoDB backed course to another location'''
def handle(self, *args, **options): def handle(self, *args, **options):
if len(args) != 2: if len(args) != 2:
......
...@@ -5,7 +5,6 @@ from django.core.management.base import BaseCommand, CommandError ...@@ -5,7 +5,6 @@ from django.core.management.base import BaseCommand, CommandError
from xmodule.modulestore.store_utilities import delete_course from xmodule.modulestore.store_utilities import delete_course
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.contentstore.django import contentstore from xmodule.contentstore.django import contentstore
from xmodule.modulestore import Location
from xmodule.course_module import CourseDescriptor from xmodule.course_module import CourseDescriptor
from .prompt import query_yes_no from .prompt import query_yes_no
...@@ -38,7 +37,7 @@ class Command(BaseCommand): ...@@ -38,7 +37,7 @@ class Command(BaseCommand):
if query_yes_no("Deleting course {0}. Confirm?".format(loc_str), default="no"): if query_yes_no("Deleting course {0}. Confirm?".format(loc_str), default="no"):
if query_yes_no("Are you sure. This action cannot be undone!", default="no"): if query_yes_no("Are you sure. This action cannot be undone!", default="no"):
loc = CourseDescriptor.id_to_location(loc_str) loc = CourseDescriptor.id_to_location(loc_str)
if delete_course(ms, cs, loc, commit) == True: if delete_course(ms, cs, loc, commit):
print 'removing User permissions from course....' print 'removing User permissions from course....'
# in the django layer, we need to remove all the user permissions groups associated with this course # in the django layer, we need to remove all the user permissions groups associated with this course
if commit: if commit:
......
...@@ -7,7 +7,6 @@ from django.core.management.base import BaseCommand, CommandError ...@@ -7,7 +7,6 @@ from django.core.management.base import BaseCommand, CommandError
from xmodule.modulestore.xml_exporter import export_to_xml from xmodule.modulestore.xml_exporter import export_to_xml
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.contentstore.django import contentstore from xmodule.contentstore.django import contentstore
from xmodule.modulestore import Location
from xmodule.course_module import CourseDescriptor from xmodule.course_module import CourseDescriptor
...@@ -15,8 +14,7 @@ unnamed_modules = 0 ...@@ -15,8 +14,7 @@ unnamed_modules = 0
class Command(BaseCommand): class Command(BaseCommand):
help = \ help = 'Import the specified data directory into the default ModuleStore'
'''Import the specified data directory into the default ModuleStore'''
def handle(self, *args, **options): def handle(self, *args, **options):
if len(args) != 2: if len(args) != 2:
......
...@@ -12,8 +12,7 @@ unnamed_modules = 0 ...@@ -12,8 +12,7 @@ unnamed_modules = 0
class Command(BaseCommand): class Command(BaseCommand):
help = \ help = 'Import the specified data directory into the default ModuleStore'
'''Import the specified data directory into the default ModuleStore'''
def handle(self, *args, **options): def handle(self, *args, **options):
if len(args) == 0: if len(args) == 0:
...@@ -28,4 +27,4 @@ class Command(BaseCommand): ...@@ -28,4 +27,4 @@ class Command(BaseCommand):
data=data_dir, data=data_dir,
courses=course_dirs) courses=course_dirs)
import_from_xml(modulestore('direct'), data_dir, course_dirs, load_error_modules=False, import_from_xml(modulestore('direct'), data_dir, course_dirs, load_error_modules=False,
static_content_store=contentstore(), verbose=True) static_content_store=contentstore(), verbose=True)
...@@ -11,8 +11,8 @@ def query_yes_no(question, default="yes"): ...@@ -11,8 +11,8 @@ def query_yes_no(question, default="yes"):
The "answer" return value is one of "yes" or "no". The "answer" return value is one of "yes" or "no".
""" """
valid = {"yes":True, "y":True, "ye":True, valid = {"yes": True, "y": True, "ye": True,
"no":False, "n":False} "no": False, "n": False}
if default is None: if default is None:
prompt = " [y/n] " prompt = " [y/n] "
elif default == "yes": elif default == "yes":
...@@ -30,5 +30,4 @@ def query_yes_no(question, default="yes"): ...@@ -30,5 +30,4 @@ def query_yes_no(question, default="yes"):
elif choice in valid: elif choice in valid:
return valid[choice] return valid[choice]
else: else:
sys.stdout.write("Please respond with 'yes' or 'no' "\ sys.stdout.write("Please respond with 'yes' or 'no' (or 'y' or 'n').\n")
"(or 'y' or 'n').\n")
from xmodule.templates import update_templates from xmodule.templates import update_templates
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
class Command(BaseCommand): class Command(BaseCommand):
help = \ help = 'Imports and updates the Studio component templates from the code pack and put in the DB'
'''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()
\ No newline at end of file
from django.core.management.base import BaseCommand, CommandError from django.core.management.base import BaseCommand, CommandError
from xmodule.modulestore.xml_importer import perform_xlint from xmodule.modulestore.xml_importer import perform_xlint
from xmodule.modulestore.django import modulestore
from xmodule.contentstore.django import contentstore
unnamed_modules = 0 unnamed_modules = 0
...@@ -9,10 +7,11 @@ unnamed_modules = 0 ...@@ -9,10 +7,11 @@ unnamed_modules = 0
class Command(BaseCommand): class Command(BaseCommand):
help = \ help = \
''' '''
Verify the structure of courseware as to it's suitability for import Verify the structure of courseware as to it's suitability for import
To run test: rake cms:xlint DATA_DIR=../data [COURSE_DIR=content-edx-101 (optional parameter)] To run test: rake cms:xlint DATA_DIR=../data [COURSE_DIR=content-edx-101 (optional parameter)]
''' '''
def handle(self, *args, **options): def handle(self, *args, **options):
if len(args) == 0: if len(args) == 0:
raise CommandError("import requires at least one argument: <data directory> [<course dir>...]") raise CommandError("import requires at least one argument: <data directory> [<course dir>...]")
......
from static_replace import replace_static_urls from static_replace import replace_static_urls
from xmodule.modulestore.exceptions import ItemNotFoundError from xmodule.modulestore.exceptions import ItemNotFoundError
from xmodule.modulestore import Location from xmodule.modulestore import Location
from django.http import Http404
def get_module_info(store, location, parent_location=None, rewrite_static_links=False): def get_module_info(store, location, parent_location=None, rewrite_static_links=False):
......
...@@ -23,14 +23,14 @@ class CachingTestCase(TestCase): ...@@ -23,14 +23,14 @@ class CachingTestCase(TestCase):
def test_put_and_get(self): def test_put_and_get(self):
set_cached_content(self.mockAsset) set_cached_content(self.mockAsset)
self.assertEqual(self.mockAsset.content, get_cached_content(self.unicodeLocation).content, self.assertEqual(self.mockAsset.content, get_cached_content(self.unicodeLocation).content,
'should be stored in cache with unicodeLocation') 'should be stored in cache with unicodeLocation')
self.assertEqual(self.mockAsset.content, get_cached_content(self.nonUnicodeLocation).content, self.assertEqual(self.mockAsset.content, get_cached_content(self.nonUnicodeLocation).content,
'should be stored in cache with nonUnicodeLocation') 'should be stored in cache with nonUnicodeLocation')
def test_delete(self): def test_delete(self):
set_cached_content(self.mockAsset) set_cached_content(self.mockAsset)
del_cached_content(self.nonUnicodeLocation) del_cached_content(self.nonUnicodeLocation)
self.assertEqual(None, get_cached_content(self.unicodeLocation), self.assertEqual(None, get_cached_content(self.unicodeLocation),
'should not be stored in cache with unicodeLocation') 'should not be stored in cache with unicodeLocation')
self.assertEqual(None, get_cached_content(self.nonUnicodeLocation), self.assertEqual(None, get_cached_content(self.nonUnicodeLocation),
'should not be stored in cache with nonUnicodeLocation') 'should not be stored in cache with nonUnicodeLocation')
from unittest import skip
from django.core.urlresolvers import reverse
from django.contrib.auth.models import User
from django.test.client import Client
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
class InternationalizationTest(ModuleStoreTestCase):
"""
Tests to validate Internationalization.
"""
def setUp(self):
"""
These tests need a user in the DB so that the django Test Client
can log them in.
They inherit from the ModuleStoreTestCase class so that the mongodb collection
will be cleared out before each test case execution and deleted
afterwards.
"""
self.uname = 'testuser'
self.email = 'test+courses@edx.org'
self.password = 'foo'
# Create the use so we can log them in.
self.user = User.objects.create_user(self.uname, self.email, self.password)
# Note that we do not actually need to do anything
# for registration if we directly mark them active.
self.user.is_active = True
# Staff has access to view all courses
self.user.is_staff = True
self.user.save()
self.course_data = {
'template': 'i4x://edx/templates/course/Empty',
'org': 'MITx',
'number': '999',
'display_name': 'Robot Super Course',
}
def test_course_plain_english(self):
"""Test viewing the index page with no courses"""
self.client = Client()
self.client.login(username=self.uname, password=self.password)
resp = self.client.get(reverse('index'))
self.assertContains(resp,
'<h1 class="title-1">My Courses</h1>',
status_code=200,
html=True)
def test_course_explicit_english(self):
"""Test viewing the index page with no courses"""
self.client = Client()
self.client.login(username=self.uname, password=self.password)
resp = self.client.get(reverse('index'),
{},
HTTP_ACCEPT_LANGUAGE='en'
)
self.assertContains(resp,
'<h1 class="title-1">My Courses</h1>',
status_code=200,
html=True)
# ****
# NOTE:
# ****
#
# This test will break when we replace this fake 'test' language
# with actual French. This test will need to be updated with
# actual French at that time.
# Test temporarily disable since it depends on creation of dummy strings
@skip
def test_course_with_accents (self):
"""Test viewing the index page with no courses"""
self.client = Client()
self.client.login(username=self.uname, password=self.password)
resp = self.client.get(reverse('index'),
{},
HTTP_ACCEPT_LANGUAGE='fr'
)
TEST_STRING = u'<h1 class="title-1">' \
+ u'My \xc7\xf6\xfcrs\xe9s L#' \
+ u'</h1>'
self.assertContains(resp,
TEST_STRING,
status_code=200,
html=True)
...@@ -3,7 +3,7 @@ from contentstore import utils ...@@ -3,7 +3,7 @@ from contentstore import utils
import mock import mock
from django.test import TestCase from django.test import TestCase
from xmodule.modulestore.tests.factories import CourseFactory from xmodule.modulestore.tests.factories import CourseFactory
from .utils import ModuleStoreTestCase from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
class LMSLinksTestCase(TestCase): class LMSLinksTestCase(TestCase):
...@@ -30,7 +30,7 @@ class LMSLinksTestCase(TestCase): ...@@ -30,7 +30,7 @@ class LMSLinksTestCase(TestCase):
class UrlReverseTestCase(ModuleStoreTestCase): class UrlReverseTestCase(ModuleStoreTestCase):
""" Tests for get_url_reverse """ """ Tests for get_url_reverse """
def test_CoursePageNames(self): def test_course_page_names(self):
""" Test the defined course pages. """ """ Test the defined course pages. """
course = CourseFactory.create(org='mitX', number='666', display_name='URL Reverse Course') course = CourseFactory.create(org='mitX', number='666', display_name='URL Reverse Course')
...@@ -69,4 +69,4 @@ class UrlReverseTestCase(ModuleStoreTestCase): ...@@ -69,4 +69,4 @@ class UrlReverseTestCase(ModuleStoreTestCase):
self.assertEquals( self.assertEquals(
'https://edge.edx.org/courses/edX/edX101/How_to_Create_an_edX_Course/about', 'https://edge.edx.org/courses/edX/edX101/How_to_Create_an_edX_Course/about',
utils.get_url_reverse('https://edge.edx.org/courses/edX/edX101/How_to_Create_an_edX_Course/about', course) utils.get_url_reverse('https://edge.edx.org/courses/edX/edX101/How_to_Create_an_edX_Course/about', course)
) )
\ No newline at end of file
import json
import shutil
from django.test.client import Client from django.test.client import Client
from django.conf import settings
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from path import path
import json
from fs.osfs import OSFS
import copy
from contentstore.utils import get_modulestore from .utils import parse_json, user, registration
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore import Location
from xmodule.modulestore.store_utilities import clone_course
from xmodule.modulestore.store_utilities import delete_course
from xmodule.modulestore.django import modulestore, _MODULESTORES
from xmodule.contentstore.django import contentstore
from xmodule.templates import update_templates
from xmodule.modulestore.xml_exporter import export_to_xml
from xmodule.modulestore.xml_importer import import_from_xml
from xmodule.capa_module import CapaDescriptor
from xmodule.course_module import CourseDescriptor
from xmodule.seq_module import SequenceDescriptor
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from .utils import ModuleStoreTestCase, parse_json, user, registration
class ContentStoreTestCase(ModuleStoreTestCase): class ContentStoreTestCase(ModuleStoreTestCase):
...@@ -84,6 +62,7 @@ class ContentStoreTestCase(ModuleStoreTestCase): ...@@ -84,6 +62,7 @@ class ContentStoreTestCase(ModuleStoreTestCase):
# Now make sure that the user is now actually activated # Now make sure that the user is now actually activated
self.assertTrue(user(email).is_active) self.assertTrue(user(email).is_active)
class AuthTestCase(ContentStoreTestCase): class AuthTestCase(ContentStoreTestCase):
"""Check that various permissions-related things work""" """Check that various permissions-related things work"""
...@@ -101,9 +80,9 @@ class AuthTestCase(ContentStoreTestCase): ...@@ -101,9 +80,9 @@ class AuthTestCase(ContentStoreTestCase):
def test_public_pages_load(self): def test_public_pages_load(self):
"""Make sure pages that don't require login load without error.""" """Make sure pages that don't require login load without error."""
pages = ( pages = (
reverse('login'), reverse('login'),
reverse('signup'), reverse('signup'),
) )
for page in pages: for page in pages:
print "Checking '{0}'".format(page) print "Checking '{0}'".format(page)
self.check_page_get(page, 200) self.check_page_get(page, 200)
...@@ -136,13 +115,13 @@ class AuthTestCase(ContentStoreTestCase): ...@@ -136,13 +115,13 @@ class AuthTestCase(ContentStoreTestCase):
"""Make sure pages that do require login work.""" """Make sure pages that do require login work."""
auth_pages = ( auth_pages = (
reverse('index'), reverse('index'),
) )
# These are pages that should just load when the user is logged in # These are pages that should just load when the user is logged in
# (no data needed) # (no data needed)
simple_auth_pages = ( simple_auth_pages = (
reverse('index'), reverse('index'),
) )
# need an activated user # need an activated user
self.test_create_account() self.test_create_account()
......
'''
Utilities for contentstore tests
'''
import json import json
import copy
from uuid import uuid4
from django.test import TestCase
from django.conf import settings
from student.models import Registration from student.models import Registration
from django.contrib.auth.models import User from django.contrib.auth.models import User
import xmodule.modulestore.django
from xmodule.templates import update_templates
class ModuleStoreTestCase(TestCase):
""" Subclass for any test case that uses the mongodb
module store. This populates a uniquely named modulestore
collection with templates before running the TestCase
and drops it they are finished. """
def _pre_setup(self):
super(ModuleStoreTestCase, self)._pre_setup()
# Use a uuid to differentiate
# the mongo collections on jenkins.
self.orig_MODULESTORE = copy.deepcopy(settings.MODULESTORE)
self.test_MODULESTORE = self.orig_MODULESTORE
self.test_MODULESTORE['default']['OPTIONS']['collection'] = 'modulestore_%s' % uuid4().hex
self.test_MODULESTORE['direct']['OPTIONS']['collection'] = 'modulestore_%s' % uuid4().hex
settings.MODULESTORE = self.test_MODULESTORE
# Flush and initialize the module store
# It needs the templates because it creates new records
# by cloning from the template.
# Note that if your test module gets in some weird state
# (though it shouldn't), do this manually
# from the bash shell to drop it:
# $ mongo test_xmodule --eval "db.dropDatabase()"
xmodule.modulestore.django._MODULESTORES = {}
update_templates()
def _post_teardown(self):
# Make sure you flush out the modulestore.
# Drop the collection at the end of the test,
# otherwise there will be lingering collections leftover
# from executing the tests.
xmodule.modulestore.django._MODULESTORES = {}
xmodule.modulestore.django.modulestore().collection.drop()
settings.MODULESTORE = self.orig_MODULESTORE
super(ModuleStoreTestCase, self)._post_teardown()
def parse_json(response): def parse_json(response):
"""Parse response, which is assumed to be json""" """Parse response, which is assumed to be json"""
......
...@@ -3,9 +3,13 @@ from xmodule.modulestore import Location ...@@ -3,9 +3,13 @@ from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.modulestore.exceptions import ItemNotFoundError from xmodule.modulestore.exceptions import ItemNotFoundError
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
import copy
DIRECT_ONLY_CATEGORIES = ['course', 'chapter', 'sequential', 'about', 'static_tab', 'course_info'] DIRECT_ONLY_CATEGORIES = ['course', 'chapter', 'sequential', 'about', 'static_tab', 'course_info']
#In order to instantiate an open ended tab automatically, need to have this data
OPEN_ENDED_PANEL = {"name": "Open Ended Panel", "type": "open_ended"}
def get_modulestore(location): def get_modulestore(location):
""" """
...@@ -82,11 +86,10 @@ def get_lms_link_for_item(location, preview=False, course_id=None): ...@@ -82,11 +86,10 @@ def get_lms_link_for_item(location, preview=False, course_id=None):
if settings.LMS_BASE is not None: if settings.LMS_BASE is not None:
if preview: if preview:
lms_base = settings.MITX_FEATURES.get('PREVIEW_LMS_BASE', lms_base = settings.MITX_FEATURES.get('PREVIEW_LMS_BASE', 'preview.' + settings.LMS_BASE)
'preview.' + settings.LMS_BASE)
else: else:
lms_base = settings.LMS_BASE lms_base = settings.LMS_BASE
lms_link = "//{lms_base}/courses/{course_id}/jump_to/{location}".format( lms_link = "//{lms_base}/courses/{course_id}/jump_to/{location}".format(
lms_base=lms_base, lms_base=lms_base,
course_id=course_id, course_id=course_id,
...@@ -137,7 +140,7 @@ def compute_unit_state(unit): ...@@ -137,7 +140,7 @@ def compute_unit_state(unit):
'private' content is editabled and not visible in the LMS 'private' content is editabled and not visible in the LMS
""" """
if unit.cms.is_draft: if getattr(unit, 'is_draft', False):
try: try:
modulestore('direct').get_item(unit.location) modulestore('direct').get_item(unit.location)
return UnitState.draft return UnitState.draft
...@@ -147,10 +150,6 @@ def compute_unit_state(unit): ...@@ -147,10 +150,6 @@ def compute_unit_state(unit):
return UnitState.public return UnitState.public
def get_date_display(date):
return date.strftime("%d %B, %Y at %I:%M %p")
def update_item(location, value): def update_item(location, value):
""" """
If value is None, delete the db entry. Otherwise, update it using the correct modulestore. If value is None, delete the db entry. Otherwise, update it using the correct modulestore.
...@@ -191,3 +190,37 @@ class CoursePageNames: ...@@ -191,3 +190,37 @@ class CoursePageNames:
SettingsGrading = "settings_grading" SettingsGrading = "settings_grading"
CourseOutline = "course_index" CourseOutline = "course_index"
Checklists = "checklists" Checklists = "checklists"
def add_open_ended_panel_tab(course):
"""
Used to add the open ended panel tab to a course if it does not exist.
@param course: A course object from the modulestore.
@return: Boolean indicating whether or not a tab was added and a list of tabs for the course.
"""
#Copy course tabs
course_tabs = copy.copy(course.tabs)
changed = False
#Check to see if open ended panel is defined in the course
if OPEN_ENDED_PANEL not in course_tabs:
#Add panel to the tabs if it is not defined
course_tabs.append(OPEN_ENDED_PANEL)
changed = True
return changed, course_tabs
def remove_open_ended_panel_tab(course):
"""
Used to remove the open ended panel tab from a course if it exists.
@param course: A course object from the modulestore.
@return: Boolean indicating whether or not a tab was added and a list of tabs for the course.
"""
#Copy course tabs
course_tabs = copy.copy(course.tabs)
changed = False
#Check to see if open ended panel is defined in the course
if OPEN_ENDED_PANEL in course_tabs:
#Add panel to the tabs if it is not defined
course_tabs = [ct for ct in course_tabs if ct != OPEN_ENDED_PANEL]
changed = True
return changed, course_tabs
from xmodule.modulestore.django import modulestore
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.modulestore.exceptions import ItemNotFoundError from xmodule.modulestore.exceptions import ItemNotFoundError
from xmodule.modulestore.inheritance import own_metadata from xmodule.modulestore.inheritance import own_metadata
...@@ -6,9 +5,9 @@ import json ...@@ -6,9 +5,9 @@ import json
from json.encoder import JSONEncoder from json.encoder import JSONEncoder
import time import time
from contentstore.utils import get_modulestore from contentstore.utils import get_modulestore
from util.converters import jsdate_to_time, time_to_date
from models.settings import course_grading from models.settings import course_grading
from contentstore.utils import update_item from contentstore.utils import update_item
from xmodule.fields import Date
import re import re
import logging import logging
...@@ -81,8 +80,14 @@ class CourseDetails(object): ...@@ -81,8 +80,14 @@ class CourseDetails(object):
dirty = False dirty = False
# In the descriptor's setter, the date is converted to JSON using Date's to_json method.
# Calling to_json on something that is already JSON doesn't work. Since reaching directly
# into the model is nasty, convert the JSON Date to a Python date, which is what the
# setter expects as input.
date = Date()
if 'start_date' in jsondict: if 'start_date' in jsondict:
converted = jsdate_to_time(jsondict['start_date']) converted = date.from_json(jsondict['start_date'])
else: else:
converted = None converted = None
if converted != descriptor.start: if converted != descriptor.start:
...@@ -90,7 +95,7 @@ class CourseDetails(object): ...@@ -90,7 +95,7 @@ class CourseDetails(object):
descriptor.start = converted descriptor.start = converted
if 'end_date' in jsondict: if 'end_date' in jsondict:
converted = jsdate_to_time(jsondict['end_date']) converted = date.from_json(jsondict['end_date'])
else: else:
converted = None converted = None
...@@ -99,7 +104,7 @@ class CourseDetails(object): ...@@ -99,7 +104,7 @@ class CourseDetails(object):
descriptor.end = converted descriptor.end = converted
if 'enrollment_start' in jsondict: if 'enrollment_start' in jsondict:
converted = jsdate_to_time(jsondict['enrollment_start']) converted = date.from_json(jsondict['enrollment_start'])
else: else:
converted = None converted = None
...@@ -108,7 +113,7 @@ class CourseDetails(object): ...@@ -108,7 +113,7 @@ class CourseDetails(object):
descriptor.enrollment_start = converted descriptor.enrollment_start = converted
if 'enrollment_end' in jsondict: if 'enrollment_end' in jsondict:
converted = jsdate_to_time(jsondict['enrollment_end']) converted = date.from_json(jsondict['enrollment_end'])
else: else:
converted = None converted = None
...@@ -169,7 +174,6 @@ class CourseDetails(object): ...@@ -169,7 +174,6 @@ class CourseDetails(object):
return result return result
# TODO move to a more general util? Is there a better way to do the isinstance model check? # TODO move to a more general util? Is there a better way to do the isinstance model check?
class CourseSettingsEncoder(json.JSONEncoder): class CourseSettingsEncoder(json.JSONEncoder):
def default(self, obj): def default(self, obj):
...@@ -178,6 +182,6 @@ class CourseSettingsEncoder(json.JSONEncoder): ...@@ -178,6 +182,6 @@ class CourseSettingsEncoder(json.JSONEncoder):
elif isinstance(obj, Location): elif isinstance(obj, Location):
return obj.dict() return obj.dict()
elif isinstance(obj, time.struct_time): elif isinstance(obj, time.struct_time):
return time_to_date(obj) return Date().to_json(obj)
else: else:
return JSONEncoder.default(self, obj) return JSONEncoder.default(self, obj)
from xmodule.modulestore import Location from xmodule.modulestore import Location
from contentstore.utils import get_modulestore from contentstore.utils import get_modulestore
import re
from util import converters
from datetime import timedelta from datetime import timedelta
...@@ -47,14 +45,13 @@ class CourseGradingModel(object): ...@@ -47,14 +45,13 @@ class CourseGradingModel(object):
# return empty model # return empty model
else: else:
return { return {"id": index,
"id": index,
"type": "", "type": "",
"min_count": 0, "min_count": 0,
"drop_count": 0, "drop_count": 0,
"short_label": None, "short_label": None,
"weight": 0 "weight": 0
} }
@staticmethod @staticmethod
def fetch_cutoffs(course_location): def fetch_cutoffs(course_location):
...@@ -97,7 +94,6 @@ class CourseGradingModel(object): ...@@ -97,7 +94,6 @@ class CourseGradingModel(object):
return CourseGradingModel.fetch(course_location) return CourseGradingModel.fetch(course_location)
@staticmethod @staticmethod
def update_grader_from_json(course_location, grader): def update_grader_from_json(course_location, grader):
""" """
...@@ -139,7 +135,6 @@ class CourseGradingModel(object): ...@@ -139,7 +135,6 @@ class CourseGradingModel(object):
return cutoffs return cutoffs
@staticmethod @staticmethod
def update_grace_period_from_json(course_location, graceperiodjson): def update_grace_period_from_json(course_location, graceperiodjson):
""" """
...@@ -212,8 +207,7 @@ class CourseGradingModel(object): ...@@ -212,8 +207,7 @@ class CourseGradingModel(object):
location = Location(location) location = Location(location)
descriptor = get_modulestore(location).get_item(location) descriptor = get_modulestore(location).get_item(location)
return { return {"graderType": descriptor.lms.format if descriptor.lms.format is not None else 'Not Graded',
"graderType": descriptor.lms.format if descriptor.lms.format is not None else 'Not Graded',
"location": location, "location": location,
"id": 99 # just an arbitrary value to "id": 99 # just an arbitrary value to
} }
...@@ -233,7 +227,6 @@ class CourseGradingModel(object): ...@@ -233,7 +227,6 @@ class CourseGradingModel(object):
get_modulestore(location).update_metadata(location, descriptor._model_data._kvs._metadata) get_modulestore(location).update_metadata(location, descriptor._model_data._kvs._metadata)
@staticmethod @staticmethod
def convert_set_grace_period(descriptor): def convert_set_grace_period(descriptor):
# 5 hours 59 minutes 59 seconds => converted to iso format # 5 hours 59 minutes 59 seconds => converted to iso format
...@@ -264,13 +257,12 @@ class CourseGradingModel(object): ...@@ -264,13 +257,12 @@ class CourseGradingModel(object):
@staticmethod @staticmethod
def parse_grader(json_grader): def parse_grader(json_grader):
# manual to clear out kruft # manual to clear out kruft
result = { result = {"type": json_grader["type"],
"type": json_grader["type"], "min_count": int(json_grader.get('min_count', 0)),
"min_count": int(json_grader.get('min_count', 0)), "drop_count": int(json_grader.get('drop_count', 0)),
"drop_count": int(json_grader.get('drop_count', 0)), "short_label": json_grader.get('short_label', None),
"short_label": json_grader.get('short_label', None), "weight": float(json_grader.get('weight', 0)) / 100.0
"weight": float(json_grader.get('weight', 0)) / 100.0 }
}
return result return result
......
...@@ -4,6 +4,7 @@ from xmodule.x_module import XModuleDescriptor ...@@ -4,6 +4,7 @@ from xmodule.x_module import XModuleDescriptor
from xmodule.modulestore.inheritance import own_metadata from xmodule.modulestore.inheritance import own_metadata
from xblock.core import Scope from xblock.core import Scope
from xmodule.course_module import CourseDescriptor from xmodule.course_module import CourseDescriptor
import copy
class CourseMetadata(object): class CourseMetadata(object):
...@@ -13,8 +14,13 @@ class CourseMetadata(object): ...@@ -13,8 +14,13 @@ 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', 'end', FILTERED_LIST = XModuleDescriptor.system_metadata_fields + ['start',
'enrollment_start', 'enrollment_end', 'tabs', 'graceperiod', 'checklists'] 'end',
'enrollment_start',
'enrollment_end',
'tabs',
'graceperiod',
'checklists']
@classmethod @classmethod
def fetch(cls, course_location): def fetch(cls, course_location):
...@@ -39,7 +45,7 @@ class CourseMetadata(object): ...@@ -39,7 +45,7 @@ class CourseMetadata(object):
return course return course
@classmethod @classmethod
def update_from_json(cls, course_location, jsondict): def update_from_json(cls, course_location, jsondict, filter_tabs=True):
""" """
Decode the json into CourseMetadata and save any changed attrs to the db. Decode the json into CourseMetadata and save any changed attrs to the db.
...@@ -49,9 +55,15 @@ class CourseMetadata(object): ...@@ -49,9 +55,15 @@ class CourseMetadata(object):
dirty = False dirty = False
#Copy the filtered list to avoid permanently changing the class attribute
filtered_list = copy.copy(cls.FILTERED_LIST)
#Don't filter on the tab attribute if filter_tabs is False
if not filter_tabs:
filtered_list.remove("tabs")
for k, v in jsondict.iteritems(): for k, v in jsondict.iteritems():
# should it be an error if one of the filtered list items is in the payload? # should it be an error if one of the filtered list items is in the payload?
if k in cls.FILTERED_LIST: if k in filtered_list:
continue continue
if hasattr(descriptor, k) and getattr(descriptor, k) != v: if hasattr(descriptor, k) and getattr(descriptor, k) != v:
...@@ -65,7 +77,7 @@ class CourseMetadata(object): ...@@ -65,7 +77,7 @@ class CourseMetadata(object):
if dirty: if dirty:
get_modulestore(course_location).update_metadata(course_location, get_modulestore(course_location).update_metadata(course_location,
own_metadata(descriptor)) own_metadata(descriptor))
# Could just generate and return a course obj w/o doing any db reads, # Could just generate and return a course obj w/o doing any db reads,
# but I put the reads in as a means to confirm it persisted correctly # but I put the reads in as a means to confirm it persisted correctly
...@@ -86,6 +98,6 @@ class CourseMetadata(object): ...@@ -86,6 +98,6 @@ class CourseMetadata(object):
delattr(descriptor.lms, key) delattr(descriptor.lms, key)
get_modulestore(course_location).update_metadata(course_location, get_modulestore(course_location).update_metadata(course_location,
own_metadata(descriptor)) own_metadata(descriptor))
return cls.fetch(course_location) return cls.fetch(course_location)
...@@ -36,3 +36,4 @@ DATABASES = { ...@@ -36,3 +36,4 @@ DATABASES = {
INSTALLED_APPS += ('lettuce.django',) INSTALLED_APPS += ('lettuce.django',)
LETTUCE_APPS = ('contentstore',) LETTUCE_APPS = ('contentstore',)
LETTUCE_SERVER_PORT = 8001 LETTUCE_SERVER_PORT = 8001
LETTUCE_BROWSER = 'chrome'
...@@ -46,6 +46,9 @@ SESSION_COOKIE_DOMAIN = ENV_TOKENS.get('SESSION_COOKIE_DOMAIN') ...@@ -46,6 +46,9 @@ SESSION_COOKIE_DOMAIN = ENV_TOKENS.get('SESSION_COOKIE_DOMAIN')
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
# load segment.io key, provide a dummy if it does not exist
SEGMENT_IO_KEY = ENV_TOKENS.get('SEGMENT_IO_KEY', '***REMOVED***')
LOGGING = get_logger_config(LOG_DIR, LOGGING = get_logger_config(LOG_DIR,
logging_env=ENV_TOKENS['LOGGING_ENV'], logging_env=ENV_TOKENS['LOGGING_ENV'],
syslog_addr=(ENV_TOKENS['SYSLOG_SERVER'], 514), syslog_addr=(ENV_TOKENS['SYSLOG_SERVER'], 514),
...@@ -64,4 +67,4 @@ MODULESTORE = AUTH_TOKENS['MODULESTORE'] ...@@ -64,4 +67,4 @@ MODULESTORE = AUTH_TOKENS['MODULESTORE']
CONTENTSTORE = AUTH_TOKENS['CONTENTSTORE'] CONTENTSTORE = AUTH_TOKENS['CONTENTSTORE']
# Datadog for events! # Datadog for events!
DATADOG_API = AUTH_TOKENS.get("DATADOG_API") DATADOG_API = AUTH_TOKENS.get("DATADOG_API")
\ No newline at end of file
...@@ -20,11 +20,8 @@ Longer TODO: ...@@ -20,11 +20,8 @@ Longer TODO:
""" """
import sys import sys
import os.path
import os
import lms.envs.common import lms.envs.common
from path import path from path import path
from xmodule.static_content import write_descriptor_styles, write_descriptor_js, write_module_js, write_module_styles
############################ FEATURE CONFIGURATION ############################# ############################ FEATURE CONFIGURATION #############################
...@@ -34,6 +31,9 @@ MITX_FEATURES = { ...@@ -34,6 +31,9 @@ MITX_FEATURES = {
'ENABLE_DISCUSSION_SERVICE': False, 'ENABLE_DISCUSSION_SERVICE': False,
'AUTH_USE_MIT_CERTIFICATES': False, 'AUTH_USE_MIT_CERTIFICATES': False,
'STUB_VIDEO_FOR_TESTING': False, # do not display video when running automated acceptance tests 'STUB_VIDEO_FOR_TESTING': False, # do not display video when running automated acceptance tests
'STAFF_EMAIL': '', # email address for staff (eg to request course creation)
'STUDIO_NPS_SURVEY': True,
'SEGMENT_IO': True,
} }
ENABLE_JASMINE = False ENABLE_JASMINE = False
...@@ -113,6 +113,7 @@ TEMPLATE_LOADERS = ( ...@@ -113,6 +113,7 @@ TEMPLATE_LOADERS = (
MIDDLEWARE_CLASSES = ( MIDDLEWARE_CLASSES = (
'contentserver.middleware.StaticContentServer', 'contentserver.middleware.StaticContentServer',
'request_cache.middleware.RequestCache',
'django.middleware.cache.UpdateCacheMiddleware', 'django.middleware.cache.UpdateCacheMiddleware',
'django.middleware.common.CommonMiddleware', 'django.middleware.common.CommonMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware',
...@@ -125,6 +126,9 @@ MIDDLEWARE_CLASSES = ( ...@@ -125,6 +126,9 @@ MIDDLEWARE_CLASSES = (
'track.middleware.TrackMiddleware', 'track.middleware.TrackMiddleware',
'mitxmako.middleware.MakoMiddleware', 'mitxmako.middleware.MakoMiddleware',
# Detects user-requested locale from 'accept-language' header in http request
'django.middleware.locale.LocaleMiddleware',
'django.middleware.transaction.TransactionMiddleware' 'django.middleware.transaction.TransactionMiddleware'
) )
...@@ -163,15 +167,19 @@ STATICFILES_DIRS = [ ...@@ -163,15 +167,19 @@ STATICFILES_DIRS = [
PROJECT_ROOT / "static", PROJECT_ROOT / "static",
# This is how you would use the textbook images locally # This is how you would use the textbook images locally
# ("book", ENV_ROOT / "book_images") # ("book", ENV_ROOT / "book_images")
] ]
# Locale/Internationalization # Locale/Internationalization
TIME_ZONE = 'America/New_York' # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name TIME_ZONE = 'America/New_York' # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
LANGUAGE_CODE = 'en' # http://www.i18nguy.com/unicode/language-identifiers.html LANGUAGE_CODE = 'en' # http://www.i18nguy.com/unicode/language-identifiers.html
USE_I18N = True USE_I18N = True
USE_L10N = True USE_L10N = True
# Localization strings (e.g. django.po) are under this directory
LOCALE_PATHS = (REPO_ROOT + '/conf/locale',) # mitx/conf/locale/
# Tracking # Tracking
TRACK_MAX_EVENT = 10000 TRACK_MAX_EVENT = 10000
...@@ -182,29 +190,7 @@ MESSAGE_STORAGE = 'django.contrib.messages.storage.session.SessionStorage' ...@@ -182,29 +190,7 @@ MESSAGE_STORAGE = 'django.contrib.messages.storage.session.SessionStorage'
STATICFILES_STORAGE = 'pipeline.storage.PipelineCachedStorage' STATICFILES_STORAGE = 'pipeline.storage.PipelineCachedStorage'
# Load javascript and css from all of the available descriptors, and from rooted_paths import rooted_glob
# prep it for use in pipeline js
from xmodule.raw_module import RawDescriptor
from xmodule.error_module import ErrorDescriptor
from rooted_paths import rooted_glob, remove_root
write_descriptor_styles(PROJECT_ROOT / "static/sass/descriptor", [RawDescriptor, ErrorDescriptor])
write_module_styles(PROJECT_ROOT / "static/sass/module", [RawDescriptor, ErrorDescriptor])
descriptor_js = remove_root(
PROJECT_ROOT / 'static',
write_descriptor_js(
PROJECT_ROOT / "static/coffee/descriptor",
[RawDescriptor, ErrorDescriptor]
)
)
module_js = remove_root(
PROJECT_ROOT / 'static',
write_module_js(
PROJECT_ROOT / "static/coffee/module",
[RawDescriptor, ErrorDescriptor]
)
)
PIPELINE_CSS = { PIPELINE_CSS = {
'base-style': { 'base-style': {
...@@ -212,39 +198,35 @@ PIPELINE_CSS = { ...@@ -212,39 +198,35 @@ PIPELINE_CSS = {
'js/vendor/CodeMirror/codemirror.css', 'js/vendor/CodeMirror/codemirror.css',
'css/vendor/ui-lightness/jquery-ui-1.8.22.custom.css', 'css/vendor/ui-lightness/jquery-ui-1.8.22.custom.css',
'css/vendor/jquery.qtip.min.css', 'css/vendor/jquery.qtip.min.css',
'sass/base-style.scss' 'sass/base-style.css',
'xmodule/modules.css',
'xmodule/descriptor.css',
], ],
'output_filename': 'css/cms-base-style.css', 'output_filename': 'css/cms-base-style.css',
}, },
} }
PIPELINE_ALWAYS_RECOMPILE = ['sass/base-style.scss'] # test_order: Determines the position of this chunk of javascript on
# the jasmine test page
PIPELINE_JS = { PIPELINE_JS = {
'main': { 'main': {
'source_filenames': sorted( 'source_filenames': sorted(
rooted_glob(COMMON_ROOT / 'static/', 'coffee/src/**/*.coffee') + rooted_glob(COMMON_ROOT / 'static/', 'coffee/src/**/*.js') +
rooted_glob(PROJECT_ROOT / 'static/', 'coffee/src/**/*.coffee') rooted_glob(PROJECT_ROOT / 'static/', 'coffee/src/**/*.js')
) + ['js/hesitate.js', 'js/base.js'], ) + ['js/hesitate.js', 'js/base.js'],
'output_filename': 'js/cms-application.js', 'output_filename': 'js/cms-application.js',
'test_order': 0
}, },
'module-js': { 'module-js': {
'source_filenames': descriptor_js + module_js, 'source_filenames': (
rooted_glob(COMMON_ROOT / 'static/', 'xmodule/descriptors/js/*.js') +
rooted_glob(COMMON_ROOT / 'static/', 'xmodule/modules/js/*.js')
),
'output_filename': 'js/cms-modules.js', 'output_filename': 'js/cms-modules.js',
'test_order': 1
}, },
'spec': {
'source_filenames': sorted(rooted_glob(PROJECT_ROOT / 'static/', 'coffee/spec/**/*.coffee')),
'output_filename': 'js/cms-spec.js'
}
} }
PIPELINE_COMPILERS = [
'pipeline.compilers.sass.SASSCompiler',
'pipeline.compilers.coffee.CoffeeScriptCompiler',
]
PIPELINE_SASS_ARGUMENTS = '-t compressed -r {proj_dir}/static/sass/bourbon/lib/bourbon.rb'.format(proj_dir=PROJECT_ROOT)
PIPELINE_CSS_COMPRESSOR = None PIPELINE_CSS_COMPRESSOR = None
PIPELINE_JS_COMPRESSOR = None PIPELINE_JS_COMPRESSOR = None
...@@ -256,11 +238,6 @@ STATICFILES_IGNORE_PATTERNS = ( ...@@ -256,11 +238,6 @@ STATICFILES_IGNORE_PATTERNS = (
) )
PIPELINE_YUI_BINARY = 'yui-compressor' PIPELINE_YUI_BINARY = 'yui-compressor'
PIPELINE_SASS_BINARY = 'sass'
PIPELINE_COFFEE_SCRIPT_BINARY = 'coffee'
# Setting that will only affect the MITx version of django-pipeline until our changes are merged upstream
PIPELINE_COMPILE_INPLACE = True
############################ APPS ##################################### ############################ APPS #####################################
......
...@@ -112,6 +112,10 @@ CACHE_TIMEOUT = 0 ...@@ -112,6 +112,10 @@ CACHE_TIMEOUT = 0
# Dummy secret key for dev # Dummy secret key for dev
SECRET_KEY = '85920908f28904ed733fe576320db18cabd7b6cd' SECRET_KEY = '85920908f28904ed733fe576320db18cabd7b6cd'
################################ PIPELINE #################################
PIPELINE_SASS_ARGUMENTS = '--debug-info --require {proj_dir}/static/sass/bourbon/lib/bourbon.rb'.format(proj_dir=PROJECT_ROOT)
################################ DEBUG TOOLBAR ################################# ################################ DEBUG TOOLBAR #################################
INSTALLED_APPS += ('debug_toolbar', 'debug_toolbar_mongo') INSTALLED_APPS += ('debug_toolbar', 'debug_toolbar_mongo')
MIDDLEWARE_CLASSES += ('django_comment_client.utils.QueryCountDebugMiddleware', MIDDLEWARE_CLASSES += ('django_comment_client.utils.QueryCountDebugMiddleware',
...@@ -142,4 +146,10 @@ DEBUG_TOOLBAR_CONFIG = { ...@@ -142,4 +146,10 @@ DEBUG_TOOLBAR_CONFIG = {
# To see stacktraces for MongoDB queries, set this to True. # To see stacktraces for MongoDB queries, set this to True.
# Stacktraces slow down page loads drastically (for pages with lots of queries). # Stacktraces slow down page loads drastically (for pages with lots of queries).
DEBUG_TOOLBAR_MONGO_STACKTRACES = False DEBUG_TOOLBAR_MONGO_STACKTRACES = True
# disable NPS survey in dev mode
MITX_FEATURES['STUDIO_NPS_SURVEY'] = False
# segment-io key for dev
SEGMENT_IO_KEY = 'mty8edrrsg'
...@@ -20,14 +20,14 @@ PIPELINE_JS['js-test-source'] = { ...@@ -20,14 +20,14 @@ PIPELINE_JS['js-test-source'] = {
'source_filenames': sum([ 'source_filenames': sum([
pipeline_group['source_filenames'] pipeline_group['source_filenames']
for group_name, pipeline_group for group_name, pipeline_group
in PIPELINE_JS.items() in sorted(PIPELINE_JS.items(), key=lambda item: item[1].get('test_order', 1e100))
if group_name != 'spec' if group_name != 'spec'
], []), ], []),
'output_filename': 'js/cms-test-source.js' 'output_filename': 'js/cms-test-source.js'
} }
PIPELINE_JS['spec'] = { PIPELINE_JS['spec'] = {
'source_filenames': sorted(rooted_glob(PROJECT_ROOT / 'static/', 'coffee/spec/**/*.coffee')), 'source_filenames': sorted(rooted_glob(PROJECT_ROOT / 'static/', 'coffee/spec/**/*.js')),
'output_filename': 'js/cms-spec.js' 'output_filename': 'js/cms-spec.js'
} }
...@@ -35,4 +35,10 @@ JASMINE_TEST_DIRECTORY = PROJECT_ROOT + '/static/coffee' ...@@ -35,4 +35,10 @@ JASMINE_TEST_DIRECTORY = PROJECT_ROOT + '/static/coffee'
STATICFILES_DIRS.append(COMMON_ROOT / 'test' / 'phantom-jasmine' / 'lib') STATICFILES_DIRS.append(COMMON_ROOT / 'test' / 'phantom-jasmine' / 'lib')
# 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
# with a browser
MIDDLEWARE_CLASSES = tuple(e for e in MIDDLEWARE_CLASSES \
if e != 'django.middleware.locale.LocaleMiddleware')
INSTALLED_APPS += ('django_jasmine', ) INSTALLED_APPS += ('django_jasmine', )
...@@ -13,14 +13,10 @@ from path import path ...@@ -13,14 +13,10 @@ from path import path
# Nose Test Runner # Nose Test Runner
INSTALLED_APPS += ('django_nose',) INSTALLED_APPS += ('django_nose',)
NOSE_ARGS = ['--with-xunit']
TEST_RUNNER = 'django_nose.NoseTestSuiteRunner' TEST_RUNNER = 'django_nose.NoseTestSuiteRunner'
TEST_ROOT = path('test_root') TEST_ROOT = path('test_root')
# Makes the tests run much faster...
SOUTH_TESTS_MIGRATE = False # To disable migrations and use syncdb instead
# Want static files in the same dir for running on jenkins. # Want static files in the same dir for running on jenkins.
STATIC_ROOT = TEST_ROOT / "staticfiles" STATIC_ROOT = TEST_ROOT / "staticfiles"
...@@ -28,7 +24,7 @@ GITHUB_REPO_ROOT = TEST_ROOT / "data" ...@@ -28,7 +24,7 @@ GITHUB_REPO_ROOT = TEST_ROOT / "data"
COMMON_TEST_DATA_ROOT = COMMON_ROOT / "test" / "data" COMMON_TEST_DATA_ROOT = COMMON_ROOT / "test" / "data"
# Makes the tests run much faster... # Makes the tests run much faster...
SOUTH_TESTS_MIGRATE = False # To disable migrations and use syncdb instead SOUTH_TESTS_MIGRATE = False # To disable migrations and use syncdb instead
# TODO (cpennington): We need to figure out how envs/test.py can inject things into common.py so that we don't have to repeat this sort of thing # TODO (cpennington): We need to figure out how envs/test.py can inject things into common.py so that we don't have to repeat this sort of thing
STATICFILES_DIRS = [ STATICFILES_DIRS = [
...@@ -41,7 +37,7 @@ STATICFILES_DIRS += [ ...@@ -41,7 +37,7 @@ STATICFILES_DIRS += [
if os.path.isdir(COMMON_TEST_DATA_ROOT / course_dir) if os.path.isdir(COMMON_TEST_DATA_ROOT / course_dir)
] ]
modulestore_options = { MODULESTORE_OPTIONS = {
'default_class': 'xmodule.raw_module.RawDescriptor', 'default_class': 'xmodule.raw_module.RawDescriptor',
'host': 'localhost', 'host': 'localhost',
'db': 'test_xmodule', 'db': 'test_xmodule',
...@@ -53,11 +49,15 @@ modulestore_options = { ...@@ -53,11 +49,15 @@ modulestore_options = {
MODULESTORE = { MODULESTORE = {
'default': { 'default': {
'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore', 'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore',
'OPTIONS': modulestore_options 'OPTIONS': MODULESTORE_OPTIONS
}, },
'direct': { 'direct': {
'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore', 'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore',
'OPTIONS': modulestore_options 'OPTIONS': MODULESTORE_OPTIONS
},
'draft': {
'ENGINE': 'xmodule.modulestore.mongo.DraftMongoModuleStore',
'OPTIONS': MODULESTORE_OPTIONS
} }
} }
...@@ -72,7 +72,7 @@ CONTENTSTORE = { ...@@ -72,7 +72,7 @@ CONTENTSTORE = {
DATABASES = { DATABASES = {
'default': { 'default': {
'ENGINE': 'django.db.backends.sqlite3', 'ENGINE': 'django.db.backends.sqlite3',
'NAME': ENV_ROOT / "db" / "cms.db", 'NAME': TEST_ROOT / "db" / "cms.db",
}, },
} }
...@@ -114,3 +114,10 @@ PASSWORD_HASHERS = ( ...@@ -114,3 +114,10 @@ PASSWORD_HASHERS = (
'django.contrib.auth.hashers.SHA1PasswordHasher', 'django.contrib.auth.hashers.SHA1PasswordHasher',
'django.contrib.auth.hashers.MD5PasswordHasher', 'django.contrib.auth.hashers.MD5PasswordHasher',
) )
# dummy segment-io key
SEGMENT_IO_KEY = '***REMOVED***'
# disable NPS survey in test mode
MITX_FEATURES['STUDIO_NPS_SURVEY'] = False
from dogapi import dog_http_api, dog_stats_api from dogapi import dog_http_api, dog_stats_api
from django.conf import settings from django.conf import settings
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from django.dispatch import Signal
from request_cache.middleware import RequestCache
from django.core.cache import get_cache, InvalidCacheBackendError from django.core.cache import get_cache
cache = get_cache('mongo_metadata_inheritance') cache = get_cache('mongo_metadata_inheritance')
for store_name in settings.MODULESTORE: for store_name in settings.MODULESTORE:
store = modulestore(store_name) store = modulestore(store_name)
store.metadata_inheritance_cache = cache store.metadata_inheritance_cache_subsystem = cache
store.request_cache = RequestCache.get_request_cache()
modulestore_update_signal = Signal(providing_args=['modulestore', 'course_id', 'location'])
store.modulestore_update_signal = modulestore_update_signal
if hasattr(settings, 'DATADOG_API'): if hasattr(settings, 'DATADOG_API'):
dog_http_api.api_key = settings.DATADOG_API dog_http_api.api_key = settings.DATADOG_API
dog_stats_api.start(api_key=settings.DATADOG_API, statsd=True) dog_stats_api.start(api_key=settings.DATADOG_API, statsd=True)
...@@ -44,7 +44,7 @@ ...@@ -44,7 +44,7 @@
<% if (item['action_text'] !== '' && item['action_url'] !== '') { %> <% if (item['action_text'] !== '' && item['action_url'] !== '') { %>
<ul class="list-actions task-actions"> <ul class="list-actions task-actions">
<li> <li class="action-item">
<a href="<%= item['action_url'] %>" class="action action-primary" <a href="<%= item['action_url'] %>" class="action action-primary"
<% if (item['action_external']) { %> <% if (item['action_external']) { %>
rel="external" title="This link will open in a new browser window/tab" rel="external" title="This link will open in a new browser window/tab"
......
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
"js/vendor/jquery.cookie.js", "js/vendor/jquery.cookie.js",
"js/vendor/json2.js", "js/vendor/json2.js",
"js/vendor/underscore-min.js", "js/vendor/underscore-min.js",
"js/vendor/backbone-min.js" "js/vendor/backbone-min.js",
"js/vendor/jquery.leanModal.min.js"
] ]
} }
...@@ -15,7 +15,7 @@ class CMS.Views.ModuleEdit extends Backbone.View ...@@ -15,7 +15,7 @@ class CMS.Views.ModuleEdit extends Backbone.View
$component_editor: => @$el.find('.component-editor') $component_editor: => @$el.find('.component-editor')
loadDisplay: -> loadDisplay: ->
XModule.loadModule(@$el.find('.xmodule_display')) XModule.loadModule(@$el.find('.xmodule_display'))
loadEdit: -> loadEdit: ->
if not @module if not @module
...@@ -55,6 +55,11 @@ class CMS.Views.ModuleEdit extends Backbone.View ...@@ -55,6 +55,11 @@ class CMS.Views.ModuleEdit extends Backbone.View
clickSaveButton: (event) => clickSaveButton: (event) =>
event.preventDefault() event.preventDefault()
data = @module.save() data = @module.save()
analytics.track "Saved Module",
course: course_location_analytics
id: _this.model.id
data.metadata = _.extend(data.metadata || {}, @metadata()) data.metadata = _.extend(data.metadata || {}, @metadata())
@hideModal() @hideModal()
@model.save(data).done( => @model.save(data).done( =>
......
...@@ -28,6 +28,10 @@ class CMS.Views.TabsEdit extends Backbone.View ...@@ -28,6 +28,10 @@ class CMS.Views.TabsEdit extends Backbone.View
@$('.component').each((idx, element) => @$('.component').each((idx, element) =>
tabs.push($(element).data('id')) tabs.push($(element).data('id'))
) )
analytics.track "Reordered Static Pages",
course: course_location_analytics
$.ajax({ $.ajax({
type:'POST', type:'POST',
url: '/reorder_static_tabs', url: '/reorder_static_tabs',
...@@ -56,10 +60,18 @@ class CMS.Views.TabsEdit extends Backbone.View ...@@ -56,10 +60,18 @@ class CMS.Views.TabsEdit extends Backbone.View
'i4x://edx/templates/static_tab/Empty' 'i4x://edx/templates/static_tab/Empty'
) )
analytics.track "Added Static Page",
course: course_location_analytics
deleteTab: (event) => deleteTab: (event) =>
if not confirm 'Are you sure you want to delete this component? This action cannot be undone.' if not confirm 'Are you sure you want to delete this component? This action cannot be undone.'
return return
$component = $(event.currentTarget).parents('.component') $component = $(event.currentTarget).parents('.component')
analytics.track "Deleted Static Page",
course: course_location_analytics
id: $component.data('id')
$.post('/delete_item', { $.post('/delete_item', {
id: $component.data('id') id: $component.data('id')
}, => }, =>
......
...@@ -35,6 +35,10 @@ class CMS.Views.UnitEdit extends Backbone.View ...@@ -35,6 +35,10 @@ class CMS.Views.UnitEdit extends Backbone.View
@$('.components').sortable( @$('.components').sortable(
handle: '.drag-handle' handle: '.drag-handle'
update: (event, ui) => update: (event, ui) =>
analytics.track "Reordered Components",
course: course_location_analytics
id: unit_location_analytics
payload = children : @components() payload = children : @components()
options = success : => @model.unset('children') options = success : => @model.unset('children')
@model.save(payload, options) @model.save(payload, options)
...@@ -89,6 +93,11 @@ class CMS.Views.UnitEdit extends Backbone.View ...@@ -89,6 +93,11 @@ class CMS.Views.UnitEdit extends Backbone.View
$(event.currentTarget).data('location') $(event.currentTarget).data('location')
) )
analytics.track "Added a Component",
course: course_location_analytics
unit_id: unit_location_analytics
type: $(event.currentTarget).data('location')
@closeNewComponent(event) @closeNewComponent(event)
components: => @$('.component').map((idx, el) -> $(el).data('id')).get() components: => @$('.component').map((idx, el) -> $(el).data('id')).get()
...@@ -111,6 +120,11 @@ class CMS.Views.UnitEdit extends Backbone.View ...@@ -111,6 +120,11 @@ class CMS.Views.UnitEdit extends Backbone.View
$.post('/delete_item', { $.post('/delete_item', {
id: $component.data('id') id: $component.data('id')
}, => }, =>
analytics.track "Deleted a Component",
course: course_location_analytics
unit_id: unit_location_analytics
id: $component.data('id')
$component.remove() $component.remove()
# b/c we don't vigilantly keep children up to date # b/c we don't vigilantly keep children up to date
# get rid of it before it hurts someone # get rid of it before it hurts someone
...@@ -129,6 +143,10 @@ class CMS.Views.UnitEdit extends Backbone.View ...@@ -129,6 +143,10 @@ class CMS.Views.UnitEdit extends Backbone.View
id: @$el.data('id') id: @$el.data('id')
delete_children: true delete_children: true
}, => }, =>
analytics.track "Deleted Draft",
course: course_location_analytics
unit_id: unit_location_analytics
window.location.reload() window.location.reload()
) )
...@@ -138,6 +156,10 @@ class CMS.Views.UnitEdit extends Backbone.View ...@@ -138,6 +156,10 @@ class CMS.Views.UnitEdit extends Backbone.View
$.post('/create_draft', { $.post('/create_draft', {
id: @$el.data('id') id: @$el.data('id')
}, => }, =>
analytics.track "Created Draft",
course: course_location_analytics
unit_id: unit_location_analytics
@model.set('state', 'draft') @model.set('state', 'draft')
) )
...@@ -148,20 +170,31 @@ class CMS.Views.UnitEdit extends Backbone.View ...@@ -148,20 +170,31 @@ class CMS.Views.UnitEdit extends Backbone.View
$.post('/publish_draft', { $.post('/publish_draft', {
id: @$el.data('id') id: @$el.data('id')
}, => }, =>
analytics.track "Published Draft",
course: course_location_analytics
unit_id: unit_location_analytics
@model.set('state', 'public') @model.set('state', 'public')
) )
setVisibility: (event) -> setVisibility: (event) ->
if @$('.visibility-select').val() == 'private' if @$('.visibility-select').val() == 'private'
target_url = '/unpublish_unit' target_url = '/unpublish_unit'
visibility = "private"
else else
target_url = '/publish_draft' target_url = '/publish_draft'
visibility = "public"
@wait(true) @wait(true)
$.post(target_url, { $.post(target_url, {
id: @$el.data('id') id: @$el.data('id')
}, => }, =>
analytics.track "Set Unit Visibility",
course: course_location_analytics
unit_id: unit_location_analytics
visibility: visibility
@model.set('state', @$('.visibility-select').val()) @model.set('state', @$('.visibility-select').val())
) )
...@@ -193,6 +226,11 @@ class CMS.Views.UnitEdit.NameEdit extends Backbone.View ...@@ -193,6 +226,11 @@ class CMS.Views.UnitEdit.NameEdit extends Backbone.View
@model.save(metadata: metadata) @model.save(metadata: metadata)
# Update name shown in the right-hand side location summary. # Update name shown in the right-hand side location summary.
$('.unit-location .editing .unit-name').html(metadata.display_name) $('.unit-location .editing .unit-name').html(metadata.display_name)
analytics.track "Edited Unit Name",
course: course_location_analytics
unit_id: unit_location_analytics
display_name: metadata.display_name
class CMS.Views.UnitEdit.LocationState extends Backbone.View class CMS.Views.UnitEdit.LocationState extends Backbone.View
initialize: => initialize: =>
......
...@@ -37,6 +37,9 @@ CMS.Models.Settings.CourseDetails = Backbone.Model.extend({ ...@@ -37,6 +37,9 @@ CMS.Models.Settings.CourseDetails = Backbone.Model.extend({
// Returns either nothing (no return call) so that validate works or an object of {field: errorstring} pairs // Returns either nothing (no return call) so that validate works or an object of {field: errorstring} pairs
// A bit funny in that the video key validation is asynchronous; so, it won't stop the validation. // A bit funny in that the video key validation is asynchronous; so, it won't stop the validation.
var errors = {}; var errors = {};
if (newattrs.start_date === null) {
errors.start_date = "The course must have an assigned start date.";
}
if (newattrs.start_date && newattrs.end_date && newattrs.start_date >= newattrs.end_date) { if (newattrs.start_date && newattrs.end_date && newattrs.start_date >= newattrs.end_date) {
errors.end_date = "The course end date cannot be before the course start date."; errors.end_date = "The course end date cannot be before the course start date.";
} }
......
...@@ -77,11 +77,18 @@ CMS.Views.Checklists = Backbone.View.extend({ ...@@ -77,11 +77,18 @@ CMS.Views.Checklists = Backbone.View.extend({
var task_index = $checkbox.data('task'); var task_index = $checkbox.data('task');
var model = this.collection.at(checklist_index); var model = this.collection.at(checklist_index);
model.attributes.items[task_index].is_checked = $task.hasClass(completed); model.attributes.items[task_index].is_checked = $task.hasClass(completed);
model.save({}, model.save({},
{ {
success : function() { success : function() {
var updatedTemplate = self.renderTemplate(model, checklist_index); var updatedTemplate = self.renderTemplate(model, checklist_index);
self.$el.find('#course-checklist'+checklist_index).first().replaceWith(updatedTemplate); self.$el.find('#course-checklist'+checklist_index).first().replaceWith(updatedTemplate);
analytics.track('Toggled a Checklist Task', {
'course': course_location_analytics,
'task': model.attributes.items[task_index].short_description,
'state': model.attributes.items[task_index].is_checked
});
}, },
error : CMS.ServerError error : CMS.ServerError
}); });
......
...@@ -107,6 +107,11 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({ ...@@ -107,6 +107,11 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
// 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', {
'course': course_location_analytics,
'date': this.dateEntry(event).val()
});
}, },
onCancel: function(event) { onCancel: function(event) {
...@@ -147,6 +152,11 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({ ...@@ -147,6 +152,11 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
return; return;
} }
analytics.track('Deleted Course Update', {
'course': course_location_analytics,
'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;
...@@ -284,6 +294,11 @@ CMS.Views.ClassInfoHandoutsView = Backbone.View.extend({ ...@@ -284,6 +294,11 @@ CMS.Views.ClassInfoHandoutsView = Backbone.View.extend({
this.model.save({}, {error: CMS.ServerError}); this.model.save({}, {error: CMS.ServerError});
this.$form.hide(); this.$form.hide();
this.closeEditor(this); this.closeEditor(this);
analytics.track('Saved Course Handouts', {
'course': course_location_analytics
});
}, },
onCancel: function(event) { onCancel: function(event) {
......
...@@ -32,7 +32,7 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({ ...@@ -32,7 +32,7 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({
var listEle$ = this.$el.find('.course-advanced-policy-list'); var listEle$ = this.$el.find('.course-advanced-policy-list');
listEle$.empty(); listEle$.empty();
// b/c we've deleted all old fields, clear the map and repopulate // b/c we've deleted all old fields, clear the map and repopulate
this.fieldToSelectorMap = {}; this.fieldToSelectorMap = {};
this.selectorToField = {}; this.selectorToField = {};
...@@ -101,13 +101,13 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({ ...@@ -101,13 +101,13 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({
}); });
}, },
showMessage: function (type) { showMessage: function (type) {
this.$el.find(".message-status").removeClass("is-shown"); $(".wrapper-alert").removeClass("is-shown");
if (type) { if (type) {
if (type === this.error_saving) { if (type === this.error_saving) {
this.$el.find(".message-status.error").addClass("is-shown"); $(".wrapper-alert-error").addClass("is-shown").attr('aria-hidden','false');
} }
else if (type === this.successful_changes) { else if (type === this.successful_changes) {
this.$el.find(".message-status.confirm").addClass("is-shown"); $(".wrapper-alert-confirmation").addClass("is-shown").attr('aria-hidden','false');
this.hideSaveCancelButtons(); this.hideSaveCancelButtons();
} }
} }
...@@ -117,17 +117,20 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({ ...@@ -117,17 +117,20 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({
} }
}, },
showSaveCancelButtons: function(event) { showSaveCancelButtons: function(event) {
if (!this.buttonsVisible) { if (!this.notificationBarShowing) {
this.$el.find(".message-status").removeClass("is-shown"); this.$el.find(".message-status").removeClass("is-shown");
$('.wrapper-notification').addClass('is-shown'); $('.wrapper-notification').removeClass('is-hiding').addClass('is-shown').attr('aria-hidden','false');
this.buttonsVisible = true; this.notificationBarShowing = true;
} }
}, },
hideSaveCancelButtons: function() { hideSaveCancelButtons: function() {
$('.wrapper-notification').removeClass('is-shown'); if (this.notificationBarShowing) {
this.buttonsVisible = false; $('.wrapper-notification').removeClass('is-shown').addClass('is-hiding').attr('aria-hidden','true');
this.notificationBarShowing = false;
}
}, },
saveView : function(event) { saveView : function(event) {
window.CmsUtils.smoothScrollTop(event);
// TODO one last verification scan: // TODO one last verification scan:
// call validateKey on each to ensure proper format // call validateKey on each to ensure proper format
// check for dupes // check for dupes
...@@ -137,11 +140,16 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({ ...@@ -137,11 +140,16 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({
success : function() { success : function() {
self.render(); self.render();
self.showMessage(self.successful_changes); self.showMessage(self.successful_changes);
analytics.track('Saved Advanced Settings', {
'course': course_location_analytics
});
}, },
error : CMS.ServerError error : CMS.ServerError
}); });
}, },
revertView : function(event) { revertView : function(event) {
event.preventDefault();
var self = event.data; var self = event.data;
self.model.deleteKeys = []; self.model.deleteKeys = [];
self.model.clear({silent : true}); self.model.clear({silent : true});
...@@ -154,7 +162,7 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({ ...@@ -154,7 +162,7 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({
var newKeyId = _.uniqueId('policy_key_'), var newKeyId = _.uniqueId('policy_key_'),
newEle = this.template({ key : key, value : JSON.stringify(value, null, 4), newEle = this.template({ key : key, value : JSON.stringify(value, null, 4),
keyUniqueId: newKeyId, valueUniqueId: _.uniqueId('policy_value_')}); keyUniqueId: newKeyId, valueUniqueId: _.uniqueId('policy_value_')});
this.fieldToSelectorMap[key] = newKeyId; this.fieldToSelectorMap[key] = newKeyId;
this.selectorToField[newKeyId] = key; this.selectorToField[newKeyId] = key;
return newEle; return newEle;
...@@ -165,4 +173,4 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({ ...@@ -165,4 +173,4 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({
blurInput : function(event) { blurInput : function(event) {
$(event.target).prev().removeClass("is-focused"); $(event.target).prev().removeClass("is-focused");
} }
}); });
\ No newline at end of file
...@@ -101,10 +101,16 @@ CMS.Views.Settings.Details = CMS.Views.ValidatingView.extend({ ...@@ -101,10 +101,16 @@ CMS.Views.Settings.Details = CMS.Views.ValidatingView.extend({
cacheModel.save(fieldName, newVal); cacheModel.save(fieldName, newVal);
} }
} }
else {
// Clear date (note that this clears the time as well, as date and time are linked).
// Note also that the validation logic prevents us from clearing the start date
// (start date is required by the back end).
cacheModel.save(fieldName, null);
}
}; };
// instrument as date and time pickers // instrument as date and time pickers
timefield.timepicker(); timefield.timepicker({'timeFormat' : 'H:i'});
datefield.datepicker(); datefield.datepicker();
// Using the change event causes savefield to be triggered twice, but it is necessary // Using the change event causes savefield to be triggered twice, but it is necessary
......
...@@ -22,10 +22,10 @@ body, input { ...@@ -22,10 +22,10 @@ body, input {
a { a {
text-decoration: none; text-decoration: none;
color: $blue; color: $blue;
@include transition(color .15s); @include transition(color 0.25s ease-in-out);
&:hover { &:hover {
color: #cb9c40; color: $orange-d1;
} }
} }
...@@ -50,12 +50,72 @@ h1 { ...@@ -50,12 +50,72 @@ h1 {
// ==================== // ====================
// typography - basic
.title-1, .title-2, .title-3, .title-4, .title-5, .title-6 {
font-weight: 600;
color: $gray-d3;
margin: 0;
padding: 0;
}
.title-1 {
@include font-size(32);
margin-bottom: ($baseline*1.5);
}
.title-2 {
@include font-size(24);
margin-bottom: $baseline;
}
.title-3 {
@include font-size(18);
margin-bottom: ($baseline/2);
}
.title-4 {
@include font-size(14);
margin-bottom: $baseline;
font-weight: 500
}
.title-5 {
@include font-size(14);
color: $gray-l1;
margin-bottom: $baseline;
font-weight: 500
}
.title-6 {
@include font-size(14);
color: $gray-l2;
margin-bottom: $baseline;
font-weight: 500
}
p, ul, ol, dl {
margin-bottom: ($baseline/2);
&:last-child {
margin-bottom: 0;
}
}
// ====================
// layout - basic
.wrapper-view {
}
// ====================
// layout - basic page header // layout - basic page header
.wrapper-mast { .wrapper-mast {
margin: 0; margin: ($baseline*1.5) 0 0 0;
padding: 0 $baseline; padding: 0 $baseline;
position: relative; position: relative;
.mast, .metadata { .mast, .metadata {
@include clearfix(); @include clearfix();
@include font-size(16); @include font-size(16);
...@@ -272,33 +332,46 @@ h1 { ...@@ -272,33 +332,46 @@ h1 {
} }
.title-1 { .title-1 {
@extend .t-title-1;
} }
.title-2 { .title-2 {
@include font-size(24); @extend .t-title-2;
margin: 0 0 ($baseline/2) 0; margin: 0 0 ($baseline/2) 0;
font-weight: 600;
} }
.title-3 { .title-3 {
@include font-size(16); @extend .t-title-3;
margin: 0 0 ($baseline/2) 0; margin: 0 0 ($baseline/2) 0;
font-weight: 600;
} }
.title-4 { header {
@include clearfix();
}
.title-5 { .title-2 {
width: flex-grid(5, 12);
margin: 0 flex-gutter() 0 0;
float: left;
}
.tip {
@include font-size(13);
width: flex-grid(7, 12);
float: right;
margin-top: ($baseline/2);
text-align: right;
color: $gray-l2;
}
} }
} }
// layout - supplemental content // layout - supplemental content
.content-supplementary { .content-supplementary {
> section {
margin: 0 0 $baseline 0;
}
.bit { .bit {
@include font-size(13); @include font-size(13);
margin: 0 0 $baseline 0; margin: 0 0 $baseline 0;
...@@ -351,7 +424,7 @@ h1 { ...@@ -351,7 +424,7 @@ h1 {
// layout - grandfathered // layout - grandfathered
.main-wrapper { .main-wrapper {
position: relative; position: relative;
margin: 40px; margin: 0 ($baseline*2);
} }
.inner-wrapper { .inner-wrapper {
...@@ -644,7 +717,7 @@ hr.divide { ...@@ -644,7 +717,7 @@ hr.divide {
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
z-index: 99999; z-index: 10000;
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);
...@@ -764,10 +837,10 @@ body.js { ...@@ -764,10 +837,10 @@ body.js {
// ==================== // ====================
// works in progress // works in progress & testing
body.hide-wip { body.hide-wip {
.wip-box { .wip-box {
display: none; display: none;
} }
} }
\ No newline at end of file
// studio - utilities - mixins and extends // studio - utilities - mixins and extends
// ==================== // ====================
// mixins - utility
@mixin clearfix { @mixin clearfix {
&:after { &:after {
content: ''; content: '';
...@@ -11,19 +12,20 @@ ...@@ -11,19 +12,20 @@
} }
} }
// mixins - grandfathered
@mixin button { @mixin button {
display: inline-block; display: inline-block;
padding: 4px 20px 6px; padding: ($baseline/5) $baseline ($baseline/4);
font-size: 14px; @include font-size(14);
font-weight: 700; font-weight: 700;
@include box-shadow(0 1px 0 rgba(255, 255, 255, .3) inset, 0 0 0 rgba(0, 0, 0, 0)); @include box-shadow(0 1px 0 rgba(255, 255, 255, .3) inset, 0 0 0 rgba(0, 0, 0, 0));
@include transition(background-color .15s, box-shadow .15s); @include transition(background-color .15s, box-shadow .15s);
&.disabled { &.disabled {
border: 1px solid $lightGrey !important; border: 1px solid $gray-l1 !important;
border-radius: 3px !important; border-radius: 3px !important;
background: $lightGrey !important; background: $gray-l1 !important;
color: $darkGrey !important; color: $gray-d1 !important;
pointer-events: none; pointer-events: none;
cursor: none; cursor: none;
&:hover { &:hover {
...@@ -36,32 +38,111 @@ ...@@ -36,32 +38,111 @@
} }
} }
@mixin green-button {
@include button;
border: 1px solid $green-d1;
border-radius: 3px;
@include linear-gradient(top, rgba(255, 255, 255, .3), rgba(255, 255, 255, 0));
background-color: $green;
@include box-shadow(0 1px 0 rgba(255, 255, 255, .3) inset);
color: $white;
&:hover {
background-color: $green-s1;
color: $white;
}
&.disabled {
border: 1px solid $green-l3 !important;
background: $green-l3 !important;
color: $white !important;
@include box-shadow(none);
}
}
@mixin blue-button { @mixin blue-button {
@include button; @include button;
border: 1px solid #437fbf; border: 1px solid $blue-d1;
border-radius: 3px; border-radius: 3px;
@include linear-gradient(top, rgba(255, 255, 255, .3), rgba(255, 255, 255, 0)); @include linear-gradient(top, rgba(255, 255, 255, .3), rgba(255, 255, 255, 0));
background-color: $blue; background-color: $blue;
color: #fff; color: $white;
&:hover, &.active { &:hover, &.active {
background-color: #62aaf5; background-color: $blue-s2;
color: #fff; color: $white;
}
&.disabled {
border: 1px solid $blue-l3 !important;
background: $blue-l3 !important;
color: $white !important;
@include box-shadow(none);
} }
} }
@mixin green-button { @mixin red-button {
@include button; @include button;
border: 1px solid #0d7011; border: 1px solid $red-d1;
border-radius: 3px; border-radius: 3px;
@include linear-gradient(top, rgba(255, 255, 255, .3), rgba(255, 255, 255, 0)); @include linear-gradient(top, rgba(255, 255, 255, .3), rgba(255, 255, 255, 0));
background-color: $green; background-color: $red;
color: #fff; color: $white;
&:hover { &:hover, &.active {
background-color: #129416; background-color: $red-s1;
color: #fff; color: $white;
} }
&.disabled {
border: 1px solid $red-l3 !important;
background: $red-l3 !important;
color: $white !important;
@include box-shadow(none);
}
}
@mixin pink-button {
@include button;
border: 1px solid $pink-d1;
border-radius: 3px;
@include linear-gradient(top, rgba(255, 255, 255, .3), rgba(255, 255, 255, 0));
background-color: $pink;
color: $white;
&:hover, &.active {
background-color: $pink-s1;
color: $white;
}
&.disabled {
border: 1px solid $pink-l3 !important;
background: $pink-l3 !important;
color: $white !important;
@include box-shadow(none);
}
}
@mixin orange-button {
@include button;
border: 1px solid $orange-d1;
border-radius: 3px;
@include linear-gradient(top, rgba(255, 255, 255, 0.4), rgba(255, 255, 255, 0) 60%);
background-color: $orange;
@include box-shadow(0 1px 0 rgba(255, 255, 255, .3) inset);
color: $gray-d2;
&:hover {
background-color: $orange-s2;
color: $gray-d2;
}
&.disabled {
border: 1px solid $orange-l3 !important;
background: $orange-l2 !important;
color: $gray-l1 !important;
@include box-shadow(none);
}
} }
@mixin white-button { @mixin white-button {
...@@ -80,24 +161,9 @@ ...@@ -80,24 +161,9 @@
} }
} }
@mixin orange-button {
@include button;
border: 1px solid #bda046;
border-radius: 3px;
@include linear-gradient(top, rgba(255, 255, 255, 0.4), rgba(255, 255, 255, 0) 60%);
background-color: #edbd3c;
@include box-shadow(0 1px 0 rgba(255, 255, 255, .3) inset);
color: #3c3c3c;
&:hover {
background-color: #ffcd46;
color: #3c3c3c;
}
}
@mixin grey-button { @mixin grey-button {
@include button; @include button;
border: 1px solid $darkGrey; border: 1px solid $gray-d2;
border-radius: 3px; border-radius: 3px;
@include linear-gradient(top, rgba(255, 255, 255, .3), rgba(255, 255, 255, 0)); @include linear-gradient(top, rgba(255, 255, 255, .3), rgba(255, 255, 255, 0));
background-color: #d1dae3; background-color: #d1dae3;
...@@ -110,39 +176,32 @@ ...@@ -110,39 +176,32 @@
} }
} }
@mixin green-button { @mixin gray-button {
@include button; @include button;
border: 1px solid $darkGreen; border: 1px solid $gray-d1;
border-radius: 3px; border-radius: 3px;
@include linear-gradient(top, rgba(255, 255, 255, .3), rgba(255, 255, 255, 0)); @include linear-gradient(top, $white-t1, rgba(255, 255, 255, 0));
background-color: $green; background-color: $gray-d2;
@include box-shadow(0 1px 0 rgba(255, 255, 255, .3) inset); @include box-shadow(0 1px 0 $white-t1 inset);
color: #fff; color: $gray-l3;
&:hover { &:hover {
background-color: $brightGreen; background-color: $gray-d3;
color: #fff; color: $white;
}
&.disabled {
border: 1px solid $disabledGreen !important;
background: $disabledGreen !important;
color: #fff !important;
@include box-shadow(none);
} }
} }
@mixin dark-grey-button { @mixin dark-grey-button {
@include button; @include button;
border: 1px solid #1c1e20; border: 1px solid $gray-d2;
border-radius: 3px; border-radius: 3px;
background: -webkit-linear-gradient(top, rgba(255, 255, 255, .2), rgba(255, 255, 255, 0)) $extraDarkGrey; background: -webkit-linear-gradient(top, rgba(255, 255, 255, .2), rgba(255, 255, 255, 0)) $gray-d1;
box-shadow: 0 1px 0 rgba(255, 255, 255, .2) inset; box-shadow: 0 1px 0 rgba(255, 255, 255, .2) inset;
color: #fff; color: $white;
&:hover { &:hover {
background-color: #595f64; background-color: $gray-d4;
color: #fff; color: $white;
} }
} }
...@@ -163,7 +222,7 @@ ...@@ -163,7 +222,7 @@
} }
textarea { textarea {
min-height: 80px; min-height: 80px;
} }
h5 { h5 {
...@@ -208,7 +267,7 @@ ...@@ -208,7 +267,7 @@
.section-item { .section-item {
position: relative; position: relative;
display: block; display: block;
padding: 6px 8px 8px 16px; padding: 6px 8px 8px 16px;
background: #edf1f5; background: #edf1f5;
font-size: 13px; font-size: 13px;
...@@ -279,20 +338,100 @@ ...@@ -279,20 +338,100 @@
} }
} }
@mixin sr-text { // ====================
border: 0;
clip: rect(0 0 0 0);
height: 1px;
margin: -1px;
overflow: hidden;
padding: 0;
position: absolute;
width: 1px;
}
// sunsetted mixins
@mixin active { @mixin active {
@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: rgba(255, 255, 255, .3); background-color: rgba(255, 255, 255, .3);
@include box-shadow(0 -1px 0 rgba(0, 0, 0, .2) inset, 0 1px 0 #fff inset); @include box-shadow(0 -1px 0 rgba(0, 0, 0, .2) inset, 0 1px 0 #fff inset);
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
} }
\ No newline at end of file
// ====================
// extends - buttons
.btn {
@include box-sizing(border-box);
@include transition(color 0.25s ease-in-out, border-color 0.25s ease-in-out, background 0.25s ease-in-out, box-shadow 0.25s ease-in-out);
display: inline-block;
cursor: pointer;
&:hover, &:active {
}
&.disabled, &[disabled] {
cursor: default;
pointer-events: none;
opacity: 0.5;
}
.icon-inline {
display: inline-block;
vertical-align: middle;
margin-right: ($baseline/4);
}
}
// pill button
.btn-pill {
@include border-radius($baseline/5);
}
.btn-rounded {
@include border-radius($baseline/2);
}
// primary button
.btn-primary {
@extend .btn;
@extend .btn-pill;
padding:($baseline/2) $baseline;
border-width: 1px;
border-style: solid;
line-height: 1.5em;
text-align: center;
&:hover, &:active {
@include box-shadow(0 2px 1px $shadow-l1);
}
&.current, &.active {
@include box-shadow(inset 1px 1px 2px $shadow-d1);
&:hover, &:active {
@include box-shadow(inset 1px 1px 1px $shadow-d1);
}
}
}
// secondary button
.btn-secondary {
@extend .btn;
@extend .btn-pill;
border-width: 1px;
border-style: solid;
padding:($baseline/2) $baseline;
background: transparent;
line-height: 1.5em;
text-align: center;
&:hover, &:active {
}
&.current, &.active {
}
}
// ====================
// extends - depth levels
.depth0 { z-index: 0; }
.depth1 { z-index: 10; }
.depth2 { z-index: 100; }
.depth3 { z-index: 1000; }
.depth4 { z-index: 10000; }
.depth5 { z-index: 100000; }
// studio - utilities - reset // studio - utilities - reset
// ==================== // ====================
// not ready for this yet, but this should be done as things get cleaner
// * { // * {
// @include box-sizing(border-box); // @include box-sizing(border-box);
// } // }
...@@ -26,6 +27,10 @@ time, mark, audio, video { ...@@ -26,6 +27,10 @@ time, mark, audio, video {
vertical-align: baseline; vertical-align: baseline;
} }
html,body {
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;
...@@ -57,6 +62,12 @@ table { ...@@ -57,6 +62,12 @@ table {
border-spacing: 0; border-spacing: 0;
} }
abbr[title] {
border-bottom: none;
text-decoration: none;
cursor: help;
}
// ==================== // ====================
// grandfathered styles // grandfathered styles
......
...@@ -22,6 +22,7 @@ $black-t0: rgba(0,0,0,0.125); ...@@ -22,6 +22,7 @@ $black-t0: rgba(0,0,0,0.125);
$black-t1: rgba(0,0,0,0.25); $black-t1: rgba(0,0,0,0.25);
$black-t2: rgba(0,0,0,0.50); $black-t2: rgba(0,0,0,0.50);
$black-t3: rgba(0,0,0,0.75); $black-t3: rgba(0,0,0,0.75);
$white: rgb(255,255,255); $white: rgb(255,255,255);
$white-t0: rgba(255,255,255,0.125); $white-t0: rgba(255,255,255,0.125);
$white-t1: rgba(255,255,255,0.25); $white-t1: rgba(255,255,255,0.25);
...@@ -56,6 +57,10 @@ $blue-s3: saturate($blue,45%); ...@@ -56,6 +57,10 @@ $blue-s3: saturate($blue,45%);
$blue-u1: desaturate($blue,15%); $blue-u1: desaturate($blue,15%);
$blue-u2: desaturate($blue,30%); $blue-u2: desaturate($blue,30%);
$blue-u3: desaturate($blue,45%); $blue-u3: desaturate($blue,45%);
$blue-t0: rgba(85, 151, 221,0.125);
$blue-t1: rgba(85, 151, 221,0.25);
$blue-t2: rgba(85, 151, 221,0.50);
$blue-t3: rgba(85, 151, 221,0.75);
$pink: rgb(183, 37, 103); $pink: rgb(183, 37, 103);
$pink-l1: tint($pink,20%); $pink-l1: tint($pink,20%);
...@@ -108,7 +113,7 @@ $green-u1: desaturate($green,15%); ...@@ -108,7 +113,7 @@ $green-u1: desaturate($green,15%);
$green-u2: desaturate($green,30%); $green-u2: desaturate($green,30%);
$green-u3: desaturate($green,45%); $green-u3: desaturate($green,45%);
$yellow: rgb(231, 214, 143); $yellow: rgb(237, 189, 60);
$yellow-l1: tint($yellow,20%); $yellow-l1: tint($yellow,20%);
$yellow-l2: tint($yellow,40%); $yellow-l2: tint($yellow,40%);
$yellow-l3: tint($yellow,60%); $yellow-l3: tint($yellow,60%);
...@@ -144,10 +149,15 @@ $orange-u3: desaturate($orange,45%); ...@@ -144,10 +149,15 @@ $orange-u3: desaturate($orange,45%);
$shadow: rgba(0,0,0,0.2); $shadow: rgba(0,0,0,0.2);
$shadow-l1: rgba(0,0,0,0.1); $shadow-l1: rgba(0,0,0,0.1);
$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
$notification-height: ($baseline*10);
// colors - inherited // colors - inherited
$baseFontColor: #3c3c3c; $baseFontColor: $gray-d2;
$offBlack: #3c3c3c; $offBlack: #3c3c3c;
$green: #108614; $green: #108614;
$lightGrey: #edf1f5; $lightGrey: #edf1f5;
......
@mixin bounce-in { // studio animations & keyframes
// ====================
// rotate clockwise
@mixin rotateClockwise {
0% {
@include transform(rotate(0deg));
}
100% {
@include transform(rotate(360deg));
}
}
@-moz-keyframes rotateClockwise { @include rotateClockwise(); }
@-webkit-keyframes rotateClockwise { @include rotateClockwise(); }
@-o-keyframes rotateClockwise { @include rotateClockwise(); }
@keyframes rotateClockwise { @include rotateClockwise();}
@mixin anim-rotateClockwise($duration, $timing: ease-in-out, $count: 1, $delay: 0) {
@include animation-name(rotateClockwise);
@include animation-duration($duration);
@include animation-delay($delay);
@include animation-timing-function($timing);
@include animation-iteration-count($count);
@include animation-fill-mode(both);
}
// ====================
// notifications slide up
@mixin notificationsSlideUp {
0% {
@include transform(translateY(0));
}
90% {
@include transform(translateY(-($notification-height)));
}
100% {
@include transform(translateY(-($notification-height*0.99)));
}
}
@-moz-keyframes notificationsSlideUp { @include notificationsSlideUp(); }
@-webkit-keyframes notificationsSlideUp { @include notificationsSlideUp(); }
@-o-keyframes notificationsSlideUp { @include notificationsSlideUp(); }
@keyframes notificationsSlideUp { @include notificationsSlideUp();}
@mixin anim-notificationsSlideUp($duration, $timing: ease-in-out, $count: 1, $delay: 0) {
@include animation-name(notificationsSlideUp);
@include animation-duration($duration);
@include animation-delay($delay);
@include animation-timing-function($timing);
@include animation-iteration-count($count);
@include animation-fill-mode(both);
}
// ====================
// notifications slide down
@mixin notificationsSlideDown {
0% {
@include transform(translateY(-($notification-height*0.99)));
}
10% {
@include transform(translateY(-($notification-height)));
}
100% {
@include transform(translateY(0));
}
}
@-moz-keyframes notificationsSlideDown { @include notificationsSlideDown(); }
@-webkit-keyframes notificationsSlideDown { @include notificationsSlideDown(); }
@-o-keyframes notificationsSlideDown { @include notificationsSlideDown(); }
@keyframes notificationsSlideDown { @include notificationsSlideDown();}
@mixin anim-notificationsSlideDown($duration, $timing: ease-in-out, $count: 1, $delay: 0) {
@include animation-name(notificationsSlideDown);
@include animation-duration($duration);
@include animation-delay($delay);
@include animation-timing-function($timing);
@include animation-iteration-count($count);
@include animation-fill-mode(both);
}
// ====================
// notifications slide up then down
@mixin notificationsSlideUpDown {
0%, 100% {
@include transform(translateY(0));
}
15%, 85% {
@include transform(translateY(-($notification-height)));
}
20%, 80% {
@include transform(translateY(-($notification-height*0.99)));
}
}
@-moz-keyframes notificationsSlideUpDown { @include notificationsSlideUpDown(); }
@-webkit-keyframes notificationsSlideUpDown { @include notificationsSlideUpDown(); }
@-o-keyframes notificationsSlideUpDown { @include notificationsSlideUpDown(); }
@keyframes notificationsSlideUpDown { @include notificationsSlideUpDown();}
@mixin anim-notificationsSlideUpDown($duration, $timing: ease-in-out, $count: 1, $delay: 0) {
@include animation-name(notificationsSlideUpDown);
@include animation-duration($duration);
@include animation-delay($delay);
@include animation-timing-function($timing);
@include animation-iteration-count($count);
@include animation-fill-mode(both);
}
// ====================
// bounce in
@mixin bounceIn {
0% { 0% {
opacity: 0; opacity: 0;
@include transform(scale(.3)); @include transform(scale(0.3));
} }
50% { 50% {
...@@ -14,14 +140,63 @@ ...@@ -14,14 +140,63 @@
} }
} }
@-moz-keyframes bounce-in { @include bounce-in(); } @-moz-keyframes bounceIn { @include bounceIn(); }
@-webkit-keyframes bounce-in { @include bounce-in(); } @-webkit-keyframes bounceIn { @include bounceIn(); }
@-o-keyframes bounce-in { @include bounce-in(); } @-o-keyframes bounceIn { @include bounceIn(); }
@keyframes bounce-in { @include bounce-in();} @keyframes bounceIn { @include bounceIn();}
@mixin bounce-in-animation($duration, $timing: ease-in-out) { @mixin anim-bounceIn($duration, $timing: ease-in-out, $count: 1, $delay: 0) {
@include animation-name(bounce-in); @include animation-name(bounceIn);
@include animation-duration($duration); @include animation-duration($duration);
@include animation-delay($delay);
@include animation-timing-function($timing); @include animation-timing-function($timing);
@include animation-iteration-count($count);
@include animation-fill-mode(both); @include animation-fill-mode(both);
} }
// ====================
// bounce in
@mixin bounceOut {
0% {
opacity: 0;
@include transform(scale(0.3));
}
50% {
opacity: 1;
@include transform(scale(1.05));
}
100% {
@include transform(scale(1));
}
0% {
@include transform(scale(1));
}
50% {
opacity: 1;
@include transform(scale(1.05));
}
100% {
opacity: 0;
@include transform(scale(0.3));
}
}
@-moz-keyframes bounceOut { @include bounceOut(); }
@-webkit-keyframes bounceOut { @include bounceOut(); }
@-o-keyframes bounceOut { @include bounceOut(); }
@keyframes bounceOut { @include bounceOut();}
@mixin anim-bounceOut($duration, $timing: ease-in-out, $count: 1, $delay: 0) {
@include animation-name(bounceOut);
@include animation-duration($duration);
@include animation-delay($delay);
@include animation-timing-function($timing);
@include animation-iteration-count($count);
@include animation-fill-mode(both);
}
\ No newline at end of file
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
// bourbon libs and resets // bourbon libs and resets
@import 'bourbon/bourbon'; @import 'bourbon/bourbon';
@import 'bourbon/addons/button'; @import 'bourbon/addons/button';
@import "variables";
@import 'vendor/normalize'; @import 'vendor/normalize';
@import 'reset'; @import 'reset';
...@@ -21,13 +22,18 @@ ...@@ -21,13 +22,18 @@
@import 'base'; @import 'base';
// elements // elements
@import 'elements/typography';
@import 'elements/icons';
@import 'elements/controls';
@import 'elements/navigation';
@import 'elements/header'; @import 'elements/header';
@import 'elements/footer'; @import 'elements/footer';
@import 'elements/navigation'; @import 'elements/sock';
@import 'elements/forms'; @import 'elements/forms';
@import 'elements/modal'; @import 'elements/modal';
@import 'elements/alerts'; @import 'elements/alerts';
@import 'elements/jquery-ui-calendar'; @import 'elements/vendor';
@import 'elements/tender-widget';
// specific views // specific views
@import 'views/account'; @import 'views/account';
...@@ -48,5 +54,5 @@ ...@@ -48,5 +54,5 @@
@import 'assets/content-types'; @import 'assets/content-types';
// xblock-related // xblock-related
@import 'module/module-styles.scss'; @import 'xmodule/modules/css/module-styles.scss';
@import 'descriptor/module-styles.scss'; @import 'xmodule/descriptors/css/module-styles.scss';
../../../common/static/sass/bourbon/
\ No newline at end of file
// studio - elements - UI controls
// ====================
// gray primary button
.btn-primary-gray {
@extend .btn-primary;
background: $gray-l1;
border-color: $gray-l2;
color: $white;
&:hover, &:active {
border-color: $gray-l1;
background: $gray;
}
&.current, &.active {
background: $gray-d1;
color: $gray-l1;
&:hover, &:active {
background: $gray-d1;
}
}
}
// blue primary button
.btn-primary-blue {
@extend .btn-primary;
background: $blue;
border-color: $blue-s1;
color: $white;
&:hover, &:active {
background: $blue-s2;
border-color: $blue-s2;
}
&.current, &.active {
background: $blue-d1;
color: $blue-l4;
border-color: $blue-d2;
&:hover, &:active {
background: $blue-d1;
}
}
}
// green primary button
.btn-primary-green {
@extend .btn-primary;
background: $green;
border-color: $green;
color: $white;
&:hover, &:active {
background: $green-s1;
border-color: $green-s1;
}
&.current, &.active {
background: $green-d1;
color: $green-l4;
border-color: $green-d2;
&:hover, &:active {
background: $green-d1;
}
}
}
// gray secondary button
.btn-secondary-gray {
@extend .btn-secondary;
border-color: $gray-l3;
color: $gray-l1;
&:hover, &:active {
background: $gray-l3;
color: $gray-d2;
}
&.current, &.active {
background: $gray-d2;
color: $gray-l5;
&:hover, &:active {
background: $gray-d2;
}
}
}
// blue secondary button
.btn-secondary-blue {
@extend .btn-secondary;
border-color: $blue-l3;
color: $blue;
&:hover, &:active {
background: $blue-l4;
color: $blue-s2;
}
&.current, &.active {
border-color: $blue-l3;
background: $blue-l3;
color: $blue-d1;
&:hover, &:active {
}
}
}
// green secondary button
.btn-secondary-green {
@extend .btn-secondary;
border-color: $green-l4;
color: $green-l2;
&:hover, &:active {
background: $green-l4;
color: $green-s1;
}
&.current, &.active {
background: $green-s1;
color: $green-l4;
&:hover, &:active {
background: $green-s1;
}
}
}
// ====================
// layout-based buttons
// ====================
// calls-to-action
...@@ -2,10 +2,10 @@ ...@@ -2,10 +2,10 @@
// ==================== // ====================
.wrapper-footer { .wrapper-footer {
margin: ($baseline*1.5) 0 $baseline 0;
padding: $baseline;
position: relative; position: relative;
width: 100%; width: 100%;
margin: 0 0 $baseline 0;
padding: $baseline;
footer.primary { footer.primary {
@include clearfix(); @include clearfix();
...@@ -14,9 +14,7 @@ ...@@ -14,9 +14,7 @@
min-width: $fg-min-width; min-width: $fg-min-width;
width: flex-grid(12); width: flex-grid(12);
margin: 0 auto; margin: 0 auto;
padding-top: $baseline; color: $gray-l1;
border-top: 1px solid $gray-l4;
color: $gray-l2;
.colophon { .colophon {
width: flex-grid(4, 12); width: flex-grid(4, 12);
...@@ -24,6 +22,14 @@ ...@@ -24,6 +22,14 @@
margin-right: flex-gutter(2); margin-right: flex-gutter(2);
} }
a {
color: $gray;
&:hover, &:active {
color: $gray-d2;
}
}
.nav-peripheral { .nav-peripheral {
width: flex-grid(6, 12); width: flex-grid(6, 12);
float: right; float: right;
...@@ -36,14 +42,33 @@ ...@@ -36,14 +42,33 @@
&:last-child { &:last-child {
margin-right: 0; margin-right: 0;
} }
}
}
a { a {
color: $gray-l1; @include border-radius(2px);
padding: ($baseline/2) ($baseline*0.75);
background: transparent;
&:hover, &:active { .ss-icon {
color: $blue; @include transition(top .25s ease-in-out .25s);
@include font-size(15);
display: inline-block;
vertical-align: middle;
margin-right: ($baseline/4);
color: $gray-l1;
}
&:hover, &:active {
color: $gray-d2;
.ss-icon {
color: $gray-d2;
}
}
&.is-active {
color: $gray-d2;
}
}
} }
} }
} }
......
...@@ -8,11 +8,11 @@ input[type="password"], ...@@ -8,11 +8,11 @@ input[type="password"],
textarea.text { textarea.text {
padding: 6px 8px 8px; padding: 6px 8px 8px;
@include box-sizing(border-box); @include box-sizing(border-box);
border: 1px solid $mediumGrey; border: 1px solid $gray-l2;
border-radius: 2px; border-radius: 2px;
@include linear-gradient($lightGrey, tint($lightGrey, 90%)); @include linear-gradient($gray-l5, $white);
background-color: $lightGrey; background-color: $gray-l5;
@include box-shadow(0 1px 2px rgba(0, 0, 0, .1) inset); @include box-shadow(inset 0 1px 2px $shadow-l1);
font-family: 'Open Sans', sans-serif; font-family: 'Open Sans', sans-serif;
font-size: 11px; font-size: 11px;
color: $baseFontColor; color: $baseFontColor;
...@@ -21,7 +21,7 @@ textarea.text { ...@@ -21,7 +21,7 @@ textarea.text {
&::-webkit-input-placeholder, &::-webkit-input-placeholder,
&:-moz-placeholder, &:-moz-placeholder,
&:-ms-input-placeholder { &:-ms-input-placeholder {
color: #979faf; color: $gray-l2;
} }
&:focus { &:focus {
...@@ -30,7 +30,72 @@ textarea.text { ...@@ -30,7 +30,72 @@ textarea.text {
} }
} }
// forms - specific // ====================
// forms - fields - not editable
.field.is-not-editable {
& label.is-focused {
color: $gray-d2;
}
label, input, textarea {
pointer-events: none;
}
}
// ====================
// field with error
.field.error {
input, textarea {
border-color: $red;
}
}
// ====================
// forms - additional UI
form {
.note {
@include box-sizing(border-box);
.title {
}
.copy {
}
// note with actions
&.has-actions {
@include clearfix();
.title {
}
.copy {
}
.list-actions {
}
}
}
.note-promotion {
}
}
// ====================
// forms - grandfathered
input.search { input.search {
padding: 6px 15px 8px 30px; padding: 6px 15px 8px 30px;
@include box-sizing(border-box); @include box-sizing(border-box);
...@@ -73,4 +138,4 @@ code { ...@@ -73,4 +138,4 @@ code {
background-color: #edf1f5; background-color: #edf1f5;
@include box-shadow(0 1px 2px rgba(0, 0, 0, 0.1) inset); @include box-shadow(0 1px 2px rgba(0, 0, 0, 0.1) inset);
font-family: Monaco, monospace; font-family: Monaco, monospace;
} }
\ No newline at end of file
...@@ -2,15 +2,15 @@ ...@@ -2,15 +2,15 @@
// ==================== // ====================
.wrapper-header { .wrapper-header {
margin: 0 0 ($baseline*1.5) 0; margin: 0;
padding: $baseline; padding: $baseline;
border-bottom: 1px solid $gray; border-bottom: 1px solid $gray;
@include box-shadow(0 1px 5px 0 rgba(0,0,0, 0.1)); @include box-shadow(0 1px 5px 0 rgba(0,0,0, 0.2));
background: $white; background: $white;
height: 76px; height: 76px;
position: relative; position: relative;
width: 100%; width: 100%;
z-index: 10; z-index: 1000;
a { a {
color: $baseFontColor; color: $baseFontColor;
...@@ -132,7 +132,7 @@ ...@@ -132,7 +132,7 @@
// specific elements - course nav // specific elements - course nav
.nav-course { .nav-course {
width: 335px; width: 285px;
margin-top: -($baseline/4); margin-top: -($baseline/4);
@include font-size(14); @include font-size(14);
......
// studio - elements - icons
// ====================
.icon {
}
.ss-icon {
}
.icon-inline {
display: inline-block;
vertical-align: middle;
margin-right: ($baseline/4);
}
\ No newline at end of file
// studio - elements - support sock
// ====================
.wrapper-sock {
@include clearfix();
position: relative;
margin: ($baseline*2) 0 0 0;
border-top: 1px solid $gray-l4;
width: 100%;
.wrapper-inner {
@include linear-gradient($gray-d3 0%, $gray-d3 98%, $black 100%);
@extend .depth0;
display: none;
width: 100% !important;
border-bottom: 1px solid $white;
padding: 0 $baseline !important;
}
// sock - actions
.list-cta {
@extend .depth1;
position: absolute;
top: -($baseline*0.75);
width: 100%;
margin: 0 auto;
text-align: center;
.cta-show-sock {
@extend .btn-pill;
@extend .t-action3;
background: $gray-l5;
padding: ($baseline/2) $baseline;
color: $gray;
.icon {
@include font-size(16);
}
&:hover {
background: $blue;
color: $white;
}
}
}
// sock - additional help
.sock {
@include clearfix();
@extend .t-copy-sub2;
max-width: $fg-max-width;
min-width: $fg-min-width;
width: flex-grid(12);
margin: 0 auto;
padding: ($baseline*2) 0;
color: $gray-l3;
// support body
header {
.title {
@extend .t-title-2;
}
.ss-icon {
@extend .t-icon;
@extend .icon-inline;
}
}
// shared elements
.support, .feedback {
@include box-sizing(border-box);
.title {
@extend .t-title-3;
color: $white;
margin-bottom: ($baseline/2);
}
.copy {
margin: 0 0 $baseline 0;
}
.list-actions {
@include clearfix();
.action-item {
float: left;
margin-right: ($baseline/2);
&:last-child {
margin-right: 0;
}
.action {
display: block;
.icon {
@include font-size(18);
}
&:hover, &:active {
}
}
.tip {
@extend .sr;
}
}
.action-primary {
@extend .btn-primary-blue;
@extend .t-action3;
}
}
}
// studio support content
.support {
width: flex-grid(8,12);
float: left;
margin-right: flex-gutter();
.action-item {
width: flexgrid(4,8);
}
}
// studio feedback content
.feedback {
width: flex-grid(4,12);
float: left;
.action-item {
width: flexgrid(4,4);
}
}
}
// case: sock content is shown
&.is-shown {
border-color: $gray-d3;
.list-cta .cta-show-sock {
background: $gray-d3;
border-color: $gray-d3;
color: $white;
}
}
}
\ No newline at end of file
// tender help/support widget
// ====================
#tender_frame, #tender_window {
background-image: none !important;
background: none;
}
#tender_window {
@include border-radius(3px);
@include box-shadow(0 2px 3px $shadow);
height: ($baseline*35) !important;
background: $white !important;
border: 1px solid $gray;
}
#tender_window {
padding: 0 !important;
}
#tender_frame {
background: $white;
}
#tender_closer {
color: $blue-l2 !important;
text-transform: uppercase;
&:hover {
color: $blue-l4 !important;
}
}
// ====================
// tender style overrides - not rendered through here, but an archive is needed
#tender_frame iframe html {
font-size: 62.5%;
}
.widget-layout {
font-family: 'Open Sans', sans-serif;
}
.widget-layout .search,
.widget-layout .tabs,
.widget-layout .footer,
.widget-layout .header h1 a {
display: none;
}
.widget-layout .header {
background: rgb(85, 151, 221);
padding: 10px 20px;
}
.widget-layout h1, .widget-layout h2, .widget-layout h3, .widget-layout h4, .widget-layout h5, .widget-layout h6, .widget-layout label {
font-weight: 600;
}
.widget-layout .header h1 {
font-size: 22px;
}
.widget-layout .content {
overflow: auto;
height: auto !important;
padding: 20px;
}
.widget-layout .flash {
margin: -10px 0 15px 0;
padding: 10px 20px !important;
background-image: none !important;
}
.widget-layout .flash-error {
background: rgb(178, 6, 16) !important;
color: rgb(255,255,255) !important;
}
.widget-layout label {
font-size: 14px;
margin-bottom: 5px;
color: #4c4c4c;
font-weight: 500;
}
.widget-layout input[type="text"], .widget-layout textarea {
padding: 10px;
font-size: 16px;
color: rgb(0,0,0) !important;
border: 1px solid #b0b6c2;
border-radius: 2px;
background-color: #edf1f5;
background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #edf1f5),color-stop(100%, #fdfdfe));
background-image: -webkit-linear-gradient(top, #edf1f5,#fdfdfe);
background-image: -moz-linear-gradient(top, #edf1f5,#fdfdfe);
background-image: -ms-linear-gradient(top, #edf1f5,#fdfdfe);
background-image: -o-linear-gradient(top, #edf1f5,#fdfdfe);
background-image: linear-gradient(top, #edf1f5,#fdfdfe);
background-color: #edf1f5;
-webkit-box-shadow: 0 1px 2px rgba(0,0,0,0.1) inset;
-moz-box-shadow: 0 1px 2px rgba(0,0,0,0.1) inset;
box-shadow: 0 1px 2px rgba(0,0,0,0.1) inset;
}
.widget-layout input[type="text"]:focus, .widget-layout textarea:focus {
background-color: #fffcf1;
background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #fffcf1),color-stop(100%, #fffefd));
background-image: -webkit-linear-gradient(top, #fffcf1,#fffefd);
background-image: -moz-linear-gradient(top, #fffcf1,#fffefd);
background-image: -ms-linear-gradient(top, #fffcf1,#fffefd);
background-image: -o-linear-gradient(top, #fffcf1,#fffefd);
background-image: linear-gradient(top, #fffcf1,#fffefd);
outline: 0;
}
.widget-layout textarea {
width: 97%;
}
.widget-layout p.note {
text-align: right !important;
display: inline-block !important;
position: absolute !important;
right: -130px !important;
top: -5px !important;
font-size: 13px !important;
opacity: 0.80;
}
.widget-layout .form-actions {
margin: 15px 0;
border: none;
padding: 0;
}
.widget-layout dl.form {
float: none;
width: 100%;
border-bottom: 1px solid #f2f2f2;
margin-bottom: 10px;
padding-bottom: 10px;
}
.widget-layout dl.form:last-child {
border: none;
padding-bottom: 0;
margin-bottom: 20px;
}
.widget-layout dl.form dt, .widget-layout dl.form dd {
display: inline-block;
vertical-align: middle;
}
.widget-layout dl.form dt {
margin-right: 15px;
width: 70px;
}
.widget-layout dl.form dd {
width: 65%;
position: relative;
}
// specific elements
.widget-layout #discussion_body {
}
.widget-layout #discussion_body:before {
content: "What Question or Feedback Would You Like to Share?";
display: block;
font-size: 14px;
margin-bottom: 5px;
color: #4c4c4c;
font-weight: 500;
}
.widget-layout dl#brain_buster_captcha {
float: none;
width: 100%;
border-top: 1px solid #f2f2f2;
margin-top: 10px;
padding-top: 10px;
}
.widget-layout dl#brain_buster_captcha dd {
display: block !important;
}
.widget-layout dl#brain_buster_captcha #captcha_answer {
border-color: #333;
}
.widget-layout dl#brain_buster_captcha dd label {
display: block;
font-weight: 700;
margin: 0 15px 5px 0 !important;
}
.widget-layout dl#brain_buster_captcha dd #captcha_answer {
display: block;
width: 97%%;
}
.widget-layout .form-actions .btn-post_topic {
display: block;
width: 100%;
height: auto !important;
font-size: 16px;
font-weight: 700;
-webkit-box-shadow: 0 1px 0 rgba(255,255,255,0.3) inset,0 0 0 rgba(0,0,0,0);
-moz-box-shadow: 0 1px 0 rgba(255,255,255,0.3) inset,0 0 0 rgba(0,0,0,0);
box-shadow: 0 1px 0 rgba(255,255,255,0.3) inset,0 0 0 rgba(0,0,0,0);
-webkit-transition-property: background-color,0.15s;
-moz-transition-property: background-color,0.15s;
-ms-transition-property: background-color,0.15s;
-o-transition-property: background-color,0.15s;
transition-property: background-color,0.15s;
-webkit-transition-duration: box-shadow,0.15s;
-moz-transition-duration: box-shadow,0.15s;
-ms-transition-duration: box-shadow,0.15s;
-o-transition-duration: box-shadow,0.15s;
transition-duration: box-shadow,0.15s;
-webkit-transition-timing-function: ease-out;
-moz-transition-timing-function: ease-out;
-ms-transition-timing-function: ease-out;
-o-transition-timing-function: ease-out;
transition-timing-function: ease-out;
-webkit-transition-delay: 0;
-moz-transition-delay: 0;
-ms-transition-delay: 0;
-o-transition-delay: 0;
transition-delay: 0;
border: 1px solid #34854c;
border-radius: 3px;
background-color: rgba(255,255,255,0.3);
background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, rgba(255,255,255,0.3)),color-stop(100%, rgba(255,255,255,0)));
background-image: -webkit-linear-gradient(top, rgba(255,255,255,0.3),rgba(255,255,255,0));
background-image: -moz-linear-gradient(top, rgba(255,255,255,0.3),rgba(255,255,255,0));
background-image: -ms-linear-gradient(top, rgba(255,255,255,0.3),rgba(255,255,255,0));
background-image: -o-linear-gradient(top, rgba(255,255,255,0.3),rgba(255,255,255,0));
background-image: linear-gradient(top, rgba(255,255,255,0.3),rgba(255,255,255,0));
background-color: #25b85a;
-webkit-box-shadow: 0 1px 0 rgba(255,255,255,0.3) inset;
-moz-box-shadow: 0 1px 0 rgba(255,255,255,0.3) inset;
box-shadow: 0 1px 0 rgba(255,255,255,0.3) inset;
color: #fff;
text-align: center;
margin-top: 20px;
padding: 10px 20px;
}
.widget-layout .form-actions #private-discussion-opt {
float: none;
text-align: left;
margin: 0 0 15px 0;
}
.widget-layout .form-actions .btn-post_topic:hover, .widget-layout .form-actions .btn-post_topic:active {
background-color: #16ca57;
color: #fff;
}
\ No newline at end of file
// studio - elements - typography
// ====================
// headings/titles
.t-title-1, .t-title-2, .t-title-3, .t-title-4, .t-title-5, .t-title-5 {
color: $gray-d3;
}
.t-title-1 {
@include font-size(36);
}
.t-title-2 {
@include font-size(24);
font-weight: 600;
}
.t-title-3 {
@include font-size(16);
font-weight: 600;
}
.t-title-4 {
}
.t-title-5 {
}
// ====================
// copy
.t-copy-base {
@include font-size(16);
}
.t-copy-lead1 {
@include font-size(18);
}
.t-copy-lead2 {
@include font-size(20);
}
.t-copy-sub1 {
@include font-size(14);
}
.t-copy-sub2 {
@include font-size(13);
}
.t-copy-sub3 {
@include font-size(12);
}
// ====================
// actions/labels
.t-action1 {
@include font-size(14);
font-weight: 600;
}
.t-action2 {
@include font-size(13);
font-weight: 600;
text-transform: uppercase;
}
.t-action3 {
@include font-size(13);
}
.t-action4 {
@include font-size(12);
}
// ====================
// misc
.t-icon {
line-height: 0;
}
\ No newline at end of file
// studio - elements - JQUI calendar // studio - elements - vendor overrides
// ==================== // ====================
// JQUI calendar
.ui-datepicker { .ui-datepicker {
border-color: $darkGrey; border-color: $darkGrey;
border-radius: 2px; border-radius: 2px;
...@@ -8,6 +9,7 @@ ...@@ -8,6 +9,7 @@
font-family: $sans-serif; font-family: $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;
.ui-widget-header { .ui-widget-header {
background: $darkGrey; background: $darkGrey;
...@@ -53,4 +55,11 @@ ...@@ -53,4 +55,11 @@
border-color: $orange; border-color: $orange;
color: #fff; color: #fff;
} }
}
// ====================
// JQUI timepicker
.ui-timepicker-list {
z-index: 100000 !important;
} }
\ No newline at end of file
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
body.signup, body.signin { body.signup, body.signin {
.wrapper-content { .wrapper-content {
margin: 0; margin: ($baseline*1.5) 0 0 0;
padding: 0 $baseline; padding: 0 $baseline;
position: relative; position: relative;
width: 100%; width: 100%;
...@@ -18,7 +18,7 @@ body.signup, body.signin { ...@@ -18,7 +18,7 @@ body.signup, body.signin {
width: flex-grid(12); width: flex-grid(12);
margin: 0 auto; margin: 0 auto;
color: $gray-d2; color: $gray-d2;
header { header {
position: relative; position: relative;
margin-bottom: $baseline; margin-bottom: $baseline;
...@@ -71,7 +71,7 @@ body.signup, body.signin { ...@@ -71,7 +71,7 @@ body.signup, body.signin {
@include blue-button; @include blue-button;
@include transition(all .15s); @include transition(all .15s);
@include font-size(15); @include font-size(15);
display:block; display: block;
width: 100%; width: 100%;
padding: ($baseline*0.75) ($baseline/2); padding: ($baseline*0.75) ($baseline/2);
font-weight: 600; font-weight: 600;
...@@ -121,7 +121,7 @@ body.signup, body.signin { ...@@ -121,7 +121,7 @@ body.signup, body.signin {
@include font-size(16); @include font-size(16);
height: 100%; height: 100%;
width: 100%; width: 100%;
padding: ($baseline/2); padding: ($baseline/2);
&.long { &.long {
width: 100%; width: 100%;
...@@ -136,15 +136,15 @@ body.signup, body.signin { ...@@ -136,15 +136,15 @@ body.signup, body.signin {
} }
:-moz-placeholder { :-moz-placeholder {
color: $gray-l3; color: $gray-l3;
} }
::-moz-placeholder { ::-moz-placeholder {
color: $gray-l3; color: $gray-l3;
} }
:-ms-input-placeholder { :-ms-input-placeholder {
color: $gray-l3; color: $gray-l3;
} }
&:focus { &:focus {
......
...@@ -9,17 +9,6 @@ body.index { ...@@ -9,17 +9,6 @@ body.index {
margin-bottom: 0; margin-bottom: 0;
} }
.wrapper-footer {
margin: 0;
border-top: 2px solid $gray-l3;
footer.primary {
border: none;
margin-top: 0;
padding-top: 0;
}
}
.wrapper-content-header, .wrapper-content-features, .wrapper-content-cta { .wrapper-content-header, .wrapper-content-features, .wrapper-content-cta {
@include box-sizing(border-box); @include box-sizing(border-box);
margin: 0; margin: 0;
...@@ -199,7 +188,7 @@ body.index { ...@@ -199,7 +188,7 @@ body.index {
img { img {
display: block; display: block;
width: 100%; width: 100%;
height: 100%; height: auto;
} }
} }
...@@ -306,8 +295,8 @@ body.index { ...@@ -306,8 +295,8 @@ body.index {
// call to action content // call to action content
.wrapper-content-cta { .wrapper-content-cta {
padding-bottom: ($baseline*2); position: relative;
padding-top: ($baseline*2); padding: ($baseline*2) 0;
background: $white; background: $white;
} }
......
...@@ -26,7 +26,7 @@ body.course.outline { ...@@ -26,7 +26,7 @@ body.course.outline {
position: relative; position: relative;
top: -4px; top: -4px;
right: 50px; right: 50px;
width: 145px; width: 100px;
.status-label { .status-label {
position: absolute; position: absolute;
...@@ -62,7 +62,7 @@ body.course.outline { ...@@ -62,7 +62,7 @@ body.course.outline {
opacity: 0.0; opacity: 0.0;
position: absolute; position: absolute;
top: -1px; top: -1px;
left: 5px; right: 0;
margin: 0; margin: 0;
padding: 8px 12px; padding: 8px 12px;
background: $white; background: $white;
...@@ -160,7 +160,7 @@ body.course.outline { ...@@ -160,7 +160,7 @@ body.course.outline {
.section-published-date { .section-published-date {
position: absolute; position: absolute;
top: 19px; top: 19px;
right: 90px; right: 80px;
padding: 4px 10px; padding: 4px 10px;
border-radius: 3px; border-radius: 3px;
background: $lightGrey; background: $lightGrey;
...@@ -271,8 +271,6 @@ body.course.outline { ...@@ -271,8 +271,6 @@ body.course.outline {
.section-published-date { .section-published-date {
float: right; float: right;
width: 265px;
margin-right: 220px;
@include border-radius(3px); @include border-radius(3px);
background: $lightGrey; background: $lightGrey;
...@@ -606,13 +604,39 @@ body.course.outline { ...@@ -606,13 +604,39 @@ body.course.outline {
} }
.picker { .picker {
@include clearfix();
margin: 30px 0 65px; margin: 30px 0 65px;
.field {
float: left;
margin-right: ($baseline/2);
&:first-child {
margin-left: ($baseline*5);
}
&:last-child {
margin-right: 0;
}
label, input {
display: block;
text-align: left;
}
label {
@include font-size(14);
margin-bottom: ($baseline/4);
}
}
} }
.description { .description {
float: left;
margin-top: 30px; margin-top: 30px;
font-size: 14px; font-size: 14px;
line-height: 20px; line-height: 20px;
width: 100%;
} }
strong { strong {
......
...@@ -147,7 +147,7 @@ body.course.settings { ...@@ -147,7 +147,7 @@ body.course.settings {
} }
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;
font-weight: 400; font-weight: 400;
...@@ -161,7 +161,7 @@ body.course.settings { ...@@ -161,7 +161,7 @@ body.course.settings {
@include placeholder($gray-l4); @include placeholder($gray-l4);
@include font-size(16); @include font-size(16);
@include size(100%,100%); @include size(100%,100%);
padding: ($baseline/2); padding: ($baseline/2);
&.long { &.long {
} }
...@@ -212,7 +212,7 @@ body.course.settings { ...@@ -212,7 +212,7 @@ body.course.settings {
padding: $baseline; padding: $baseline;
&:last-child { &:last-child {
padding-bottom: $baseline; padding-bottom: $baseline;
} }
.actions { .actions {
...@@ -238,33 +238,36 @@ body.course.settings { ...@@ -238,33 +238,36 @@ body.course.settings {
} }
} }
// not editable fields
.field.is-not-editable {
& label.is-focused {
color: $gray-d2;
}
}
// field with error
.field.error {
input, textarea {
border-color: $red;
}
}
// specific fields - basic // specific fields - basic
&.basic { &.basic {
.list-input { .list-input {
@include clearfix(); @include clearfix();
padding: 0 ($baseline/2);
.field { .field {
margin-bottom: 0; margin-bottom: 0;
} }
} }
// course details that should appear more like content than elements to change
.field.is-not-editable {
label {
}
input, textarea {
@extend .t-copy-lead1;
@include box-shadow(none);
border: none;
background: none;
padding: 0;
margin: 0;
font-weight: 600;
}
}
#field-course-organization { #field-course-organization {
float: left; float: left;
width: flex-grid(2, 9); width: flex-grid(2, 9);
...@@ -281,6 +284,58 @@ body.course.settings { ...@@ -281,6 +284,58 @@ body.course.settings {
float: left; float: left;
width: flex-grid(5, 9); width: flex-grid(5, 9);
} }
// course link note
.note-promotion-courseURL {
@include box-shadow(0 2px 1px $shadow-l1);
@include border-radius(($baseline/5));
margin-top: ($baseline*1.5);
border: 1px solid $gray-l2;
padding: ($baseline/2) 0 0 0;
.title {
@extend .t-copy-sub1;
margin: 0 0 ($baseline/10) 0;
padding: 0 ($baseline/2);
.tip {
display: inline;
margin-left: ($baseline/4);
}
}
.copy {
padding: 0 ($baseline/2) ($baseline/2) ($baseline/2);
.link-courseURL {
@extend .t-copy-lead1;
&:hover {
}
}
}
.list-actions {
@include box-shadow(inset 0 1px 1px $shadow-l1);
border-top: 1px solid $gray-l2;
padding: ($baseline/2);
background: $gray-l5;
.action-primary {
@include blue-button();
@include font-size(13);
font-weight: 600;
.icon {
@extend .t-icon;
@include font-size(16);
display: inline-block;
vertical-align: middle;
}
}
}
}
} }
// specific fields - schedule // specific fields - schedule
...@@ -322,7 +377,7 @@ body.course.settings { ...@@ -322,7 +377,7 @@ body.course.settings {
} }
} }
} }
// specific fields - overview // specific fields - overview
#field-course-overview { #field-course-overview {
...@@ -468,7 +523,7 @@ body.course.settings { ...@@ -468,7 +523,7 @@ body.course.settings {
} }
} }
} }
.grade-specific-bar { .grade-specific-bar {
height: 50px !important; height: 50px !important;
} }
...@@ -479,7 +534,7 @@ body.course.settings { ...@@ -479,7 +534,7 @@ body.course.settings {
li { li {
position: absolute; position: absolute;
top: 0; top: 0;
height: 50px; height: 50px;
text-align: right; text-align: right;
@include border-radius(2px); @include border-radius(2px);
...@@ -600,8 +655,8 @@ body.course.settings { ...@@ -600,8 +655,8 @@ body.course.settings {
} }
#field-course-grading-assignment-shortname, #field-course-grading-assignment-shortname,
#field-course-grading-assignment-totalassignments, #field-course-grading-assignment-totalassignments,
#field-course-grading-assignment-gradeweight, #field-course-grading-assignment-gradeweight,
#field-course-grading-assignment-droppable { #field-course-grading-assignment-droppable {
width: flex-grid(2, 6); width: flex-grid(2, 6);
} }
...@@ -734,4 +789,4 @@ body.course.settings { ...@@ -734,4 +789,4 @@ body.course.settings {
.content-supplementary { .content-supplementary {
width: flex-grid(3, 12); width: flex-grid(3, 12);
} }
} }
\ No newline at end of file
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
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