Merge branch 'master' of github.com:MITx/mitx
This commit is contained in:
25
.pylintrc
25
.pylintrc
@@ -34,15 +34,23 @@ load-plugins=
|
||||
# multiple time (only on the command line, not in the configuration file where
|
||||
# it should appear only once).
|
||||
disable=
|
||||
# W0141: Used builtin function 'map'
|
||||
# Never going to use these
|
||||
# C0301: Line too long
|
||||
# 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
|
||||
# R0901: Too many ancestors
|
||||
# R0902: Too many instance attributes
|
||||
# R0903: Too few public methods (1/2)
|
||||
# R0904: Too many public methods
|
||||
# R0911: Too many return statements
|
||||
# R0912: Too many branches
|
||||
# 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]
|
||||
@@ -91,7 +99,18 @@ zope=no
|
||||
# List of members which are set dynamically and missed by pylint inference
|
||||
# system, and so shouldn't trigger E0201 when accessed. Python regular
|
||||
# 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]
|
||||
|
||||
@@ -22,5 +22,4 @@ libreadline6
|
||||
libreadline6-dev
|
||||
mongodb
|
||||
nodejs
|
||||
npm
|
||||
coffeescript
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
import logging
|
||||
import sys
|
||||
|
||||
from django.contrib.auth.models import User, Group
|
||||
from django.core.exceptions import PermissionDenied
|
||||
|
||||
@@ -131,7 +128,7 @@ def remove_user_from_course_group(caller, user, location, role):
|
||||
raise PermissionDenied
|
||||
|
||||
# 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)
|
||||
|
||||
group = Group.objects.get(name=groupname)
|
||||
|
||||
@@ -97,8 +97,7 @@ def update_course_updates(location, update, passed_id=None):
|
||||
if (len(new_html_parsed) == 1):
|
||||
content = new_html_parsed[0].tail
|
||||
else:
|
||||
content = "\n".join([html.tostring(ele)
|
||||
for ele in new_html_parsed[1:]])
|
||||
content = "\n".join([html.tostring(ele) for ele in new_html_parsed[1:]])
|
||||
|
||||
return {"id": passed_id,
|
||||
"date": update['date'],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
Feature: Advanced (manual) course policy
|
||||
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
|
||||
Given I have opened a new course in Studio
|
||||
@@ -11,6 +11,7 @@ Feature: Advanced (manual) course policy
|
||||
Given I am on the Advanced Course Settings page in Studio
|
||||
Then the settings are alphabetized
|
||||
|
||||
@skip-phantom
|
||||
Scenario: Test cancel editing key value
|
||||
Given I am on the Advanced Course Settings page in Studio
|
||||
When I edit the value of a policy key
|
||||
@@ -19,14 +20,15 @@ Feature: Advanced (manual) course policy
|
||||
And I reload the page
|
||||
Then the policy key value is unchanged
|
||||
|
||||
@skip-phantom
|
||||
Scenario: Test editing key value
|
||||
Given I am on the Advanced Course Settings page in Studio
|
||||
When I edit the value of a policy key
|
||||
And I press the "Save" notification button
|
||||
When I edit the value of a policy key and save
|
||||
Then the policy key value is changed
|
||||
And I reload the page
|
||||
Then the policy key value is changed
|
||||
|
||||
@skip-phantom
|
||||
Scenario: Test how multi-line input appears
|
||||
Given I am on the Advanced Course Settings page in Studio
|
||||
When I create a JSON object as a value
|
||||
@@ -34,6 +36,7 @@ Feature: Advanced (manual) course policy
|
||||
And I reload the page
|
||||
Then it is displayed as formatted
|
||||
|
||||
@skip-phantom
|
||||
Scenario: Test automatic quoting of non-JSON values
|
||||
Given I am on the Advanced Course Settings page in Studio
|
||||
When I create a non-JSON value not in quotes
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
#pylint: disable=C0111
|
||||
#pylint: disable=W0621
|
||||
|
||||
from lettuce import world, step
|
||||
from common import *
|
||||
import time
|
||||
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
|
||||
from nose.tools import assert_false, assert_equal
|
||||
|
||||
"""
|
||||
http://selenium.googlecode.com/svn/trunk/docs/api/py/webdriver/selenium.webdriver.common.keys.html
|
||||
@@ -17,14 +15,15 @@ VALUE_CSS = 'textarea.json'
|
||||
DISPLAY_NAME_KEY = "display_name"
|
||||
DISPLAY_NAME_VALUE = '"Robot Super Course"'
|
||||
|
||||
|
||||
############### ACTIONS ####################
|
||||
@step('I select the Advanced Settings$')
|
||||
def i_select_advanced_settings(step):
|
||||
expand_icon_css = 'li.nav-course-settings i.icon-expand'
|
||||
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'
|
||||
css_click(link_css)
|
||||
world.css_click(link_css)
|
||||
|
||||
|
||||
@step('I am on the Advanced Course Settings page in Studio$')
|
||||
@@ -35,24 +34,8 @@ def i_am_on_advanced_course_settings(step):
|
||||
|
||||
@step(u'I press the "([^"]*)" notification button$')
|
||||
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()
|
||||
wait_for(is_visible)
|
||||
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)
|
||||
world.css_click(css)
|
||||
|
||||
|
||||
@step(u'I edit the value of a policy key$')
|
||||
@@ -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
|
||||
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')
|
||||
|
||||
|
||||
@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$')
|
||||
def create_JSON_object(step):
|
||||
change_display_name_value(step, '{"key": "value", "key_2": "value_2"}')
|
||||
@@ -85,7 +73,7 @@ def i_see_default_advanced_settings(step):
|
||||
|
||||
@step('the settings are alphabetized$')
|
||||
def they_are_alphabetized(step):
|
||||
key_elements = css_find(KEY_CSS)
|
||||
key_elements = world.css_find(KEY_CSS)
|
||||
all_keys = []
|
||||
for key in key_elements:
|
||||
all_keys.append(key.value)
|
||||
@@ -99,7 +87,7 @@ def it_is_formatted(step):
|
||||
|
||||
|
||||
@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"'])
|
||||
|
||||
|
||||
@@ -110,7 +98,7 @@ def the_policy_key_value_is_unchanged(step):
|
||||
|
||||
@step(u'the policy key value is changed$')
|
||||
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 ###############
|
||||
@@ -118,13 +106,13 @@ def assert_policy_entries(expected_keys, expected_values):
|
||||
for counter in range(len(expected_keys)):
|
||||
index = get_index_of(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):
|
||||
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
|
||||
key = css_find(KEY_CSS)[counter].value
|
||||
key = world.css_find(KEY_CSS)[counter].value
|
||||
if key == expected_key:
|
||||
return counter
|
||||
|
||||
@@ -133,14 +121,14 @@ def get_index_of(expected_key):
|
||||
|
||||
def get_display_name_value():
|
||||
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):
|
||||
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()
|
||||
for count in range(len(display_name)):
|
||||
e._element.send_keys(Keys.TAB, Keys.END, Keys.BACK_SPACE)
|
||||
# Must delete "" before typing the JSON 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")
|
||||
|
||||
@@ -10,6 +10,8 @@ Feature: Course checklists
|
||||
Then I can check and uncheck tasks in a checklist
|
||||
And They are correctly selected after I reload the page
|
||||
|
||||
@skip-phantom
|
||||
@skip-firefox
|
||||
Scenario: A task can link to a location within Studio
|
||||
Given I have opened Checklists
|
||||
When I select a link to the course outline
|
||||
@@ -17,8 +19,9 @@ Feature: Course checklists
|
||||
And I press the browser back button
|
||||
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
|
||||
Given I have opened Checklists
|
||||
When I select a link to help page
|
||||
Then I am brought to the help page in a new window
|
||||
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
#pylint: disable=C0111
|
||||
#pylint: disable=W0621
|
||||
|
||||
from lettuce import world, step
|
||||
from common import *
|
||||
from nose.tools import assert_true, assert_equal
|
||||
from terrain.steps import reload_the_page
|
||||
from selenium.common.exceptions import StaleElementReferenceException
|
||||
|
||||
|
||||
############### ACTIONS ####################
|
||||
@step('I select Checklists from the Tools menu$')
|
||||
def i_select_checklists(step):
|
||||
expand_icon_css = 'li.nav-course-tools i.icon-expand'
|
||||
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'
|
||||
css_click(link_css)
|
||||
world.css_click(link_css)
|
||||
|
||||
|
||||
@step('I have opened Checklists$')
|
||||
@@ -20,7 +25,7 @@ def i_have_opened_checklists(step):
|
||||
|
||||
@step('I see the four default edX checklists$')
|
||||
def i_see_default_checklists(step):
|
||||
checklists = css_find('.checklist-title')
|
||||
checklists = world.css_find('.checklist-title')
|
||||
assert_equal(4, len(checklists))
|
||||
assert_true(checklists[0].text.endswith('Getting Started With Studio'))
|
||||
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):
|
||||
|
||||
@step('I am brought to the course outline page$')
|
||||
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))
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
|
||||
############### HELPER METHODS ####################
|
||||
def verifyChecklist2Status(completed, total, percentage):
|
||||
def verify_count(driver):
|
||||
try:
|
||||
statusCount = css_find('#course-checklist1 .status-count').first
|
||||
statusCount = world.css_find('#course-checklist1 .status-count').first
|
||||
return statusCount.text == str(completed)
|
||||
except StaleElementReferenceException:
|
||||
return False
|
||||
|
||||
wait_for(verify_count)
|
||||
assert_equal(str(total), css_find('#course-checklist1 .status-amount').first.text)
|
||||
world.wait_for(verify_count)
|
||||
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.
|
||||
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):
|
||||
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):
|
||||
# toggle checklist item to make sure that the link button is showing
|
||||
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
|
||||
def verify_action_link_text(driver):
|
||||
return action_link.text == actionText
|
||||
|
||||
wait_for(verify_action_link_text)
|
||||
world.wait_for(verify_action_link_text)
|
||||
action_link.click()
|
||||
|
||||
|
||||
@@ -1,28 +1,30 @@
|
||||
#pylint: disable=C0111
|
||||
#pylint: disable=W0621
|
||||
|
||||
from lettuce import world, step
|
||||
from lettuce.django import django_url
|
||||
from nose.tools import assert_true
|
||||
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.templates import update_templates
|
||||
from auth.authz import get_user_by_email
|
||||
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
import time
|
||||
|
||||
from logging import getLogger
|
||||
logger = getLogger(__name__)
|
||||
|
||||
########### STEP HELPERS ##############
|
||||
|
||||
@step('I (?:visit|access|open) the Studio homepage$')
|
||||
def i_visit_the_studio_homepage(step):
|
||||
# To make this go to port 8001, put
|
||||
# LETTUCE_SERVER_PORT = 8001
|
||||
# in your settings.py file.
|
||||
world.browser.visit(django_url('/'))
|
||||
world.visit('/')
|
||||
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$')
|
||||
@@ -43,12 +45,12 @@ def i_press_the_category_delete_icon(step, category):
|
||||
css = 'a.delete-button.delete-subsection-button span.delete-icon'
|
||||
else:
|
||||
assert False, 'Invalid category: %s' % category
|
||||
css_click(css)
|
||||
world.css_click(css)
|
||||
|
||||
|
||||
@step('I have opened a new course in Studio$')
|
||||
def i_have_opened_a_new_course(step):
|
||||
clear_courses()
|
||||
world.clear_courses()
|
||||
log_into_studio()
|
||||
create_a_course()
|
||||
|
||||
@@ -74,80 +76,13 @@ def create_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(
|
||||
name='Robot Super Course',
|
||||
org='MITx',
|
||||
num='101'):
|
||||
css_fill('.new-course-name', name)
|
||||
css_fill('.new-course-org', org)
|
||||
css_fill('.new-course-number', num)
|
||||
world.css_fill('.new-course-name', name)
|
||||
world.css_fill('.new-course-org', org)
|
||||
world.css_fill('.new-course-number', num)
|
||||
|
||||
|
||||
def log_into_studio(
|
||||
@@ -155,21 +90,22 @@ def log_into_studio(
|
||||
email='robot+studio@edx.org',
|
||||
password='test',
|
||||
is_staff=False):
|
||||
create_studio_user(uname=uname, email=email, is_staff=is_staff)
|
||||
world.browser.cookies.delete()
|
||||
world.browser.visit(django_url('/'))
|
||||
signin_css = 'a.action-signin'
|
||||
world.browser.is_element_present_by_css(signin_css, 10)
|
||||
|
||||
# click the signin button
|
||||
css_click(signin_css)
|
||||
create_studio_user(uname=uname, email=email, is_staff=is_staff)
|
||||
|
||||
world.browser.cookies.delete()
|
||||
world.visit('/')
|
||||
|
||||
signin_css = 'a.action-signin'
|
||||
world.is_css_present(signin_css)
|
||||
world.css_click(signin_css)
|
||||
|
||||
login_form = world.browser.find_by_css('form#login_form')
|
||||
login_form.find_by_name('email').fill(email)
|
||||
login_form.find_by_name('password').fill(password)
|
||||
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():
|
||||
@@ -184,26 +120,37 @@ def create_a_course():
|
||||
world.browser.reload()
|
||||
|
||||
course_link_css = 'span.class-name'
|
||||
css_click(course_link_css)
|
||||
world.css_click(course_link_css)
|
||||
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'):
|
||||
link_css = 'a.new-courseware-section-button'
|
||||
css_click(link_css)
|
||||
world.css_click(link_css)
|
||||
name_css = 'input.new-section-name'
|
||||
save_css = 'input.new-section-name-save'
|
||||
css_fill(name_css, name)
|
||||
css_click(save_css)
|
||||
world.css_fill(name_css, name)
|
||||
world.css_click(save_css)
|
||||
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'):
|
||||
css = 'a.new-subsection-item'
|
||||
css_click(css)
|
||||
world.css_click(css)
|
||||
name_css = 'input.new-subsection-name-input'
|
||||
save_css = 'input.new-subsection-name-save'
|
||||
css_fill(name_css, name)
|
||||
css_click(save_css)
|
||||
world.css_fill(name_css, name)
|
||||
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))
|
||||
|
||||
28
cms/djangoapps/contentstore/features/course-settings.feature
Normal file
28
cms/djangoapps/contentstore/features/course-settings.feature
Normal file
@@ -0,0 +1,28 @@
|
||||
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
|
||||
165
cms/djangoapps/contentstore/features/course-settings.py
Normal file
165
cms/djangoapps/contentstore/features/course-settings.py
Normal file
@@ -0,0 +1,165 @@
|
||||
#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
|
||||
And I fill in the new course information
|
||||
And I press the "Save" button
|
||||
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
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
#pylint: disable=C0111
|
||||
#pylint: disable=W0621
|
||||
|
||||
from lettuce import world, step
|
||||
from common import *
|
||||
|
||||
@@ -6,12 +9,12 @@ from common import *
|
||||
|
||||
@step('There are no courses$')
|
||||
def no_courses(step):
|
||||
clear_courses()
|
||||
world.clear_courses()
|
||||
|
||||
|
||||
@step('I click the New Course button$')
|
||||
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$')
|
||||
@@ -27,7 +30,7 @@ def i_create_a_course(step):
|
||||
@step('I click the course link in My Courses$')
|
||||
def i_click_the_course_link_in_my_courses(step):
|
||||
course_css = 'span.class-name'
|
||||
css_click(course_css)
|
||||
world.css_click(course_css)
|
||||
|
||||
############ ASSERTIONS ###################
|
||||
|
||||
@@ -35,28 +38,28 @@ def i_click_the_course_link_in_my_courses(step):
|
||||
@step('the Courseware page has loaded in Studio$')
|
||||
def courseware_page_has_loaded_in_studio(step):
|
||||
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$')
|
||||
def i_see_the_course_in_my_courses(step):
|
||||
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$')
|
||||
def course_is_loaded(step):
|
||||
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$')
|
||||
def i_am_on_tab(step, tab_name):
|
||||
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$')
|
||||
def i_see_new_section_link(step):
|
||||
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
|
||||
As a course author
|
||||
I want to create and edit sections
|
||||
|
||||
@skip-phantom
|
||||
Scenario: Add a new section to a course
|
||||
Given I have opened a new course in Studio
|
||||
When I click the New Section link
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
#pylint: disable=C0111
|
||||
#pylint: disable=W0621
|
||||
|
||||
from lettuce import world, step
|
||||
from common import *
|
||||
from nose.tools import assert_equal
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
import time
|
||||
|
||||
############### ACTIONS ####################
|
||||
|
||||
@@ -10,7 +11,7 @@ import time
|
||||
@step('I click the new section link$')
|
||||
def i_click_new_section_link(step):
|
||||
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$')
|
||||
@@ -31,21 +32,13 @@ def i_have_added_new_section(step):
|
||||
@step('I click the Edit link for the release date$')
|
||||
def i_click_the_edit_link_for_the_release_date(step):
|
||||
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$')
|
||||
def i_save_a_new_section_release_date(step):
|
||||
date_css = 'input.start-date.date.hasDatepicker'
|
||||
time_css = 'input.start-time.time.ui-timepicker-input'
|
||||
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))
|
||||
set_date_and_time('input.start-date.date.hasDatepicker', '12/25/2013',
|
||||
'input.start-time.time.ui-timepicker-input', '00:00')
|
||||
world.browser.click_link_by_text('Save')
|
||||
|
||||
|
||||
@@ -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$')
|
||||
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$')
|
||||
def i_see_complete_section_name_with_quote_in_editor(step):
|
||||
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"')
|
||||
|
||||
|
||||
@@ -85,7 +78,7 @@ def i_see_a_release_date_for_my_section(step):
|
||||
import re
|
||||
|
||||
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
|
||||
|
||||
# e.g. 11/06/2012 at 16:25
|
||||
@@ -99,20 +92,20 @@ def i_see_a_release_date_for_my_section(step):
|
||||
@step('I see a link to create a new subsection$')
|
||||
def i_see_a_link_to_create_a_new_subsection(step):
|
||||
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$')
|
||||
def the_section_release_date_picker_not_visible(step):
|
||||
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$')
|
||||
def the_section_release_date_is_updated(step):
|
||||
css = 'span.published-status'
|
||||
status_text = world.browser.find_by_css(css).text
|
||||
assert_equal(status_text,'Will Release: 12/25/2013 at 12:00am')
|
||||
status_text = world.css_text(css)
|
||||
assert_equal(status_text, 'Will Release: 12/25/2013 at 00:00 UTC')
|
||||
|
||||
|
||||
############ HELPER METHODS ###################
|
||||
@@ -120,10 +113,10 @@ def the_section_release_date_is_updated(step):
|
||||
def save_section_name(name):
|
||||
name_css = '.new-section-name'
|
||||
save_css = '.new-section-name-save'
|
||||
css_fill(name_css, name)
|
||||
css_click(save_css)
|
||||
world.css_fill(name_css, name)
|
||||
world.css_click(save_css)
|
||||
|
||||
|
||||
def see_my_section_on_the_courseware_page(name):
|
||||
section_css = 'span.section-name-span'
|
||||
assert_css_with_text(section_css, name)
|
||||
assert world.css_has_text(section_css, name)
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
#pylint: disable=C0111
|
||||
#pylint: disable=W0621
|
||||
|
||||
from lettuce import world, step
|
||||
from common import *
|
||||
|
||||
@@ -17,9 +20,10 @@ def i_press_the_button_on_the_registration_form(step):
|
||||
submit_css = 'form#register_form button#submit'
|
||||
# Workaround for click not working on ubuntu
|
||||
# for some unknown reason.
|
||||
e = css_find(submit_css)
|
||||
e = world.css_find(submit_css)
|
||||
e.type(' ')
|
||||
|
||||
|
||||
@step('I should see be on the studio home page$')
|
||||
def i_should_see_be_on_the_studio_home_page(step):
|
||||
assert world.browser.find_by_css('div.inner-wrapper')
|
||||
|
||||
@@ -1,32 +1,33 @@
|
||||
Feature: Overview Toggle Section
|
||||
In order to quickly view the details of a course's section or to scan the inventory of sections
|
||||
As a course author
|
||||
I want to toggle the visibility of each section's subsection details in the overview listing
|
||||
As a course author
|
||||
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
|
||||
Given I have a course with multiple sections
|
||||
When I navigate to the course overview page
|
||||
Then I see the "Collapse All Sections" link
|
||||
And all sections are expanded
|
||||
When I navigate to the course overview page
|
||||
Then I see the "Collapse All Sections" link
|
||||
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
|
||||
When I navigate to the course overview page
|
||||
Then I do not see the "Collapse All Sections" link
|
||||
When I navigate to the course overview page
|
||||
Then I do not see the "Collapse All Sections" link
|
||||
|
||||
@skip-phantom
|
||||
Scenario: Collapse link appears after creating first section of a course
|
||||
Given I have a course with no sections
|
||||
When I navigate to the course overview page
|
||||
And I add a section
|
||||
Then I see the "Collapse All Sections" link
|
||||
And all sections are expanded
|
||||
When I navigate to the course overview page
|
||||
And I add a section
|
||||
Then I see the "Collapse All Sections" link
|
||||
And all sections are expanded
|
||||
|
||||
@skip-phantom
|
||||
Scenario: Collapse link is not removed after last section of a course is deleted
|
||||
Given I have a course with 1 section
|
||||
And I navigate to the course overview page
|
||||
When I press the "section" delete icon
|
||||
And I confirm the alert
|
||||
And I navigate to the course overview page
|
||||
When I press the "section" delete icon
|
||||
And I confirm the alert
|
||||
Then I see the "Collapse All Sections" link
|
||||
|
||||
Scenario: Collapsing all sections when all sections are expanded
|
||||
@@ -57,4 +58,4 @@ Feature: Overview Toggle Section
|
||||
When I expand the first section
|
||||
And I click the "Expand All Sections" link
|
||||
Then I see the "Collapse All Sections" link
|
||||
And all sections are expanded
|
||||
And all sections are expanded
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
#pylint: disable=C0111
|
||||
#pylint: disable=W0621
|
||||
|
||||
from lettuce import world, step
|
||||
from common import *
|
||||
from nose.tools import assert_true, assert_false, assert_equal
|
||||
@@ -8,13 +11,13 @@ logger = getLogger(__name__)
|
||||
|
||||
@step(u'I have a course with no sections$')
|
||||
def have_a_course(step):
|
||||
clear_courses()
|
||||
world.clear_courses()
|
||||
course = world.CourseFactory.create()
|
||||
|
||||
|
||||
@step(u'I have a course with 1 section$')
|
||||
def have_a_course_with_1_section(step):
|
||||
clear_courses()
|
||||
world.clear_courses()
|
||||
course = world.CourseFactory.create()
|
||||
section = world.ItemFactory.create(parent_location=course.location)
|
||||
subsection1 = world.ItemFactory.create(
|
||||
@@ -25,7 +28,7 @@ def have_a_course_with_1_section(step):
|
||||
|
||||
@step(u'I have a course with multiple sections$')
|
||||
def have_a_course_with_two_sections(step):
|
||||
clear_courses()
|
||||
world.clear_courses()
|
||||
course = world.CourseFactory.create()
|
||||
section = world.ItemFactory.create(parent_location=course.location)
|
||||
subsection1 = world.ItemFactory.create(
|
||||
@@ -49,7 +52,7 @@ def have_a_course_with_two_sections(step):
|
||||
def navigate_to_the_course_overview_page(step):
|
||||
log_into_studio(is_staff=True)
|
||||
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')
|
||||
@@ -66,44 +69,44 @@ def i_add_a_section(step):
|
||||
@step(u'I click the "([^"]*)" link$')
|
||||
def i_click_the_text_span(step, text):
|
||||
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
|
||||
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$')
|
||||
def i_collapse_a_section(step):
|
||||
collapse_locator = 'section.courseware-section a.collapse'
|
||||
css_click(collapse_locator)
|
||||
world.css_click(collapse_locator)
|
||||
|
||||
|
||||
@step(u'I expand the first section$')
|
||||
def i_expand_a_section(step):
|
||||
expand_locator = 'section.courseware-section a.expand'
|
||||
css_click(expand_locator)
|
||||
world.css_click(expand_locator)
|
||||
|
||||
|
||||
@step(u'I see the "([^"]*)" link$')
|
||||
def i_see_the_span_with_text(step, text):
|
||||
span_locator = '.toggle-button-sections span'
|
||||
assert_true(world.browser.is_element_present_by_css(span_locator, 5))
|
||||
assert_equal(world.browser.find_by_css(span_locator).value, text)
|
||||
assert_true(world.browser.find_by_css(span_locator).visible)
|
||||
assert_true(world.is_css_present(span_locator))
|
||||
assert_equal(world.css_find(span_locator).value, text)
|
||||
assert_true(world.css_visible(span_locator))
|
||||
|
||||
|
||||
@step(u'I do not see the "([^"]*)" link$')
|
||||
def i_do_not_see_the_span_with_text(step, text):
|
||||
# Note that the span will exist on the page but not be visible
|
||||
span_locator = '.toggle-button-sections span'
|
||||
assert_true(world.browser.is_element_present_by_css(span_locator))
|
||||
assert_false(world.browser.find_by_css(span_locator).visible)
|
||||
assert_true(world.is_css_present(span_locator))
|
||||
assert_false(world.css_visible(span_locator))
|
||||
|
||||
|
||||
@step(u'all sections are expanded$')
|
||||
def all_sections_are_expanded(step):
|
||||
subsection_locator = 'div.subsection-list'
|
||||
subsections = world.browser.find_by_css(subsection_locator)
|
||||
subsections = world.css_find(subsection_locator)
|
||||
for s in subsections:
|
||||
assert_true(s.visible)
|
||||
|
||||
@@ -111,6 +114,6 @@ def all_sections_are_expanded(step):
|
||||
@step(u'all sections are collapsed$')
|
||||
def all_sections_are_expanded(step):
|
||||
subsection_locator = 'div.subsection-list'
|
||||
subsections = world.browser.find_by_css(subsection_locator)
|
||||
subsections = world.css_find(subsection_locator)
|
||||
for s in subsections:
|
||||
assert_false(s.visible)
|
||||
|
||||
@@ -3,13 +3,15 @@ Feature: Create Subsection
|
||||
As a course author
|
||||
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
|
||||
When I click the New Subsection link
|
||||
And I enter the subsection name and click save
|
||||
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
|
||||
When I click the New Subsection link
|
||||
And I enter a subsection name with a quote and click save
|
||||
@@ -17,8 +19,24 @@ Feature: Create Subsection
|
||||
And I click to edit the subsection name
|
||||
Then I see the complete subsection name with a quote in the editor
|
||||
|
||||
@skip-phantom
|
||||
Scenario: Delete a subsection
|
||||
Scenario: Assign grading type to a subsection and verify it is still shown after refresh (bug #258)
|
||||
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
|
||||
And I have added a new subsection
|
||||
And I see my subsection on the Courseware page
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
#pylint: disable=C0111
|
||||
#pylint: disable=W0621
|
||||
|
||||
from lettuce import world, step
|
||||
from common import *
|
||||
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$')
|
||||
def i_have_opened_a_new_course_section(step):
|
||||
clear_courses()
|
||||
world.clear_courses()
|
||||
log_into_studio()
|
||||
create_a_course()
|
||||
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')
|
||||
def i_click_the_new_subsection_link(step):
|
||||
css = 'a.new-subsection-item'
|
||||
css_click(css)
|
||||
world.css_click('a.new-subsection-item')
|
||||
|
||||
|
||||
@step('I enter the subsection name and click save$')
|
||||
@@ -31,19 +45,41 @@ def i_save_subsection_name_with_quote(step):
|
||||
|
||||
@step('I click to edit the subsection name$')
|
||||
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$')
|
||||
def i_see_complete_subsection_name_with_quote_in_editor(step):
|
||||
css = '.subsection-display-name-input'
|
||||
assert world.browser.is_element_present_by_css(css, 5)
|
||||
assert_equal(world.browser.find_by_css(css).value, 'Subsection With "Quote"')
|
||||
assert world.is_css_present(css)
|
||||
assert_equal(world.css_find(css).value, 'Subsection With "Quote"')
|
||||
|
||||
|
||||
@step('I have added a new subsection$')
|
||||
def i_have_added_a_new_subsection(step):
|
||||
add_subsection()
|
||||
@step('I have set a release date and due date in different years$')
|
||||
def test_have_set_dates_in_different_years(step):
|
||||
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 ###################
|
||||
@@ -70,11 +106,12 @@ def the_subsection_does_not_exist(step):
|
||||
def save_subsection_name(name):
|
||||
name_css = 'input.new-subsection-name-input'
|
||||
save_css = 'input.new-subsection-name-save'
|
||||
css_fill(name_css, name)
|
||||
css_click(save_css)
|
||||
world.css_fill(name_css, name)
|
||||
world.css_click(save_css)
|
||||
|
||||
|
||||
def see_subsection_name(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'
|
||||
assert_css_with_text(css, name)
|
||||
assert world.css_has_text(css, name)
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
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
|
||||
from xmodule.modulestore.store_utilities import clone_course
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.contentstore.django import contentstore
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.course_module import CourseDescriptor
|
||||
|
||||
from auth.authz import _copy_course_group
|
||||
@@ -16,8 +15,7 @@ from auth.authz import _copy_course_group
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = \
|
||||
'''Clone a MongoDB backed course to another location'''
|
||||
help = 'Clone a MongoDB backed course to another location'
|
||||
|
||||
def handle(self, *args, **options):
|
||||
if len(args) != 2:
|
||||
|
||||
@@ -5,7 +5,6 @@ from django.core.management.base import BaseCommand, CommandError
|
||||
from xmodule.modulestore.store_utilities import delete_course
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.contentstore.django import contentstore
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.course_module import CourseDescriptor
|
||||
from .prompt import query_yes_no
|
||||
|
||||
@@ -38,7 +37,7 @@ class Command(BaseCommand):
|
||||
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"):
|
||||
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....'
|
||||
# in the django layer, we need to remove all the user permissions groups associated with this course
|
||||
if commit:
|
||||
|
||||
@@ -7,7 +7,6 @@ from django.core.management.base import BaseCommand, CommandError
|
||||
from xmodule.modulestore.xml_exporter import export_to_xml
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.contentstore.django import contentstore
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.course_module import CourseDescriptor
|
||||
|
||||
|
||||
@@ -15,8 +14,7 @@ unnamed_modules = 0
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = \
|
||||
'''Import the specified data directory into the default ModuleStore'''
|
||||
help = 'Import the specified data directory into the default ModuleStore'
|
||||
|
||||
def handle(self, *args, **options):
|
||||
if len(args) != 2:
|
||||
|
||||
@@ -12,8 +12,7 @@ unnamed_modules = 0
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = \
|
||||
'''Import the specified data directory into the default ModuleStore'''
|
||||
help = 'Import the specified data directory into the default ModuleStore'
|
||||
|
||||
def handle(self, *args, **options):
|
||||
if len(args) == 0:
|
||||
@@ -28,4 +27,4 @@ class Command(BaseCommand):
|
||||
data=data_dir,
|
||||
courses=course_dirs)
|
||||
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"):
|
||||
|
||||
The "answer" return value is one of "yes" or "no".
|
||||
"""
|
||||
valid = {"yes":True, "y":True, "ye":True,
|
||||
"no":False, "n":False}
|
||||
valid = {"yes": True, "y": True, "ye": True,
|
||||
"no": False, "n": False}
|
||||
if default is None:
|
||||
prompt = " [y/n] "
|
||||
elif default == "yes":
|
||||
@@ -30,5 +30,4 @@ def query_yes_no(question, default="yes"):
|
||||
elif choice in valid:
|
||||
return valid[choice]
|
||||
else:
|
||||
sys.stdout.write("Please respond with 'yes' or 'no' "\
|
||||
"(or 'y' or 'n').\n")
|
||||
sys.stdout.write("Please respond with 'yes' or 'no' (or 'y' or 'n').\n")
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
from xmodule.templates import update_templates
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = \
|
||||
'''Imports and updates the Studio component templates from the code pack and put in the DB'''
|
||||
help = 'Imports and updates the Studio component templates from the code pack and put in the DB'
|
||||
|
||||
def handle(self, *args, **options):
|
||||
update_templates()
|
||||
update_templates()
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from xmodule.modulestore.xml_importer import perform_xlint
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.contentstore.django import contentstore
|
||||
|
||||
|
||||
unnamed_modules = 0
|
||||
@@ -9,10 +7,11 @@ unnamed_modules = 0
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = \
|
||||
'''
|
||||
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)]
|
||||
'''
|
||||
'''
|
||||
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)]
|
||||
'''
|
||||
|
||||
def handle(self, *args, **options):
|
||||
if len(args) == 0:
|
||||
raise CommandError("import requires at least one argument: <data directory> [<course dir>...]")
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from static_replace import replace_static_urls
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
from xmodule.modulestore import Location
|
||||
from django.http import Http404
|
||||
|
||||
|
||||
def get_module_info(store, location, parent_location=None, rewrite_static_links=False):
|
||||
|
||||
@@ -6,16 +6,17 @@ from django.conf import settings
|
||||
from django.core.urlresolvers import reverse
|
||||
from path import path
|
||||
from tempdir import mkdtemp_clean
|
||||
from datetime import timedelta
|
||||
import json
|
||||
from fs.osfs import OSFS
|
||||
import copy
|
||||
from json import loads
|
||||
from datetime import timedelta
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.dispatch import Signal
|
||||
from contentstore.utils import get_modulestore
|
||||
from contentstore.tests.utils import parse_json
|
||||
|
||||
from .utils import ModuleStoreTestCase, parse_json
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
|
||||
|
||||
from xmodule.modulestore import Location
|
||||
@@ -25,7 +26,7 @@ from xmodule.modulestore.django import modulestore
|
||||
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.modulestore.xml_importer import import_from_xml, perform_xlint
|
||||
from xmodule.modulestore.inheritance import own_metadata
|
||||
|
||||
from xmodule.capa_module import CapaDescriptor
|
||||
@@ -38,6 +39,16 @@ TEST_DATA_MODULESTORE['default']['OPTIONS']['fs_root'] = path('common/test/data'
|
||||
TEST_DATA_MODULESTORE['direct']['OPTIONS']['fs_root'] = path('common/test/data')
|
||||
|
||||
|
||||
class MongoCollectionFindWrapper(object):
|
||||
def __init__(self, original):
|
||||
self.original = original
|
||||
self.counter = 0
|
||||
|
||||
def find(self, query, *args, **kwargs):
|
||||
self.counter = self.counter+1
|
||||
return self.original(query, *args, **kwargs)
|
||||
|
||||
|
||||
@override_settings(MODULESTORE=TEST_DATA_MODULESTORE)
|
||||
class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
"""
|
||||
@@ -77,6 +88,138 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
def test_edit_unit_full(self):
|
||||
self.check_edit_unit('full')
|
||||
|
||||
def _get_draft_counts(self, item):
|
||||
cnt = 1 if getattr(item, 'is_draft', False) else 0
|
||||
for child in item.get_children():
|
||||
cnt = cnt + self._get_draft_counts(child)
|
||||
|
||||
return cnt
|
||||
|
||||
def test_get_items(self):
|
||||
'''
|
||||
This verifies a bug we had where the None setting in get_items() meant 'wildcard'
|
||||
Unfortunately, None = published for the revision field, so get_items() would return
|
||||
both draft and non-draft copies.
|
||||
'''
|
||||
store = modulestore()
|
||||
draft_store = modulestore('draft')
|
||||
import_from_xml(store, 'common/test/data/', ['simple'])
|
||||
|
||||
html_module = draft_store.get_item(['i4x', 'edX', 'simple', 'html', 'test_html', None])
|
||||
|
||||
draft_store.clone_item(html_module.location, html_module.location)
|
||||
|
||||
# now query get_items() to get this location with revision=None, this should just
|
||||
# return back a single item (not 2)
|
||||
|
||||
items = store.get_items(['i4x', 'edX', 'simple', 'html', 'test_html', None])
|
||||
self.assertEqual(len(items), 1)
|
||||
self.assertFalse(getattr(items[0], 'is_draft', False))
|
||||
|
||||
# now refetch from the draft store. Note that even though we pass
|
||||
# None in the revision field, the draft store will replace that with 'draft'
|
||||
items = draft_store.get_items(['i4x', 'edX', 'simple', 'html', 'test_html', None])
|
||||
self.assertEqual(len(items), 1)
|
||||
self.assertTrue(getattr(items[0], 'is_draft', False))
|
||||
|
||||
def test_draft_metadata(self):
|
||||
'''
|
||||
This verifies a bug we had where inherited metadata was getting written to the
|
||||
module as 'own-metadata' when publishing. Also verifies the metadata inheritance is
|
||||
properly computed
|
||||
'''
|
||||
store = modulestore()
|
||||
draft_store = modulestore('draft')
|
||||
import_from_xml(store, 'common/test/data/', ['simple'])
|
||||
|
||||
course = draft_store.get_item(Location(['i4x', 'edX', 'simple',
|
||||
'course', '2012_Fall', None]), depth=None)
|
||||
html_module = draft_store.get_item(['i4x', 'edX', 'simple', 'html', 'test_html', None])
|
||||
|
||||
self.assertEqual(html_module.lms.graceperiod, course.lms.graceperiod)
|
||||
self.assertNotIn('graceperiod', own_metadata(html_module))
|
||||
|
||||
draft_store.clone_item(html_module.location, html_module.location)
|
||||
|
||||
# refetch to check metadata
|
||||
html_module = draft_store.get_item(['i4x', 'edX', 'simple', 'html', 'test_html', None])
|
||||
|
||||
self.assertEqual(html_module.lms.graceperiod, course.lms.graceperiod)
|
||||
self.assertNotIn('graceperiod', own_metadata(html_module))
|
||||
|
||||
# publish module
|
||||
draft_store.publish(html_module.location, 0)
|
||||
|
||||
# refetch to check metadata
|
||||
html_module = draft_store.get_item(['i4x', 'edX', 'simple', 'html', 'test_html', None])
|
||||
|
||||
self.assertEqual(html_module.lms.graceperiod, course.lms.graceperiod)
|
||||
self.assertNotIn('graceperiod', own_metadata(html_module))
|
||||
|
||||
# put back in draft and change metadata and see if it's now marked as 'own_metadata'
|
||||
draft_store.clone_item(html_module.location, html_module.location)
|
||||
html_module = draft_store.get_item(['i4x', 'edX', 'simple', 'html', 'test_html', None])
|
||||
|
||||
new_graceperiod = timedelta(**{'hours': 1})
|
||||
|
||||
self.assertNotIn('graceperiod', own_metadata(html_module))
|
||||
html_module.lms.graceperiod = new_graceperiod
|
||||
self.assertIn('graceperiod', own_metadata(html_module))
|
||||
self.assertEqual(html_module.lms.graceperiod, new_graceperiod)
|
||||
|
||||
draft_store.update_metadata(html_module.location, own_metadata(html_module))
|
||||
|
||||
# read back to make sure it reads as 'own-metadata'
|
||||
html_module = draft_store.get_item(['i4x', 'edX', 'simple', 'html', 'test_html', None])
|
||||
|
||||
self.assertIn('graceperiod', own_metadata(html_module))
|
||||
self.assertEqual(html_module.lms.graceperiod, new_graceperiod)
|
||||
|
||||
# republish
|
||||
draft_store.publish(html_module.location, 0)
|
||||
|
||||
# and re-read and verify 'own-metadata'
|
||||
draft_store.clone_item(html_module.location, html_module.location)
|
||||
html_module = draft_store.get_item(['i4x', 'edX', 'simple', 'html', 'test_html', None])
|
||||
|
||||
self.assertIn('graceperiod', own_metadata(html_module))
|
||||
self.assertEqual(html_module.lms.graceperiod, new_graceperiod)
|
||||
|
||||
def test_get_depth_with_drafts(self):
|
||||
import_from_xml(modulestore(), 'common/test/data/', ['simple'])
|
||||
|
||||
course = modulestore('draft').get_item(
|
||||
Location(['i4x', 'edX', 'simple', 'course', '2012_Fall', None]),
|
||||
depth=None
|
||||
)
|
||||
|
||||
# make sure no draft items have been returned
|
||||
num_drafts = self._get_draft_counts(course)
|
||||
self.assertEqual(num_drafts, 0)
|
||||
|
||||
problem = modulestore('draft').get_item(
|
||||
Location(['i4x', 'edX', 'simple', 'problem', 'ps01-simple', None])
|
||||
)
|
||||
|
||||
# put into draft
|
||||
modulestore('draft').clone_item(problem.location, problem.location)
|
||||
|
||||
# make sure we can query that item and verify that it is a draft
|
||||
draft_problem = modulestore('draft').get_item(
|
||||
Location(['i4x', 'edX', 'simple', 'problem', 'ps01-simple', None])
|
||||
)
|
||||
self.assertTrue(getattr(draft_problem, 'is_draft', False))
|
||||
|
||||
#now requery with depth
|
||||
course = modulestore('draft').get_item(
|
||||
Location(['i4x', 'edX', 'simple', 'course', '2012_Fall', None]),
|
||||
depth=None
|
||||
)
|
||||
|
||||
# make sure just one draft item have been returned
|
||||
num_drafts = self._get_draft_counts(course)
|
||||
self.assertEqual(num_drafts, 1)
|
||||
|
||||
def test_static_tab_reordering(self):
|
||||
import_from_xml(modulestore(), 'common/test/data/', ['full'])
|
||||
|
||||
@@ -107,13 +250,16 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
module_store = modulestore('direct')
|
||||
found = False
|
||||
|
||||
item = None
|
||||
items = module_store.get_items(['i4x', 'edX', 'full', 'poll_question', None, None])
|
||||
found = len(items) > 0
|
||||
|
||||
self.assertTrue(found)
|
||||
# check that there's actually content in the 'question' field
|
||||
self.assertGreater(len(items[0].question),0)
|
||||
self.assertGreater(len(items[0].question), 0)
|
||||
|
||||
def test_xlint_fails(self):
|
||||
err_cnt = perform_xlint('common/test/data', ['full'])
|
||||
self.assertGreater(err_cnt, 0)
|
||||
|
||||
def test_delete(self):
|
||||
import_from_xml(modulestore(), 'common/test/data/', ['full'])
|
||||
@@ -122,14 +268,16 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
|
||||
sequential = module_store.get_item(Location(['i4x', 'edX', 'full', 'sequential', 'Administrivia_and_Circuit_Elements', None]))
|
||||
|
||||
chapter = module_store.get_item(Location(['i4x', 'edX', 'full', 'chapter','Week_1', None]))
|
||||
chapter = module_store.get_item(Location(['i4x', 'edX', 'full', 'chapter', 'Week_1', None]))
|
||||
|
||||
# make sure the parent no longer points to the child object which was deleted
|
||||
self.assertTrue(sequential.location.url() in chapter.children)
|
||||
|
||||
self.client.post(reverse('delete_item'),
|
||||
self.client.post(
|
||||
reverse('delete_item'),
|
||||
json.dumps({'id': sequential.location.url(), 'delete_children': 'true', 'delete_all_versions': 'true'}),
|
||||
"application/json")
|
||||
"application/json"
|
||||
)
|
||||
|
||||
found = False
|
||||
try:
|
||||
@@ -140,13 +288,11 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
|
||||
self.assertFalse(found)
|
||||
|
||||
chapter = module_store.get_item(Location(['i4x', 'edX', 'full', 'chapter','Week_1', None]))
|
||||
chapter = module_store.get_item(Location(['i4x', 'edX', 'full', 'chapter', 'Week_1', None]))
|
||||
|
||||
# make sure the parent no longer points to the child object which was deleted
|
||||
self.assertFalse(sequential.location.url() in chapter.children)
|
||||
|
||||
|
||||
|
||||
def test_about_overrides(self):
|
||||
'''
|
||||
This test case verifies that a course can use specialized override for about data, e.g. /about/Fall_2012/effort.html
|
||||
@@ -165,7 +311,6 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
import_from_xml(modulestore(), 'common/test/data/', ['full'])
|
||||
|
||||
module_store = modulestore('direct')
|
||||
content_store = contentstore()
|
||||
|
||||
source_location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
|
||||
course = module_store.get_item(source_location)
|
||||
@@ -178,7 +323,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
'org': 'MITx',
|
||||
'number': '999',
|
||||
'display_name': 'Robot Super Course',
|
||||
}
|
||||
}
|
||||
|
||||
import_from_xml(modulestore(), 'common/test/data/', ['full'])
|
||||
|
||||
@@ -207,6 +352,10 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
resp = self.client.get(reverse('edit_unit', kwargs={'location': new_loc.url()}))
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
def test_bad_contentstore_request(self):
|
||||
resp = self.client.get('http://localhost:8001/c4x/CDX/123123/asset/&images_circuits_Lab7Solution2.png')
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
|
||||
def test_delete_course(self):
|
||||
import_from_xml(modulestore(), 'common/test/data/', ['full'])
|
||||
|
||||
@@ -233,17 +382,48 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
|
||||
def test_export_course(self):
|
||||
module_store = modulestore('direct')
|
||||
draft_store = modulestore('draft')
|
||||
content_store = contentstore()
|
||||
|
||||
import_from_xml(module_store, 'common/test/data/', ['full'])
|
||||
location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
|
||||
|
||||
# get a vertical (and components in it) to put into 'draft'
|
||||
vertical = module_store.get_item(Location(['i4x', 'edX', 'full',
|
||||
'vertical', 'vertical_66', None]), depth=1)
|
||||
|
||||
draft_store.clone_item(vertical.location, vertical.location)
|
||||
|
||||
# We had a bug where orphaned draft nodes caused export to fail. This is here to cover that case.
|
||||
draft_store.clone_item(vertical.location, Location(['i4x', 'edX', 'full',
|
||||
'vertical', 'no_references', 'draft']))
|
||||
|
||||
for child in vertical.get_children():
|
||||
draft_store.clone_item(child.location, child.location)
|
||||
|
||||
root_dir = path(mkdtemp_clean())
|
||||
|
||||
# now create a private vertical
|
||||
private_vertical = draft_store.clone_item(vertical.location,
|
||||
Location(['i4x', 'edX', 'full', 'vertical', 'a_private_vertical', None]))
|
||||
|
||||
# add private to list of children
|
||||
sequential = module_store.get_item(Location(['i4x', 'edX', 'full',
|
||||
'sequential', 'Administrivia_and_Circuit_Elements', None]))
|
||||
private_location_no_draft = private_vertical.location._replace(revision=None)
|
||||
module_store.update_children(sequential.location, sequential.children +
|
||||
[private_location_no_draft.url()])
|
||||
|
||||
# read back the sequential, to make sure we have a pointer to
|
||||
sequential = module_store.get_item(Location(['i4x', 'edX', 'full',
|
||||
'sequential', 'Administrivia_and_Circuit_Elements', None]))
|
||||
|
||||
self.assertIn(private_location_no_draft.url(), sequential.children)
|
||||
|
||||
print 'Exporting to tempdir = {0}'.format(root_dir)
|
||||
|
||||
# export out to a tempdir
|
||||
export_to_xml(module_store, content_store, location, root_dir, 'test_export')
|
||||
export_to_xml(module_store, content_store, location, root_dir, 'test_export', draft_modulestore=draft_store)
|
||||
|
||||
# check for static tabs
|
||||
self.verify_content_existence(module_store, root_dir, location, 'tabs', 'static_tab', '.html')
|
||||
@@ -277,20 +457,42 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
delete_course(module_store, content_store, location)
|
||||
|
||||
# reimport
|
||||
import_from_xml(module_store, root_dir, ['test_export'])
|
||||
import_from_xml(module_store, root_dir, ['test_export'], draft_store=draft_store)
|
||||
|
||||
items = module_store.get_items(Location(['i4x', 'edX', 'full', 'vertical', None]))
|
||||
self.assertGreater(len(items), 0)
|
||||
for descriptor in items:
|
||||
print "Checking {0}....".format(descriptor.location.url())
|
||||
resp = self.client.get(reverse('edit_unit', kwargs={'location': descriptor.location.url()}))
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
# don't try to look at private verticals. Right now we're running
|
||||
# the service in non-draft aware
|
||||
if getattr(descriptor, 'is_draft', False):
|
||||
print "Checking {0}....".format(descriptor.location.url())
|
||||
resp = self.client.get(reverse('edit_unit', kwargs={'location': descriptor.location.url()}))
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
# verify that we have the content in the draft store as well
|
||||
vertical = draft_store.get_item(Location(['i4x', 'edX', 'full',
|
||||
'vertical', 'vertical_66', None]), depth=1)
|
||||
|
||||
self.assertTrue(getattr(vertical, 'is_draft', False))
|
||||
for child in vertical.get_children():
|
||||
self.assertTrue(getattr(child, 'is_draft', False))
|
||||
|
||||
# make sure that we don't have a sequential that is in draft mode
|
||||
sequential = draft_store.get_item(Location(['i4x', 'edX', 'full',
|
||||
'sequential', 'Administrivia_and_Circuit_Elements', None]))
|
||||
|
||||
self.assertFalse(getattr(sequential, 'is_draft', False))
|
||||
|
||||
# verify that we have the private vertical
|
||||
test_private_vertical = draft_store.get_item(Location(['i4x', 'edX', 'full',
|
||||
'vertical', 'vertical_66', None]))
|
||||
|
||||
self.assertTrue(getattr(test_private_vertical, 'is_draft', False))
|
||||
|
||||
shutil.rmtree(root_dir)
|
||||
|
||||
def test_course_handouts_rewrites(self):
|
||||
module_store = modulestore('direct')
|
||||
content_store = contentstore()
|
||||
|
||||
# import a test course
|
||||
import_from_xml(module_store, 'common/test/data/', ['full'])
|
||||
@@ -307,6 +509,28 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
# note, we know the link it should be because that's what in the 'full' course in the test data
|
||||
self.assertContains(resp, '/c4x/edX/full/asset/handouts_schematic_tutorial.pdf')
|
||||
|
||||
def test_prefetch_children(self):
|
||||
import_from_xml(modulestore(), 'common/test/data/', ['full'])
|
||||
module_store = modulestore('direct')
|
||||
location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
|
||||
|
||||
wrapper = MongoCollectionFindWrapper(module_store.collection.find)
|
||||
module_store.collection.find = wrapper.find
|
||||
course = module_store.get_item(location, depth=2)
|
||||
|
||||
# make sure we haven't done too many round trips to DB
|
||||
# note we say 4 round trips here for 1) the course, 2 & 3) for the chapters and sequentials, and
|
||||
# 4) because of the RT due to calculating the inherited metadata
|
||||
self.assertEqual(wrapper.counter, 4)
|
||||
|
||||
# make sure we pre-fetched a known sequential which should be at depth=2
|
||||
self.assertTrue(Location(['i4x', 'edX', 'full', 'sequential',
|
||||
'Administrivia_and_Circuit_Elements', None]) in course.system.module_data)
|
||||
|
||||
# make sure we don't have a specific vertical which should be at depth=3
|
||||
self.assertFalse(Location(['i4x', 'edX', 'full', 'vertical', 'vertical_58', None])
|
||||
in course.system.module_data)
|
||||
|
||||
def test_export_course_with_unknown_metadata(self):
|
||||
module_store = modulestore('direct')
|
||||
content_store = contentstore()
|
||||
@@ -327,14 +551,8 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
print 'Exporting to tempdir = {0}'.format(root_dir)
|
||||
|
||||
# export out to a tempdir
|
||||
exported = False
|
||||
try:
|
||||
export_to_xml(module_store, content_store, location, root_dir, 'test_export')
|
||||
exported = True
|
||||
except Exception:
|
||||
pass
|
||||
export_to_xml(module_store, content_store, location, root_dir, 'test_export')
|
||||
|
||||
self.assertTrue(exported)
|
||||
|
||||
class ContentStoreTest(ModuleStoreTestCase):
|
||||
"""
|
||||
@@ -370,7 +588,7 @@ class ContentStoreTest(ModuleStoreTestCase):
|
||||
'org': 'MITx',
|
||||
'number': '999',
|
||||
'display_name': 'Robot Super Course',
|
||||
}
|
||||
}
|
||||
|
||||
def test_create_course(self):
|
||||
"""Test new course creation - happy path"""
|
||||
@@ -397,7 +615,7 @@ class ContentStoreTest(ModuleStoreTestCase):
|
||||
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertEqual(data['ErrMsg'],
|
||||
'There is already a course defined with the same organization and course number.')
|
||||
'There is already a course defined with the same organization and course number.')
|
||||
|
||||
def test_create_course_with_bad_organization(self):
|
||||
"""Test new course creation - error path for bad organization name"""
|
||||
@@ -407,16 +625,18 @@ class ContentStoreTest(ModuleStoreTestCase):
|
||||
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertEqual(data['ErrMsg'],
|
||||
"Unable to create course 'Robot Super Course'.\n\nInvalid characters in 'University of California, Berkeley'.")
|
||||
"Unable to create course 'Robot Super Course'.\n\nInvalid characters in 'University of California, Berkeley'.")
|
||||
|
||||
def test_course_index_view_with_no_courses(self):
|
||||
"""Test viewing the index page with no courses"""
|
||||
# Create a course so there is something to view
|
||||
resp = self.client.get(reverse('index'))
|
||||
self.assertContains(resp,
|
||||
self.assertContains(
|
||||
resp,
|
||||
'<h1 class="title-1">My Courses</h1>',
|
||||
status_code=200,
|
||||
html=True)
|
||||
html=True
|
||||
)
|
||||
|
||||
def test_course_factory(self):
|
||||
"""Test that the course factory works correctly."""
|
||||
@@ -433,26 +653,30 @@ class ContentStoreTest(ModuleStoreTestCase):
|
||||
"""Test viewing the index page with an existing course"""
|
||||
CourseFactory.create(display_name='Robot Super Educational Course')
|
||||
resp = self.client.get(reverse('index'))
|
||||
self.assertContains(resp,
|
||||
self.assertContains(
|
||||
resp,
|
||||
'<span class="class-name">Robot Super Educational Course</span>',
|
||||
status_code=200,
|
||||
html=True)
|
||||
html=True
|
||||
)
|
||||
|
||||
def test_course_overview_view_with_course(self):
|
||||
"""Test viewing the course overview page with an existing course"""
|
||||
CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course')
|
||||
|
||||
data = {
|
||||
'org': 'MITx',
|
||||
'course': '999',
|
||||
'name': Location.clean('Robot Super Course'),
|
||||
}
|
||||
'org': 'MITx',
|
||||
'course': '999',
|
||||
'name': Location.clean('Robot Super Course'),
|
||||
}
|
||||
|
||||
resp = self.client.get(reverse('course_index', kwargs=data))
|
||||
self.assertContains(resp,
|
||||
self.assertContains(
|
||||
resp,
|
||||
'<article class="courseware-overview" data-course-id="i4x://MITx/999/course/Robot_Super_Course">',
|
||||
status_code=200,
|
||||
html=True)
|
||||
html=True
|
||||
)
|
||||
|
||||
def test_clone_item(self):
|
||||
"""Test cloning an item. E.g. creating a new section"""
|
||||
@@ -462,14 +686,16 @@ class ContentStoreTest(ModuleStoreTestCase):
|
||||
'parent_location': 'i4x://MITx/999/course/Robot_Super_Course',
|
||||
'template': 'i4x://edx/templates/chapter/Empty',
|
||||
'display_name': 'Section One',
|
||||
}
|
||||
}
|
||||
|
||||
resp = self.client.post(reverse('clone_item'), section_data)
|
||||
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
data = parse_json(resp)
|
||||
self.assertRegexpMatches(data['id'],
|
||||
'^i4x:\/\/MITx\/999\/chapter\/([0-9]|[a-f]){32}$')
|
||||
self.assertRegexpMatches(
|
||||
data['id'],
|
||||
r"^i4x://MITx/999/chapter/([0-9]|[a-f]){32}$"
|
||||
)
|
||||
|
||||
def test_capa_module(self):
|
||||
"""Test that a problem treats markdown specially."""
|
||||
@@ -478,7 +704,7 @@ class ContentStoreTest(ModuleStoreTestCase):
|
||||
problem_data = {
|
||||
'parent_location': 'i4x://MITx/999/course/Robot_Super_Course',
|
||||
'template': 'i4x://edx/templates/problem/Blank_Common_Problem'
|
||||
}
|
||||
}
|
||||
|
||||
resp = self.client.post(reverse('clone_item'), problem_data)
|
||||
|
||||
@@ -492,6 +718,113 @@ class ContentStoreTest(ModuleStoreTestCase):
|
||||
self.assertIn('markdown', context, "markdown is missing from context")
|
||||
self.assertNotIn('markdown', problem.editable_metadata_fields, "Markdown slipped into the editable metadata fields")
|
||||
|
||||
def test_cms_imported_course_walkthrough(self):
|
||||
"""
|
||||
Import and walk through some common URL endpoints. This just verifies non-500 and no other
|
||||
correct behavior, so it is not a deep test
|
||||
"""
|
||||
import_from_xml(modulestore(), 'common/test/data/', ['simple'])
|
||||
loc = Location(['i4x', 'edX', 'simple', 'course', '2012_Fall', None])
|
||||
resp = self.client.get(reverse('course_index',
|
||||
kwargs={'org': loc.org,
|
||||
'course': loc.course,
|
||||
'name': loc.name}))
|
||||
|
||||
self.assertEqual(200, resp.status_code)
|
||||
self.assertContains(resp, 'Chapter 2')
|
||||
|
||||
# go to various pages
|
||||
|
||||
# import page
|
||||
resp = self.client.get(reverse('import_course',
|
||||
kwargs={'org': loc.org,
|
||||
'course': loc.course,
|
||||
'name': loc.name}))
|
||||
self.assertEqual(200, resp.status_code)
|
||||
|
||||
# export page
|
||||
resp = self.client.get(reverse('export_course',
|
||||
kwargs={'org': loc.org,
|
||||
'course': loc.course,
|
||||
'name': loc.name}))
|
||||
self.assertEqual(200, resp.status_code)
|
||||
|
||||
# manage users
|
||||
resp = self.client.get(reverse('manage_users',
|
||||
kwargs={'location': loc.url()}))
|
||||
self.assertEqual(200, resp.status_code)
|
||||
|
||||
# course info
|
||||
resp = self.client.get(reverse('course_info',
|
||||
kwargs={'org': loc.org,
|
||||
'course': loc.course,
|
||||
'name': loc.name}))
|
||||
self.assertEqual(200, resp.status_code)
|
||||
|
||||
# settings_details
|
||||
resp = self.client.get(reverse('settings_details',
|
||||
kwargs={'org': loc.org,
|
||||
'course': loc.course,
|
||||
'name': loc.name}))
|
||||
self.assertEqual(200, resp.status_code)
|
||||
|
||||
# settings_details
|
||||
resp = self.client.get(reverse('settings_grading',
|
||||
kwargs={'org': loc.org,
|
||||
'course': loc.course,
|
||||
'name': loc.name}))
|
||||
self.assertEqual(200, resp.status_code)
|
||||
|
||||
# static_pages
|
||||
resp = self.client.get(reverse('static_pages',
|
||||
kwargs={'org': loc.org,
|
||||
'course': loc.course,
|
||||
'coursename': loc.name}))
|
||||
self.assertEqual(200, resp.status_code)
|
||||
|
||||
# static_pages
|
||||
resp = self.client.get(reverse('asset_index',
|
||||
kwargs={'org': loc.org,
|
||||
'course': loc.course,
|
||||
'name': loc.name}))
|
||||
self.assertEqual(200, resp.status_code)
|
||||
|
||||
# go look at a subsection page
|
||||
subsection_location = loc._replace(category='sequential', name='test_sequence')
|
||||
resp = self.client.get(reverse('edit_subsection',
|
||||
kwargs={'location': subsection_location.url()}))
|
||||
self.assertEqual(200, resp.status_code)
|
||||
|
||||
# go look at the Edit page
|
||||
unit_location = loc._replace(category='vertical', name='test_vertical')
|
||||
resp = self.client.get(reverse('edit_unit',
|
||||
kwargs={'location': unit_location.url()}))
|
||||
self.assertEqual(200, resp.status_code)
|
||||
|
||||
# delete a component
|
||||
del_loc = loc._replace(category='html', name='test_html')
|
||||
resp = self.client.post(reverse('delete_item'),
|
||||
json.dumps({'id': del_loc.url()}), "application/json")
|
||||
self.assertEqual(200, resp.status_code)
|
||||
|
||||
# delete a unit
|
||||
del_loc = loc._replace(category='vertical', name='test_vertical')
|
||||
resp = self.client.post(reverse('delete_item'),
|
||||
json.dumps({'id': del_loc.url()}), "application/json")
|
||||
self.assertEqual(200, resp.status_code)
|
||||
|
||||
# delete a unit
|
||||
del_loc = loc._replace(category='sequential', name='test_sequence')
|
||||
resp = self.client.post(reverse('delete_item'),
|
||||
json.dumps({'id': del_loc.url()}), "application/json")
|
||||
self.assertEqual(200, resp.status_code)
|
||||
|
||||
# delete a chapter
|
||||
del_loc = loc._replace(category='chapter', name='chapter_2')
|
||||
resp = self.client.post(reverse('delete_item'),
|
||||
json.dumps({'id': del_loc.url()}), "application/json")
|
||||
self.assertEqual(200, resp.status_code)
|
||||
|
||||
def test_import_metadata_with_attempts_empty_string(self):
|
||||
import_from_xml(modulestore(), 'common/test/data/', ['simple'])
|
||||
module_store = modulestore('direct')
|
||||
@@ -505,6 +838,45 @@ class ContentStoreTest(ModuleStoreTestCase):
|
||||
# make sure we found the item (e.g. it didn't error while loading)
|
||||
self.assertTrue(did_load_item)
|
||||
|
||||
def test_forum_id_generation(self):
|
||||
import_from_xml(modulestore(), 'common/test/data/', ['full'])
|
||||
module_store = modulestore('direct')
|
||||
new_component_location = Location('i4x', 'edX', 'full', 'discussion', 'new_component')
|
||||
source_template_location = Location('i4x', 'edx', 'templates', 'discussion', 'Discussion_Tag')
|
||||
|
||||
# crate a new module and add it as a child to a vertical
|
||||
module_store.clone_item(source_template_location, new_component_location)
|
||||
|
||||
new_discussion_item = module_store.get_item(new_component_location)
|
||||
|
||||
self.assertNotEquals(new_discussion_item.discussion_id, '$$GUID$$')
|
||||
|
||||
def test_update_modulestore_signal_did_fire(self):
|
||||
|
||||
import_from_xml(modulestore(), 'common/test/data/', ['full'])
|
||||
module_store = modulestore('direct')
|
||||
|
||||
try:
|
||||
module_store.modulestore_update_signal = Signal(providing_args=['modulestore', 'course_id', 'location'])
|
||||
|
||||
self.got_signal = False
|
||||
|
||||
def _signal_hander(modulestore=None, course_id=None, location=None, **kwargs):
|
||||
self.got_signal = True
|
||||
|
||||
module_store.modulestore_update_signal.connect(_signal_hander)
|
||||
|
||||
new_component_location = Location('i4x', 'edX', 'full', 'html', 'new_component')
|
||||
source_template_location = Location('i4x', 'edx', 'templates', 'html', 'Blank_HTML_Page')
|
||||
|
||||
# crate a new module
|
||||
module_store.clone_item(source_template_location, new_component_location)
|
||||
|
||||
finally:
|
||||
module_store.modulestore_update_signal = None
|
||||
|
||||
self.assertTrue(self.got_signal)
|
||||
|
||||
def test_metadata_inheritance(self):
|
||||
import_from_xml(modulestore(), 'common/test/data/', ['full'])
|
||||
|
||||
@@ -528,7 +900,7 @@ class ContentStoreTest(ModuleStoreTestCase):
|
||||
module_store.update_children(parent.location, parent.children + [new_component_location.url()])
|
||||
|
||||
# flush the cache
|
||||
module_store.get_cached_metadata_inheritance_tree(new_component_location, -1)
|
||||
module_store.refresh_cached_metadata_inheritance_tree(new_component_location)
|
||||
new_module = module_store.get_item(new_component_location)
|
||||
|
||||
# check for grace period definition which should be defined at the course level
|
||||
@@ -543,7 +915,7 @@ class ContentStoreTest(ModuleStoreTestCase):
|
||||
module_store.update_metadata(new_module.location, own_metadata(new_module))
|
||||
|
||||
# flush the cache and refetch
|
||||
module_store.get_cached_metadata_inheritance_tree(new_component_location, -1)
|
||||
module_store.refresh_cached_metadata_inheritance_tree(new_component_location)
|
||||
new_module = module_store.get_item(new_component_location)
|
||||
|
||||
self.assertEqual(timedelta(1), new_module.lms.graceperiod)
|
||||
|
||||
@@ -23,14 +23,14 @@ class CachingTestCase(TestCase):
|
||||
def test_put_and_get(self):
|
||||
set_cached_content(self.mockAsset)
|
||||
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,
|
||||
'should be stored in cache with nonUnicodeLocation')
|
||||
'should be stored in cache with nonUnicodeLocation')
|
||||
|
||||
def test_delete(self):
|
||||
set_cached_content(self.mockAsset)
|
||||
del_cached_content(self.nonUnicodeLocation)
|
||||
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),
|
||||
'should not be stored in cache with nonUnicodeLocation')
|
||||
'should not be stored in cache with nonUnicodeLocation')
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import datetime
|
||||
import json
|
||||
import copy
|
||||
from util import converters
|
||||
from util.converters import jsdate_to_time
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.test.client import Client
|
||||
@@ -10,73 +8,17 @@ from django.core.urlresolvers import reverse
|
||||
from django.utils.timezone import UTC
|
||||
|
||||
from xmodule.modulestore import Location
|
||||
from models.settings.course_details import (CourseDetails,
|
||||
CourseSettingsEncoder)
|
||||
from models.settings.course_details import (CourseDetails, CourseSettingsEncoder)
|
||||
from models.settings.course_grading import CourseGradingModel
|
||||
from contentstore.utils import get_modulestore
|
||||
|
||||
from django.test import TestCase
|
||||
from .utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
|
||||
from models.settings.course_metadata import CourseMetadata
|
||||
from xmodule.modulestore.xml_importer import import_from_xml
|
||||
from xmodule.modulestore.django import modulestore
|
||||
import time
|
||||
|
||||
|
||||
# YYYY-MM-DDThh:mm:ss.s+/-HH:MM
|
||||
class ConvertersTestCase(TestCase):
|
||||
@staticmethod
|
||||
def struct_to_datetime(struct_time):
|
||||
return datetime.datetime(struct_time.tm_year, struct_time.tm_mon,
|
||||
struct_time.tm_mday, struct_time.tm_hour,
|
||||
struct_time.tm_min, struct_time.tm_sec, tzinfo=UTC())
|
||||
|
||||
def compare_dates(self, date1, date2, expected_delta):
|
||||
dt1 = ConvertersTestCase.struct_to_datetime(date1)
|
||||
dt2 = ConvertersTestCase.struct_to_datetime(date2)
|
||||
self.assertEqual(dt1 - dt2, expected_delta, str(date1) + "-"
|
||||
+ str(date2) + "!=" + str(expected_delta))
|
||||
|
||||
def test_iso_to_struct(self):
|
||||
'''Test conversion from iso compatible date strings to struct_time'''
|
||||
self.compare_dates(converters.jsdate_to_time("2013-01-01"),
|
||||
converters.jsdate_to_time("2012-12-31"),
|
||||
datetime.timedelta(days=1))
|
||||
self.compare_dates(converters.jsdate_to_time("2013-01-01T00"),
|
||||
converters.jsdate_to_time("2012-12-31T23"),
|
||||
datetime.timedelta(hours=1))
|
||||
self.compare_dates(converters.jsdate_to_time("2013-01-01T00:00"),
|
||||
converters.jsdate_to_time("2012-12-31T23:59"),
|
||||
datetime.timedelta(minutes=1))
|
||||
self.compare_dates(converters.jsdate_to_time("2013-01-01T00:00:00"),
|
||||
converters.jsdate_to_time("2012-12-31T23:59:59"),
|
||||
datetime.timedelta(seconds=1))
|
||||
self.compare_dates(converters.jsdate_to_time("2013-01-01T00:00:00Z"),
|
||||
converters.jsdate_to_time("2012-12-31T23:59:59Z"),
|
||||
datetime.timedelta(seconds=1))
|
||||
self.compare_dates(
|
||||
converters.jsdate_to_time("2012-12-31T23:00:01-01:00"),
|
||||
converters.jsdate_to_time("2013-01-01T00:00:00+01:00"),
|
||||
datetime.timedelta(hours=1, seconds=1))
|
||||
|
||||
def test_struct_to_iso(self):
|
||||
'''
|
||||
Test converting time reprs to iso dates
|
||||
'''
|
||||
self.assertEqual(
|
||||
converters.time_to_isodate(
|
||||
time.strptime("2012-12-31T23:59:59Z", "%Y-%m-%dT%H:%M:%SZ")),
|
||||
"2012-12-31T23:59:59Z")
|
||||
self.assertEqual(
|
||||
converters.time_to_isodate(
|
||||
jsdate_to_time("2012-12-31T23:59:59Z")),
|
||||
"2012-12-31T23:59:59Z")
|
||||
self.assertEqual(
|
||||
converters.time_to_isodate(
|
||||
jsdate_to_time("2012-12-31T23:00:01-01:00")),
|
||||
"2013-01-01T00:00:01Z")
|
||||
from xmodule.fields import Date
|
||||
|
||||
|
||||
class CourseTestCase(ModuleStoreTestCase):
|
||||
@@ -105,12 +47,8 @@ class CourseTestCase(ModuleStoreTestCase):
|
||||
self.client = Client()
|
||||
self.client.login(username=uname, password=password)
|
||||
|
||||
t = 'i4x://edx/templates/course/Empty'
|
||||
o = 'MITx'
|
||||
n = '999'
|
||||
dn = 'Robot Super Course'
|
||||
self.course_location = Location('i4x', o, n, 'course', 'Robot_Super_Course')
|
||||
CourseFactory.create(template=t, org=o, number=n, display_name=dn)
|
||||
course = CourseFactory.create(template='i4x://edx/templates/course/Empty', org='MITx', number='999', display_name='Robot Super Course')
|
||||
self.course_location = course.location
|
||||
|
||||
|
||||
class CourseDetailsTestCase(CourseTestCase):
|
||||
@@ -144,17 +82,25 @@ class CourseDetailsTestCase(CourseTestCase):
|
||||
jsondetails = CourseDetails.fetch(self.course_location)
|
||||
jsondetails.syllabus = "<a href='foo'>bar</a>"
|
||||
# encode - decode to convert date fields and other data which changes form
|
||||
self.assertEqual(CourseDetails.update_from_json(jsondetails.__dict__).syllabus,
|
||||
jsondetails.syllabus, "After set syllabus")
|
||||
self.assertEqual(
|
||||
CourseDetails.update_from_json(jsondetails.__dict__).syllabus,
|
||||
jsondetails.syllabus, "After set syllabus"
|
||||
)
|
||||
jsondetails.overview = "Overview"
|
||||
self.assertEqual(CourseDetails.update_from_json(jsondetails.__dict__).overview,
|
||||
jsondetails.overview, "After set overview")
|
||||
self.assertEqual(
|
||||
CourseDetails.update_from_json(jsondetails.__dict__).overview,
|
||||
jsondetails.overview, "After set overview"
|
||||
)
|
||||
jsondetails.intro_video = "intro_video"
|
||||
self.assertEqual(CourseDetails.update_from_json(jsondetails.__dict__).intro_video,
|
||||
jsondetails.intro_video, "After set intro_video")
|
||||
self.assertEqual(
|
||||
CourseDetails.update_from_json(jsondetails.__dict__).intro_video,
|
||||
jsondetails.intro_video, "After set intro_video"
|
||||
)
|
||||
jsondetails.effort = "effort"
|
||||
self.assertEqual(CourseDetails.update_from_json(jsondetails.__dict__).effort,
|
||||
jsondetails.effort, "After set effort")
|
||||
self.assertEqual(
|
||||
CourseDetails.update_from_json(jsondetails.__dict__).effort,
|
||||
jsondetails.effort, "After set effort"
|
||||
)
|
||||
|
||||
|
||||
class CourseDetailsViewTest(CourseTestCase):
|
||||
@@ -206,17 +152,22 @@ class CourseDetailsViewTest(CourseTestCase):
|
||||
self.assertEqual(details['intro_video'], encoded.get('intro_video', None), context + " intro_video not ==")
|
||||
self.assertEqual(details['effort'], encoded['effort'], context + " efforts not ==")
|
||||
|
||||
@staticmethod
|
||||
def struct_to_datetime(struct_time):
|
||||
return datetime.datetime(*struct_time[:6], tzinfo=UTC())
|
||||
|
||||
def compare_date_fields(self, details, encoded, context, field):
|
||||
if details[field] is not None:
|
||||
date = Date()
|
||||
if field in encoded and encoded[field] is not None:
|
||||
encoded_encoded = jsdate_to_time(encoded[field])
|
||||
dt1 = ConvertersTestCase.struct_to_datetime(encoded_encoded)
|
||||
encoded_encoded = date.from_json(encoded[field])
|
||||
dt1 = CourseDetailsViewTest.struct_to_datetime(encoded_encoded)
|
||||
|
||||
if isinstance(details[field], datetime.datetime):
|
||||
dt2 = details[field]
|
||||
else:
|
||||
details_encoded = jsdate_to_time(details[field])
|
||||
dt2 = ConvertersTestCase.struct_to_datetime(details_encoded)
|
||||
details_encoded = date.from_json(details[field])
|
||||
dt2 = CourseDetailsViewTest.struct_to_datetime(details_encoded)
|
||||
|
||||
expected_delta = datetime.timedelta(0)
|
||||
self.assertEqual(dt1 - dt2, expected_delta, str(dt1) + "!=" + str(dt2) + " at " + context)
|
||||
@@ -300,6 +251,7 @@ class CourseGradingTest(CourseTestCase):
|
||||
altered_grader = CourseGradingModel.update_grader_from_json(test_grader.course_location, test_grader.graders[1])
|
||||
self.assertDictEqual(test_grader.graders[1], altered_grader, "drop_count[1] + 2")
|
||||
|
||||
|
||||
class CourseMetadataEditingTest(CourseTestCase):
|
||||
def setUp(self):
|
||||
CourseTestCase.setUp(self)
|
||||
@@ -307,7 +259,6 @@ class CourseMetadataEditingTest(CourseTestCase):
|
||||
import_from_xml(modulestore(), 'common/test/data/', ['full'])
|
||||
self.fullcourse_location = Location(['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None])
|
||||
|
||||
|
||||
def test_fetch_initial_fields(self):
|
||||
test_model = CourseMetadata.fetch(self.course_location)
|
||||
self.assertIn('display_name', test_model, 'Missing editable metadata field')
|
||||
@@ -322,18 +273,20 @@ class CourseMetadataEditingTest(CourseTestCase):
|
||||
self.assertIn('xqa_key', test_model, 'xqa_key field ')
|
||||
|
||||
def test_update_from_json(self):
|
||||
test_model = CourseMetadata.update_from_json(self.course_location,
|
||||
{ "advertised_start" : "start A",
|
||||
"testcenter_info" : { "c" : "test" },
|
||||
"days_early_for_beta" : 2})
|
||||
test_model = CourseMetadata.update_from_json(self.course_location, {
|
||||
"advertised_start": "start A",
|
||||
"testcenter_info": {"c": "test"},
|
||||
"days_early_for_beta": 2
|
||||
})
|
||||
self.update_check(test_model)
|
||||
# try fresh fetch to ensure persistence
|
||||
test_model = CourseMetadata.fetch(self.course_location)
|
||||
self.update_check(test_model)
|
||||
# now change some of the existing metadata
|
||||
test_model = CourseMetadata.update_from_json(self.course_location,
|
||||
{ "advertised_start" : "start B",
|
||||
"display_name" : "jolly roger"})
|
||||
test_model = CourseMetadata.update_from_json(self.course_location, {
|
||||
"advertised_start": "start B",
|
||||
"display_name": "jolly roger"}
|
||||
)
|
||||
self.assertIn('display_name', test_model, 'Missing editable metadata field')
|
||||
self.assertEqual(test_model['display_name'], 'jolly roger', "not expected value")
|
||||
self.assertIn('advertised_start', test_model, 'Missing revised advertised_start metadata field')
|
||||
@@ -345,13 +298,12 @@ class CourseMetadataEditingTest(CourseTestCase):
|
||||
self.assertIn('advertised_start', test_model, 'Missing new advertised_start metadata field')
|
||||
self.assertEqual(test_model['advertised_start'], 'start A', "advertised_start not expected value")
|
||||
self.assertIn('testcenter_info', test_model, 'Missing testcenter_info metadata field')
|
||||
self.assertDictEqual(test_model['testcenter_info'], { "c" : "test" }, "testcenter_info not expected value")
|
||||
self.assertDictEqual(test_model['testcenter_info'], {"c": "test"}, "testcenter_info not expected value")
|
||||
self.assertIn('days_early_for_beta', test_model, 'Missing days_early_for_beta metadata field')
|
||||
self.assertEqual(test_model['days_early_for_beta'], 2, "days_early_for_beta not expected value")
|
||||
|
||||
|
||||
def test_delete_key(self):
|
||||
test_model = CourseMetadata.delete_key(self.fullcourse_location, { 'deleteKeys' : ['doesnt_exist', 'showanswer', 'xqa_key']})
|
||||
test_model = CourseMetadata.delete_key(self.fullcourse_location, {'deleteKeys': ['doesnt_exist', 'showanswer', 'xqa_key']})
|
||||
# ensure no harm
|
||||
self.assertNotIn('graceperiod', test_model, 'blacklisted field leaked in')
|
||||
self.assertIn('display_name', test_model, 'full missing editable metadata field')
|
||||
|
||||
@@ -3,7 +3,7 @@ from contentstore import utils
|
||||
import mock
|
||||
from django.test import TestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
from .utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
|
||||
|
||||
class LMSLinksTestCase(TestCase):
|
||||
@@ -30,7 +30,7 @@ class LMSLinksTestCase(TestCase):
|
||||
|
||||
class UrlReverseTestCase(ModuleStoreTestCase):
|
||||
""" Tests for get_url_reverse """
|
||||
def test_CoursePageNames(self):
|
||||
def test_course_page_names(self):
|
||||
""" Test the defined course pages. """
|
||||
course = CourseFactory.create(org='mitX', number='666', display_name='URL Reverse Course')
|
||||
|
||||
@@ -69,4 +69,4 @@ class UrlReverseTestCase(ModuleStoreTestCase):
|
||||
self.assertEquals(
|
||||
'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)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1,30 +1,8 @@
|
||||
import json
|
||||
import shutil
|
||||
from django.test.client import Client
|
||||
from django.conf import settings
|
||||
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 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
|
||||
from .utils import parse_json, user, registration
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
|
||||
|
||||
class ContentStoreTestCase(ModuleStoreTestCase):
|
||||
@@ -84,6 +62,7 @@ class ContentStoreTestCase(ModuleStoreTestCase):
|
||||
# Now make sure that the user is now actually activated
|
||||
self.assertTrue(user(email).is_active)
|
||||
|
||||
|
||||
class AuthTestCase(ContentStoreTestCase):
|
||||
"""Check that various permissions-related things work"""
|
||||
|
||||
@@ -101,9 +80,9 @@ class AuthTestCase(ContentStoreTestCase):
|
||||
def test_public_pages_load(self):
|
||||
"""Make sure pages that don't require login load without error."""
|
||||
pages = (
|
||||
reverse('login'),
|
||||
reverse('signup'),
|
||||
)
|
||||
reverse('login'),
|
||||
reverse('signup'),
|
||||
)
|
||||
for page in pages:
|
||||
print "Checking '{0}'".format(page)
|
||||
self.check_page_get(page, 200)
|
||||
@@ -136,13 +115,13 @@ class AuthTestCase(ContentStoreTestCase):
|
||||
"""Make sure pages that do require login work."""
|
||||
auth_pages = (
|
||||
reverse('index'),
|
||||
)
|
||||
)
|
||||
|
||||
# These are pages that should just load when the user is logged in
|
||||
# (no data needed)
|
||||
simple_auth_pages = (
|
||||
reverse('index'),
|
||||
)
|
||||
)
|
||||
|
||||
# need an activated user
|
||||
self.test_create_account()
|
||||
|
||||
@@ -1,54 +1,12 @@
|
||||
'''
|
||||
Utilities for contentstore tests
|
||||
'''
|
||||
|
||||
import json
|
||||
import copy
|
||||
from uuid import uuid4
|
||||
from django.test import TestCase
|
||||
from django.conf import settings
|
||||
|
||||
from student.models import Registration
|
||||
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):
|
||||
"""Parse response, which is assumed to be json"""
|
||||
|
||||
@@ -3,9 +3,13 @@ from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
from django.core.urlresolvers import reverse
|
||||
import copy
|
||||
|
||||
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):
|
||||
"""
|
||||
@@ -82,11 +86,10 @@ def get_lms_link_for_item(location, preview=False, course_id=None):
|
||||
|
||||
if settings.LMS_BASE is not None:
|
||||
if preview:
|
||||
lms_base = settings.MITX_FEATURES.get('PREVIEW_LMS_BASE',
|
||||
'preview.' + settings.LMS_BASE)
|
||||
lms_base = settings.MITX_FEATURES.get('PREVIEW_LMS_BASE', 'preview.' + settings.LMS_BASE)
|
||||
else:
|
||||
lms_base = settings.LMS_BASE
|
||||
|
||||
|
||||
lms_link = "//{lms_base}/courses/{course_id}/jump_to/{location}".format(
|
||||
lms_base=lms_base,
|
||||
course_id=course_id,
|
||||
@@ -137,7 +140,7 @@ def compute_unit_state(unit):
|
||||
'private' content is editabled and not visible in the LMS
|
||||
"""
|
||||
|
||||
if unit.cms.is_draft:
|
||||
if getattr(unit, 'is_draft', False):
|
||||
try:
|
||||
modulestore('direct').get_item(unit.location)
|
||||
return UnitState.draft
|
||||
@@ -147,10 +150,6 @@ def compute_unit_state(unit):
|
||||
return UnitState.public
|
||||
|
||||
|
||||
def get_date_display(date):
|
||||
return date.strftime("%d %B, %Y at %I:%M %p")
|
||||
|
||||
|
||||
def update_item(location, value):
|
||||
"""
|
||||
If value is None, delete the db entry. Otherwise, update it using the correct modulestore.
|
||||
@@ -191,3 +190,37 @@ class CoursePageNames:
|
||||
SettingsGrading = "settings_grading"
|
||||
CourseOutline = "course_index"
|
||||
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
|
||||
|
||||
@@ -6,7 +6,6 @@ import sys
|
||||
import time
|
||||
import tarfile
|
||||
import shutil
|
||||
from datetime import datetime
|
||||
from collections import defaultdict
|
||||
from uuid import uuid4
|
||||
from path import path
|
||||
@@ -15,9 +14,6 @@ from tempfile import mkdtemp
|
||||
from django.core.servers.basehttp import FileWrapper
|
||||
from django.core.files.temp import NamedTemporaryFile
|
||||
|
||||
# to install PIL on MacOSX: 'easy_install http://dist.repoze.org/PIL-1.1.6.tar.gz'
|
||||
from PIL import Image
|
||||
|
||||
from django.http import HttpResponse, Http404, HttpResponseBadRequest, HttpResponseForbidden, HttpResponseServerError
|
||||
from django.http import HttpResponseNotFound
|
||||
from django.contrib.auth.decorators import login_required
|
||||
@@ -42,17 +38,19 @@ from xmodule.modulestore.mongo import MongoUsage
|
||||
from mitxmako.shortcuts import render_to_response, render_to_string
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule_modifiers import replace_static_urls, wrap_xmodule
|
||||
from xmodule.exceptions import NotFoundError
|
||||
from xmodule.exceptions import NotFoundError, ProcessingError
|
||||
from functools import partial
|
||||
|
||||
from xmodule.contentstore.django import contentstore
|
||||
from xmodule.contentstore.content import StaticContent
|
||||
from xmodule.util.date_utils import get_default_time_display
|
||||
|
||||
from auth.authz import is_user_in_course_group_role, get_users_in_course_group_by_role
|
||||
from auth.authz import get_user_by_email, add_user_to_course_group, remove_user_from_course_group
|
||||
from auth.authz import INSTRUCTOR_ROLE_NAME, STAFF_ROLE_NAME, create_all_course_groups
|
||||
from .utils import get_course_location_for_item, get_lms_link_for_item, compute_unit_state, \
|
||||
get_date_display, UnitState, get_course_for_item, get_url_reverse
|
||||
UnitState, get_course_for_item, get_url_reverse, add_open_ended_panel_tab, \
|
||||
remove_open_ended_panel_tab
|
||||
|
||||
from xmodule.modulestore.xml_importer import import_from_xml
|
||||
from contentstore.course_info_model import get_course_updates, \
|
||||
@@ -73,7 +71,8 @@ log = logging.getLogger(__name__)
|
||||
|
||||
COMPONENT_TYPES = ['customtag', 'discussion', 'html', 'problem', 'video']
|
||||
|
||||
ADVANCED_COMPONENT_TYPES = ['annotatable', 'combinedopenended', 'peergrading']
|
||||
OPEN_ENDED_COMPONENT_TYPES = ["combinedopenended", "peergrading"]
|
||||
ADVANCED_COMPONENT_TYPES = ['annotatable'] + OPEN_ENDED_COMPONENT_TYPES
|
||||
ADVANCED_COMPONENT_CATEGORY = 'advanced'
|
||||
ADVANCED_COMPONENT_POLICY_KEY = 'advanced_modules'
|
||||
|
||||
@@ -118,6 +117,12 @@ def howitworks(request):
|
||||
else:
|
||||
return render_to_response('howitworks.html', {})
|
||||
|
||||
|
||||
# static/proof-of-concept views
|
||||
def ux_alerts(request):
|
||||
return render_to_response('ux-alerts.html', {})
|
||||
|
||||
|
||||
# ==== Views for any logged-in user ==================================
|
||||
|
||||
|
||||
@@ -188,7 +193,7 @@ def course_index(request, org, course, name):
|
||||
'coursename': name
|
||||
})
|
||||
|
||||
course = modulestore().get_item(location)
|
||||
course = modulestore().get_item(location, depth=3)
|
||||
sections = course.get_children()
|
||||
|
||||
return render_to_response('overview.html', {
|
||||
@@ -208,19 +213,14 @@ def course_index(request, org, course, name):
|
||||
@login_required
|
||||
def edit_subsection(request, location):
|
||||
# check that we have permissions to edit this item
|
||||
if not has_access(request.user, location):
|
||||
course = get_course_for_item(location)
|
||||
if not has_access(request.user, course.location):
|
||||
raise PermissionDenied()
|
||||
|
||||
item = modulestore().get_item(location)
|
||||
item = modulestore().get_item(location, depth=1)
|
||||
|
||||
# TODO: we need a smarter way to figure out what course an item is in
|
||||
for course in modulestore().get_courses():
|
||||
if (course.location.org == item.location.org and
|
||||
course.location.course == item.location.course):
|
||||
break
|
||||
|
||||
lms_link = get_lms_link_for_item(location)
|
||||
preview_link = get_lms_link_for_item(location, preview=True)
|
||||
lms_link = get_lms_link_for_item(location, course_id=course.location.course_id)
|
||||
preview_link = get_lms_link_for_item(location, course_id=course.location.course_id, preview=True)
|
||||
|
||||
# make sure that location references a 'sequential', otherwise return BadRequest
|
||||
if item.location.category != 'sequential':
|
||||
@@ -241,8 +241,7 @@ def edit_subsection(request, location):
|
||||
(field.name, field.read_from(item))
|
||||
for field
|
||||
in item.fields
|
||||
if field.name not in ['display_name', 'start', 'due', 'format'] and
|
||||
field.scope == Scope.settings
|
||||
if field.name not in ['display_name', 'start', 'due', 'format'] and field.scope == Scope.settings
|
||||
)
|
||||
|
||||
can_view_live = False
|
||||
@@ -254,18 +253,18 @@ def edit_subsection(request, location):
|
||||
break
|
||||
|
||||
return render_to_response('edit_subsection.html',
|
||||
{'subsection': item,
|
||||
'context_course': course,
|
||||
'create_new_unit_template': Location('i4x', 'edx', 'templates', 'vertical', 'Empty'),
|
||||
'lms_link': lms_link,
|
||||
'preview_link': preview_link,
|
||||
'course_graders': json.dumps(CourseGradingModel.fetch(course.location).graders),
|
||||
'parent_location': course.location,
|
||||
'parent_item': parent,
|
||||
'policy_metadata': policy_metadata,
|
||||
'subsection_units': subsection_units,
|
||||
'can_view_live': can_view_live
|
||||
})
|
||||
{'subsection': item,
|
||||
'context_course': course,
|
||||
'create_new_unit_template': Location('i4x', 'edx', 'templates', 'vertical', 'Empty'),
|
||||
'lms_link': lms_link,
|
||||
'preview_link': preview_link,
|
||||
'course_graders': json.dumps(CourseGradingModel.fetch(course.location).graders),
|
||||
'parent_location': course.location,
|
||||
'parent_item': parent,
|
||||
'policy_metadata': policy_metadata,
|
||||
'subsection_units': subsection_units,
|
||||
'can_view_live': can_view_live
|
||||
})
|
||||
|
||||
|
||||
@login_required
|
||||
@@ -277,19 +276,13 @@ def edit_unit(request, location):
|
||||
|
||||
id: A Location URL
|
||||
"""
|
||||
# check that we have permissions to edit this item
|
||||
if not has_access(request.user, location):
|
||||
course = get_course_for_item(location)
|
||||
if not has_access(request.user, course.location):
|
||||
raise PermissionDenied()
|
||||
|
||||
item = modulestore().get_item(location)
|
||||
item = modulestore().get_item(location, depth=1)
|
||||
|
||||
# TODO: we need a smarter way to figure out what course an item is in
|
||||
for course in modulestore().get_courses():
|
||||
if (course.location.org == item.location.org and
|
||||
course.location.course == item.location.course):
|
||||
break
|
||||
|
||||
lms_link = get_lms_link_for_item(item.location)
|
||||
lms_link = get_lms_link_for_item(item.location, course_id=course.location.course_id)
|
||||
|
||||
component_templates = defaultdict(list)
|
||||
|
||||
@@ -350,17 +343,17 @@ def edit_unit(request, location):
|
||||
index = index + 1
|
||||
|
||||
preview_lms_base = settings.MITX_FEATURES.get('PREVIEW_LMS_BASE',
|
||||
'preview.' + settings.LMS_BASE)
|
||||
'preview.' + settings.LMS_BASE)
|
||||
|
||||
preview_lms_link = '//{preview_lms_base}/courses/{org}/{course}/{course_name}/courseware/{section}/{subsection}/{index}'.format(
|
||||
preview_lms_base=preview_lms_base,
|
||||
lms_base=settings.LMS_BASE,
|
||||
org=course.location.org,
|
||||
course=course.location.course,
|
||||
course_name=course.location.name,
|
||||
section=containing_section.location.name,
|
||||
subsection=containing_subsection.location.name,
|
||||
index=index)
|
||||
preview_lms_base=preview_lms_base,
|
||||
lms_base=settings.LMS_BASE,
|
||||
org=course.location.org,
|
||||
course=course.location.course,
|
||||
course_name=course.location.name,
|
||||
section=containing_section.location.name,
|
||||
subsection=containing_subsection.location.name,
|
||||
index=index)
|
||||
|
||||
unit_state = compute_unit_state(item)
|
||||
|
||||
@@ -374,7 +367,7 @@ def edit_unit(request, location):
|
||||
'draft_preview_link': preview_lms_link,
|
||||
'published_preview_link': lms_link,
|
||||
'subsection': containing_subsection,
|
||||
'release_date': get_date_display(datetime.fromtimestamp(time.mktime(containing_subsection.lms.start))) if containing_subsection.lms.start is not None else None,
|
||||
'release_date': get_default_time_display(containing_subsection.lms.start) if containing_subsection.lms.start is not None else None,
|
||||
'section': containing_section,
|
||||
'create_new_unit_template': Location('i4x', 'edx', 'templates', 'vertical', 'Empty'),
|
||||
'unit_state': unit_state,
|
||||
@@ -448,9 +441,16 @@ def preview_dispatch(request, preview_id, location, dispatch=None):
|
||||
# Let the module handle the AJAX
|
||||
try:
|
||||
ajax_return = instance.handle_ajax(dispatch, request.POST)
|
||||
|
||||
except NotFoundError:
|
||||
log.exception("Module indicating to user that request doesn't exist")
|
||||
raise Http404
|
||||
|
||||
except ProcessingError:
|
||||
log.warning("Module raised an error while processing AJAX request",
|
||||
exc_info=True)
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
except:
|
||||
log.exception("error processing ajax call")
|
||||
raise
|
||||
@@ -619,7 +619,6 @@ def delete_item(request):
|
||||
|
||||
store = get_modulestore(item_loc)
|
||||
|
||||
|
||||
# @TODO: this probably leaves draft items dangling. My preferance would be for the semantic to be
|
||||
# if item.location.revision=None, then delete both draft and published version
|
||||
# if caller wants to only delete the draft than the caller should put item.location.revision='draft'
|
||||
@@ -661,7 +660,7 @@ def save_item(request):
|
||||
if not has_access(request.user, item_location):
|
||||
raise PermissionDenied()
|
||||
|
||||
store = get_modulestore(Location(item_location));
|
||||
store = get_modulestore(Location(item_location))
|
||||
|
||||
if request.POST.get('data') is not None:
|
||||
data = request.POST['data']
|
||||
@@ -796,7 +795,7 @@ def upload_asset(request, org, course, coursename):
|
||||
# Does the course actually exist?!? Get anything from it to prove its existance
|
||||
|
||||
try:
|
||||
item = modulestore().get_item(location)
|
||||
modulestore().get_item(location)
|
||||
except:
|
||||
# no return it as a Bad Request response
|
||||
logging.error('Could not find course' + location)
|
||||
@@ -830,24 +829,23 @@ def upload_asset(request, org, course, coursename):
|
||||
readback = contentstore().find(content.location)
|
||||
|
||||
response_payload = {'displayname': content.name,
|
||||
'uploadDate': get_date_display(readback.last_modified_at),
|
||||
'url': StaticContent.get_url_path_from_location(content.location),
|
||||
'thumb_url': StaticContent.get_url_path_from_location(thumbnail_location) if thumbnail_content is not None else None,
|
||||
'msg': 'Upload completed'
|
||||
}
|
||||
'uploadDate': get_default_time_display(readback.last_modified_at.timetuple()),
|
||||
'url': StaticContent.get_url_path_from_location(content.location),
|
||||
'thumb_url': StaticContent.get_url_path_from_location(thumbnail_location) if thumbnail_content is not None else None,
|
||||
'msg': 'Upload completed'
|
||||
}
|
||||
|
||||
response = HttpResponse(json.dumps(response_payload))
|
||||
response['asset_url'] = StaticContent.get_url_path_from_location(content.location)
|
||||
return response
|
||||
|
||||
|
||||
'''
|
||||
This view will return all CMS users who are editors for the specified course
|
||||
'''
|
||||
@login_required
|
||||
@ensure_csrf_cookie
|
||||
def manage_users(request, location):
|
||||
|
||||
'''
|
||||
This view will return all CMS users who are editors for the specified course
|
||||
'''
|
||||
# check that logged in user has permissions to this item
|
||||
if not has_access(request.user, location, role=INSTRUCTOR_ROLE_NAME) and not has_access(request.user, location, role=STAFF_ROLE_NAME):
|
||||
raise PermissionDenied()
|
||||
@@ -874,14 +872,14 @@ def create_json_response(errmsg=None):
|
||||
return resp
|
||||
|
||||
|
||||
'''
|
||||
This POST-back view will add a user - specified by email - to the list of editors for
|
||||
the specified course
|
||||
'''
|
||||
@expect_json
|
||||
@login_required
|
||||
@ensure_csrf_cookie
|
||||
def add_user(request, location):
|
||||
'''
|
||||
This POST-back view will add a user - specified by email - to the list of editors for
|
||||
the specified course
|
||||
'''
|
||||
email = request.POST["email"]
|
||||
|
||||
if email == '':
|
||||
@@ -907,14 +905,15 @@ def add_user(request, location):
|
||||
return create_json_response()
|
||||
|
||||
|
||||
'''
|
||||
This POST-back view will remove a user - specified by email - from the list of editors for
|
||||
the specified course
|
||||
'''
|
||||
@expect_json
|
||||
@login_required
|
||||
@ensure_csrf_cookie
|
||||
def remove_user(request, location):
|
||||
'''
|
||||
This POST-back view will remove a user - specified by email - from the list of editors for
|
||||
the specified course
|
||||
'''
|
||||
|
||||
email = request.POST["email"]
|
||||
|
||||
# check that logged in user has admin permissions on this course
|
||||
@@ -989,13 +988,12 @@ def reorder_static_tabs(request):
|
||||
for tab in course.tabs:
|
||||
if tab['type'] == 'static_tab':
|
||||
reordered_tabs.append({'type': 'static_tab',
|
||||
'name': tab_items[static_tab_idx].display_name,
|
||||
'url_slug': tab_items[static_tab_idx].location.name})
|
||||
'name': tab_items[static_tab_idx].display_name,
|
||||
'url_slug': tab_items[static_tab_idx].location.name})
|
||||
static_tab_idx += 1
|
||||
else:
|
||||
reordered_tabs.append(tab)
|
||||
|
||||
|
||||
# OK, re-assemble the static tabs in the new order
|
||||
course.tabs = reordered_tabs
|
||||
modulestore('direct').update_metadata(course.location, own_metadata(course))
|
||||
@@ -1007,7 +1005,6 @@ def reorder_static_tabs(request):
|
||||
def edit_tabs(request, org, course, coursename):
|
||||
location = ['i4x', org, course, 'course', coursename]
|
||||
course_item = modulestore().get_item(location)
|
||||
static_tabs_loc = Location('i4x', org, course, 'static_tab', None)
|
||||
|
||||
# check that logged in user has permissions to this item
|
||||
if not has_access(request.user, location):
|
||||
@@ -1036,7 +1033,7 @@ def edit_tabs(request, org, course, coursename):
|
||||
'active_tab': 'pages',
|
||||
'context_course': course_item,
|
||||
'components': components
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
def not_found(request):
|
||||
@@ -1098,21 +1095,21 @@ def course_info_updates(request, org, course, provided_id=None):
|
||||
|
||||
if request.method == 'GET':
|
||||
return HttpResponse(json.dumps(get_course_updates(location)),
|
||||
mimetype="application/json")
|
||||
mimetype="application/json")
|
||||
elif real_method == 'DELETE':
|
||||
try:
|
||||
return HttpResponse(json.dumps(delete_course_update(location,
|
||||
request.POST, provided_id)), mimetype="application/json")
|
||||
request.POST, provided_id)), mimetype="application/json")
|
||||
except:
|
||||
return HttpResponseBadRequest("Failed to delete",
|
||||
content_type="text/plain")
|
||||
content_type="text/plain")
|
||||
elif request.method == 'POST':
|
||||
try:
|
||||
return HttpResponse(json.dumps(update_course_updates(location,
|
||||
request.POST, provided_id)), mimetype="application/json")
|
||||
request.POST, provided_id)), mimetype="application/json")
|
||||
except:
|
||||
return HttpResponseBadRequest("Failed to save",
|
||||
content_type="text/plain")
|
||||
content_type="text/plain")
|
||||
|
||||
|
||||
@expect_json
|
||||
@@ -1180,7 +1177,7 @@ def course_config_graders_page(request, org, course, name):
|
||||
|
||||
return render_to_response('settings_graders.html', {
|
||||
'context_course': course_module,
|
||||
'course_location' : location,
|
||||
'course_location': location,
|
||||
'course_details': json.dumps(course_details, cls=CourseSettingsEncoder)
|
||||
})
|
||||
|
||||
@@ -1199,8 +1196,8 @@ def course_config_advanced_page(request, org, course, name):
|
||||
|
||||
return render_to_response('settings_advanced.html', {
|
||||
'context_course': course_module,
|
||||
'course_location' : location,
|
||||
'advanced_dict' : json.dumps(CourseMetadata.fetch(location)),
|
||||
'course_location': location,
|
||||
'advanced_dict': json.dumps(CourseMetadata.fetch(location)),
|
||||
})
|
||||
|
||||
|
||||
@@ -1221,7 +1218,8 @@ def course_settings_updates(request, org, course, name, section):
|
||||
manager = CourseDetails
|
||||
elif section == 'grading':
|
||||
manager = CourseGradingModel
|
||||
else: return
|
||||
else:
|
||||
return
|
||||
|
||||
if request.method == 'GET':
|
||||
# Cannot just do a get w/o knowing the course name :-(
|
||||
@@ -1273,14 +1271,48 @@ def course_advanced_updates(request, org, course, name):
|
||||
location = get_location_and_verify_access(request, org, course, name)
|
||||
|
||||
real_method = get_request_method(request)
|
||||
|
||||
|
||||
if real_method == 'GET':
|
||||
return HttpResponse(json.dumps(CourseMetadata.fetch(location)), mimetype="application/json")
|
||||
elif real_method == 'DELETE':
|
||||
return HttpResponse(json.dumps(CourseMetadata.delete_key(location, json.loads(request.body))), mimetype="application/json")
|
||||
return HttpResponse(json.dumps(CourseMetadata.delete_key(location, json.loads(request.body))),
|
||||
mimetype="application/json")
|
||||
elif real_method == 'POST' or real_method == 'PUT':
|
||||
# NOTE: request.POST is messed up because expect_json cloned_request.POST.copy() is creating a defective entry w/ the whole payload as the key
|
||||
return HttpResponse(json.dumps(CourseMetadata.update_from_json(location, json.loads(request.body))), mimetype="application/json")
|
||||
request_body = json.loads(request.body)
|
||||
#Whether or not to filter the tabs key out of the settings metadata
|
||||
filter_tabs = True
|
||||
#Check to see if the user instantiated any advanced components. This is a hack to add the open ended panel tab
|
||||
#to a course automatically if the user has indicated that they want to edit the combinedopenended or peergrading
|
||||
#module, and to remove it if they have removed the open ended elements.
|
||||
if ADVANCED_COMPONENT_POLICY_KEY in request_body:
|
||||
#Check to see if the user instantiated any open ended components
|
||||
found_oe_type = False
|
||||
#Get the course so that we can scrape current tabs
|
||||
course_module = modulestore().get_item(location)
|
||||
for oe_type in OPEN_ENDED_COMPONENT_TYPES:
|
||||
if oe_type in request_body[ADVANCED_COMPONENT_POLICY_KEY]:
|
||||
#Add an open ended tab to the course if needed
|
||||
changed, new_tabs = add_open_ended_panel_tab(course_module)
|
||||
#If a tab has been added to the course, then send the metadata along to CourseMetadata.update_from_json
|
||||
if changed:
|
||||
request_body.update({'tabs': new_tabs})
|
||||
#Indicate that tabs should not be filtered out of the metadata
|
||||
filter_tabs = False
|
||||
#Set this flag to avoid the open ended tab removal code below.
|
||||
found_oe_type = True
|
||||
break
|
||||
#If we did not find an open ended module type in the advanced settings,
|
||||
# we may need to remove the open ended tab from the course.
|
||||
if not found_oe_type:
|
||||
#Remove open ended tab to the course if needed
|
||||
changed, new_tabs = remove_open_ended_panel_tab(course_module)
|
||||
if changed:
|
||||
request_body.update({'tabs': new_tabs})
|
||||
#Indicate that tabs should not be filtered out of the metadata
|
||||
filter_tabs = False
|
||||
response_json = json.dumps(CourseMetadata.update_from_json(location, request_body, filter_tabs=filter_tabs))
|
||||
return HttpResponse(response_json, mimetype="application/json")
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
@@ -1308,10 +1340,10 @@ def get_checklists(request, org, course, name):
|
||||
if copied or modified:
|
||||
modulestore.update_metadata(location, own_metadata(course_module))
|
||||
return render_to_response('checklists.html',
|
||||
{
|
||||
'context_course': course_module,
|
||||
'checklists': checklists
|
||||
})
|
||||
{
|
||||
'context_course': course_module,
|
||||
'checklists': checklists
|
||||
})
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
@@ -1396,13 +1428,12 @@ def asset_index(request, org, course, name):
|
||||
# sort in reverse upload date order
|
||||
assets = sorted(assets, key=lambda asset: asset['uploadDate'], reverse=True)
|
||||
|
||||
thumbnails = contentstore().get_all_content_thumbnails_for_course(course_reference)
|
||||
asset_display = []
|
||||
for asset in assets:
|
||||
id = asset['_id']
|
||||
display_info = {}
|
||||
display_info['displayname'] = asset['displayname']
|
||||
display_info['uploadDate'] = get_date_display(asset['uploadDate'])
|
||||
display_info['uploadDate'] = get_default_time_display(asset['uploadDate'].timetuple())
|
||||
|
||||
asset_location = StaticContent.compute_location(id['org'], id['course'], id['name'])
|
||||
display_info['url'] = StaticContent.get_url_path_from_location(asset_location)
|
||||
@@ -1490,10 +1521,10 @@ def initialize_course_tabs(course):
|
||||
# This logic is repeated in xmodule/modulestore/tests/factories.py
|
||||
# so if you change anything here, you need to also change it there.
|
||||
course.tabs = [{"type": "courseware"},
|
||||
{"type": "course_info", "name": "Course Info"},
|
||||
{"type": "discussion", "name": "Discussion"},
|
||||
{"type": "wiki", "name": "Wiki"},
|
||||
{"type": "progress", "name": "Progress"}]
|
||||
{"type": "course_info", "name": "Course Info"},
|
||||
{"type": "discussion", "name": "Discussion"},
|
||||
{"type": "wiki", "name": "Wiki"},
|
||||
{"type": "progress", "name": "Progress"}]
|
||||
|
||||
modulestore('direct').update_metadata(course.location.url(), own_metadata(course))
|
||||
|
||||
@@ -1549,7 +1580,10 @@ def import_course(request, org, course, name):
|
||||
shutil.move(r / fname, course_dir)
|
||||
|
||||
module_store, course_items = import_from_xml(modulestore('direct'), settings.GITHUB_REPO_ROOT,
|
||||
[course_subdir], load_error_modules=False, static_content_store=contentstore(), target_location_namespace=Location(location))
|
||||
[course_subdir], load_error_modules=False,
|
||||
static_content_store=contentstore(),
|
||||
target_location_namespace=Location(location),
|
||||
draft_store=modulestore())
|
||||
|
||||
# we can blow this away when we're done importing.
|
||||
shutil.rmtree(course_dir)
|
||||
@@ -1583,8 +1617,8 @@ def generate_export_course(request, org, course, name):
|
||||
|
||||
logging.debug('root = {0}'.format(root_dir))
|
||||
|
||||
export_to_xml(modulestore('direct'), contentstore(), loc, root_dir, name)
|
||||
# filename = root_dir / name + '.tar.gz'
|
||||
export_to_xml(modulestore('direct'), contentstore(), loc, root_dir, name, modulestore())
|
||||
#filename = root_dir / name + '.tar.gz'
|
||||
|
||||
logging.debug('tar file being generated at {0}'.format(export_file.name))
|
||||
tf = tarfile.open(name=export_file.name, mode='w:gz')
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
from xmodule.modulestore.inheritance import own_metadata
|
||||
@@ -6,9 +5,9 @@ import json
|
||||
from json.encoder import JSONEncoder
|
||||
import time
|
||||
from contentstore.utils import get_modulestore
|
||||
from util.converters import jsdate_to_time, time_to_date
|
||||
from models.settings import course_grading
|
||||
from contentstore.utils import update_item
|
||||
from xmodule.fields import Date
|
||||
import re
|
||||
import logging
|
||||
|
||||
@@ -81,8 +80,14 @@ class CourseDetails(object):
|
||||
|
||||
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:
|
||||
converted = jsdate_to_time(jsondict['start_date'])
|
||||
converted = date.from_json(jsondict['start_date'])
|
||||
else:
|
||||
converted = None
|
||||
if converted != descriptor.start:
|
||||
@@ -90,7 +95,7 @@ class CourseDetails(object):
|
||||
descriptor.start = converted
|
||||
|
||||
if 'end_date' in jsondict:
|
||||
converted = jsdate_to_time(jsondict['end_date'])
|
||||
converted = date.from_json(jsondict['end_date'])
|
||||
else:
|
||||
converted = None
|
||||
|
||||
@@ -99,7 +104,7 @@ class CourseDetails(object):
|
||||
descriptor.end = converted
|
||||
|
||||
if 'enrollment_start' in jsondict:
|
||||
converted = jsdate_to_time(jsondict['enrollment_start'])
|
||||
converted = date.from_json(jsondict['enrollment_start'])
|
||||
else:
|
||||
converted = None
|
||||
|
||||
@@ -108,7 +113,7 @@ class CourseDetails(object):
|
||||
descriptor.enrollment_start = converted
|
||||
|
||||
if 'enrollment_end' in jsondict:
|
||||
converted = jsdate_to_time(jsondict['enrollment_end'])
|
||||
converted = date.from_json(jsondict['enrollment_end'])
|
||||
else:
|
||||
converted = None
|
||||
|
||||
@@ -169,7 +174,6 @@ class CourseDetails(object):
|
||||
return result
|
||||
|
||||
|
||||
|
||||
# TODO move to a more general util? Is there a better way to do the isinstance model check?
|
||||
class CourseSettingsEncoder(json.JSONEncoder):
|
||||
def default(self, obj):
|
||||
@@ -178,6 +182,6 @@ class CourseSettingsEncoder(json.JSONEncoder):
|
||||
elif isinstance(obj, Location):
|
||||
return obj.dict()
|
||||
elif isinstance(obj, time.struct_time):
|
||||
return time_to_date(obj)
|
||||
return Date().to_json(obj)
|
||||
else:
|
||||
return JSONEncoder.default(self, obj)
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
from xmodule.modulestore import Location
|
||||
from contentstore.utils import get_modulestore
|
||||
import re
|
||||
from util import converters
|
||||
from datetime import timedelta
|
||||
|
||||
|
||||
@@ -47,14 +45,13 @@ class CourseGradingModel(object):
|
||||
|
||||
# return empty model
|
||||
else:
|
||||
return {
|
||||
"id": index,
|
||||
return {"id": index,
|
||||
"type": "",
|
||||
"min_count": 0,
|
||||
"drop_count": 0,
|
||||
"short_label": None,
|
||||
"weight": 0
|
||||
}
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def fetch_cutoffs(course_location):
|
||||
@@ -97,7 +94,6 @@ class CourseGradingModel(object):
|
||||
|
||||
return CourseGradingModel.fetch(course_location)
|
||||
|
||||
|
||||
@staticmethod
|
||||
def update_grader_from_json(course_location, grader):
|
||||
"""
|
||||
@@ -139,7 +135,6 @@ class CourseGradingModel(object):
|
||||
|
||||
return cutoffs
|
||||
|
||||
|
||||
@staticmethod
|
||||
def update_grace_period_from_json(course_location, graceperiodjson):
|
||||
"""
|
||||
@@ -212,8 +207,7 @@ class CourseGradingModel(object):
|
||||
location = Location(location)
|
||||
|
||||
descriptor = get_modulestore(location).get_item(location)
|
||||
return {
|
||||
"graderType": descriptor.lms.format if descriptor.lms.format is not None else 'Not Graded',
|
||||
return {"graderType": descriptor.lms.format if descriptor.lms.format is not None else 'Not Graded',
|
||||
"location": location,
|
||||
"id": 99 # just an arbitrary value to
|
||||
}
|
||||
@@ -233,7 +227,6 @@ class CourseGradingModel(object):
|
||||
|
||||
get_modulestore(location).update_metadata(location, descriptor._model_data._kvs._metadata)
|
||||
|
||||
|
||||
@staticmethod
|
||||
def convert_set_grace_period(descriptor):
|
||||
# 5 hours 59 minutes 59 seconds => converted to iso format
|
||||
@@ -264,13 +257,12 @@ class CourseGradingModel(object):
|
||||
@staticmethod
|
||||
def parse_grader(json_grader):
|
||||
# manual to clear out kruft
|
||||
result = {
|
||||
"type": json_grader["type"],
|
||||
"min_count": int(json_grader.get('min_count', 0)),
|
||||
"drop_count": int(json_grader.get('drop_count', 0)),
|
||||
"short_label": json_grader.get('short_label', None),
|
||||
"weight": float(json_grader.get('weight', 0)) / 100.0
|
||||
}
|
||||
result = {"type": json_grader["type"],
|
||||
"min_count": int(json_grader.get('min_count', 0)),
|
||||
"drop_count": int(json_grader.get('drop_count', 0)),
|
||||
"short_label": json_grader.get('short_label', None),
|
||||
"weight": float(json_grader.get('weight', 0)) / 100.0
|
||||
}
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ from xmodule.x_module import XModuleDescriptor
|
||||
from xmodule.modulestore.inheritance import own_metadata
|
||||
from xblock.core import Scope
|
||||
from xmodule.course_module import CourseDescriptor
|
||||
import copy
|
||||
|
||||
|
||||
class CourseMetadata(object):
|
||||
@@ -13,8 +14,13 @@ class CourseMetadata(object):
|
||||
The objects have no predefined attrs but instead are obj encodings of the
|
||||
editable metadata.
|
||||
'''
|
||||
FILTERED_LIST = XModuleDescriptor.system_metadata_fields + ['start', 'end',
|
||||
'enrollment_start', 'enrollment_end', 'tabs', 'graceperiod', 'checklists']
|
||||
FILTERED_LIST = XModuleDescriptor.system_metadata_fields + ['start',
|
||||
'end',
|
||||
'enrollment_start',
|
||||
'enrollment_end',
|
||||
'tabs',
|
||||
'graceperiod',
|
||||
'checklists']
|
||||
|
||||
@classmethod
|
||||
def fetch(cls, course_location):
|
||||
@@ -39,7 +45,7 @@ class CourseMetadata(object):
|
||||
return course
|
||||
|
||||
@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.
|
||||
|
||||
@@ -49,9 +55,15 @@ class CourseMetadata(object):
|
||||
|
||||
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():
|
||||
# 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
|
||||
|
||||
if hasattr(descriptor, k) and getattr(descriptor, k) != v:
|
||||
@@ -65,7 +77,7 @@ class CourseMetadata(object):
|
||||
|
||||
if dirty:
|
||||
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,
|
||||
# but I put the reads in as a means to confirm it persisted correctly
|
||||
@@ -86,6 +98,6 @@ class CourseMetadata(object):
|
||||
delattr(descriptor.lms, key)
|
||||
|
||||
get_modulestore(course_location).update_metadata(course_location,
|
||||
own_metadata(descriptor))
|
||||
own_metadata(descriptor))
|
||||
|
||||
return cls.fetch(course_location)
|
||||
|
||||
@@ -36,3 +36,4 @@ DATABASES = {
|
||||
INSTALLED_APPS += ('lettuce.django',)
|
||||
LETTUCE_APPS = ('contentstore',)
|
||||
LETTUCE_SERVER_PORT = 8001
|
||||
LETTUCE_BROWSER = 'chrome'
|
||||
|
||||
@@ -46,6 +46,9 @@ SESSION_COOKIE_DOMAIN = ENV_TOKENS.get('SESSION_COOKIE_DOMAIN')
|
||||
for feature, value in ENV_TOKENS.get('MITX_FEATURES', {}).items():
|
||||
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_env=ENV_TOKENS['LOGGING_ENV'],
|
||||
syslog_addr=(ENV_TOKENS['SYSLOG_SERVER'], 514),
|
||||
@@ -64,4 +67,4 @@ MODULESTORE = AUTH_TOKENS['MODULESTORE']
|
||||
CONTENTSTORE = AUTH_TOKENS['CONTENTSTORE']
|
||||
|
||||
# Datadog for events!
|
||||
DATADOG_API = AUTH_TOKENS.get("DATADOG_API")
|
||||
DATADOG_API = AUTH_TOKENS.get("DATADOG_API")
|
||||
|
||||
@@ -34,6 +34,9 @@ MITX_FEATURES = {
|
||||
'ENABLE_DISCUSSION_SERVICE': False,
|
||||
'AUTH_USE_MIT_CERTIFICATES': False,
|
||||
'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
|
||||
|
||||
@@ -113,6 +116,7 @@ TEMPLATE_LOADERS = (
|
||||
|
||||
MIDDLEWARE_CLASSES = (
|
||||
'contentserver.middleware.StaticContentServer',
|
||||
'request_cache.middleware.RequestCache',
|
||||
'django.middleware.cache.UpdateCacheMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
@@ -163,7 +167,7 @@ STATICFILES_DIRS = [
|
||||
PROJECT_ROOT / "static",
|
||||
|
||||
# This is how you would use the textbook images locally
|
||||
# ("book", ENV_ROOT / "book_images")
|
||||
# ("book", ENV_ROOT / "book_images")
|
||||
]
|
||||
|
||||
# Locale/Internationalization
|
||||
|
||||
@@ -112,6 +112,10 @@ CACHE_TIMEOUT = 0
|
||||
# Dummy secret key for dev
|
||||
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 #################################
|
||||
INSTALLED_APPS += ('debug_toolbar', 'debug_toolbar_mongo')
|
||||
MIDDLEWARE_CLASSES += ('django_comment_client.utils.QueryCountDebugMiddleware',
|
||||
@@ -142,4 +146,10 @@ DEBUG_TOOLBAR_CONFIG = {
|
||||
|
||||
# To see stacktraces for MongoDB queries, set this to True.
|
||||
# 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'
|
||||
|
||||
@@ -13,7 +13,6 @@ from path import path
|
||||
|
||||
# Nose Test Runner
|
||||
INSTALLED_APPS += ('django_nose',)
|
||||
NOSE_ARGS = ['--with-xunit']
|
||||
TEST_RUNNER = 'django_nose.NoseTestSuiteRunner'
|
||||
|
||||
TEST_ROOT = path('test_root')
|
||||
@@ -28,7 +27,7 @@ GITHUB_REPO_ROOT = TEST_ROOT / "data"
|
||||
COMMON_TEST_DATA_ROOT = COMMON_ROOT / "test" / "data"
|
||||
|
||||
# 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
|
||||
STATICFILES_DIRS = [
|
||||
@@ -41,7 +40,7 @@ STATICFILES_DIRS += [
|
||||
if os.path.isdir(COMMON_TEST_DATA_ROOT / course_dir)
|
||||
]
|
||||
|
||||
modulestore_options = {
|
||||
MODULESTORE_OPTIONS = {
|
||||
'default_class': 'xmodule.raw_module.RawDescriptor',
|
||||
'host': 'localhost',
|
||||
'db': 'test_xmodule',
|
||||
@@ -53,11 +52,15 @@ modulestore_options = {
|
||||
MODULESTORE = {
|
||||
'default': {
|
||||
'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore',
|
||||
'OPTIONS': modulestore_options
|
||||
'OPTIONS': MODULESTORE_OPTIONS
|
||||
},
|
||||
'direct': {
|
||||
'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore',
|
||||
'OPTIONS': modulestore_options
|
||||
'OPTIONS': MODULESTORE_OPTIONS
|
||||
},
|
||||
'draft': {
|
||||
'ENGINE': 'xmodule.modulestore.mongo.DraftMongoModuleStore',
|
||||
'OPTIONS': MODULESTORE_OPTIONS
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,7 +75,7 @@ CONTENTSTORE = {
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': ENV_ROOT / "db" / "cms.db",
|
||||
'NAME': TEST_ROOT / "db" / "cms.db",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -114,3 +117,10 @@ PASSWORD_HASHERS = (
|
||||
'django.contrib.auth.hashers.SHA1PasswordHasher',
|
||||
'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
|
||||
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
from dogapi import dog_http_api, dog_stats_api
|
||||
from django.conf import settings
|
||||
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')
|
||||
for store_name in settings.MODULESTORE:
|
||||
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'):
|
||||
dog_http_api.api_key = settings.DATADOG_API
|
||||
dog_stats_api.start(api_key=settings.DATADOG_API, statsd=True)
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
|
||||
<% if (item['action_text'] !== '' && item['action_url'] !== '') { %>
|
||||
<ul class="list-actions task-actions">
|
||||
<li>
|
||||
<li class="action-item">
|
||||
<a href="<%= item['action_url'] %>" class="action action-primary"
|
||||
<% if (item['action_external']) { %>
|
||||
rel="external" title="This link will open in a new browser window/tab"
|
||||
|
||||
@@ -15,7 +15,7 @@ class CMS.Views.ModuleEdit extends Backbone.View
|
||||
$component_editor: => @$el.find('.component-editor')
|
||||
|
||||
loadDisplay: ->
|
||||
XModule.loadModule(@$el.find('.xmodule_display'))
|
||||
XModule.loadModule(@$el.find('.xmodule_display'))
|
||||
|
||||
loadEdit: ->
|
||||
if not @module
|
||||
@@ -55,6 +55,11 @@ class CMS.Views.ModuleEdit extends Backbone.View
|
||||
clickSaveButton: (event) =>
|
||||
event.preventDefault()
|
||||
data = @module.save()
|
||||
|
||||
analytics.track "Saved Module",
|
||||
course: course_location_analytics
|
||||
id: _this.model.id
|
||||
|
||||
data.metadata = _.extend(data.metadata || {}, @metadata())
|
||||
@hideModal()
|
||||
@model.save(data).done( =>
|
||||
|
||||
@@ -28,6 +28,10 @@ class CMS.Views.TabsEdit extends Backbone.View
|
||||
@$('.component').each((idx, element) =>
|
||||
tabs.push($(element).data('id'))
|
||||
)
|
||||
|
||||
analytics.track "Reordered Static Pages",
|
||||
course: course_location_analytics
|
||||
|
||||
$.ajax({
|
||||
type:'POST',
|
||||
url: '/reorder_static_tabs',
|
||||
@@ -56,10 +60,18 @@ class CMS.Views.TabsEdit extends Backbone.View
|
||||
'i4x://edx/templates/static_tab/Empty'
|
||||
)
|
||||
|
||||
analytics.track "Added Static Page",
|
||||
course: course_location_analytics
|
||||
|
||||
deleteTab: (event) =>
|
||||
if not confirm 'Are you sure you want to delete this component? This action cannot be undone.'
|
||||
return
|
||||
$component = $(event.currentTarget).parents('.component')
|
||||
|
||||
analytics.track "Deleted Static Page",
|
||||
course: course_location_analytics
|
||||
id: $component.data('id')
|
||||
|
||||
$.post('/delete_item', {
|
||||
id: $component.data('id')
|
||||
}, =>
|
||||
|
||||
@@ -35,6 +35,10 @@ class CMS.Views.UnitEdit extends Backbone.View
|
||||
@$('.components').sortable(
|
||||
handle: '.drag-handle'
|
||||
update: (event, ui) =>
|
||||
analytics.track "Reordered Components",
|
||||
course: course_location_analytics
|
||||
id: unit_location_analytics
|
||||
|
||||
payload = children : @components()
|
||||
options = success : => @model.unset('children')
|
||||
@model.save(payload, options)
|
||||
@@ -89,6 +93,11 @@ class CMS.Views.UnitEdit extends Backbone.View
|
||||
$(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)
|
||||
|
||||
components: => @$('.component').map((idx, el) -> $(el).data('id')).get()
|
||||
@@ -111,6 +120,11 @@ class CMS.Views.UnitEdit extends Backbone.View
|
||||
$.post('/delete_item', {
|
||||
id: $component.data('id')
|
||||
}, =>
|
||||
analytics.track "Deleted a Component",
|
||||
course: course_location_analytics
|
||||
unit_id: unit_location_analytics
|
||||
id: $component.data('id')
|
||||
|
||||
$component.remove()
|
||||
# b/c we don't vigilantly keep children up to date
|
||||
# get rid of it before it hurts someone
|
||||
@@ -129,6 +143,10 @@ class CMS.Views.UnitEdit extends Backbone.View
|
||||
id: @$el.data('id')
|
||||
delete_children: true
|
||||
}, =>
|
||||
analytics.track "Deleted Draft",
|
||||
course: course_location_analytics
|
||||
unit_id: unit_location_analytics
|
||||
|
||||
window.location.reload()
|
||||
)
|
||||
|
||||
@@ -138,6 +156,10 @@ class CMS.Views.UnitEdit extends Backbone.View
|
||||
$.post('/create_draft', {
|
||||
id: @$el.data('id')
|
||||
}, =>
|
||||
analytics.track "Created Draft",
|
||||
course: course_location_analytics
|
||||
unit_id: unit_location_analytics
|
||||
|
||||
@model.set('state', 'draft')
|
||||
)
|
||||
|
||||
@@ -148,20 +170,31 @@ class CMS.Views.UnitEdit extends Backbone.View
|
||||
$.post('/publish_draft', {
|
||||
id: @$el.data('id')
|
||||
}, =>
|
||||
analytics.track "Published Draft",
|
||||
course: course_location_analytics
|
||||
unit_id: unit_location_analytics
|
||||
|
||||
@model.set('state', 'public')
|
||||
)
|
||||
|
||||
setVisibility: (event) ->
|
||||
if @$('.visibility-select').val() == 'private'
|
||||
target_url = '/unpublish_unit'
|
||||
visibility = "private"
|
||||
else
|
||||
target_url = '/publish_draft'
|
||||
visibility = "public"
|
||||
|
||||
@wait(true)
|
||||
|
||||
$.post(target_url, {
|
||||
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())
|
||||
)
|
||||
|
||||
@@ -193,6 +226,11 @@ class CMS.Views.UnitEdit.NameEdit extends Backbone.View
|
||||
@model.save(metadata: metadata)
|
||||
# Update name shown in the right-hand side location summary.
|
||||
$('.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
|
||||
initialize: =>
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
if (!window.CmsUtils) window.CmsUtils = {};
|
||||
|
||||
var $body;
|
||||
var $modal;
|
||||
var $modalCover;
|
||||
var $newComponentItem;
|
||||
var $changedInput;
|
||||
var $spinner;
|
||||
var $newComponentTypePicker;
|
||||
var $newComponentTemplatePickers;
|
||||
var $newComponentButton;
|
||||
|
||||
$(document).ready(function () {
|
||||
$body = $('body');
|
||||
@@ -34,17 +39,21 @@ $(document).ready(function () {
|
||||
$(this).select();
|
||||
});
|
||||
|
||||
$('body').addClass('js');
|
||||
|
||||
$('.unit .item-actions .delete-button').bind('click', deleteUnit);
|
||||
$('.new-unit-item').bind('click', createNewUnit);
|
||||
|
||||
$('body').addClass('js');
|
||||
|
||||
// lean/simple modal
|
||||
$('a[rel*=modal]').leanModal({overlay : 0.80, closeButton: '.action-modal-close' });
|
||||
$('a.action-modal-close').click(function(e){
|
||||
(e).preventDefault();
|
||||
});
|
||||
|
||||
// alerts/notifications - manual close
|
||||
$('.action-alert-close, .alert.has-actions .nav-actions a').bind('click', hideAlert);
|
||||
$('.action-notification-close').bind('click', hideNotification);
|
||||
|
||||
// nav - dropdown related
|
||||
$body.click(function (e) {
|
||||
$('.nav-dropdown .nav-item .wrapper-nav-sub').removeClass('is-shown');
|
||||
@@ -81,8 +90,13 @@ $(document).ready(function () {
|
||||
});
|
||||
|
||||
// general link management - smooth scrolling page links
|
||||
$('a[rel*="view"]').bind('click', linkSmoothScroll);
|
||||
$('a[rel*="view"][href^="#"]').bind('click', smoothScrollLink);
|
||||
|
||||
// tender feedback window scrolling
|
||||
$('a.show-tender').bind('click', window.CmsUtils.smoothScrollTop);
|
||||
|
||||
// toggling footer additional support
|
||||
$('.cta-show-sock').bind('click', toggleSock);
|
||||
|
||||
// toggling overview section details
|
||||
$(function () {
|
||||
@@ -148,18 +162,33 @@ $(document).ready(function () {
|
||||
});
|
||||
});
|
||||
|
||||
function linkSmoothScroll(e) {
|
||||
function smoothScrollLink(e) {
|
||||
(e).preventDefault();
|
||||
|
||||
$.smoothScroll({
|
||||
offset: -200,
|
||||
easing: 'swing',
|
||||
$.smoothScroll({
|
||||
offset: -200,
|
||||
easing: 'swing',
|
||||
speed: 1000,
|
||||
scrollElement: null,
|
||||
scrollTarget: $(this).attr('href')
|
||||
});
|
||||
}
|
||||
|
||||
// On AWS instances, this base.js gets wrapped in a separate scope as part of Django static
|
||||
// pipelining (note, this doesn't happen on local runtimes). So if we set it on window,
|
||||
// when we can access it from other scopes (namely Course Advanced Settings).
|
||||
window.CmsUtils.smoothScrollTop = function (e) {
|
||||
(e).preventDefault();
|
||||
|
||||
$.smoothScroll({
|
||||
offset: -200,
|
||||
easing: 'swing',
|
||||
speed: 1000,
|
||||
scrollElement: null,
|
||||
scrollTarget: $('#view-top')
|
||||
});
|
||||
}
|
||||
|
||||
function linkNewWindow(e) {
|
||||
window.open($(e.target).attr('href'));
|
||||
e.preventDefault();
|
||||
@@ -196,7 +225,6 @@ function toggleSections(e) {
|
||||
function editSectionPublishDate(e) {
|
||||
e.preventDefault();
|
||||
$modal = $('.edit-subsection-publish-settings').show();
|
||||
$modal = $('.edit-subsection-publish-settings').show();
|
||||
$modal.attr('data-id', $(this).attr('data-id'));
|
||||
$modal.find('.start-date').val($(this).attr('data-date'));
|
||||
$modal.find('.start-time').val($(this).attr('data-time'));
|
||||
@@ -228,7 +256,7 @@ function syncReleaseDate(e) {
|
||||
$("#start_time").val("");
|
||||
}
|
||||
|
||||
function getEdxTimeFromDateTimeVals(date_val, time_val, format) {
|
||||
function getEdxTimeFromDateTimeVals(date_val, time_val) {
|
||||
var edxTimeStr = null;
|
||||
|
||||
if (date_val != '') {
|
||||
@@ -236,24 +264,22 @@ function getEdxTimeFromDateTimeVals(date_val, time_val, format) {
|
||||
time_val = '00:00';
|
||||
|
||||
// Note, we are using date.js utility which has better parsing abilities than the built in JS date parsing
|
||||
date = Date.parse(date_val + " " + time_val);
|
||||
if (format == null)
|
||||
format = 'yyyy-MM-ddTHH:mm';
|
||||
|
||||
edxTimeStr = date.toString(format);
|
||||
var date = Date.parse(date_val + " " + time_val);
|
||||
edxTimeStr = date.toString('yyyy-MM-ddTHH:mm');
|
||||
}
|
||||
|
||||
return edxTimeStr;
|
||||
}
|
||||
|
||||
function getEdxTimeFromDateTimeInputs(date_id, time_id, format) {
|
||||
function getEdxTimeFromDateTimeInputs(date_id, time_id) {
|
||||
var input_date = $('#' + date_id).val();
|
||||
var input_time = $('#' + time_id).val();
|
||||
|
||||
return getEdxTimeFromDateTimeVals(input_date, input_time, format);
|
||||
return getEdxTimeFromDateTimeVals(input_date, input_time);
|
||||
}
|
||||
|
||||
function autosaveInput(e) {
|
||||
var self = this;
|
||||
if (this.saveTimer) {
|
||||
clearTimeout(this.saveTimer);
|
||||
}
|
||||
@@ -261,7 +287,7 @@ function autosaveInput(e) {
|
||||
this.saveTimer = setTimeout(function () {
|
||||
$changedInput = $(e.target);
|
||||
saveSubsection();
|
||||
this.saveTimer = null;
|
||||
self.saveTimer = null;
|
||||
}, 500);
|
||||
}
|
||||
|
||||
@@ -290,10 +316,8 @@ function saveSubsection() {
|
||||
}
|
||||
|
||||
// Piece back together the date/time UI elements into one date/time string
|
||||
// NOTE: our various "date/time" metadata elements don't always utilize the same formatting string
|
||||
// so make sure we're passing back the correct format
|
||||
metadata['start'] = getEdxTimeFromDateTimeInputs('start_date', 'start_time');
|
||||
metadata['due'] = getEdxTimeFromDateTimeInputs('due_date', 'due_time', 'MMMM dd HH:mm');
|
||||
metadata['due'] = getEdxTimeFromDateTimeInputs('due_date', 'due_time');
|
||||
|
||||
$.ajax({
|
||||
url: "/save_item",
|
||||
@@ -303,6 +327,7 @@ function saveSubsection() {
|
||||
data: JSON.stringify({ 'id': id, 'metadata': metadata}),
|
||||
success: function () {
|
||||
$spinner.delay(500).fadeOut(150);
|
||||
$changedInput = null;
|
||||
},
|
||||
error: function () {
|
||||
showToastMessage('There has been an error while saving your changes.');
|
||||
@@ -314,8 +339,14 @@ function saveSubsection() {
|
||||
function createNewUnit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
parent = $(this).data('parent');
|
||||
template = $(this).data('template');
|
||||
var parent = $(this).data('parent');
|
||||
var template = $(this).data('template');
|
||||
|
||||
analytics.track('Created a Unit', {
|
||||
'course': course_location_analytics,
|
||||
'parent_location': parent
|
||||
});
|
||||
|
||||
|
||||
$.post('/clone_item',
|
||||
{'parent_location': parent,
|
||||
@@ -349,6 +380,12 @@ function _deleteItem($el) {
|
||||
|
||||
var id = $el.data('id');
|
||||
|
||||
analytics.track('Deleted an Item', {
|
||||
'course': course_location_analytics,
|
||||
'id': id
|
||||
});
|
||||
|
||||
|
||||
$.post('/delete_item',
|
||||
{'id': id, 'delete_children': true, 'delete_all_versions': true},
|
||||
function (data) {
|
||||
@@ -412,6 +449,11 @@ function displayFinishedUpload(xhr) {
|
||||
var html = Mustache.to_html(template, resp);
|
||||
$('table > tbody').prepend(html);
|
||||
|
||||
analytics.track('Uploaded a File', {
|
||||
'course': course_location_analytics,
|
||||
'asset_url': resp.url
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function markAsLoaded() {
|
||||
@@ -439,6 +481,33 @@ function onKeyUp(e) {
|
||||
}
|
||||
}
|
||||
|
||||
function toggleSock(e) {
|
||||
e.preventDefault();
|
||||
|
||||
var $btnLabel = $(this).find('.copy');
|
||||
var $sock = $('.wrapper-sock');
|
||||
var $sockContent = $sock.find('.wrapper-inner');
|
||||
|
||||
$sock.toggleClass('is-shown');
|
||||
$sockContent.toggle('fast');
|
||||
|
||||
$.smoothScroll({
|
||||
offset: -200,
|
||||
easing: 'swing',
|
||||
speed: 1000,
|
||||
scrollElement: null,
|
||||
scrollTarget: $sock
|
||||
});
|
||||
|
||||
if($sock.hasClass('is-shown')) {
|
||||
$btnLabel.text('Hide Studio Help');
|
||||
}
|
||||
|
||||
else {
|
||||
$btnLabel.text('Looking for Help with Studio?');
|
||||
}
|
||||
}
|
||||
|
||||
function toggleSubmodules(e) {
|
||||
e.preventDefault();
|
||||
$(this).toggleClass('expand').toggleClass('collapse');
|
||||
@@ -477,6 +546,17 @@ function removeDateSetter(e) {
|
||||
$block.find('.time').val('');
|
||||
}
|
||||
|
||||
|
||||
function hideNotification(e) {
|
||||
(e).preventDefault();
|
||||
$(this).closest('.wrapper-notification').removeClass('is-shown').addClass('is-hiding').attr('aria-hidden','true');
|
||||
}
|
||||
|
||||
function hideAlert(e) {
|
||||
(e).preventDefault();
|
||||
$(this).closest('.wrapper-alert').removeClass('is-shown');
|
||||
}
|
||||
|
||||
function showToastMessage(message, $button, lifespan) {
|
||||
var $toast = $('<div class="toast-notification"></div>');
|
||||
var $closeBtn = $('<a href="#" class="close-button">×</a>');
|
||||
@@ -541,6 +621,11 @@ function saveNewSection(e) {
|
||||
var template = $saveButton.data('template');
|
||||
var display_name = $(this).find('.new-section-name').val();
|
||||
|
||||
analytics.track('Created a Section', {
|
||||
'course': course_location_analytics,
|
||||
'display_name': display_name
|
||||
});
|
||||
|
||||
$.post('/clone_item', {
|
||||
'parent_location': parent,
|
||||
'template': template,
|
||||
@@ -586,6 +671,12 @@ function saveNewCourse(e) {
|
||||
return;
|
||||
}
|
||||
|
||||
analytics.track('Created a Course', {
|
||||
'org': org,
|
||||
'number': number,
|
||||
'display_name': display_name
|
||||
});
|
||||
|
||||
$.post('/create_new_course', {
|
||||
'template': template,
|
||||
'org': org,
|
||||
@@ -632,9 +723,14 @@ function saveNewSubsection(e) {
|
||||
|
||||
var parent = $(this).find('.new-subsection-name-save').data('parent');
|
||||
var template = $(this).find('.new-subsection-name-save').data('template');
|
||||
|
||||
var display_name = $(this).find('.new-subsection-name-input').val();
|
||||
|
||||
analytics.track('Created a Subsection', {
|
||||
'course': course_location_analytics,
|
||||
'display_name': display_name
|
||||
});
|
||||
|
||||
|
||||
$.post('/clone_item', {
|
||||
'parent_location': parent,
|
||||
'template': template,
|
||||
@@ -688,6 +784,13 @@ function saveEditSectionName(e) {
|
||||
return;
|
||||
}
|
||||
|
||||
analytics.track('Edited Section Name', {
|
||||
'course': course_location_analytics,
|
||||
'display_name': display_name,
|
||||
'id': id
|
||||
});
|
||||
|
||||
|
||||
var $_this = $(this);
|
||||
// call into server to commit the new order
|
||||
$.ajax({
|
||||
@@ -727,6 +830,12 @@ function saveSetSectionScheduleDate(e) {
|
||||
|
||||
var id = $modal.attr('data-id');
|
||||
|
||||
analytics.track('Edited Section Release Date', {
|
||||
'course': course_location_analytics,
|
||||
'id': id,
|
||||
'start': start
|
||||
});
|
||||
|
||||
// call into server to commit the new order
|
||||
$.ajax({
|
||||
url: "/save_item",
|
||||
@@ -736,7 +845,7 @@ function saveSetSectionScheduleDate(e) {
|
||||
data: JSON.stringify({ 'id': id, 'metadata': {'start': start}})
|
||||
}).success(function () {
|
||||
var $thisSection = $('.courseware-section[data-id="' + id + '"]');
|
||||
$thisSection.find('.section-published-date').html('<span class="published-status"><strong>Will Release:</strong> ' + input_date + ' at ' + input_time + '</span><a href="#" class="edit-button" data-date="' + input_date + '" data-time="' + input_time + '" data-id="' + id + '">Edit</a>');
|
||||
$thisSection.find('.section-published-date').html('<span class="published-status"><strong>Will Release:</strong> ' + input_date + ' at ' + input_time + ' UTC</span><a href="#" class="edit-button" data-date="' + input_date + '" data-time="' + input_time + '" data-id="' + id + '">Edit</a>');
|
||||
$thisSection.find('.section-published-date').animate({
|
||||
'background-color': 'rgb(182,37,104)'
|
||||
}, 300).animate({
|
||||
@@ -749,4 +858,4 @@ function saveSetSectionScheduleDate(e) {
|
||||
|
||||
hideModal();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
// A bit funny in that the video key validation is asynchronous; so, it won't stop the validation.
|
||||
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) {
|
||||
errors.end_date = "The course end date cannot be before the course start date.";
|
||||
}
|
||||
|
||||
@@ -77,11 +77,18 @@ CMS.Views.Checklists = Backbone.View.extend({
|
||||
var task_index = $checkbox.data('task');
|
||||
var model = this.collection.at(checklist_index);
|
||||
model.attributes.items[task_index].is_checked = $task.hasClass(completed);
|
||||
|
||||
model.save({},
|
||||
{
|
||||
success : function() {
|
||||
var updatedTemplate = self.renderTemplate(model, checklist_index);
|
||||
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
|
||||
});
|
||||
|
||||
@@ -107,6 +107,11 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
|
||||
// push change to display, hide the editor, submit the change
|
||||
targetModel.save({}, {error : CMS.ServerError});
|
||||
this.closeEditor(this);
|
||||
|
||||
analytics.track('Saved Course Update', {
|
||||
'course': course_location_analytics,
|
||||
'date': this.dateEntry(event).val()
|
||||
});
|
||||
},
|
||||
|
||||
onCancel: function(event) {
|
||||
@@ -147,6 +152,11 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
|
||||
return;
|
||||
}
|
||||
|
||||
analytics.track('Deleted Course Update', {
|
||||
'course': course_location_analytics,
|
||||
'date': this.dateEntry(event).val()
|
||||
});
|
||||
|
||||
var targetModel = this.eventModel(event);
|
||||
this.modelDom(event).remove();
|
||||
var cacheThis = this;
|
||||
@@ -284,6 +294,11 @@ CMS.Views.ClassInfoHandoutsView = Backbone.View.extend({
|
||||
this.model.save({}, {error: CMS.ServerError});
|
||||
this.$form.hide();
|
||||
this.closeEditor(this);
|
||||
|
||||
analytics.track('Saved Course Handouts', {
|
||||
'course': course_location_analytics
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
onCancel: function(event) {
|
||||
|
||||
@@ -32,7 +32,7 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({
|
||||
|
||||
var listEle$ = this.$el.find('.course-advanced-policy-list');
|
||||
listEle$.empty();
|
||||
|
||||
|
||||
// b/c we've deleted all old fields, clear the map and repopulate
|
||||
this.fieldToSelectorMap = {};
|
||||
this.selectorToField = {};
|
||||
@@ -101,13 +101,13 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({
|
||||
});
|
||||
},
|
||||
showMessage: function (type) {
|
||||
this.$el.find(".message-status").removeClass("is-shown");
|
||||
$(".wrapper-alert").removeClass("is-shown");
|
||||
if (type) {
|
||||
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) {
|
||||
this.$el.find(".message-status.confirm").addClass("is-shown");
|
||||
$(".wrapper-alert-confirmation").addClass("is-shown").attr('aria-hidden','false');
|
||||
this.hideSaveCancelButtons();
|
||||
}
|
||||
}
|
||||
@@ -117,17 +117,20 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({
|
||||
}
|
||||
},
|
||||
showSaveCancelButtons: function(event) {
|
||||
if (!this.buttonsVisible) {
|
||||
if (!this.notificationBarShowing) {
|
||||
this.$el.find(".message-status").removeClass("is-shown");
|
||||
$('.wrapper-notification').addClass('is-shown');
|
||||
this.buttonsVisible = true;
|
||||
$('.wrapper-notification').removeClass('is-hiding').addClass('is-shown').attr('aria-hidden','false');
|
||||
this.notificationBarShowing = true;
|
||||
}
|
||||
},
|
||||
hideSaveCancelButtons: function() {
|
||||
$('.wrapper-notification').removeClass('is-shown');
|
||||
this.buttonsVisible = false;
|
||||
if (this.notificationBarShowing) {
|
||||
$('.wrapper-notification').removeClass('is-shown').addClass('is-hiding').attr('aria-hidden','true');
|
||||
this.notificationBarShowing = false;
|
||||
}
|
||||
},
|
||||
saveView : function(event) {
|
||||
window.CmsUtils.smoothScrollTop(event);
|
||||
// TODO one last verification scan:
|
||||
// call validateKey on each to ensure proper format
|
||||
// check for dupes
|
||||
@@ -137,11 +140,16 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({
|
||||
success : function() {
|
||||
self.render();
|
||||
self.showMessage(self.successful_changes);
|
||||
analytics.track('Saved Advanced Settings', {
|
||||
'course': course_location_analytics
|
||||
});
|
||||
|
||||
},
|
||||
error : CMS.ServerError
|
||||
});
|
||||
},
|
||||
revertView : function(event) {
|
||||
event.preventDefault();
|
||||
var self = event.data;
|
||||
self.model.deleteKeys = [];
|
||||
self.model.clear({silent : true});
|
||||
@@ -154,7 +162,7 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({
|
||||
var newKeyId = _.uniqueId('policy_key_'),
|
||||
newEle = this.template({ key : key, value : JSON.stringify(value, null, 4),
|
||||
keyUniqueId: newKeyId, valueUniqueId: _.uniqueId('policy_value_')});
|
||||
|
||||
|
||||
this.fieldToSelectorMap[key] = newKeyId;
|
||||
this.selectorToField[newKeyId] = key;
|
||||
return newEle;
|
||||
@@ -165,4 +173,4 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({
|
||||
blurInput : function(event) {
|
||||
$(event.target).prev().removeClass("is-focused");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -101,10 +101,16 @@ CMS.Views.Settings.Details = CMS.Views.ValidatingView.extend({
|
||||
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
|
||||
timefield.timepicker();
|
||||
timefield.timepicker({'timeFormat' : 'H:i'});
|
||||
datefield.datepicker();
|
||||
|
||||
// Using the change event causes savefield to be triggered twice, but it is necessary
|
||||
|
||||
@@ -25,14 +25,7 @@ CMS.Views.ValidatingView = Backbone.View.extend({
|
||||
for (var field in error) {
|
||||
var ele = this.$el.find('#' + this.fieldToSelectorMap[field]);
|
||||
this._cacheValidationErrors.push(ele);
|
||||
var inputElements = 'input, textarea';
|
||||
if ($(ele).is(inputElements)) {
|
||||
$(ele).addClass('error');
|
||||
}
|
||||
else {
|
||||
// put error on the contained inputs
|
||||
$(ele).find(inputElements).addClass('error');
|
||||
}
|
||||
this.getInputElements(ele).addClass('error');
|
||||
$(ele).parent().append(this.errorTemplate({message : error[field]}));
|
||||
}
|
||||
},
|
||||
@@ -40,12 +33,8 @@ CMS.Views.ValidatingView = Backbone.View.extend({
|
||||
clearValidationErrors : function() {
|
||||
// error is object w/ fields and error strings
|
||||
while (this._cacheValidationErrors.length > 0) {
|
||||
var ele = this._cacheValidationErrors.pop();
|
||||
if ($(ele).is('div')) {
|
||||
// put error on the contained inputs
|
||||
$(ele).find('input, textarea').removeClass('error');
|
||||
}
|
||||
else $(ele).removeClass('error');
|
||||
var ele = this._cacheValidationErrors.pop();
|
||||
this.getInputElements(ele).removeClass('error');
|
||||
$(ele).nextAll('.message-error').remove();
|
||||
}
|
||||
},
|
||||
@@ -68,5 +57,16 @@ CMS.Views.ValidatingView = Backbone.View.extend({
|
||||
},
|
||||
inputUnfocus : function(event) {
|
||||
$("label[for='" + event.currentTarget.id + "']").removeClass("is-focused");
|
||||
},
|
||||
|
||||
getInputElements: function(ele) {
|
||||
var inputElements = 'input, textarea';
|
||||
if ($(ele).is(inputElements)) {
|
||||
return $(ele);
|
||||
}
|
||||
else {
|
||||
// put error on the contained inputs
|
||||
return $(ele).find(inputElements);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -22,10 +22,10 @@ body, input {
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: $blue;
|
||||
@include transition(color .15s);
|
||||
@include transition(color 0.25s ease-in-out);
|
||||
|
||||
&:hover {
|
||||
color: #cb9c40;
|
||||
color: $orange-d1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
.wrapper-mast {
|
||||
margin: 0;
|
||||
margin: ($baseline*1.5) 0 0 0;
|
||||
padding: 0 $baseline;
|
||||
position: relative;
|
||||
|
||||
|
||||
.mast, .metadata {
|
||||
@include clearfix();
|
||||
@include font-size(16);
|
||||
@@ -272,33 +332,46 @@ h1 {
|
||||
}
|
||||
|
||||
.title-1 {
|
||||
|
||||
@extend .t-title-1;
|
||||
}
|
||||
|
||||
.title-2 {
|
||||
@include font-size(24);
|
||||
@extend .t-title-2;
|
||||
margin: 0 0 ($baseline/2) 0;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.title-3 {
|
||||
@include font-size(16);
|
||||
@extend .t-title-3;
|
||||
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
|
||||
.content-supplementary {
|
||||
|
||||
> section {
|
||||
margin: 0 0 $baseline 0;
|
||||
}
|
||||
|
||||
.bit {
|
||||
@include font-size(13);
|
||||
margin: 0 0 $baseline 0;
|
||||
@@ -351,7 +424,7 @@ h1 {
|
||||
// layout - grandfathered
|
||||
.main-wrapper {
|
||||
position: relative;
|
||||
margin: 40px;
|
||||
margin: 0 ($baseline*2);
|
||||
}
|
||||
|
||||
.inner-wrapper {
|
||||
@@ -644,7 +717,7 @@ hr.divide {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 99999;
|
||||
z-index: 10000;
|
||||
padding: 0 10px;
|
||||
border-radius: 3px;
|
||||
background: rgba(0, 0, 0, 0.85);
|
||||
@@ -764,10 +837,10 @@ body.js {
|
||||
|
||||
// ====================
|
||||
|
||||
// works in progress
|
||||
// works in progress & testing
|
||||
body.hide-wip {
|
||||
|
||||
.wip-box {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// studio - utilities - mixins and extends
|
||||
// ====================
|
||||
|
||||
// mixins - utility
|
||||
@mixin clearfix {
|
||||
&:after {
|
||||
content: '';
|
||||
@@ -11,19 +12,20 @@
|
||||
}
|
||||
}
|
||||
|
||||
// mixins - grandfathered
|
||||
@mixin button {
|
||||
display: inline-block;
|
||||
padding: 4px 20px 6px;
|
||||
font-size: 14px;
|
||||
padding: ($baseline/5) $baseline ($baseline/4);
|
||||
@include font-size(14);
|
||||
font-weight: 700;
|
||||
@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);
|
||||
|
||||
&.disabled {
|
||||
border: 1px solid $lightGrey !important;
|
||||
border: 1px solid $gray-l1 !important;
|
||||
border-radius: 3px !important;
|
||||
background: $lightGrey !important;
|
||||
color: $darkGrey !important;
|
||||
background: $gray-l1 !important;
|
||||
color: $gray-d1 !important;
|
||||
pointer-events: none;
|
||||
cursor: none;
|
||||
&:hover {
|
||||
@@ -36,32 +38,111 @@
|
||||
}
|
||||
}
|
||||
|
||||
@mixin blue-button {
|
||||
@mixin green-button {
|
||||
@include button;
|
||||
border: 1px solid #437fbf;
|
||||
border: 1px solid $green-d1;
|
||||
border-radius: 3px;
|
||||
@include linear-gradient(top, rgba(255, 255, 255, .3), rgba(255, 255, 255, 0));
|
||||
background-color: $blue;
|
||||
color: #fff;
|
||||
background-color: $green;
|
||||
@include box-shadow(0 1px 0 rgba(255, 255, 255, .3) inset);
|
||||
color: $white;
|
||||
|
||||
&:hover, &.active {
|
||||
background-color: #62aaf5;
|
||||
color: #fff;
|
||||
&: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 green-button {
|
||||
@include button;
|
||||
border: 1px solid #0d7011;
|
||||
border-radius: 3px;
|
||||
@include linear-gradient(top, rgba(255, 255, 255, .3), rgba(255, 255, 255, 0));
|
||||
background-color: $green;
|
||||
color: #fff;
|
||||
@mixin blue-button {
|
||||
@include button;
|
||||
border: 1px solid $blue-d1;
|
||||
border-radius: 3px;
|
||||
@include linear-gradient(top, rgba(255, 255, 255, .3), rgba(255, 255, 255, 0));
|
||||
background-color: $blue;
|
||||
color: $white;
|
||||
|
||||
&:hover {
|
||||
background-color: #129416;
|
||||
color: #fff;
|
||||
}
|
||||
&:hover, &.active {
|
||||
background-color: $blue-s2;
|
||||
color: $white;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
border: 1px solid $blue-l3 !important;
|
||||
background: $blue-l3 !important;
|
||||
color: $white !important;
|
||||
@include box-shadow(none);
|
||||
}
|
||||
}
|
||||
|
||||
@mixin red-button {
|
||||
@include button;
|
||||
border: 1px solid $red-d1;
|
||||
border-radius: 3px;
|
||||
@include linear-gradient(top, rgba(255, 255, 255, .3), rgba(255, 255, 255, 0));
|
||||
background-color: $red;
|
||||
color: $white;
|
||||
|
||||
&:hover, &.active {
|
||||
background-color: $red-s1;
|
||||
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 {
|
||||
@@ -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 {
|
||||
@include button;
|
||||
border: 1px solid $darkGrey;
|
||||
border: 1px solid $gray-d2;
|
||||
border-radius: 3px;
|
||||
@include linear-gradient(top, rgba(255, 255, 255, .3), rgba(255, 255, 255, 0));
|
||||
background-color: #d1dae3;
|
||||
@@ -110,39 +176,32 @@
|
||||
}
|
||||
}
|
||||
|
||||
@mixin green-button {
|
||||
@mixin gray-button {
|
||||
@include button;
|
||||
border: 1px solid $darkGreen;
|
||||
border: 1px solid $gray-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: #fff;
|
||||
@include linear-gradient(top, $white-t1, rgba(255, 255, 255, 0));
|
||||
background-color: $gray-d2;
|
||||
@include box-shadow(0 1px 0 $white-t1 inset);
|
||||
color: $gray-l3;
|
||||
|
||||
&:hover {
|
||||
background-color: $brightGreen;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
border: 1px solid $disabledGreen !important;
|
||||
background: $disabledGreen !important;
|
||||
color: #fff !important;
|
||||
@include box-shadow(none);
|
||||
background-color: $gray-d3;
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin dark-grey-button {
|
||||
@include button;
|
||||
border: 1px solid #1c1e20;
|
||||
border: 1px solid $gray-d2;
|
||||
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;
|
||||
color: #fff;
|
||||
color: $white;
|
||||
|
||||
&:hover {
|
||||
background-color: #595f64;
|
||||
color: #fff;
|
||||
background-color: $gray-d4;
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,7 +222,7 @@
|
||||
}
|
||||
|
||||
textarea {
|
||||
min-height: 80px;
|
||||
min-height: 80px;
|
||||
}
|
||||
|
||||
h5 {
|
||||
@@ -208,7 +267,7 @@
|
||||
.section-item {
|
||||
position: relative;
|
||||
display: block;
|
||||
padding: 6px 8px 8px 16px;
|
||||
padding: 6px 8px 8px 16px;
|
||||
background: #edf1f5;
|
||||
font-size: 13px;
|
||||
|
||||
@@ -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 {
|
||||
@include linear-gradient(top, rgba(255, 255, 255, .4), rgba(255, 255, 255, 0));
|
||||
background-color: rgba(255, 255, 255, .3);
|
||||
@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);
|
||||
}
|
||||
}
|
||||
|
||||
// ====================
|
||||
|
||||
// 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; }
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// studio - utilities - reset
|
||||
// ====================
|
||||
|
||||
// not ready for this yet, but this should be done as things get cleaner
|
||||
// * {
|
||||
// @include box-sizing(border-box);
|
||||
// }
|
||||
@@ -26,6 +27,10 @@ time, mark, audio, video {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
html,body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
article, aside, details, figcaption, figure,
|
||||
footer, header, hgroup, menu, nav, section {
|
||||
display: block;
|
||||
@@ -57,6 +62,12 @@ table {
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
abbr[title] {
|
||||
border-bottom: none;
|
||||
text-decoration: none;
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
// ====================
|
||||
|
||||
// grandfathered styles
|
||||
|
||||
@@ -22,6 +22,7 @@ $black-t0: rgba(0,0,0,0.125);
|
||||
$black-t1: rgba(0,0,0,0.25);
|
||||
$black-t2: rgba(0,0,0,0.50);
|
||||
$black-t3: rgba(0,0,0,0.75);
|
||||
|
||||
$white: rgb(255,255,255);
|
||||
$white-t0: rgba(255,255,255,0.125);
|
||||
$white-t1: rgba(255,255,255,0.25);
|
||||
@@ -56,6 +57,10 @@ $blue-s3: saturate($blue,45%);
|
||||
$blue-u1: desaturate($blue,15%);
|
||||
$blue-u2: desaturate($blue,30%);
|
||||
$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-l1: tint($pink,20%);
|
||||
@@ -108,7 +113,7 @@ $green-u1: desaturate($green,15%);
|
||||
$green-u2: desaturate($green,30%);
|
||||
$green-u3: desaturate($green,45%);
|
||||
|
||||
$yellow: rgb(231, 214, 143);
|
||||
$yellow: rgb(237, 189, 60);
|
||||
$yellow-l1: tint($yellow,20%);
|
||||
$yellow-l2: tint($yellow,40%);
|
||||
$yellow-l3: tint($yellow,60%);
|
||||
@@ -144,10 +149,15 @@ $orange-u3: desaturate($orange,45%);
|
||||
|
||||
$shadow: rgba(0,0,0,0.2);
|
||||
$shadow-l1: rgba(0,0,0,0.1);
|
||||
$shadow-l2: rgba(0,0,0,0.05);
|
||||
$shadow-d1: rgba(0,0,0,0.4);
|
||||
|
||||
|
||||
// specific UI
|
||||
$notification-height: ($baseline*10);
|
||||
|
||||
// colors - inherited
|
||||
$baseFontColor: #3c3c3c;
|
||||
$baseFontColor: $gray-d2;
|
||||
$offBlack: #3c3c3c;
|
||||
$green: #108614;
|
||||
$lightGrey: #edf1f5;
|
||||
|
||||
@@ -1,7 +1,133 @@
|
||||
@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% {
|
||||
opacity: 0;
|
||||
@include transform(scale(.3));
|
||||
@include transform(scale(0.3));
|
||||
}
|
||||
|
||||
50% {
|
||||
@@ -14,14 +140,63 @@
|
||||
}
|
||||
}
|
||||
|
||||
@-moz-keyframes bounce-in { @include bounce-in(); }
|
||||
@-webkit-keyframes bounce-in { @include bounce-in(); }
|
||||
@-o-keyframes bounce-in { @include bounce-in(); }
|
||||
@keyframes bounce-in { @include bounce-in();}
|
||||
@-moz-keyframes bounceIn { @include bounceIn(); }
|
||||
@-webkit-keyframes bounceIn { @include bounceIn(); }
|
||||
@-o-keyframes bounceIn { @include bounceIn(); }
|
||||
@keyframes bounceIn { @include bounceIn();}
|
||||
|
||||
@mixin bounce-in-animation($duration, $timing: ease-in-out) {
|
||||
@include animation-name(bounce-in);
|
||||
@mixin anim-bounceIn($duration, $timing: ease-in-out, $count: 1, $delay: 0) {
|
||||
@include animation-name(bounceIn);
|
||||
@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 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);
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
// bourbon libs and resets
|
||||
@import 'bourbon/bourbon';
|
||||
@import 'bourbon/addons/button';
|
||||
@import "variables";
|
||||
@import 'vendor/normalize';
|
||||
@import 'reset';
|
||||
|
||||
@@ -21,13 +22,18 @@
|
||||
@import 'base';
|
||||
|
||||
// elements
|
||||
@import 'elements/typography';
|
||||
@import 'elements/icons';
|
||||
@import 'elements/controls';
|
||||
@import 'elements/navigation';
|
||||
@import 'elements/header';
|
||||
@import 'elements/footer';
|
||||
@import 'elements/navigation';
|
||||
@import 'elements/sock';
|
||||
@import 'elements/forms';
|
||||
@import 'elements/modal';
|
||||
@import 'elements/alerts';
|
||||
@import 'elements/jquery-ui-calendar';
|
||||
@import 'elements/vendor';
|
||||
@import 'elements/tender-widget';
|
||||
|
||||
// specific views
|
||||
@import 'views/account';
|
||||
|
||||
@@ -1,135 +1,761 @@
|
||||
// studio - elements - alerts, notifications, prompts
|
||||
// studio alerts, prompts and notifications
|
||||
// ====================
|
||||
|
||||
// shared
|
||||
.wrapper-notification, .wrapper-alert, .prompt {
|
||||
@include box-sizing(border-box);
|
||||
|
||||
.copy {
|
||||
@include font-size(13);
|
||||
}
|
||||
}
|
||||
|
||||
.wrapper-notification, .wrapper-alert, .prompt {
|
||||
background: $gray-d3;
|
||||
|
||||
.copy {
|
||||
color: $gray-l2;
|
||||
|
||||
.title {
|
||||
color: $white;
|
||||
}
|
||||
|
||||
.nav-actions {
|
||||
|
||||
.action-primary {
|
||||
color: $gray-d4;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.alert, .notification, .prompt {
|
||||
|
||||
// types - confirm
|
||||
&.confirm {
|
||||
|
||||
.nav-actions .action-primary {
|
||||
@include blue-button();
|
||||
border-color: $blue-d2;
|
||||
}
|
||||
|
||||
a {
|
||||
color: $blue;
|
||||
|
||||
&:hover {
|
||||
color: $blue-s2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// types - warning
|
||||
&.warning {
|
||||
|
||||
.nav-actions .action-primary {
|
||||
@include orange-button();
|
||||
border-color: $orange-d2;
|
||||
color: $gray-d4;
|
||||
}
|
||||
|
||||
a {
|
||||
color: $orange;
|
||||
|
||||
&:hover {
|
||||
color: $orange-s2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// types - error
|
||||
&.error {
|
||||
|
||||
.nav-actions .action-primary {
|
||||
@include red-button();
|
||||
border-color: $red-d2;
|
||||
}
|
||||
|
||||
a {
|
||||
color: $red-l1;
|
||||
|
||||
&:hover {
|
||||
color: $red;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// types - announcement
|
||||
&.announcement {
|
||||
|
||||
.nav-actions .action-primary {
|
||||
@include blue-button();
|
||||
border-color: $blue-d2;
|
||||
}
|
||||
|
||||
a {
|
||||
color: $blue;
|
||||
|
||||
&:hover {
|
||||
color: $blue-s2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// types - confirmation
|
||||
&.confirmation {
|
||||
|
||||
.nav-actions .action-primary {
|
||||
@include green-button();
|
||||
border-color: $green-d2;
|
||||
}
|
||||
|
||||
a {
|
||||
color: $green;
|
||||
|
||||
&:hover {
|
||||
color: $green-s2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// types - step required
|
||||
&.step-required {
|
||||
|
||||
.nav-actions .action-primary {
|
||||
border-color: $pink-d2;
|
||||
@include pink-button();
|
||||
}
|
||||
|
||||
a {
|
||||
color: $pink;
|
||||
|
||||
&:hover {
|
||||
color: $pink-s1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// prompts
|
||||
.wrapper-prompt {
|
||||
@include transition(all 0.05s ease-in-out);
|
||||
position: fixed;
|
||||
top: 0;
|
||||
background: $black-t0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
z-index: 10000;
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
height: 100%;
|
||||
vertical-align: middle;
|
||||
margin-right: -0.25em; /* Adjusts for spacing */
|
||||
}
|
||||
|
||||
.prompt {
|
||||
@include border-radius(($baseline/5));
|
||||
@include box-shadow(0 0 3px $shadow-d1);
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
width: $baseline*17.5;
|
||||
border: 4px solid $black;
|
||||
text-align: left;
|
||||
|
||||
.copy {
|
||||
border-top: 4px solid $blue;
|
||||
padding: $baseline;
|
||||
}
|
||||
|
||||
.nav-actions {
|
||||
@include box-shadow(inset 0 1px 2px $shadow-d1);
|
||||
border-top: 1px solid $black-t1;
|
||||
padding: ($baseline*0.75) $baseline;
|
||||
background: $gray-d4;
|
||||
|
||||
.nav-item {
|
||||
display: inline-block;
|
||||
margin-right: ($baseline*0.75);
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.action-primary {
|
||||
@include font-size(13);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.action-secondary {
|
||||
@include font-size(13);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// types of prompts - error
|
||||
.prompt.error {
|
||||
|
||||
.icon-error {
|
||||
color: $red-l1;
|
||||
}
|
||||
|
||||
.copy {
|
||||
border-top-color: $red-l1;
|
||||
}
|
||||
}
|
||||
|
||||
// types of prompts - confirmation
|
||||
.prompt.confirmation {
|
||||
|
||||
.icon-error {
|
||||
color: $green;
|
||||
}
|
||||
|
||||
.copy {
|
||||
border-top-color: $green;
|
||||
}
|
||||
}
|
||||
|
||||
// types of prompts - error
|
||||
.prompt.warning {
|
||||
|
||||
.icon-warning {
|
||||
color: $orange;
|
||||
}
|
||||
|
||||
.copy {
|
||||
border-top-color: $orange;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ====================
|
||||
|
||||
// notifications
|
||||
.wrapper-notification {
|
||||
@include clearfix();
|
||||
@include box-sizing(border-box);
|
||||
@include transition (bottom 2.0s ease-in-out 5s);
|
||||
@include box-shadow(0 -1px 2px rgba(0,0,0,0.1));
|
||||
position: fixed;
|
||||
bottom: -100px;
|
||||
z-index: 1000;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
opacity: 0;
|
||||
border-top: 1px solid $darkGrey;
|
||||
padding: 20px 40px;
|
||||
@include clearfix();
|
||||
@include box-shadow(0 -1px 3px $shadow, inset 0 3px 1px $blue);
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
z-index: 1000;
|
||||
width: 100%;
|
||||
padding: $baseline ($baseline*2);
|
||||
|
||||
&.is-shown {
|
||||
bottom: 0;
|
||||
opacity: 1.0;
|
||||
}
|
||||
&.wrapper-notification-warning {
|
||||
@include box-shadow(0 -1px 3px $shadow, inset 0 3px 1px $orange);
|
||||
|
||||
&.wrapper-notification-warning {
|
||||
border-color: shade($yellow, 25%);
|
||||
background: tint($yellow, 25%);
|
||||
}
|
||||
.icon-warning {
|
||||
color: $orange;
|
||||
}
|
||||
}
|
||||
|
||||
&.wrapper-notification-error {
|
||||
border-color: shade($red, 50%);
|
||||
background: tint($red, 20%);
|
||||
color: $white;
|
||||
}
|
||||
&.wrapper-notification-error {
|
||||
@include box-shadow(0 -1px 3px $shadow, inset 0 3px 1px $red-l1);
|
||||
|
||||
&.wrapper-notification-confirm {
|
||||
border-color: shade($green, 30%);
|
||||
background: tint($green, 40%);
|
||||
color: shade($green, 30%);
|
||||
}
|
||||
.icon-error {
|
||||
color: $red-l1;
|
||||
}
|
||||
}
|
||||
|
||||
&.wrapper-notification-confirmation {
|
||||
@include box-shadow(0 -1px 3px $shadow, inset 0 3px 1px $green);
|
||||
|
||||
.icon-confirmation {
|
||||
color: $green;
|
||||
}
|
||||
}
|
||||
|
||||
&.wrapper-notification-saving {
|
||||
@include box-shadow(0 -1px 3px $shadow, inset 0 3px 1px $pink);
|
||||
}
|
||||
|
||||
// shorter/status notifications
|
||||
&.wrapper-notification-status {
|
||||
@include border-top-radius(3px);
|
||||
width: ($baseline*8);
|
||||
right: ($baseline);
|
||||
border: 4px solid $black;
|
||||
border-bottom: none;
|
||||
padding: ($baseline/2) $baseline;
|
||||
|
||||
.notification {
|
||||
@include box-sizing(border-box);
|
||||
@include clearfix();
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
min-width: none;
|
||||
|
||||
.icon, .copy {
|
||||
float: none;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: $baseline;
|
||||
height: ($baseline*1.25);
|
||||
margin-right: ($baseline*0.75);
|
||||
line-height: 3rem;
|
||||
}
|
||||
|
||||
.copy {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// help notifications
|
||||
&.wrapper-notification-help {
|
||||
@include border-top-radius(3px);
|
||||
width: ($baseline*14);
|
||||
right: ($baseline);
|
||||
border: 4px solid $black;
|
||||
border-bottom: none;
|
||||
padding: $baseline;
|
||||
|
||||
.notification {
|
||||
@include box-sizing(border-box);
|
||||
@include clearfix();
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
min-width: none;
|
||||
|
||||
.icon-help {
|
||||
width: $baseline;
|
||||
margin-right: ($baseline*0.75);
|
||||
}
|
||||
|
||||
.action-notification-close {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.copy {
|
||||
width: ($baseline*10);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.notification {
|
||||
@include box-sizing(border-box);
|
||||
margin: 0 auto;
|
||||
width: flex-grid(12);
|
||||
max-width: $fg-max-width;
|
||||
min-width: $fg-min-width;
|
||||
@include box-sizing(border-box);
|
||||
@include clearfix();
|
||||
margin: 0 auto;
|
||||
width: flex-grid(12);
|
||||
max-width: $fg-max-width;
|
||||
min-width: $fg-min-width;
|
||||
|
||||
.copy {
|
||||
float: left;
|
||||
width: flex-grid(9, 12);
|
||||
margin-right: flex-gutter();
|
||||
margin-top: 5px;
|
||||
font-size: 14px;
|
||||
strong {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.icon {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
margin-right: 5px;
|
||||
font-size: 20px;
|
||||
}
|
||||
.icon, .copy {
|
||||
float: left;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
p {
|
||||
width: flex-grid(8, 9);
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
}
|
||||
.icon {
|
||||
@include transition (color 0.5s ease-in-out);
|
||||
@include font-size(22);
|
||||
width: flex-grid(1, 12);
|
||||
height: ($baseline*1.25);
|
||||
margin-right: flex-gutter();
|
||||
text-align: right;
|
||||
color: $white;
|
||||
}
|
||||
|
||||
.actions {
|
||||
float: right;
|
||||
width: flex-grid(3, 12);
|
||||
margin-top: ($baseline/2);
|
||||
text-align: right;
|
||||
.copy {
|
||||
@include font-size(13);
|
||||
width: flex-grid(10, 12);
|
||||
color: $gray-l2;
|
||||
|
||||
li {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin-right: 10px;
|
||||
.title {
|
||||
@include font-size(14);
|
||||
margin-bottom: 0;
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
// with actions
|
||||
&.has-actions {
|
||||
|
||||
.save-button {
|
||||
@include blue-button;
|
||||
}
|
||||
.icon {
|
||||
width: flex-grid(1, 12);
|
||||
}
|
||||
|
||||
.cancel-button {
|
||||
@include white-button;
|
||||
}
|
||||
}
|
||||
.copy {
|
||||
width: flex-grid(7, 12);
|
||||
margin-right: flex-gutter();
|
||||
}
|
||||
|
||||
strong {
|
||||
font-weight: 700;
|
||||
}
|
||||
.nav-actions {
|
||||
width: flex-grid(4, 12);
|
||||
float: right;
|
||||
margin-top: ($baseline/4);
|
||||
text-align: right;
|
||||
|
||||
.nav-item {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin-right: ($baseline/2);
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.action-primary {
|
||||
@include blue-button();
|
||||
@include font-size(13);
|
||||
border-color: $blue-d2;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.action-secondary {
|
||||
@include font-size(13);
|
||||
}
|
||||
}
|
||||
|
||||
&.confirmation {
|
||||
|
||||
.copy {
|
||||
margin-top: ($baseline/5);
|
||||
}
|
||||
}
|
||||
|
||||
&.saving {
|
||||
|
||||
.icon-saving {
|
||||
@include anim-rotateClockwise(3s, linear, infinite);
|
||||
width: 22px;
|
||||
}
|
||||
|
||||
.copy p {
|
||||
@include text-sr();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ====================
|
||||
|
||||
// alerts
|
||||
.wrapper-alert {
|
||||
@include box-sizing(border-box);
|
||||
@include box-shadow(0 1px 1px $white, inset 0 2px 2px $shadow-d1, inset 0 -4px 1px $blue);
|
||||
position: relative;
|
||||
z-index: 100;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
border-top: 1px solid $black;
|
||||
padding: $baseline ($baseline*2) ($baseline*1.5) ($baseline*2);
|
||||
background: $gray-d3;
|
||||
|
||||
// needed since page load is very slow
|
||||
display: none;
|
||||
|
||||
// needed since page load is very slow
|
||||
&.is-shown {
|
||||
display: block;
|
||||
}
|
||||
|
||||
&.wrapper-alert-warning {
|
||||
@include box-shadow(0 1px 1px $white, inset 0 2px 2px $shadow-d1, inset 0 -4px 1px $orange);
|
||||
|
||||
.icon-warning {
|
||||
color: $orange;
|
||||
}
|
||||
}
|
||||
|
||||
&.wrapper-alert-error {
|
||||
@include box-shadow(0 1px 1px $white, inset 0 2px 2px $shadow-d1, inset 0 -4px 1px $red-l1);
|
||||
|
||||
.icon-error {
|
||||
color: $red-l1;
|
||||
}
|
||||
}
|
||||
|
||||
&.wrapper-alert-confirmation {
|
||||
@include box-shadow(0 1px 1px $white, inset 0 2px 2px $shadow-d1, inset 0 -4px 1px $green);
|
||||
|
||||
.icon-confirmation {
|
||||
color: $green;
|
||||
}
|
||||
}
|
||||
|
||||
&.wrapper-alert-announcement {
|
||||
@include box-shadow(0 1px 1px $white, inset 0 2px 2px $shadow-d1, inset 0 -4px 1px $blue);
|
||||
|
||||
.icon-announcement {
|
||||
color: $blue;
|
||||
}
|
||||
}
|
||||
|
||||
&.wrapper-alert-step-required {
|
||||
@include box-shadow(0 1px 1px $white, inset 0 2px 2px $shadow-d1, inset 0 -4px 1px $pink);
|
||||
|
||||
.icon-step-required {
|
||||
color: $pink;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// adopted alerts
|
||||
.alert {
|
||||
padding: 15px 20px;
|
||||
margin-bottom: 30px;
|
||||
border-radius: 3px;
|
||||
border: 1px solid #edbd3c;
|
||||
border-radius: 3px;
|
||||
background: #fbf6e1;
|
||||
// background: #edbd3c;
|
||||
font-size: 14px;
|
||||
@include clearfix;
|
||||
@include font-size(14);
|
||||
@include box-sizing(border-box);
|
||||
@include clearfix();
|
||||
margin: 0 auto;
|
||||
width: flex-grid(12);
|
||||
max-width: $fg-max-width;
|
||||
min-width: $fg-min-width;
|
||||
color: $white;
|
||||
|
||||
.alert-message {
|
||||
float: left;
|
||||
margin-top: 4px;
|
||||
}
|
||||
strong {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
strong {
|
||||
font-weight: 700;
|
||||
}
|
||||
.icon, .copy {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.alert-action {
|
||||
float: right;
|
||||
.icon {
|
||||
@include transition (color 0.5s ease-in-out);
|
||||
@include font-size(22);
|
||||
width: flex-grid(1, 12);
|
||||
margin: ($baseline/4) flex-gutter() 0 0;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
&.secondary {
|
||||
@include orange-button;
|
||||
}
|
||||
}
|
||||
.copy {
|
||||
@include font-size(13);
|
||||
width: flex-grid(10, 12);
|
||||
color: $gray-l2;
|
||||
|
||||
.title {
|
||||
margin-bottom: 0;
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
|
||||
// with actions
|
||||
&.has-actions {
|
||||
|
||||
.icon {
|
||||
width: flex-grid(1, 12);
|
||||
}
|
||||
|
||||
.copy {
|
||||
width: flex-grid(7, 12);
|
||||
margin-right: flex-gutter();
|
||||
}
|
||||
|
||||
.nav-actions {
|
||||
width: flex-grid(4, 12);
|
||||
float: right;
|
||||
margin-top: ($baseline/2);
|
||||
text-align: right;
|
||||
|
||||
.nav-item {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin-right: ($baseline/2);
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.action-primary {
|
||||
@include font-size(13);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.action-secondary {
|
||||
@include font-size(13);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// with cancel
|
||||
.action-alert-close {
|
||||
@include border-bottom-radius(($baseline/5));
|
||||
position: absolute;
|
||||
top: -($baseline/10);
|
||||
right: $baseline;
|
||||
padding: ($baseline/4) ($baseline/2) 0 ($baseline/2);
|
||||
background: $gray-d4;
|
||||
text-align: center;
|
||||
|
||||
.label {
|
||||
@include text-sr();
|
||||
}
|
||||
|
||||
.icon {
|
||||
@include font-size(14);
|
||||
color: $white;
|
||||
width: auto;
|
||||
margin: 0;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: $gray-d1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ====================
|
||||
|
||||
// js enabled
|
||||
.js {
|
||||
|
||||
// prompt set-up
|
||||
.wrapper-prompt {
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
|
||||
.prompt {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// prompt showing/hiding
|
||||
&.prompt-is-shown {
|
||||
|
||||
.wrapper-view {
|
||||
-webkit-filter: blur(2px) grayscale(25%);
|
||||
filter: blur(2px) grayscale(25%);
|
||||
}
|
||||
|
||||
.wrapper-prompt.is-shown {
|
||||
visibility: visible;
|
||||
pointer-events: auto;
|
||||
|
||||
.prompt {
|
||||
@include anim-bounceIn(0.5s);
|
||||
opacity: 1.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// alert showing/hiding
|
||||
.wrapper-alert {
|
||||
display: none;
|
||||
|
||||
&.is-shown {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
// notification showing/hiding
|
||||
.wrapper-notification {
|
||||
bottom: -($notification-height);
|
||||
|
||||
// varying animations
|
||||
&.is-shown {
|
||||
@include anim-notificationsSlideUp(1s);
|
||||
}
|
||||
|
||||
&.is-hiding {
|
||||
@include anim-notificationsSlideDown(0.25s);
|
||||
}
|
||||
|
||||
&.is-fleeting {
|
||||
@include anim-notificationsSlideUpDown(2s);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ====================
|
||||
|
||||
// temporary
|
||||
body.uxdesign.alerts {
|
||||
|
||||
.content-primary, .content-supplementary {
|
||||
@include box-sizing(border-box);
|
||||
float: left;
|
||||
}
|
||||
|
||||
.content-primary {
|
||||
@extend .window;
|
||||
width: flex-grid(12, 12);
|
||||
margin-right: flex-gutter();
|
||||
padding: $baseline ($baseline*1.5);
|
||||
|
||||
> section {
|
||||
margin-bottom: ($baseline*2);
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
|
||||
li {
|
||||
@include clearfix();
|
||||
width: flex-grid(12, 12);
|
||||
margin-bottom: ($baseline/4);
|
||||
border-bottom: 1px solid $gray-l4;
|
||||
padding-bottom: ($baseline/4);
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
border-bottom: none;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
a {
|
||||
float: left;
|
||||
width: flex-grid(5, 12);
|
||||
margin-right: flex-gutter();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ====================
|
||||
|
||||
// artifact styles
|
||||
.main-wrapper {
|
||||
.alert {
|
||||
padding: 15px 20px;
|
||||
margin-bottom: 30px;
|
||||
border-radius: 3px;
|
||||
border: 1px solid #edbd3c;
|
||||
border-radius: 3px;
|
||||
background: #fbf6e1;
|
||||
// background: #edbd3c;
|
||||
font-size: 14px;
|
||||
@include clearfix;
|
||||
|
||||
.alert-message {
|
||||
float: left;
|
||||
margin: 4px 0 0 0;
|
||||
color: $gray-d3;
|
||||
}
|
||||
|
||||
strong {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.alert-action {
|
||||
float: right;
|
||||
|
||||
&.secondary {
|
||||
@include orange-button;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
body.error {
|
||||
background: $darkGrey;
|
||||
color: #3c3c3c;
|
||||
background: $gray-d4;
|
||||
color: $gray-d3;
|
||||
|
||||
.primary-header {
|
||||
display: none;
|
||||
@@ -140,7 +766,7 @@ body.error {
|
||||
margin: 150px auto;
|
||||
padding: 60px 50px 90px;
|
||||
border-radius: 3px;
|
||||
background: #fff;
|
||||
background: $white;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@@ -148,8 +774,8 @@ body.error {
|
||||
float: none;
|
||||
margin: 0;
|
||||
font-size: 60px;
|
||||
font-weight: 300;
|
||||
color: #3c3c3c;
|
||||
font-weight: 300;
|
||||
color: $gray-d3;
|
||||
}
|
||||
|
||||
.description {
|
||||
@@ -162,4 +788,4 @@ body.error {
|
||||
padding: 14px 40px 18px;
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
143
cms/static/sass/elements/_controls.scss
Normal file
143
cms/static/sass/elements/_controls.scss
Normal file
@@ -0,0 +1,143 @@
|
||||
// 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 @@
|
||||
// ====================
|
||||
|
||||
.wrapper-footer {
|
||||
margin: ($baseline*1.5) 0 $baseline 0;
|
||||
padding: $baseline;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
margin: 0 0 $baseline 0;
|
||||
padding: $baseline;
|
||||
|
||||
footer.primary {
|
||||
@include clearfix();
|
||||
@@ -14,9 +14,7 @@
|
||||
min-width: $fg-min-width;
|
||||
width: flex-grid(12);
|
||||
margin: 0 auto;
|
||||
padding-top: $baseline;
|
||||
border-top: 1px solid $gray-l4;
|
||||
color: $gray-l2;
|
||||
color: $gray-l1;
|
||||
|
||||
.colophon {
|
||||
width: flex-grid(4, 12);
|
||||
@@ -24,6 +22,14 @@
|
||||
margin-right: flex-gutter(2);
|
||||
}
|
||||
|
||||
a {
|
||||
color: $gray;
|
||||
|
||||
&:hover, &:active {
|
||||
color: $gray-d2;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-peripheral {
|
||||
width: flex-grid(6, 12);
|
||||
float: right;
|
||||
@@ -36,14 +42,33 @@
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
color: $gray-l1;
|
||||
a {
|
||||
@include border-radius(2px);
|
||||
padding: ($baseline/2) ($baseline*0.75);
|
||||
background: transparent;
|
||||
|
||||
&:hover, &:active {
|
||||
color: $blue;
|
||||
.ss-icon {
|
||||
@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"],
|
||||
textarea.text {
|
||||
padding: 6px 8px 8px;
|
||||
@include box-sizing(border-box);
|
||||
border: 1px solid $mediumGrey;
|
||||
border: 1px solid $gray-l2;
|
||||
border-radius: 2px;
|
||||
@include linear-gradient($lightGrey, tint($lightGrey, 90%));
|
||||
background-color: $lightGrey;
|
||||
@include box-shadow(0 1px 2px rgba(0, 0, 0, .1) inset);
|
||||
@include linear-gradient($gray-l5, $white);
|
||||
background-color: $gray-l5;
|
||||
@include box-shadow(inset 0 1px 2px $shadow-l1);
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
font-size: 11px;
|
||||
color: $baseFontColor;
|
||||
@@ -21,7 +21,7 @@ textarea.text {
|
||||
&::-webkit-input-placeholder,
|
||||
&:-moz-placeholder,
|
||||
&:-ms-input-placeholder {
|
||||
color: #979faf;
|
||||
color: $gray-l2;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
@@ -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 {
|
||||
padding: 6px 15px 8px 30px;
|
||||
@include box-sizing(border-box);
|
||||
@@ -73,4 +138,4 @@ code {
|
||||
background-color: #edf1f5;
|
||||
@include box-shadow(0 1px 2px rgba(0, 0, 0, 0.1) inset);
|
||||
font-family: Monaco, monospace;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,15 +2,15 @@
|
||||
// ====================
|
||||
|
||||
.wrapper-header {
|
||||
margin: 0 0 ($baseline*1.5) 0;
|
||||
margin: 0;
|
||||
padding: $baseline;
|
||||
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;
|
||||
height: 76px;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
z-index: 10;
|
||||
z-index: 1000;
|
||||
|
||||
a {
|
||||
color: $baseFontColor;
|
||||
@@ -132,7 +132,7 @@
|
||||
|
||||
// specific elements - course nav
|
||||
.nav-course {
|
||||
width: 335px;
|
||||
width: 285px;
|
||||
margin-top: -($baseline/4);
|
||||
@include font-size(14);
|
||||
|
||||
|
||||
16
cms/static/sass/elements/_icons.scss
Normal file
16
cms/static/sass/elements/_icons.scss
Normal file
@@ -0,0 +1,16 @@
|
||||
// studio - elements - icons
|
||||
// ====================
|
||||
|
||||
.icon {
|
||||
|
||||
}
|
||||
|
||||
.ss-icon {
|
||||
|
||||
}
|
||||
|
||||
.icon-inline {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin-right: ($baseline/4);
|
||||
}
|
||||
152
cms/static/sass/elements/_sock.scss
Normal file
152
cms/static/sass/elements/_sock.scss
Normal file
@@ -0,0 +1,152 @@
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
267
cms/static/sass/elements/_tender-widget.scss
Normal file
267
cms/static/sass/elements/_tender-widget.scss
Normal file
@@ -0,0 +1,267 @@
|
||||
// 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;
|
||||
}
|
||||
85
cms/static/sass/elements/_typography.scss
Normal file
85
cms/static/sass/elements/_typography.scss
Normal file
@@ -0,0 +1,85 @@
|
||||
// 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;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
// studio - elements - JQUI calendar
|
||||
// studio - elements - vendor overrides
|
||||
// ====================
|
||||
|
||||
// JQUI calendar
|
||||
.ui-datepicker {
|
||||
border-color: $darkGrey;
|
||||
border-radius: 2px;
|
||||
@@ -8,6 +9,7 @@
|
||||
font-family: $sans-serif;
|
||||
font-size: 12px;
|
||||
@include box-shadow(0 5px 10px rgba(0, 0, 0, 0.1));
|
||||
z-index: 100000 !important;
|
||||
|
||||
.ui-widget-header {
|
||||
background: $darkGrey;
|
||||
@@ -53,4 +55,11 @@
|
||||
border-color: $orange;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
// ====================
|
||||
|
||||
// JQUI timepicker
|
||||
.ui-timepicker-list {
|
||||
z-index: 100000 !important;
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
body.signup, body.signin {
|
||||
|
||||
.wrapper-content {
|
||||
margin: 0;
|
||||
margin: ($baseline*1.5) 0 0 0;
|
||||
padding: 0 $baseline;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
@@ -18,7 +18,7 @@ body.signup, body.signin {
|
||||
width: flex-grid(12);
|
||||
margin: 0 auto;
|
||||
color: $gray-d2;
|
||||
|
||||
|
||||
header {
|
||||
position: relative;
|
||||
margin-bottom: $baseline;
|
||||
@@ -71,7 +71,7 @@ body.signup, body.signin {
|
||||
@include blue-button;
|
||||
@include transition(all .15s);
|
||||
@include font-size(15);
|
||||
display:block;
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: ($baseline*0.75) ($baseline/2);
|
||||
font-weight: 600;
|
||||
@@ -121,7 +121,7 @@ body.signup, body.signin {
|
||||
@include font-size(16);
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding: ($baseline/2);
|
||||
padding: ($baseline/2);
|
||||
|
||||
&.long {
|
||||
width: 100%;
|
||||
@@ -136,15 +136,15 @@ body.signup, body.signin {
|
||||
}
|
||||
|
||||
:-moz-placeholder {
|
||||
color: $gray-l3;
|
||||
color: $gray-l3;
|
||||
}
|
||||
|
||||
::-moz-placeholder {
|
||||
color: $gray-l3;
|
||||
color: $gray-l3;
|
||||
}
|
||||
|
||||
:-ms-input-placeholder {
|
||||
color: $gray-l3;
|
||||
:-ms-input-placeholder {
|
||||
color: $gray-l3;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
|
||||
@@ -9,17 +9,6 @@ body.index {
|
||||
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 {
|
||||
@include box-sizing(border-box);
|
||||
margin: 0;
|
||||
@@ -199,7 +188,7 @@ body.index {
|
||||
img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -306,8 +295,8 @@ body.index {
|
||||
|
||||
// call to action content
|
||||
.wrapper-content-cta {
|
||||
padding-bottom: ($baseline*2);
|
||||
padding-top: ($baseline*2);
|
||||
position: relative;
|
||||
padding: ($baseline*2) 0;
|
||||
background: $white;
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ body.course.outline {
|
||||
position: relative;
|
||||
top: -4px;
|
||||
right: 50px;
|
||||
width: 145px;
|
||||
width: 100px;
|
||||
|
||||
.status-label {
|
||||
position: absolute;
|
||||
@@ -62,7 +62,7 @@ body.course.outline {
|
||||
opacity: 0.0;
|
||||
position: absolute;
|
||||
top: -1px;
|
||||
left: 5px;
|
||||
right: 0;
|
||||
margin: 0;
|
||||
padding: 8px 12px;
|
||||
background: $white;
|
||||
@@ -160,7 +160,7 @@ body.course.outline {
|
||||
.section-published-date {
|
||||
position: absolute;
|
||||
top: 19px;
|
||||
right: 90px;
|
||||
right: 80px;
|
||||
padding: 4px 10px;
|
||||
border-radius: 3px;
|
||||
background: $lightGrey;
|
||||
@@ -271,8 +271,6 @@ body.course.outline {
|
||||
|
||||
.section-published-date {
|
||||
float: right;
|
||||
width: 265px;
|
||||
margin-right: 220px;
|
||||
@include border-radius(3px);
|
||||
background: $lightGrey;
|
||||
|
||||
@@ -606,13 +604,39 @@ body.course.outline {
|
||||
}
|
||||
|
||||
.picker {
|
||||
@include clearfix();
|
||||
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 {
|
||||
float: left;
|
||||
margin-top: 30px;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
strong {
|
||||
|
||||
@@ -147,7 +147,7 @@ body.course.settings {
|
||||
}
|
||||
|
||||
label {
|
||||
@include font-size(14);
|
||||
@extend .t-copy-sub1;
|
||||
@include transition(color, 0.15s, ease-in-out);
|
||||
margin: 0 0 ($baseline/4) 0;
|
||||
font-weight: 400;
|
||||
@@ -161,7 +161,7 @@ body.course.settings {
|
||||
@include placeholder($gray-l4);
|
||||
@include font-size(16);
|
||||
@include size(100%,100%);
|
||||
padding: ($baseline/2);
|
||||
padding: ($baseline/2);
|
||||
|
||||
&.long {
|
||||
}
|
||||
@@ -212,7 +212,7 @@ body.course.settings {
|
||||
padding: $baseline;
|
||||
|
||||
&:last-child {
|
||||
padding-bottom: $baseline;
|
||||
padding-bottom: $baseline;
|
||||
}
|
||||
|
||||
.actions {
|
||||
@@ -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
|
||||
&.basic {
|
||||
|
||||
.list-input {
|
||||
@include clearfix();
|
||||
padding: 0 ($baseline/2);
|
||||
|
||||
.field {
|
||||
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 {
|
||||
float: left;
|
||||
width: flex-grid(2, 9);
|
||||
@@ -281,6 +284,58 @@ body.course.settings {
|
||||
float: left;
|
||||
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
|
||||
@@ -322,7 +377,7 @@ body.course.settings {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// specific fields - overview
|
||||
#field-course-overview {
|
||||
|
||||
@@ -468,7 +523,7 @@ body.course.settings {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.grade-specific-bar {
|
||||
height: 50px !important;
|
||||
}
|
||||
@@ -479,7 +534,7 @@ body.course.settings {
|
||||
li {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
height: 50px;
|
||||
height: 50px;
|
||||
text-align: right;
|
||||
@include border-radius(2px);
|
||||
|
||||
@@ -600,8 +655,8 @@ body.course.settings {
|
||||
}
|
||||
|
||||
#field-course-grading-assignment-shortname,
|
||||
#field-course-grading-assignment-totalassignments,
|
||||
#field-course-grading-assignment-gradeweight,
|
||||
#field-course-grading-assignment-totalassignments,
|
||||
#field-course-grading-assignment-gradeweight,
|
||||
#field-course-grading-assignment-droppable {
|
||||
width: flex-grid(2, 6);
|
||||
}
|
||||
@@ -734,4 +789,4 @@ body.course.settings {
|
||||
.content-supplementary {
|
||||
width: flex-grid(3, 12);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,11 +3,41 @@
|
||||
|
||||
body.course.subsection {
|
||||
|
||||
.main-wrapper {
|
||||
margin-top: ($baseline*2);
|
||||
}
|
||||
|
||||
.unit-settings {
|
||||
.window-contents {
|
||||
padding: 10px 20px;
|
||||
}
|
||||
|
||||
.datepair {
|
||||
|
||||
.field {
|
||||
display: inline-block;
|
||||
margin-right: ($baseline/4);
|
||||
width: 45%;
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
label, input {
|
||||
display: block;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
label {
|
||||
margin-bottom: ($baseline/4);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.unit-actions {
|
||||
border-bottom: none;
|
||||
padding-bottom: 0;
|
||||
@@ -74,7 +104,7 @@ body.course.subsection {
|
||||
}
|
||||
|
||||
.window-contents {
|
||||
display: none;
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -232,6 +262,7 @@ body.course.subsection {
|
||||
|
||||
.remove-date {
|
||||
display: block;
|
||||
margin-top: ($baseline/4);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -259,7 +290,7 @@ body.course.subsection {
|
||||
background-position: 0 -50px;
|
||||
|
||||
.hidden {
|
||||
background-position: 0 -5px;
|
||||
background-position: 0 -5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -369,4 +400,4 @@ body.course.subsection {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,8 @@
|
||||
|
||||
body.course.unit {
|
||||
|
||||
.unit .main-wrapper {
|
||||
@include clearfix();
|
||||
margin: 40px;
|
||||
.main-wrapper {
|
||||
margin-top: ($baseline*2);
|
||||
}
|
||||
|
||||
//Problem Selector tab menu requirements
|
||||
@@ -31,7 +30,7 @@ body.course.unit {
|
||||
}
|
||||
|
||||
.unit-body {
|
||||
|
||||
|
||||
.unit-name-input {
|
||||
padding: 20px 40px;
|
||||
|
||||
@@ -44,7 +43,7 @@ body.course.unit {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.breadcrumbs {
|
||||
border-radius: 3px 3px 0 0;
|
||||
border-bottom: 1px solid #cbd1db;
|
||||
@@ -189,10 +188,10 @@ body.course.unit {
|
||||
@include clearfix;
|
||||
|
||||
a {
|
||||
position: relative;
|
||||
position: relative;
|
||||
border: 1px solid $darkGreen;
|
||||
background: tint($green,20%);
|
||||
color: #fff;
|
||||
color: #fff;
|
||||
|
||||
&:hover {
|
||||
background: $brightGreen;
|
||||
@@ -254,8 +253,8 @@ body.course.unit {
|
||||
@include transition (none);
|
||||
|
||||
&:hover {
|
||||
background: tint($green,30%);
|
||||
color: #fff;
|
||||
background: tint($green,30%);
|
||||
color: #fff;
|
||||
@include transition(background-color .15s);
|
||||
}
|
||||
}
|
||||
@@ -263,7 +262,7 @@ body.course.unit {
|
||||
li {
|
||||
border:none;
|
||||
border-bottom: 1px dashed $lightGrey;
|
||||
color: #fff;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
li:first-child {
|
||||
@@ -326,7 +325,7 @@ body.course.unit {
|
||||
}
|
||||
}
|
||||
|
||||
// specific editor types
|
||||
// specific editor types
|
||||
.empty {
|
||||
|
||||
a {
|
||||
@@ -337,20 +336,20 @@ body.course.unit {
|
||||
|
||||
|
||||
&:hover {
|
||||
background: tint($green,30%);
|
||||
background: tint($green,30%);
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.new-component {
|
||||
.new-component {
|
||||
text-align: center;
|
||||
|
||||
h5 {
|
||||
color: $darkGreen;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -374,7 +373,7 @@ body.course.unit {
|
||||
&.editing {
|
||||
border: 1px solid $lightBluishGrey2;
|
||||
z-index: auto;
|
||||
|
||||
|
||||
.drag-handle,
|
||||
.component-actions {
|
||||
display: none;
|
||||
@@ -434,7 +433,7 @@ body.course.unit {
|
||||
|
||||
label {
|
||||
display: inline-block;
|
||||
margin-right: 10px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -528,7 +527,7 @@ body.course.unit {
|
||||
}
|
||||
|
||||
.window-contents {
|
||||
display: none;
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -678,4 +677,4 @@ body.unit {
|
||||
padding-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,13 +23,13 @@
|
||||
<link rel="stylesheet" type="text/css" href="${static.url('css/vendor/symbolset.ss-symbolicons-block.css')}" />
|
||||
<link rel="stylesheet" type="text/css" href="${static.url('css/vendor/symbolset.ss-standard.css')}" />
|
||||
|
||||
<%include file="widgets/segment-io.html" />
|
||||
|
||||
<%block name="header_extras"></%block>
|
||||
</head>
|
||||
|
||||
<body class="<%block name='bodyclass'></%block> hide-wip">
|
||||
<%include file="widgets/header.html" />
|
||||
<%include file="courseware_vendor_js.html"/>
|
||||
|
||||
<script type="text/javascript" src="${static.url('js/vendor/json2.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/vendor/underscore-min.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/vendor/backbone-min.js')}"></script>
|
||||
@@ -49,15 +49,32 @@
|
||||
<script type="text/javascript" src="${static.url('js/vendor/CodeMirror/htmlmixed.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/vendor/CodeMirror/css.js')}"></script>
|
||||
<script type="text/javascript">
|
||||
document.write('\x3Cscript type="text/javascript" src="' +
|
||||
document.location.protocol + '//www.youtube.com/player_api">\x3C/script>');
|
||||
document.write('\x3Cscript type="text/javascript" src="' +
|
||||
document.location.protocol + '//www.youtube.com/player_api">\x3C/script>');
|
||||
</script>
|
||||
|
||||
<%block name="content"></%block>
|
||||
<%include file="widgets/footer.html" />
|
||||
<!-- view -->
|
||||
<div class="wrapper wrapper-view">
|
||||
<%include file="widgets/header.html" />
|
||||
|
||||
<%block name="view_alerts"></%block>
|
||||
<%block name="view_banners"></%block>
|
||||
|
||||
<%block name="content"></%block>
|
||||
|
||||
% if user.is_authenticated():
|
||||
<%include file="widgets/sock.html" />
|
||||
% endif
|
||||
|
||||
<%include file="widgets/footer.html" />
|
||||
<%include file="widgets/tender.html" />
|
||||
|
||||
<%block name="view_notifications"></%block>
|
||||
</div>
|
||||
|
||||
<%block name="view_prompts"></%block>
|
||||
<%block name="jsextra"></%block>
|
||||
</body>
|
||||
|
||||
<%include file="widgets/qualaroo.html" />
|
||||
</html>
|
||||
|
||||
|
||||
|
||||
@@ -80,5 +80,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</%block>
|
||||
@@ -1,9 +1,7 @@
|
||||
<%inherit file="base.html" />
|
||||
<%!
|
||||
from time import mktime
|
||||
import dateutil.parser
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from xmodule.util.date_utils import get_time_struct_display
|
||||
%>
|
||||
|
||||
<%! from django.core.urlresolvers import reverse %>
|
||||
@@ -13,7 +11,6 @@
|
||||
|
||||
<%namespace name="units" file="widgets/units.html" />
|
||||
<%namespace name='static' file='static_content.html'/>
|
||||
<%namespace name='datetime' module='datetime'/>
|
||||
|
||||
<%block name="content">
|
||||
<div class="main-wrapper">
|
||||
@@ -36,20 +33,22 @@
|
||||
<h4 class="header">Subsection Settings</h4>
|
||||
<div class="window-contents">
|
||||
<div class="scheduled-date-input row">
|
||||
<label>Release date:<!-- <span class="description">Determines when this subsection and the units within it will be released publicly.</span>--></label>
|
||||
<div class="datepair" data-language="javascript">
|
||||
<%
|
||||
start_date = datetime.fromtimestamp(mktime(subsection.lms.start)) if subsection.lms.start is not None else None
|
||||
parent_start_date = datetime.fromtimestamp(mktime(parent_item.lms.start)) if parent_item.lms.start is not None else None
|
||||
%>
|
||||
<input type="text" id="start_date" name="start_date" value="${start_date.strftime('%m/%d/%Y') if start_date is not None else ''}" placeholder="MM/DD/YYYY" class="date" size='15' autocomplete="off"/>
|
||||
<input type="text" id="start_time" name="start_time" value="${start_date.strftime('%H:%M') if start_date is not None else ''}" placeholder="HH:MM" class="time" size='10' autocomplete="off"/>
|
||||
<div class="field field-start-date">
|
||||
<label for="start_date">Release Day</label>
|
||||
<input type="text" id="start_date" name="start_date" value="${get_time_struct_display(subsection.lms.start, '%m/%d/%Y')}" placeholder="MM/DD/YYYY" class="date" size='15' autocomplete="off"/>
|
||||
</div>
|
||||
<div class="field field-start-time">
|
||||
<label for="start_time">Release Time (<abbr title="Coordinated Universal Time">UTC</abbr>)</label>
|
||||
<input type="text" id="start_time" name="start_time" value="${get_time_struct_display(subsection.lms.start, '%H:%M')}" placeholder="HH:MM" class="time" size='10' autocomplete="off"/>
|
||||
</div>
|
||||
</div>
|
||||
% if subsection.lms.start != parent_item.lms.start and subsection.lms.start:
|
||||
% if parent_start_date is None:
|
||||
% if parent_item.lms.start is None:
|
||||
<p class="notice">The date above differs from the release date of ${parent_item.display_name_with_default}, which is unset.
|
||||
% else:
|
||||
<p class="notice">The date above differs from the release date of ${parent_item.display_name_with_default} – ${parent_start_date.strftime('%m/%d/%Y')} at ${parent_start_date.strftime('%H:%M')}.
|
||||
<p class="notice">The date above differs from the release date of ${parent_item.display_name_with_default} –
|
||||
${get_time_struct_display(parent_item.lms.start, '%m/%d/%Y at %H:%M UTC')}.
|
||||
% endif
|
||||
<a href="#" class="sync-date no-spinner">Sync to ${parent_item.display_name_with_default}.</a></p>
|
||||
% endif
|
||||
@@ -62,18 +61,17 @@
|
||||
</div>
|
||||
|
||||
<div class="due-date-input row">
|
||||
<label>Due date:</label>
|
||||
<a href="#" class="set-date">Set a due date</a>
|
||||
<div class="datepair date-setter">
|
||||
<p class="date-description">
|
||||
<%
|
||||
# due date uses it own formatting for stringifying the date. As with capa_module.py, there's a utility module available for us to use
|
||||
due_date = dateutil.parser.parse(subsection.lms.due) if subsection.lms.due else None
|
||||
%>
|
||||
<input type="text" id="due_date" name="due_date" value="${due_date.strftime('%m/%d/%Y') if due_date is not None else ''}" placeholder="MM/DD/YYYY" class="date" size='15' autocomplete="off"/>
|
||||
<input type="text" id="due_time" name="due_time" value="${due_date.strftime('%H:%M') if due_date is not None else ''}" placeholder="HH:MM" class="time" size='10' autocomplete="off"/>
|
||||
<a href="#" class="remove-date">Remove due date</a>
|
||||
</p>
|
||||
<div class="field field-start-date">
|
||||
<label for="due_date">Due Day</label>
|
||||
<input type="text" id="due_date" name="due_date" value="${get_time_struct_display(subsection.lms.due, '%m/%d/%Y')}" placeholder="MM/DD/YYYY" class="date" size='15' autocomplete="off"/>
|
||||
</div>
|
||||
<div class="field field-start-time">
|
||||
<label for="due_time">Due Time (<abbr title="Coordinated Universal Time">UTC</abbr>)</label>
|
||||
<input type="text" id="due_time" name="due_time" value="${get_time_struct_display(subsection.lms.due, '%H:%M')}" placeholder="HH:MM" class="time" size='10' autocomplete="off"/>
|
||||
</div>
|
||||
<a href="#" class="remove-date">Remove due date</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row unit-actions">
|
||||
|
||||
@@ -134,10 +134,10 @@
|
||||
</header>
|
||||
|
||||
<ul class="list-actions">
|
||||
<li>
|
||||
<li class="action-item">
|
||||
<a href="${reverse('signup')}" class="action action-primary">Sign Up & Start Making an edX Course</a>
|
||||
</li>
|
||||
<li>
|
||||
<li class="action-item">
|
||||
<a href="${reverse('login')}" class="action action-secondary">Already have a Studio Account? Sign In</a>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -151,7 +151,7 @@
|
||||
<figcaption class="description">Simple two-level outline to organize your couse. Drag and drop, and see your course at a glance.</figcaption>
|
||||
</figure>
|
||||
|
||||
<a href="#" rel="view" class="action action-modal-close">
|
||||
<a href="" rel="view" class="action action-modal-close">
|
||||
<i class="ss-icon ss-symbolicons-block icon icon-close">␡</i>
|
||||
<span class="label">close modal</span>
|
||||
</a>
|
||||
@@ -164,7 +164,7 @@
|
||||
<figcaption class="description">Quickly create videos, text snippets, inline discussions, and a variety of problem types.</figcaption>
|
||||
</figure>
|
||||
|
||||
<a href="#" rel="view" class="action action-modal-close">
|
||||
<a href="" rel="view" class="action action-modal-close">
|
||||
<i class="ss-icon ss-symbolicons-block icon icon-close">␡</i>
|
||||
<span class="label">close modal</span>
|
||||
</a>
|
||||
@@ -177,7 +177,7 @@
|
||||
<figcaption class="description">Simply set the date of a section or subsection, and Studio will publish it to your students for you.</figcaption>
|
||||
</figure>
|
||||
|
||||
<a href="#" rel="view" class="action action-modal-close">
|
||||
<a href="" rel="view" class="action action-modal-close">
|
||||
<i class="ss-icon ss-symbolicons-block icon icon-close">␡</i>
|
||||
<span class="label">close modal</span>
|
||||
</a>
|
||||
|
||||
@@ -46,6 +46,8 @@
|
||||
<li class="nav-item">
|
||||
% if not disable_course_creation:
|
||||
<a href="#" class="button new-button new-course-button"><i class="ss-icon ss-symbolicons-standard icon icon-create">+</i> New Course</a>
|
||||
% elif settings.MITX_FEATURES.get('STAFF_EMAIL',''):
|
||||
<a href="mailto:${settings.MITX_FEATURES.get('STAFF_EMAIL','')}">Email staff to create course</a>
|
||||
% endif
|
||||
</li>
|
||||
</ul>
|
||||
@@ -67,7 +69,7 @@
|
||||
<article class="my-classes">
|
||||
% if user.is_active:
|
||||
<ul class="class-list">
|
||||
%for course, url, lms_link in courses:
|
||||
%for course, url, lms_link in sorted(courses, key=lambda s: s[0].lower() if s[0] is not None else ''):
|
||||
<li>
|
||||
<a class="class-link" href="${url}" class="class-name">
|
||||
<span class="class-name">${course}</span>
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
<%inherit file="base.html" />
|
||||
<%!
|
||||
from time import mktime
|
||||
import dateutil.parser
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from xmodule.util.date_utils import get_time_struct_display
|
||||
%>
|
||||
<%! from django.core.urlresolvers import reverse %>
|
||||
<%block name="title">Course Outline</%block>
|
||||
@@ -28,9 +26,9 @@
|
||||
// I believe that current (New Section/New Subsection) cause full page reloads which means these aren't needed globally
|
||||
// but we really should change that behavior.
|
||||
if (!window.graderTypes) {
|
||||
window.graderTypes = new CMS.Models.Settings.CourseGraderCollection();
|
||||
window.graderTypes.course_location = new CMS.Models.Location('${parent_location}');
|
||||
window.graderTypes.reset(${course_graders|n});
|
||||
window.graderTypes = new CMS.Models.Settings.CourseGraderCollection();
|
||||
window.graderTypes.course_location = new CMS.Models.Location('${parent_location}');
|
||||
window.graderTypes.reset(${course_graders|n});
|
||||
}
|
||||
|
||||
$(".gradable-status").each(function(index, ele) {
|
||||
@@ -106,20 +104,6 @@
|
||||
</%block>
|
||||
|
||||
<%block name="content">
|
||||
<div class="edit-subsection-publish-settings">
|
||||
<div class="settings">
|
||||
<h3>Section Release Date</h3>
|
||||
<div class="picker datepair">
|
||||
<input class="start-date date" type="text" name="start_date" value="" placeholder="MM/DD/YYYY" class="date" size='15' autocomplete="off"/>
|
||||
<input class="start-time time" type="text" name="start_time" value="" placeholder="HH:MM" class="time" size='10' autocomplete="off"/>
|
||||
<div class="description">
|
||||
<p>On the date set above, this section – <strong class="section-name"></strong> – will be released to students. Any units marked private will only be visible to admins.</p>
|
||||
</div>
|
||||
</div>
|
||||
<a href="#" class="save-button">Save</a><a href="#" class="cancel-button">Cancel</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wrapper-mast wrapper">
|
||||
<header class="mast has-actions has-subtitle">
|
||||
<div class="title">
|
||||
@@ -163,15 +147,14 @@
|
||||
</h3>
|
||||
<div class="section-published-date">
|
||||
<%
|
||||
start_date = datetime.fromtimestamp(mktime(section.lms.start)) if section.lms.start is not None else None
|
||||
start_date_str = start_date.strftime('%m/%d/%Y') if start_date is not None else ''
|
||||
start_time_str = start_date.strftime('%H:%M') if start_date is not None else ''
|
||||
start_date_str = get_time_struct_display(section.lms.start, '%m/%d/%Y')
|
||||
start_time_str = get_time_struct_display(section.lms.start, '%H:%M')
|
||||
%>
|
||||
%if start_date is None:
|
||||
%if section.lms.start is None:
|
||||
<span class="published-status">This section has not been released.</span>
|
||||
<a href="#" class="schedule-button" data-date="" data-time="" data-id="${section.location}">Schedule</a>
|
||||
%else:
|
||||
<span class="published-status"><strong>Will Release:</strong> ${start_date_str} at ${start_time_str}</span>
|
||||
<span class="published-status"><strong>Will Release:</strong> ${get_time_struct_display(section.lms.start, '%m/%d/%Y at %H:%M UTC')}</span>
|
||||
<a href="#" class="edit-button" data-date="${start_date_str}" data-time="${start_time_str}" data-id="${section.location}">Edit</a>
|
||||
%endif
|
||||
</div>
|
||||
@@ -200,7 +183,7 @@
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="gradable-status" data-initial-status="${subsection.lms.format if section.lms.format is not None else 'Not Graded'}">
|
||||
<div class="gradable-status" data-initial-status="${subsection.lms.format if subsection.lms.format is not None else 'Not Graded'}">
|
||||
</div>
|
||||
|
||||
<div class="item-actions">
|
||||
@@ -219,4 +202,25 @@
|
||||
</div>
|
||||
</div>
|
||||
<footer></footer>
|
||||
|
||||
<div class="edit-subsection-publish-settings">
|
||||
<div class="settings">
|
||||
<h3>Section Release Date</h3>
|
||||
<div class="picker datepair">
|
||||
<div class="field field-start-date">
|
||||
<label for="">Release Day</label>
|
||||
<input class="start-date date" type="text" name="start_date" value="" placeholder="MM/DD/YYYY" class="date" size='15' autocomplete="off"/>
|
||||
</div>
|
||||
<div class="field field-start-time">
|
||||
<label for="">Release Time (<abbr title="Coordinated Universal Time">UTC</abbr>)</label>
|
||||
<input class="start-time time" type="text" name="start_time" value="" placeholder="HH:MM" class="time" size='10' autocomplete="off"/>
|
||||
</div>
|
||||
|
||||
<div class="description">
|
||||
<p>On the date set above, this section – <strong class="section-name"></strong> – will be released to students. Any units marked private will only be visible to admins.</p>
|
||||
</div>
|
||||
</div>
|
||||
<a href="#" class="save-button">Save</a><a href="#" class="cancel-button">Cancel</a>
|
||||
</div>
|
||||
</div>
|
||||
</%block>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
<%namespace name='static' file='static_content.html'/>
|
||||
<%!
|
||||
from contentstore import utils
|
||||
from contentstore import utils
|
||||
%>
|
||||
|
||||
|
||||
@@ -13,17 +13,17 @@ from contentstore import utils
|
||||
<script src="${static.url('js/vendor/timepicker/jquery.timepicker.js')}"></script>
|
||||
<script src="${static.url('js/vendor/timepicker/datepair.js')}"></script>
|
||||
<script src="${static.url('js/vendor/date.js')}"></script>
|
||||
|
||||
|
||||
<script type="text/javascript" src="${static.url('js/template_loader.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/views/server_error.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/models/course_relative.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/views/validating_view.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/views/settings/main_settings_view.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/models/settings/course_details.js')}"></script>
|
||||
|
||||
|
||||
<script type="text/javascript">
|
||||
$(document).ready(function(){
|
||||
|
||||
|
||||
// hilighting labels when fields are focused in
|
||||
$("form :input").focus(function() {
|
||||
$("label[for='" + this.id + "']").addClass("is-focused");
|
||||
@@ -32,18 +32,18 @@ from contentstore import utils
|
||||
});
|
||||
var model = new CMS.Models.Settings.CourseDetails();
|
||||
model.urlRoot = '${details_url}';
|
||||
model.fetch({success :
|
||||
model.fetch({success :
|
||||
function(model) {
|
||||
var editor = new CMS.Views.Settings.Details({
|
||||
el: $('.settings-details'),
|
||||
model: model
|
||||
});
|
||||
|
||||
|
||||
editor.render();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
</script>
|
||||
</%block>
|
||||
|
||||
@@ -62,10 +62,10 @@ from contentstore import utils
|
||||
<article class="content-primary" role="main">
|
||||
<form id="settings_details" class="settings-details" method="post" action="">
|
||||
<section class="group-settings basic">
|
||||
<header>
|
||||
<header>
|
||||
<h2 class="title-2">Basic Information</h2>
|
||||
<span class="tip">The nuts and bolts of your course</span>
|
||||
</header>
|
||||
</header>
|
||||
|
||||
<ol class="list-input">
|
||||
<li class="field text is-not-editable" id="field-course-organization">
|
||||
@@ -83,45 +83,57 @@ from contentstore import utils
|
||||
<input title="This field is disabled: this information cannot be changed." type="text" class="long" id="course-name" value="[Course Name]" readonly />
|
||||
</li>
|
||||
</ol>
|
||||
<span class="tip tip-stacked">These are used in <a rel="external" href="${utils.get_lms_link_for_about_page(course_location)}" />your course URL</a>, and cannot be changed</span>
|
||||
|
||||
<div class="note note-promotion note-promotion-courseURL has-actions">
|
||||
<h3 class="title">Course Summary Page <span class="tip">(for student enrollment and access)</span></h3>
|
||||
<div class="copy">
|
||||
<p><a class="link-courseURL" rel="external" href="https:${utils.get_lms_link_for_about_page(course_location)}" />https:${utils.get_lms_link_for_about_page(course_location)}</a></p>
|
||||
</div>
|
||||
|
||||
<ul class="list-actions">
|
||||
<li class="action-item">
|
||||
<a title="Send a note to students via email" href="mailto:someone@domain.com?Subject=Enroll%20in%20${context_course.display_name_with_default}&body=The%20course%20"${context_course.display_name_with_default}",%20provided%20by%20edX,%20is%20open%20for%20enrollment.%20Please%20navigate%20to%20this%20course%20at%20https:${utils.get_lms_link_for_about_page(course_location)}%20to%20enroll." class="action action-primary"><i class="ss-icon icon ss-symbolicons-standard icon icon-inline icon-announcement">✉</i> Invite your students</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<hr class="divide" />
|
||||
<hr class="divide" />
|
||||
|
||||
<section class="group-settings schedule">
|
||||
<header>
|
||||
<header>
|
||||
<h2 class="title-2">Course Schedule</h2>
|
||||
<span class="tip">Important steps and segments of your course</span>
|
||||
</header>
|
||||
</header>
|
||||
|
||||
<ol class="list-input">
|
||||
<li class="field-group field-group-course-start" id="course-start">
|
||||
<div class="field date" id="field-course-start-date">
|
||||
<label for="course-start-date">Course Start Date</label>
|
||||
<input type="text" class="start-date date start datepicker" id="course-start-date" placeholder="MM/DD/YYYY" autocomplete="off" />
|
||||
<span class="tip tip-stacked">First day the course begins</span>
|
||||
</div>
|
||||
<span class="tip tip-stacked">First day the course begins</span>
|
||||
</div>
|
||||
|
||||
<div class="field time" id="field-course-start-time">
|
||||
<label for="course-start-time">Course Start Time</label>
|
||||
<input type="text" class="time start timepicker" id="course-start-time" value="" placeholder="HH:MM" autocomplete="off" />
|
||||
<span class="tip tip-stacked" id="timezone"></span>
|
||||
</div>
|
||||
<span class="tip tip-stacked" id="timezone"></span>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="field-group field-group-course-end" id="course-end">
|
||||
<div class="field date" id="field-course-end-date">
|
||||
<label for="course-end-date">Course End Date</label>
|
||||
<input type="text" class="end-date date end" id="course-end-date" placeholder="MM/DD/YYYY" autocomplete="off" />
|
||||
<span class="tip tip-stacked">Last day your course is active</span>
|
||||
</div>
|
||||
<span class="tip tip-stacked">Last day your course is active</span>
|
||||
</div>
|
||||
|
||||
<div class="field time" id="field-course-end-time">
|
||||
<label for="course-end-time">Course End Time</label>
|
||||
<input type="text" class="time end" id="course-end-time" value="" placeholder="HH:MM" autocomplete="off" />
|
||||
<span class="tip tip-stacked" id="timezone"></span>
|
||||
</div>
|
||||
</li>
|
||||
<span class="tip tip-stacked" id="timezone"></span>
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<ol class="list-input">
|
||||
@@ -129,33 +141,33 @@ from contentstore import utils
|
||||
<div class="field date" id="field-enrollment-start-date">
|
||||
<label for="course-enrollment-start-date">Enrollment Start Date</label>
|
||||
<input type="text" class="start-date date start" id="course-enrollment-start-date" placeholder="MM/DD/YYYY" autocomplete="off" />
|
||||
<span class="tip tip-stacked">First day students can enroll</span>
|
||||
</div>
|
||||
<span class="tip tip-stacked">First day students can enroll</span>
|
||||
</div>
|
||||
|
||||
<div class="field time" id="field-enrollment-start-time">
|
||||
<label for="course-enrollment-start-time">Enrollment Start Time</label>
|
||||
<input type="text" class="time start" id="course-enrollment-start-time" value="" placeholder="HH:MM" autocomplete="off" />
|
||||
<span class="tip tip-stacked" id="timezone"></span>
|
||||
</div>
|
||||
<span class="tip tip-stacked" id="timezone"></span>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="field-group field-group-enrollment-end" id="enrollment-end">
|
||||
<div class="field date" id="field-enrollment-end-date">
|
||||
<label for="course-enrollment-end-date">Enrollment End Date</label>
|
||||
<input type="text" class="end-date date end" id="course-enrollment-end-date" placeholder="MM/DD/YYYY" autocomplete="off" />
|
||||
<span class="tip tip-stacked">Last day students can enroll</span>
|
||||
</div>
|
||||
<span class="tip tip-stacked">Last day students can enroll</span>
|
||||
</div>
|
||||
|
||||
<div class="field time" id="field-enrollment-end-time">
|
||||
<label for="course-enrollment-end-time">Enrollment End Time</label>
|
||||
<input type="text" class="time end" id="course-enrollment-end-time" value="" placeholder="HH:MM" autocomplete="off" />
|
||||
<span class="tip tip-stacked" id="timezone"></span>
|
||||
</div>
|
||||
</li>
|
||||
<span class="tip tip-stacked" id="timezone"></span>
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
<hr class="divide" />
|
||||
<hr class="divide" />
|
||||
|
||||
<section class="group-settings marketing">
|
||||
<header>
|
||||
@@ -167,45 +179,44 @@ from contentstore import utils
|
||||
<li class="field text" id="field-course-overview">
|
||||
<label for="course-overview">Course Overview</label>
|
||||
<textarea class="tinymce text-editor" id="course-overview"></textarea>
|
||||
<span class="tip tip-stacked">Introductions, prerequisites, FAQs that are used on <a href="${utils.get_lms_link_for_about_page(course_location)}">your course summary page</a></span>
|
||||
<span class="tip tip-stacked">Introductions, prerequisites, FAQs that are used on <a class="link-courseURL" rel="external" href="${utils.get_lms_link_for_about_page(course_location)}">your course summary page</a></span>
|
||||
</li>
|
||||
|
||||
<li class="field video" id="field-course-introduction-video">
|
||||
<label for="course-overview">Course Introduction Video</label>
|
||||
<div class="input input-existing">
|
||||
<div class="current current-course-introduction-video">
|
||||
<div class="input input-existing">
|
||||
<div class="current current-course-introduction-video">
|
||||
<iframe width="618" height="350" src="" frameborder="0" allowfullscreen></iframe>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<a href="#" class="remove-item remove-course-introduction-video remove-video-data"><span class="delete-icon"></span> Delete Current Video</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input">
|
||||
<div class="input">
|
||||
<input type="text" class="long new-course-introduction-video add-video-data" id="course-introduction-video" value="" placeholder="your YouTube video's ID" autocomplete="off" />
|
||||
<span class="tip tip-stacked">Enter your YouTube video's ID (along with any restriction parameters)</span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
<hr class="divide" />
|
||||
<hr class="divide" />
|
||||
|
||||
<section class="group-settings requirements">
|
||||
<header>
|
||||
<header>
|
||||
<h2 class="title-2">Requirements</h2>
|
||||
<span class="tip">Expectations of the students taking this course</span>
|
||||
</header>
|
||||
</header>
|
||||
|
||||
<ol class="list-input">
|
||||
<li class="field text" id="field-course-effort">
|
||||
<label for="course-effort">Hours of Effort per Week</label>
|
||||
<input type="text" class="short time" id="course-effort" placeholder="HH:MM" />
|
||||
<span class="tip tip-inline">Time spent on all course work</span>
|
||||
</li>
|
||||
</ol>
|
||||
</section>
|
||||
<span class="tip tip-inline">Time spent on all course work</span>
|
||||
</li>
|
||||
</ol>
|
||||
</section>
|
||||
</form>
|
||||
</article>
|
||||
|
||||
@@ -215,7 +226,7 @@ from contentstore import utils
|
||||
<p>Your course's schedule settings determine when students can enroll in and begin a course as well as when the course.</p>
|
||||
|
||||
<p>Additionally, details provided on this page are also used in edX's catalog of courses, which new and returning students use to choose new courses to study.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bit">
|
||||
% if context_course:
|
||||
@@ -234,4 +245,4 @@ from contentstore import utils
|
||||
</aside>
|
||||
</section>
|
||||
</div>
|
||||
</%block>
|
||||
</%block>
|
||||
|
||||
@@ -19,6 +19,12 @@ from contentstore import utils
|
||||
<script type="text/javascript">
|
||||
$(document).ready(function () {
|
||||
|
||||
$("form :input").focus(function() {
|
||||
$("label[for='" + this.id + "']").addClass("is-focused");
|
||||
}).blur(function() {
|
||||
$("label").removeClass("is-focused");
|
||||
});
|
||||
|
||||
// proactively populate advanced b/c it has the filtered list and doesn't really follow the model pattern
|
||||
var advancedModel = new CMS.Models.Settings.Advanced(${advanced_dict | n}, {parse: true});
|
||||
advancedModel.url = "${reverse('course_advanced_settings_updates', kwargs=dict(org=context_course.location.org, course=context_course.location.course, name=context_course.location.name))}";
|
||||
@@ -36,13 +42,17 @@ editor.render();
|
||||
</%block>
|
||||
|
||||
<%block name="content">
|
||||
<div class="wrapper-content wrapper">
|
||||
<section class="content">
|
||||
<header class="page">
|
||||
<div class="wrapper-mast wrapper">
|
||||
<header class="mast has-subtitle">
|
||||
<div class="title">
|
||||
<span class="title-sub">Settings</span>
|
||||
<h1 class="title-1">Advanced Settings</h1>
|
||||
</header>
|
||||
</div>
|
||||
</header>
|
||||
</div>
|
||||
|
||||
<div class="wrapper-content wrapper">
|
||||
<section class="content">
|
||||
<article class="content-primary" role="main">
|
||||
<form id="settings_advanced" class="settings-advanced" method="post" action="">
|
||||
|
||||
@@ -63,7 +73,7 @@ editor.render();
|
||||
<p class="instructions"><strong>Warning</strong>: Do not modify these policies unless you are familiar with their purpose.</p>
|
||||
|
||||
<ul class="list-input course-advanced-policy-list enum">
|
||||
|
||||
|
||||
</ul>
|
||||
</section>
|
||||
</form>
|
||||
@@ -94,23 +104,61 @@ editor.render();
|
||||
</aside>
|
||||
</section>
|
||||
</div>
|
||||
</%block>
|
||||
|
||||
<%block name="view_notifications">
|
||||
<!-- notification: change has been made and a save is needed -->
|
||||
<div class="wrapper wrapper-notification wrapper-notification-warning">
|
||||
<div class="notification warning">
|
||||
<div class="copy">
|
||||
<i class="ss-icon ss-symbolicons-block icon icon-warning">⚠</i>
|
||||
<div class="wrapper wrapper-notification wrapper-notification-warning" aria-hidden="true" role="dialog" aria-labelledby="notification-changesMade-title" aria-describedby="notification-changesMade-description">
|
||||
<div class="notification warning has-actions">
|
||||
<i class="ss-icon ss-symbolicons-block icon icon-warning">⚠</i>
|
||||
|
||||
<p><strong>Note: </strong>Your changes will not take effect until you <strong>save your
|
||||
progress</strong>. Take care with policy value formatting, as validation is <strong>not implemented</strong>.</p>
|
||||
<div class="copy">
|
||||
<h2 class="title title-3" id="notification-changesMade-title">You've Made Some Changes</h2>
|
||||
<p id="notification-changesMade-description">Your changes will not take effect until you <strong>save your progress</strong>. Take care with key and value formatting, as validation is <strong>not implemented</strong>.</p>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<nav class="nav-actions">
|
||||
<h3 class="sr">Notification Actions</h3>
|
||||
<ul>
|
||||
<li><a href="#" class="save-button">Save</a></li>
|
||||
<li><a href="#" class="cancel-button">Cancel</a></li>
|
||||
<li class="nav-item">
|
||||
<a href="" class="action-primary save-button">Save Changes</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="" class="action-secondary cancel-button">Cancel</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</%block>
|
||||
|
||||
<%block name="view_alerts">
|
||||
<!-- alert: save confirmed with close -->
|
||||
<div class="wrapper wrapper-alert wrapper-alert-confirmation" role="status">
|
||||
<div class="alert confirmation">
|
||||
<i class="ss-icon ss-symbolicons-standard icon icon-confirmation">✓</i>
|
||||
|
||||
<div class="copy">
|
||||
<h2 class="title title-3">Your policy changes have been saved.</h2>
|
||||
<p>Please note that validation of your policy key and value pairs is not currently in place yet. If you are having difficulties, please review your policy pairs.</p>
|
||||
</div>
|
||||
|
||||
<a href="" rel="view" class="action action-alert-close">
|
||||
<i class="ss-icon ss-symbolicons-block icon icon-close">␡</i>
|
||||
<span class="label">close alert</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- alert: error -->
|
||||
<div class="wrapper wrapper-alert wrapper-alert-error" role="status">
|
||||
<div class="alert error">
|
||||
<i class="ss-icon ss-symbolicons-block icon icon-error">⚠</i>
|
||||
|
||||
<div class="copy">
|
||||
<h2 class="title title-3">There was an error saving your information</h2>
|
||||
<p>Please see the error below and correct it to ensure there are no problems in rendering your course.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</%block>
|
||||
</%block>
|
||||
|
||||
@@ -4,20 +4,20 @@
|
||||
|
||||
<%namespace name='static' file='static_content.html'/>
|
||||
<%!
|
||||
from contentstore import utils
|
||||
from contentstore import utils
|
||||
%>
|
||||
|
||||
<%block name="jsextra">
|
||||
<link rel="stylesheet" type="text/css" href="${static.url('js/vendor/timepicker/jquery.timepicker.css')}" />
|
||||
<script src="${static.url('js/vendor/timepicker/jquery.timepicker.js')}"></script>
|
||||
|
||||
|
||||
<script type="text/javascript" src="${static.url('js/template_loader.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/views/server_error.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/models/course_relative.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/views/validating_view.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/models/settings/course_grading_policy.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/views/settings/settings_grading_view.js')}"></script>
|
||||
|
||||
|
||||
<script type="text/javascript">
|
||||
$(document).ready(function(){
|
||||
|
||||
@@ -26,15 +26,15 @@ from contentstore import utils
|
||||
}).blur(function() {
|
||||
$("label").removeClass("is-focused");
|
||||
});
|
||||
|
||||
|
||||
var editor = new CMS.Views.Settings.Grading({
|
||||
el: $('.settings-grading'),
|
||||
model : new CMS.Models.Settings.CourseGradingPolicy(${course_details|n},{parse:true})
|
||||
});
|
||||
|
||||
|
||||
editor.render();
|
||||
});
|
||||
|
||||
|
||||
</script>
|
||||
</%block>
|
||||
|
||||
@@ -97,7 +97,7 @@ from contentstore import utils
|
||||
<ol class="list-input">
|
||||
<li class="field text" id="field-course-grading-graceperiod">
|
||||
<label for="course-grading-graceperiod">Grace Period on Deadline:</label>
|
||||
<input type="text" class="short time" id="course-grading-graceperiod" value="0:00" placeholder="e.g. 10 minutes">
|
||||
<input type="text" class="short time" id="course-grading-graceperiod" value="0:00" placeholder="HH:MM" autocomplete="off" />
|
||||
<span class="tip tip-inline">Leeway on due dates</span>
|
||||
</li>
|
||||
</ol>
|
||||
@@ -112,13 +112,13 @@ from contentstore import utils
|
||||
</header>
|
||||
|
||||
<ol class="list-input course-grading-assignment-list enum">
|
||||
|
||||
</ol>
|
||||
|
||||
</ol>
|
||||
|
||||
<div class="actions">
|
||||
<a href="#" class="new-button new-course-grading-item add-grading-data">
|
||||
<span class="plus-icon white"></span>New Assignment Type
|
||||
</a>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
</form>
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
});
|
||||
|
||||
$(document).ready(function() {
|
||||
$('body').addClass('js');
|
||||
|
||||
// tabs
|
||||
$('.tab-group').tabs();
|
||||
@@ -28,6 +27,7 @@
|
||||
});
|
||||
});
|
||||
|
||||
var unit_location_analytics = '${unit_location}';
|
||||
</script>
|
||||
|
||||
</%block>
|
||||
|
||||
544
cms/templates/ux-alerts.html
Normal file
544
cms/templates/ux-alerts.html
Normal file
@@ -0,0 +1,544 @@
|
||||
<%inherit file="base.html" />
|
||||
<%block name="title">Studio Alerts</%block>
|
||||
<%block name="bodyclass">is-signedin course uxdesign alerts</%block>
|
||||
|
||||
<%block name="jsextra">
|
||||
<script type="text/javascript">
|
||||
// notifications - demo
|
||||
$(document).ready(function() {
|
||||
|
||||
// alert and notifications - manual close
|
||||
$('.action-alert-close, .alert.has-actions .nav-actions a').click(function(e) {
|
||||
(e).preventDefault();
|
||||
console.log('closing alert');
|
||||
$(this).closest('.wrapper-alert').removeClass('is-shown');
|
||||
});
|
||||
|
||||
// alert and notifications - manual & action-based close
|
||||
$('.action-notification-close').click(function(e) {
|
||||
(e).preventDefault();
|
||||
$(this).closest('.wrapper-notification').removeClass('is-shown').addClass('is-hiding');
|
||||
});
|
||||
|
||||
// prompt pop
|
||||
$('.action-prompt').click(function(e){
|
||||
(e).preventDefault();
|
||||
$body.toggleClass('prompt-is-shown');
|
||||
$(this).closest('.wrapper-prompt').attr('aria-hidden','false');
|
||||
});
|
||||
|
||||
// prompt close
|
||||
$('.prompt .action-cancel, .prompt .action-proceed').click(function(e) {
|
||||
(e).preventDefault();
|
||||
$body.removeClass('prompt-is-shown');
|
||||
$(this).closest('.wrapper-prompt').attr('aria-hidden','true');
|
||||
});
|
||||
|
||||
$('.hide-notification').click(function(e) {
|
||||
(e).preventDefault();
|
||||
$('.wrapper-notification').removeClass('is-hiding is-shown').attr('aria-hidden','true');
|
||||
$(this.hash).addClass('is-hiding').attr('aria-hidden','true');
|
||||
});
|
||||
|
||||
$('.show-notification').click(function(e) {
|
||||
(e).preventDefault();
|
||||
$('.wrapper-notification').removeClass('is-shown is-hiding');
|
||||
$(this.hash).addClass('is-shown').attr('aria-hidden','false');
|
||||
});
|
||||
|
||||
$('.show-notification-fleeting').click(function(e) {
|
||||
(e).preventDefault();
|
||||
$('.wrapper-notification').removeClass('is-fleeting').attr('aria-hidden','true');
|
||||
$(this.hash).addClass('is-fleeting').attr('aria-hidden','false');
|
||||
});
|
||||
|
||||
$('.hide-alert').click(function(e) {
|
||||
(e).preventDefault();
|
||||
$('.wrapper-alert').removeClass('is-hiding');
|
||||
$(this.hash).addClass('is-hiding');
|
||||
});
|
||||
|
||||
$('.show-alert').click(function(e) {
|
||||
(e).preventDefault();
|
||||
$('.wrapper-alert').removeClass('is-shown');
|
||||
$(this.hash).addClass('is-shown');
|
||||
});
|
||||
|
||||
$('.hide-prompt').click(function(e){
|
||||
(e).preventDefault();
|
||||
$body.removeClass('prompt-is-shown');
|
||||
});
|
||||
|
||||
$('.show-prompt').click(function(e) {
|
||||
(e).preventDefault();
|
||||
$body.toggleClass('prompt-is-shown');
|
||||
$('.wrapper-prompt').removeClass('is-shown');
|
||||
$(this.hash).addClass('is-shown').attr('aria-hidden','false');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</%block>
|
||||
|
||||
<%block name="content">
|
||||
<div class="wrapper-mast wrapper">
|
||||
<header class="mast has-actions has-subtitle">
|
||||
<div class="title">
|
||||
<span class="title-sub">UX Design</span>
|
||||
<h1 class="title-1">System Notifications</h1>
|
||||
</div>
|
||||
</header>
|
||||
</div>
|
||||
|
||||
<div class="wrapper-content wrapper">
|
||||
<section class="content">
|
||||
<article class="content-primary" role="main">
|
||||
<section>
|
||||
<header>
|
||||
<h2 class="title-2">Alerts</h2>
|
||||
<span class="tip">persistant, static messages to the user</span>
|
||||
</header>
|
||||
|
||||
<p>In Studio, alerts are 1) general warnings/notes (e.g. drafts, published content, next steps) about the current view a user is interacting with or 2) notes about the status (e.g. saved confirmations, errors, next system steps) of any previous state that need to communicated to the user when arriving at the current view.</p>
|
||||
|
||||
<h3 class="title-3">Different Static Examples of Alerts</h3>
|
||||
<p>Note: alerts will probably never been shown based on click or page action and will primarily be loaded along with a pageload and initial display</p>
|
||||
|
||||
<ul>
|
||||
<li><a href="#alert-draft" class="show-alert">Show Draft</a></li>
|
||||
<li><a href="#alert-version" class="show-alert">Show Version</a></li>
|
||||
<li><a href="#alert-saved" class="show-alert">Show Previous View/Action</a></li>
|
||||
<li><a href="#alert-close" class="show-alert">Show Alert with Close Option</a></li>
|
||||
<li><a href="#alert-removed" class="show-alert">Show Previous View/Action Removed</a></li>
|
||||
<li><a href="#alert-system-error" class="show-alert">Show System Error</a></li>
|
||||
<li><a href="#alert-user-error" class="show-alert">Show User Error</a></li>
|
||||
<li><a href="#alert-announcement2" class="show-alert">Show Announcement</a></li>
|
||||
<li><a href="#alert-announcement1" class="show-alert">Show Announcement with Actions</a></li>
|
||||
<li><a href="#alert-activation" class="show-alert">Show Activiation</a></li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<header>
|
||||
<h2 class="title-2">Notifications</h2>
|
||||
<span class="tip">contextual, feedback-based, and temporal messages to the user</span>
|
||||
</header>
|
||||
|
||||
<p>In Studio, notifications are meant to inform the user of 1) any system status (e.g. saving, processing/validating) occurring based on any action they have taken or 2) any decisions (e.g. saving/discarding) a user must make to confirm.</p>
|
||||
|
||||
<h3 class="title-3">Different Static Examples of Notifications</h3>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<a href="#notification-change" class="show-notification">Show Change Warning</a>
|
||||
<a href="#notification-change" class="hide-notification">Hide Change Warning</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#notification-version" class="show-notification">Show New Version Warning</a>
|
||||
<a href="#notification-version" class="hide-notification">Hide New Version Warning</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#notification-dangerous" class="show-notification">Show Editing Warning</a>
|
||||
<a href="#notification-dangerous" class="hide-notification">Hide Editing Warning</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#notification-confirmation" class="show-notification-fleeting">Show Confirmation (Fleeting Notification)</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#notification-saving" class="show-notification">Show Saving</a>
|
||||
<a href="#notification-saving" class="hide-notification">Hide Saving</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#notification-help" class="show-notification">Show Help</a>
|
||||
<a href="#notification-help" class="hide-notification">Hide Help</a>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<header>
|
||||
<h2 class="title-2">Prompts</h2>
|
||||
<span class="tip">presents a user with a choice, based on their previous interaction, that must be decided before they can proceed</span>
|
||||
</header>
|
||||
|
||||
<p>In Studio, prompts are dialogs that are presented above all other page components and present a user with a choice, based on their previous interaction, that must be decided before they can proceed (or return to the previous interaction step).</p>
|
||||
|
||||
<h3 class="title-3">Different Static Examples of Prompts</h3>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<a href="#prompt-confirm" class="show-prompt">Show Confirm Prompt</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#prompt-warning" class="show-prompt">Show Warning Prompt</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#prompt-error" class="show-prompt">Show Error Prompt</a>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</article>
|
||||
</section>
|
||||
</div>
|
||||
</%block>
|
||||
|
||||
<%block name="view_alerts">
|
||||
<!-- alert: you're editing a draft -->
|
||||
<div class="wrapper wrapper-alert wrapper-alert-warning" id="alert-draft">
|
||||
<div class="alert warning has-actions">
|
||||
<i class="ss-icon ss-symbolicons-block icon icon-warning">⚠</i>
|
||||
|
||||
<div class="copy">
|
||||
<h2 class="title title-3">You are editing a draft</h2>
|
||||
<p class="message">Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa.</p>
|
||||
</div>
|
||||
|
||||
<nav class="nav-actions">
|
||||
<h3 class="sr">Alert Actions</h3>
|
||||
<ul>
|
||||
<li class="nav-item">
|
||||
<a href="#" class="button save-button action-primary">Save Draft</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="#" class="button cancel-button action-secondary">Disgard Draft</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- alert: newer version -->
|
||||
<div class="wrapper wrapper-alert wrapper-alert-warning" id="alert-version">
|
||||
<div class="alert warning has-actions">
|
||||
<i class="ss-icon ss-symbolicons-block icon icon-warning">⚠</i>
|
||||
|
||||
<div class="copy">
|
||||
<h2 class="title title-3">A Newer Version of This Exists</h2>
|
||||
<p class="message">Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa.</p>
|
||||
</div>
|
||||
|
||||
<nav class="nav-actions">
|
||||
<h3 class="sr">Alert Actions</h3>
|
||||
<ul>
|
||||
<li class="nav-item">
|
||||
<a href="#" class="button save-button action-primary">Go to Newer Version</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="#" class="button cancel-button action-secondary">Continue Editing</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- alert: save confirmed -->
|
||||
<div class="wrapper wrapper-alert wrapper-alert-confirmation" id="alert-saved">
|
||||
<div class="alert confirmation">
|
||||
<i class="ss-icon ss-symbolicons-standard icon icon-confirmation">✓</i>
|
||||
|
||||
<div class="copy">
|
||||
<h2 class="title title-3">Your changes have been saved</h2>
|
||||
<p class="message">Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. <a rel="page" href="#test">Please see below</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- alert: save confirmed with close -->
|
||||
<div class="wrapper wrapper-alert wrapper-alert-confirmation" id="alert-close">
|
||||
<div class="alert confirmation">
|
||||
<i class="ss-icon ss-symbolicons-standard icon icon-confirmation">✓</i>
|
||||
|
||||
<div class="copy">
|
||||
<h2 class="title title-3">Your changes have been saved</h2>
|
||||
<p class="message">Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. <a rel="page" href="#test">Please see below</a></p>
|
||||
</div>
|
||||
|
||||
<a href="#" rel="view" class="action action-alert-close">
|
||||
<i class="ss-icon ss-symbolicons-block icon icon-close">␡</i>
|
||||
<span class="label">close alert</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- alert: delete confirmed -->
|
||||
<div class="wrapper wrapper-alert wrapper-alert-confirmation" id="alert-removed">
|
||||
<div class="alert confirmation">
|
||||
<i class="ss-icon ss-symbolicons-standard icon icon-confirmation">✓</i>
|
||||
|
||||
<div class="copy">
|
||||
<h2 class="title title-3">X Has been removed</h2>
|
||||
<p class="message">Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- alert: system error -->
|
||||
<div class="wrapper wrapper-alert wrapper-alert-error" id="alert-system-error">
|
||||
<div class="alert error">
|
||||
<i class="ss-icon ss-symbolicons-block icon icon-error">⚠</i>
|
||||
|
||||
<div class="copy">
|
||||
<h2 class="title title-3">We're sorry, there was a error with Studio</h2>
|
||||
<p class="message">Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- alert: user error -->
|
||||
<div class="wrapper wrapper-alert wrapper-alert-error" id="alert-user-error">
|
||||
<div class="alert error has-actions">
|
||||
<i class="ss-icon ss-symbolicons-block icon icon-error">⚠</i>
|
||||
|
||||
<div class="copy">
|
||||
<h2 class="title title-3">There was an error in your submission</h2>
|
||||
<p class="message">Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. <a rel="page" href="#test">Please see below</a></p>
|
||||
</div>
|
||||
|
||||
<nav class="nav-actions">
|
||||
<h3 class="sr">Alert Actions</h3>
|
||||
<ul>
|
||||
<li class="nav-item">
|
||||
<a href="#" class="button cancel-button action-primary">Cancel Your Submission</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- alert: announcement w/ actions -->
|
||||
<div class="wrapper wrapper-alert wrapper-alert-announcement" id="alert-announcement1">
|
||||
<div class="alert announcement has-actions">
|
||||
<i class="ss-icon ss-symbolicons-block icon icon-announcement">📢</i>
|
||||
|
||||
<div class="copy">
|
||||
<h2 class="title title-3">Studio will be unavailable this weekend</h2>
|
||||
<p class="message">Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa.</p>
|
||||
</div>
|
||||
|
||||
<nav class="nav-actions">
|
||||
<h3 class="sr">Alert Actions</h3>
|
||||
<ul>
|
||||
<li class="nav-item">
|
||||
<a href="#" rel="external" class="action-primary">Contact edX Staff</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="#" class="action-secondary">Manage Your edX Notifications</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- alert: announcement -->
|
||||
<div class="wrapper wrapper-alert wrapper-alert-announcement" id="alert-announcement2">
|
||||
<div class="alert announcement">
|
||||
<i class="ss-icon ss-symbolicons-block icon icon-announcement">📢</i>
|
||||
|
||||
<div class="copy">
|
||||
<h2 class="title title-3">Studio will be unavailable this weekend</h2>
|
||||
<p class="message">Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- alert: step required -->
|
||||
<div class="wrapper wrapper-alert wrapper-alert-step-required" id="alert-activation">
|
||||
<div class="alert step-required has-actions">
|
||||
<i class="ss-icon ss-symbolicons-block icon icon-step-required"></i>
|
||||
|
||||
<div class="copy">
|
||||
<h2 class="title title-3">Your Studio account has been created, but needs to be activated</h2>
|
||||
<p class="message">Donec sed odio dui. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Etiam porta sem malesuada magna mollis euismod. Cras mattis consectetur purus sit amet fermentum. Curabitur blandit tempus porttitor.</p>
|
||||
</div>
|
||||
|
||||
<nav class="nav-actions">
|
||||
<h3 class="sr">Alert Actions</h3>
|
||||
<ul>
|
||||
<li class="nav-item">
|
||||
<a href="#" class="action-primary">Re-send Activation Message</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="#" rel="external" class="action-secondary">Contact edX Support</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</%block>
|
||||
|
||||
<%block name="view_notifications">
|
||||
<!-- notification: change has been made and a save is needed -->
|
||||
<div class="wrapper wrapper-notification wrapper-notification-change" id="notification-change" role="status">
|
||||
<div class="notification change has-actions">
|
||||
<i class="ss-icon ss-symbolicons-block icon icon-change">📝</i>
|
||||
|
||||
<div class="copy">
|
||||
<h2 class="title title-3">You've Made Some Changes</h2>
|
||||
<p class="message">Your changes will not take effect until you <strong>save your progress</strong>.</p>
|
||||
</div>
|
||||
|
||||
<nav class="nav-actions">
|
||||
<h3 class="sr">Notification Actions</h3>
|
||||
<ul>
|
||||
<li class="nav-item">
|
||||
<a href="#" class="action-primary">Save Changes</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="#" class="action-secondary">Don't Save</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- notification: newer version exists -->
|
||||
<div class="wrapper wrapper-notification wrapper-notification-warning" id="notification-version" aria-hidden="true" role="dialog" aria-labelledby="notification-warning-title" aria-describedby="notification-warning-description">
|
||||
<div class="notification warning has-actions">
|
||||
<i class="ss-icon ss-symbolicons-block icon icon-warning">⚠</i>
|
||||
|
||||
<div class="copy">
|
||||
<h2 class="title title-3" id="notification-warning-title">A Newer Version of This Exists</h2>
|
||||
<p class="message" id="notification-warning-description">Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa.</p>
|
||||
</div>
|
||||
|
||||
<nav class="nav-actions">
|
||||
<h3 class="sr">Notification Actions</h3>
|
||||
<ul>
|
||||
<li class="nav-item">
|
||||
<a href="#" class="button save-button action-primary">Go to Newer Version</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="#" class="button cancel-button action-secondary">Continue Editing</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- notification: warning about editing something dangerous -->
|
||||
<div class="wrapper wrapper-notification wrapper-notification-warning" id="notification-dangerous" aria-hidden="true" role="dialog" aria-labelledby="notification-dangerous-title" aria-describedby="notification-dangerous-description">
|
||||
<div class="notification warning has-actions">
|
||||
<i class="ss-icon ss-symbolicons-block icon icon-warning">⚠</i>
|
||||
|
||||
<div class="copy">
|
||||
<h2 class="title title-3" id="notification-dangerous-title">Are You Sure You Want to Edit That?</h2>
|
||||
<p class="message" id="notification-dangerous-description">Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa.</p>
|
||||
</div>
|
||||
|
||||
<nav class="nav-actions">
|
||||
<h3 class="sr">Notification Actions</h3>
|
||||
<ul>
|
||||
<li class="nav-item">
|
||||
<a href="#" class="action-primary">Yes, I want to Edit X</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="#" class="action-secondary">No, I do not</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- notification: status - saving -->
|
||||
<div class="wrapper wrapper-notification wrapper-notification-status wrapper-notification-saving" id="notification-saving">
|
||||
<div class="notification saving">
|
||||
<i class="ss-icon ss-symbolicons-block icon icon-saving">⚙</i>
|
||||
|
||||
<div class="copy">
|
||||
<h2 class="title title-3" role="status">Saving …</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- notification: status- confirmed -->
|
||||
<div class="wrapper wrapper-notification wrapper-notification-confirmation" id="notification-confirmation">
|
||||
<div class="notification confirmation">
|
||||
<i class="ss-icon ss-symbolicons-standard icon icon-confirmation">✓</i>
|
||||
|
||||
<div class="copy">
|
||||
<h2 class="title title-3" role="status"><a href="#">Your Section</a> Has Been Created</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- notification: help - DYK -->
|
||||
<div class="wrapper wrapper-notification wrapper-notification-help" id="notification-help">
|
||||
<div class="notification help">
|
||||
<i class="ss-icon ss-symbolicons-block icon icon-help">❓</i>
|
||||
|
||||
<div class="copy">
|
||||
<h2 class="title title-3">Fun Fact:</h2>
|
||||
<p class="message">Using the checkmark will allow you make a subsection gradable as an assignment, which counts towards a student's total grade</p>
|
||||
</div>
|
||||
|
||||
<a href="#" rel="view" class="action action-notification-close">
|
||||
<i class="ss-icon ss-symbolicons-block icon icon-close">␡</i>
|
||||
<span class="label">close notification</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</%block>
|
||||
|
||||
<%block name="view_prompts">
|
||||
<!-- prompt - confirm deletion -->
|
||||
<div class="wrapper wrapper-prompt wrapper-prompt-confirm" id="prompt-confirm" aria-hidden="true" role="dialog" aria-labelledby="prompt-confirm-sectionDelete-title" aria-describedby="prompt-confirm-sectionDelete-description">
|
||||
<div class="prompt confirm">
|
||||
<div class="copy">
|
||||
<h2 class="title title-3" id="prompt-confirm-sectionDelete-title">Delete "Introduction & Overview"?</h2>
|
||||
<p class="message" id="prompt-confirm-sectionDelete-description">Deleting a section cannot be undone and its contents cannot be recovered.</p>
|
||||
</div>
|
||||
|
||||
<nav class="nav-actions">
|
||||
<h3 class="sr">Prompt Actions</h3>
|
||||
<ul>
|
||||
<li class="nav-item">
|
||||
<a href="#" class="action-primary action-proceed">Yes, delete this section</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="#" class="action-secondary action-cancel">Cancel</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- prompt - warning -->
|
||||
<div class="wrapper wrapper-prompt wrapper-prompt-warning" id="prompt-warning" aria-hidden="true" role="dialog" aria-labelledby="prompt-warning-useAdvanced-title" aria-describedby="prompt-warning-useAdvanced-description">
|
||||
<div class="prompt warning">
|
||||
<div class="copy">
|
||||
<h2 class="title title-3" id="prompt-warning-useAdvanced-title">Use Advanced Problem Editor?</h2>
|
||||
<p class="message" id="prompt-warning-useAdvanced-description">If you proceed, you cannot edit this problem using the simple problem editor.</p>
|
||||
</div>
|
||||
|
||||
<nav class="nav-actions">
|
||||
<h3 class="sr">Prompt Actions</h3>
|
||||
<ul>
|
||||
<li class="nav-item">
|
||||
<a href="#" class="action-primary action-proceed">Continue to Advanced Editor</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="#" class="action-secondary action-cancel">Cancel</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- prompt - error -->
|
||||
<div class="wrapper wrapper-prompt wrapper-prompt-error" id="prompt-error" aria-hidden="true" role="dialog" aria-labelledby="prompt-errorUser-title" aria-describedby="prompt-errorUser-description">
|
||||
<div class="prompt error">
|
||||
<div class="copy">
|
||||
<h2 class="title title-3" id="prompt-errorUser-title">There Were Errors in Your Submission</h2>
|
||||
<p class="message" id="prompt-errorUser-description">Please correct the errors noted on the page and try again.</p>
|
||||
</div>
|
||||
|
||||
<nav class="nav-actions">
|
||||
<h3 class="sr">Prompt Actions</h3>
|
||||
<ul>
|
||||
<li class="nav-item">
|
||||
<a href="#" class="action-primary action-proceed">Correct errors & try again</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</%block>
|
||||
@@ -1,6 +1,5 @@
|
||||
<%! from django.core.urlresolvers import reverse %>
|
||||
|
||||
<div class="wrapper-footer wrapper">
|
||||
<div class="wrapper-footer wrapper">
|
||||
<footer class="primary" role="contentinfo">
|
||||
<div class="colophon">
|
||||
<p>© 2013 <a href="http://www.edx.org" rel="external">edX</a>. All rights reserved.</p>
|
||||
@@ -14,16 +13,11 @@
|
||||
<li class="nav-item nav-peripheral-pp">
|
||||
<a href="#">Privacy Policy</a>
|
||||
</li> -->
|
||||
<li class="nav-item nav-peripheral-help">
|
||||
<a href="http://help.edge.edx.org/" rel="external">edX Studio Help</a>
|
||||
</li>
|
||||
<li class="nav-item nav-peripheral-contact">
|
||||
<a href="https://www.edx.org/contact" rel="external">Contact edX</a>
|
||||
</li>
|
||||
|
||||
% if user.is_authenticated():
|
||||
<!-- add in zendesk/tender feedback form UI -->
|
||||
% endif
|
||||
<li class="nav-item nav-peripheral-feedback">
|
||||
<a href="http://help.edge.edx.org/discussion/new" class="show-tender" title="Use our feedback tool, Tender, to share your feedback">Contact Us</a>
|
||||
</li>
|
||||
% endif
|
||||
</ol>
|
||||
</nav>
|
||||
</footer>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<%! from django.core.urlresolvers import reverse %>
|
||||
|
||||
<div class="wrapper-header wrapper">
|
||||
<div class="wrapper-header wrapper" id="view-top">
|
||||
<header class="primary" role="banner">
|
||||
|
||||
<div class="wrapper wrapper-left ">
|
||||
|
||||
1
cms/templates/widgets/metadata-only-edit.html
Normal file
1
cms/templates/widgets/metadata-only-edit.html
Normal file
@@ -0,0 +1 @@
|
||||
<%include file="metadata-edit.html" />
|
||||
13
cms/templates/widgets/qualaroo.html
Normal file
13
cms/templates/widgets/qualaroo.html
Normal file
@@ -0,0 +1,13 @@
|
||||
% if settings.MITX_FEATURES.get('STUDIO_NPS_SURVEY'):
|
||||
<!-- Qualaroo is used for net promoter score surveys -->
|
||||
<script type="text/javascript">
|
||||
% if user.is_authenticated():
|
||||
var _kiq = _kiq || [];
|
||||
_kiq.push(['identify', "${ user.email }" ]);
|
||||
% endif
|
||||
</script>
|
||||
|
||||
<!-- Qualaroo for edx.org -->
|
||||
<script type="text/javascript" src="//s3.amazonaws.com/ki.js/48221/9SN.js" async="true"></script>
|
||||
<!-- end Qualaroo -->
|
||||
% endif
|
||||
32
cms/templates/widgets/segment-io.html
Normal file
32
cms/templates/widgets/segment-io.html
Normal file
@@ -0,0 +1,32 @@
|
||||
% if settings.MITX_FEATURES.get('SEGMENT_IO'):
|
||||
<!-- begin Segment.io -->
|
||||
<script type="text/javascript">
|
||||
// if inside course, inject the course location into the JS namespace
|
||||
%if context_course:
|
||||
var course_location_analytics = "${context_course.location}";
|
||||
%endif
|
||||
|
||||
var analytics=analytics||[];analytics.load=function(e){var t=document.createElement("script");t.type="text/javascript",t.async=!0,t.src=("https:"===document.location.protocol?"https://":"http://")+"d2dq2ahtl5zl1z.cloudfront.net/analytics.js/v1/"+e+"/analytics.min.js";var n=document.getElementsByTagName("script")[0];n.parentNode.insertBefore(t,n);var r=function(e){return function(){analytics.push([e].concat(Array.prototype.slice.call(arguments,0)))}},i=["identify","track","trackLink","trackForm","trackClick","trackSubmit","pageview","ab","alias","ready"];for(var s=0;s<i.length;s++)analytics[i[s]]=r(i[s])};
|
||||
analytics.load("${ settings.SEGMENT_IO_KEY }");
|
||||
|
||||
% if user.is_authenticated():
|
||||
analytics.identify("${ user.id }", {
|
||||
email : "${ user.email }",
|
||||
username : "${ user.username }"
|
||||
});
|
||||
|
||||
% endif
|
||||
</script>
|
||||
<!-- end Segment.io -->
|
||||
% else:
|
||||
<!-- dummy segment.io -->
|
||||
<script type="text/javascript">
|
||||
%if context_course:
|
||||
var course_location_analytics = "${context_course.location}";
|
||||
%endif
|
||||
var analytics = {
|
||||
track: function() { return; }
|
||||
};
|
||||
</script>
|
||||
<!-- end dummy segment.io -->
|
||||
% endif
|
||||
54
cms/templates/widgets/sock.html
Normal file
54
cms/templates/widgets/sock.html
Normal file
@@ -0,0 +1,54 @@
|
||||
<%! from django.core.urlresolvers import reverse %>
|
||||
<div class="wrapper-sock wrapper">
|
||||
<ul class="list-actions list-cta">
|
||||
<li class="action-item">
|
||||
<a href="#sock" class="cta cta-show-sock"><i class="ss-icon ss-symbolicons-block icon icon-inline icon-help">❓</i> <span class="copy">Looking for Help with Studio?</span></a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="wrapper-inner wrapper">
|
||||
<section class="sock" id="sock">
|
||||
<header>
|
||||
<h2 class="title sr"><i class="ss-icon ss-symbolicons-block icon icon-inline icon-help">❓</i> edX Studio Help</h2>
|
||||
</header>
|
||||
|
||||
<div class="support">
|
||||
<h3 class="title">Studio Support</h3>
|
||||
|
||||
<div class="copy">
|
||||
<p>Need help with Studio? Creating a course is complex, so we're here to help. Take advantage of our documentation, help center, as well as our edX101 introduction course for course authors.</p>
|
||||
</div>
|
||||
|
||||
<ul class="list-actions">
|
||||
<li class="action-item">
|
||||
<a href="http://files.edx.org/Getting_Started_with_Studio.pdf" class="action action-primary" title="This is a PDF Document"><i class="ss-icon icon ss-symbolicons-block icon icon-inline icon-pdf"></i> Download Studio Documentation</a>
|
||||
<span class="tip">How to use Studio to build your course</span>
|
||||
</li>
|
||||
<li class="action-item">
|
||||
<a href="http://help.edge.edx.org/" rel="external" class="action action-primary"><i class="ss-icon icon ss-symbolicons-block icon icon-inline icon-help"></i> Studio Help Center</a>
|
||||
<span class="tip"><i class="ss-icon ss-symbolicons-block icon-help"></i> Studio Help Center</span>
|
||||
</li>
|
||||
<li class="action-item">
|
||||
<a href="https://edge.edx.org/courses/edX/edX101/How_to_Create_an_edX_Course/about" rel="external" class="action action-primary"><i class="ss-icon icon ss-symbolicons-block icon icon-inline icon-enroll">🎓</i> Enroll in edX101</a>
|
||||
<span class="tip">How to use Studio to build your course</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="feedback">
|
||||
<h3 class="title">Contact us about Studio</h3>
|
||||
|
||||
<div class="copy">
|
||||
<p>Have problems, questions, or suggestions about Studio? We're also here to listen to any feedback you want to share.</p>
|
||||
</div>
|
||||
|
||||
<ul class="list-actions">
|
||||
<li class="action-item">
|
||||
|
||||
<a href="http://help.edge.edx.org/discussion/new" class="action action-primary show-tender" title="Use our feedback tool, Tender, to share your feedback"><i class="ss-icon ss-symbolicons-block icon icon-inline icon-feedback"></i> Contact Us</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
<form id="hls-form" enctype="multipart/form-data">
|
||||
<section class="source-edit">
|
||||
<textarea name="" data-metadata-name="source_code" class="source-edit-box hls-data" rows="8" cols="40">${metadata['source_code']|h}</textarea>
|
||||
<textarea name="" data-metadata-name="source_code" class="source-edit-box hls-data" rows="8" cols="40">${editable_metadata_fields['source_code']|h}</textarea>
|
||||
</section>
|
||||
<div class="submit">
|
||||
<button type="reset" class="hls-compile">Save & Compile to edX XML</button>
|
||||
|
||||
13
cms/templates/widgets/tender.html
Normal file
13
cms/templates/widgets/tender.html
Normal file
@@ -0,0 +1,13 @@
|
||||
% if user.is_authenticated():
|
||||
|
||||
<script type="text/javascript">
|
||||
Tender = {
|
||||
hideToggle: true,
|
||||
title: '',
|
||||
body: '',
|
||||
hide_kb: 'true',
|
||||
widgetToggles: $('.show-tender')
|
||||
}
|
||||
</script>
|
||||
<script src="https://edxedge.tenderapp.com/tender_widget.js" type="text/javascript"></script>
|
||||
% endif
|
||||
@@ -116,6 +116,8 @@ urlpatterns += (
|
||||
|
||||
url(r'^logout$', 'student.views.logout_user', name='logout'),
|
||||
|
||||
# static/proof-of-concept views
|
||||
url(r'^ux-alerts$', 'contentstore.views.ux_alerts', name='ux-alerts')
|
||||
)
|
||||
|
||||
if settings.ENABLE_JASMINE:
|
||||
|
||||
@@ -40,7 +40,6 @@ class CmsNamespace(Namespace):
|
||||
"""
|
||||
Namespace with fields common to all blocks in Studio
|
||||
"""
|
||||
is_draft = Boolean(help="Whether this module is a draft", default=False, scope=Scope.settings)
|
||||
published_date = DateTuple(help="Date when the module was published", scope=Scope.settings)
|
||||
published_by = String(help="Id of the user who published this module", scope=Scope.settings)
|
||||
empty = StringyBoolean(help="Whether this is an empty template", scope=Scope.settings, default=False)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user