Merge remote-tracking branch 'origin/master' into fix/vik/add-oe-tab

Conflicts:
	cms/djangoapps/contentstore/utils.py
	cms/djangoapps/contentstore/views.py
	cms/djangoapps/models/settings/course_metadata.py
This commit is contained in:
Vik Paruchuri
2013-03-29 13:10:47 -04:00
431 changed files with 16099 additions and 12559 deletions

View File

@@ -36,8 +36,13 @@ load-plugins=
disable=
# W0141: Used builtin function 'map'
# W0142: Used * or ** magic
# R0201: Method could be a function
# R0901: Too many ancestors
# R0902: Too many instance attributes
# R0903: Too few public methods (1/2)
W0141,W0142,R0903
# R0904: Too many public methods
# R0913: Too many arguments
W0141,W0142,R0201,R0901,R0902,R0903,R0904,R0913
[REPORTS]
@@ -133,7 +138,7 @@ bad-names=foo,bar,baz,toto,tutu,tata
# Regular expression which should only match functions or classes name which do
# not require a docstring
no-docstring-rgx=__.*__
no-docstring-rgx=(__.*__|test_.*)
[MISCELLANEOUS]

View File

@@ -5,9 +5,11 @@ from lxml import html
import re
from django.http import HttpResponseBadRequest
import logging
import django.utils
## TODO store as array of { date, content } and override course_info_module.definition_from_xml
## This should be in a class which inherits from XmlDescriptor
# # TODO store as array of { date, content } and override course_info_module.definition_from_xml
# # This should be in a class which inherits from XmlDescriptor
log = logging.getLogger(__name__)
def get_course_updates(location):
@@ -26,9 +28,11 @@ def get_course_updates(location):
# purely to handle free formed updates not done via editor. Actually kills them, but at least doesn't break.
try:
course_html_parsed = html.fromstring(course_updates.definition['data'])
course_html_parsed = html.fromstring(course_updates.data)
except:
course_html_parsed = html.fromstring("<ol></ol>")
log.error("Cannot parse: " + course_updates.data)
escaped = django.utils.html.escape(course_updates.data)
course_html_parsed = html.fromstring("<ol><li>" + escaped + "</li></ol>")
# Confirm that root is <ol>, iterate over <li>, pull out <h2> subs and then rest of val
course_upd_collection = []
@@ -60,13 +64,15 @@ def update_course_updates(location, update, passed_id=None):
try:
course_updates = modulestore('direct').get_item(location)
except ItemNotFoundError:
return HttpResponseBadRequest
return HttpResponseBadRequest()
# purely to handle free formed updates not done via editor. Actually kills them, but at least doesn't break.
try:
course_html_parsed = html.fromstring(course_updates.definition['data'])
course_html_parsed = html.fromstring(course_updates.data)
except:
course_html_parsed = html.fromstring("<ol></ol>")
log.error("Cannot parse: " + course_updates.data)
escaped = django.utils.html.escape(course_updates.data)
course_html_parsed = html.fromstring("<ol><li>" + escaped + "</li></ol>")
# No try/catch b/c failure generates an error back to client
new_html_parsed = html.fromstring('<li><h2>' + update['date'] + '</h2>' + update['content'] + '</li>')
@@ -85,12 +91,18 @@ def update_course_updates(location, update, passed_id=None):
passed_id = course_updates.location.url() + "/" + str(idx)
# update db record
course_updates.definition['data'] = html.tostring(course_html_parsed)
modulestore('direct').update_item(location, course_updates.definition['data'])
course_updates.data = html.tostring(course_html_parsed)
modulestore('direct').update_item(location, course_updates.data)
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:]])
return {"id": passed_id,
"date": update['date'],
"content": update['content']}
"content": content}
def delete_course_update(location, update, passed_id):
@@ -99,19 +111,21 @@ def delete_course_update(location, update, passed_id):
Returns the resulting course_updates b/c their ids change.
"""
if not passed_id:
return HttpResponseBadRequest
return HttpResponseBadRequest()
try:
course_updates = modulestore('direct').get_item(location)
except ItemNotFoundError:
return HttpResponseBadRequest
return HttpResponseBadRequest()
# TODO use delete_blank_text parser throughout and cache as a static var in a class
# purely to handle free formed updates not done via editor. Actually kills them, but at least doesn't break.
try:
course_html_parsed = html.fromstring(course_updates.definition['data'])
course_html_parsed = html.fromstring(course_updates.data)
except:
course_html_parsed = html.fromstring("<ol></ol>")
log.error("Cannot parse: " + course_updates.data)
escaped = django.utils.html.escape(course_updates.data)
course_html_parsed = html.fromstring("<ol><li>" + escaped + "</li></ol>")
if course_html_parsed.tag == 'ol':
# ??? Should this use the id in the json or in the url or does it matter?
@@ -122,9 +136,9 @@ def delete_course_update(location, update, passed_id):
course_html_parsed.remove(element_to_delete)
# update db record
course_updates.definition['data'] = html.tostring(course_html_parsed)
course_updates.data = html.tostring(course_html_parsed)
store = modulestore('direct')
store.update_item(location, course_updates.definition['data'])
store.update_item(location, course_updates.data)
return get_course_updates(location)
@@ -133,7 +147,6 @@ def get_idx(passed_id):
"""
From the url w/ idx appended, get the idx.
"""
# TODO compile this regex into a class static and reuse for each call
idx_matcher = re.search(r'.*/(\d+)$', passed_id)
idx_matcher = re.search(r'.*?/?(\d+)$', passed_id)
if idx_matcher:
return int(idx_matcher.group(1))

View File

@@ -1,54 +1,42 @@
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 only display_name on a newly created course
Scenario: A course author sees default advanced settings
Given I have opened a new course in Studio
When I select the Advanced Settings
Then I see only the display name
Then I see default advanced settings
@skip-phantom
Scenario: Test if there are no policy settings without existing UI controls
Scenario: Add new entries, and they appear alphabetically after save
Given I am on the Advanced Course Settings page in Studio
When I delete the display name
Then there are no advanced policy settings
And I reload the page
Then there are no advanced policy settings
@skip-phantom
Scenario: Test cancel editing key name
Given I am on the Advanced Course Settings page in Studio
When I edit the name of a policy key
And I press the "Cancel" notification button
Then the policy key name is unchanged
Scenario: Test editing key name
Given I am on the Advanced Course Settings page in Studio
When I edit the name of a policy key
And I press the "Save" notification button
Then the policy key name is changed
Then the settings are alphabetized
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
And I press the "Cancel" notification button
Then the policy key value is unchanged
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
Then the policy key value is changed
Scenario: Add new entries, and they appear alphabetically after save
Given I am on the Advanced Course Settings page in Studio
When I create New Entries
Then they are alphabetized
And I reload the page
Then they are alphabetized
Then the policy key value is changed
Scenario: Test how multi-line input appears
Given I am on the Advanced Course Settings page in Studio
When I create a JSON object
When I create a JSON object as a value
Then it is displayed as formatted
And I reload the page
Then it is displayed as formatted
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
Then it is displayed as a string
And I reload the page
Then it is displayed as a string

View File

@@ -1,8 +1,10 @@
#pylint: disable=C0111
#pylint: disable=W0621
from lettuce import world, step
from common import *
import time
from selenium.common.exceptions import WebDriverException
from selenium.webdriver.support import expected_conditions as EC
from terrain.steps import reload_the_page
from nose.tools import assert_true, assert_false, assert_equal
@@ -11,16 +13,20 @@ http://selenium.googlecode.com/svn/trunk/docs/api/py/webdriver/selenium.webdrive
"""
from selenium.webdriver.common.keys import Keys
KEY_CSS = '.key input.policy-key'
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.browser.click_link_by_text('Advanced Settings')
world.css_click(link_css)
@step('I am on the Advanced Course Settings page in Studio$')
@@ -29,35 +35,11 @@ def i_am_on_advanced_course_settings(step):
step.given('I select the Advanced Settings')
# TODO: this is copied from terrain's step.py. Need to figure out how to share that code.
@step('I reload the page$')
def reload_the_page(step):
world.browser.reload()
@step(u'I edit the name of a policy key$')
def edit_the_name_of_a_policy_key(step):
policy_key_css = 'input.policy-key'
e = css_find(policy_key_css).first
e.type('_new')
@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)
world.css_click_at(css)
try:
css_click_at(css)
wait_for(is_invisible)
except WebDriverException, e:
css_click_at(css)
wait_for(is_invisible)
@step(u'I edit the value of a policy key$')
def edit_the_value_of_a_policy_key(step):
@@ -65,133 +47,86 @@ 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 :)
"""
policy_key_css = 'input.policy-key'
e = css_find(policy_key_css).first
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('I delete the display name$')
def delete_the_display_name(step):
delete_entry(0)
click_save()
@step('create New Entries$')
def create_new_entries(step):
create_entry("z", "apple")
create_entry("a", "zebra")
click_save()
@step('I create a JSON object$')
@step('I create a JSON object as a value$')
def create_JSON_object(step):
create_entry("json", '{"key": "value", "key_2": "value_2"}')
click_save()
change_display_name_value(step, '{"key": "value", "key_2": "value_2"}')
@step('I create a non-JSON value not in quotes$')
def create_value_not_in_quotes(step):
change_display_name_value(step, 'quote me')
############### RESULTS ####################
@step('I see only the display name$')
def i_see_only_display_name(step):
assert_policy_entries(["display_name"], ['"Robot Super Course"'])
@step('I see default advanced settings$')
def i_see_default_advanced_settings(step):
# Test only a few of the existing properties (there are around 34 of them)
assert_policy_entries(
["advanced_modules", DISPLAY_NAME_KEY, "show_calculator"], ["[]", DISPLAY_NAME_VALUE, "false"])
@step('there are no advanced policy settings$')
def no_policy_settings(step):
keys_css = 'input.policy-key'
val_css = 'textarea.json'
k = world.browser.is_element_not_present_by_css(keys_css, 5)
v = world.browser.is_element_not_present_by_css(val_css, 5)
assert_true(k)
assert_true(v)
@step('they are alphabetized$')
@step('the settings are alphabetized$')
def they_are_alphabetized(step):
assert_policy_entries(["a", "display_name", "z"], ['"zebra"', '"Robot Super Course"', '"apple"'])
key_elements = world.css_find(KEY_CSS)
all_keys = []
for key in key_elements:
all_keys.append(key.value)
assert_equal(sorted(all_keys), all_keys, "policy keys were not sorted")
@step('it is displayed as formatted$')
def it_is_formatted(step):
assert_policy_entries(["display_name", "json"], ['"Robot Super Course"', '{\n "key": "value",\n "key_2": "value_2"\n}'])
assert_policy_entries([DISPLAY_NAME_KEY], ['{\n "key": "value",\n "key_2": "value_2"\n}'])
@step(u'the policy key name is unchanged$')
def the_policy_key_name_is_unchanged(step):
policy_key_css = 'input.policy-key'
val = css_find(policy_key_css).first.value
assert_equal(val, 'display_name')
@step(u'the policy key name is changed$')
def the_policy_key_name_is_changed(step):
policy_key_css = 'input.policy-key'
val = css_find(policy_key_css).first.value
assert_equal(val, 'display_name_new')
@step('it is displayed as a string')
def it_is_formatted(step):
assert_policy_entries([DISPLAY_NAME_KEY], ['"quote me"'])
@step(u'the policy key value is unchanged$')
def the_policy_key_value_is_unchanged(step):
policy_value_css = 'li.course-advanced-policy-list-item div.value textarea'
val = css_find(policy_value_css).first.value
assert_equal(val, '"Robot Super Course"')
assert_equal(get_display_name_value(), DISPLAY_NAME_VALUE)
@step(u'the policy key value is changed$')
def the_policy_key_value_is_unchanged(step):
policy_value_css = 'li.course-advanced-policy-list-item div.value textarea'
val = css_find(policy_value_css).first.value
assert_equal(val, '"Robot Super Course X"')
def the_policy_key_value_is_changed(step):
assert_equal(get_display_name_value(), '"Robot Super Course X"')
############# HELPERS ###############
def create_entry(key, value):
# Scroll down the page so the button is visible
world.scroll_to_bottom()
css_click_at('a.new-advanced-policy-item', 10, 10)
new_key_css = 'div#__new_advanced_key__ input'
new_key_element = css_find(new_key_css).first
new_key_element.fill(key)
# For some reason have to get the instance for each command
# (get error that it is no longer attached to the DOM)
# Have to do all this because Selenium fill does not remove existing text
new_value_css = 'div.CodeMirror textarea'
css_find(new_value_css).last.fill("")
css_find(new_value_css).last._element.send_keys(Keys.DELETE, Keys.DELETE)
css_find(new_value_css).last.fill(value)
# Add in a TAB key press because intermittently on ubuntu the
# last character of "value" above was not getting typed in
css_find(new_value_css).last._element.send_keys(Keys.TAB)
def delete_entry(index):
"""
Delete the nth entry where index is 0-based
"""
css = 'a.delete-button'
assert_true(world.browser.is_element_present_by_css(css, 5))
delete_buttons = css_find(css)
assert_true(len(delete_buttons) > index, "no delete button exists for entry " + str(index))
delete_buttons[index].click()
def assert_policy_entries(expected_keys, expected_values):
assert_entries('.key input.policy-key', expected_keys)
assert_entries('textarea.json', 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], world.css_find(VALUE_CSS)[index].value, "value is incorrect")
def assert_entries(css, expected_values):
webElements = css_find(css)
assert_equal(len(expected_values), len(webElements))
# Sometimes get stale reference if I hold on to the array of elements
for counter in range(len(expected_values)):
assert_equal(expected_values[counter], css_find(css)[counter].value)
def get_index_of(expected_key):
for counter in range(len(world.css_find(KEY_CSS))):
# Sometimes get stale reference if I hold on to the array of elements
key = world.css_find(KEY_CSS)[counter].value
if key == expected_key:
return counter
return -1
def click_save():
css = "a.save-button"
css_click_at(css)
def get_display_name_value():
index = get_index_of(DISPLAY_NAME_KEY)
return world.css_find(VALUE_CSS)[index].value
def fill_last_field(value):
newValue = css_find('#__new_advanced_key__ input').first
newValue.fill(value)
def change_display_name_value(step, new_value):
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")

View File

@@ -0,0 +1,24 @@
Feature: Course checklists
Scenario: A course author sees checklists defined by edX
Given I have opened a new course in Studio
When I select Checklists from the Tools menu
Then I see the four default edX checklists
Scenario: A course author can mark tasks as complete
Given I have opened Checklists
Then I can check and uncheck tasks in a checklist
And They are correctly selected after I reload the page
Scenario: A task can link to a location within Studio
Given I have opened Checklists
When I select a link to the course outline
Then I am brought to the course outline page
And I press the browser back button
Then I am brought back to the course outline in the correct state
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

View File

@@ -0,0 +1,123 @@
#pylint: disable=C0111
#pylint: disable=W0621
from lettuce import world, step
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):
world.css_click(expand_icon_css)
link_css = 'li.nav-course-tools-checklists a'
world.css_click(link_css)
@step('I have opened Checklists$')
def i_have_opened_checklists(step):
step.given('I have opened a new course in Studio')
step.given('I select Checklists from the Tools menu')
@step('I see the four default edX checklists$')
def i_see_default_checklists(step):
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'))
assert_true(checklists[2].text.endswith("Explore edX\'s Support Tools"))
assert_true(checklists[3].text.endswith('Draft Your Course About Page'))
@step('I can check and uncheck tasks in a checklist$')
def i_can_check_and_uncheck_tasks(step):
# Use the 2nd checklist as a reference
verifyChecklist2Status(0, 7, 0)
toggleTask(1, 0)
verifyChecklist2Status(1, 7, 14)
toggleTask(1, 3)
verifyChecklist2Status(2, 7, 29)
toggleTask(1, 6)
verifyChecklist2Status(3, 7, 43)
toggleTask(1, 3)
verifyChecklist2Status(2, 7, 29)
@step('They are correctly selected after I reload the page$')
def tasks_correctly_selected_after_reload(step):
reload_the_page(step)
verifyChecklist2Status(2, 7, 29)
# verify that task 7 is still selected by toggling its checkbox state and making sure that it deselects
toggleTask(1, 6)
verifyChecklist2Status(1, 7, 14)
@step('I select a link to the course outline$')
def i_select_a_link_to_the_course_outline(step):
clickActionLink(1, 0, 'Edit Course Outline')
@step('I am brought to the course outline page$')
def i_am_brought_to_course_outline(step):
assert_equal('Course Outline', world.css_find('.outline .title-1')[0].text)
assert_equal(1, len(world.browser.windows))
@step('I am brought back to the course outline in the correct state$')
def i_am_brought_back_to_course_outline(step):
step.given('I see the four default edX checklists')
# In a previous step, we selected (1, 0) in order to click the 'Edit Course Outline' link.
# Make sure the task is still showing as selected (there was a caching bug with the collection).
verifyChecklist2Status(1, 7, 14)
@step('I select a link to help page$')
def i_select_a_link_to_the_help_page(step):
clickActionLink(2, 0, 'Visit Studio Help')
@step('I am brought to the help page in a new window$')
def i_am_brought_to_help_page_in_new_window(step):
step.given('I see the four default edX checklists')
windows = world.browser.windows
assert_equal(2, len(windows))
world.browser.switch_to_window(windows[1])
assert_equal('http://help.edge.edx.org/', world.browser.url)
############### HELPER METHODS ####################
def verifyChecklist2Status(completed, total, percentage):
def verify_count(driver):
try:
statusCount = world.css_find('#course-checklist1 .status-count').first
return statusCount.text == str(completed)
except StaleElementReferenceException:
return False
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), world.css_find('#course-checklist1 .viz-checklist-status-value .int').first.text)
def toggleTask(checklist, task):
world.css_click('#course-checklist' + str(checklist) +'-task' + str(task))
def clickActionLink(checklist, task, actionText):
# toggle checklist item to make sure that the link button is showing
toggleTask(checklist, 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
world.wait_for(verify_action_link_text)
action_link.click()

View File

@@ -1,14 +1,10 @@
#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 terrain.factories import UserFactory, RegistrationFactory, UserProfileFactory
from terrain.factories import CourseFactory, GroupFactory
from xmodule.modulestore.django import _MODULESTORES, modulestore
from xmodule.templates import update_templates
from auth.authz import get_user_by_email
@@ -17,14 +13,15 @@ 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$')
@@ -45,12 +42,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()
@@ -61,7 +58,7 @@ def create_studio_user(
email='robot+studio@edx.org',
password='test',
is_staff=False):
studio_user = UserFactory.build(
studio_user = world.UserFactory.build(
username=uname,
email=email,
password=password,
@@ -69,87 +66,20 @@ def create_studio_user(
studio_user.set_password(password)
studio_user.save()
registration = RegistrationFactory(user=studio_user)
registration = world.RegistrationFactory(user=studio_user)
registration.register(studio_user)
registration.activate()
user_profile = 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()
user_profile = world.UserProfileFactory(user=studio_user)
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(
@@ -157,55 +87,56 @@ 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():
c = CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course')
c = world.CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course')
# Add the user to the instructor group of the course
# so they will have the permissions to see it in studio
g = GroupFactory.create(name='instructor_MITx/999/Robot_Super_Course')
g = world.GroupFactory.create(name='instructor_MITx/999/Robot_Super_Course')
u = get_user_by_email('robot+studio@edx.org')
u.groups.add(g)
u.save()
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)

View File

@@ -0,0 +1,25 @@
Feature: Course Settings
As a course author, I want to be able to configure my course settings.
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
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
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

View 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 = "3:30pm"
DEFAULT_TIME = "12:00am"
############### 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))

View File

@@ -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

View File

@@ -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')

View File

@@ -1,34 +0,0 @@
import factory
from student.models import User, UserProfile, Registration
from datetime import datetime
import uuid
class UserProfileFactory(factory.Factory):
FACTORY_FOR = UserProfile
user = None
name = 'Robot Studio'
courseware = 'course.xml'
class RegistrationFactory(factory.Factory):
FACTORY_FOR = Registration
user = None
activation_key = uuid.uuid4().hex
class UserFactory(factory.Factory):
FACTORY_FOR = User
username = 'robot-studio'
email = 'robot+studio@edx.org'
password = 'test'
first_name = 'Robot'
last_name = 'Studio'
is_staff = False
is_active = True
is_superuser = False
last_login = datetime.now()
date_joined = datetime.now()

View File

@@ -1,3 +1,6 @@
#pylint: disable=C0111
#pylint: disable=W0621
from lettuce import world, step
from common import *
from nose.tools import assert_equal
@@ -10,7 +13,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,19 +34,19 @@ 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')
world.css_fill(date_css, '12/25/2013')
# hit TAB to get to the time field
e = css_find(date_css).first
e = world.css_find(date_css).first
e._element.send_keys(Keys.TAB)
css_fill(time_css, '12:00am')
e = css_find(time_css).first
world.css_fill(time_css, '12:00am')
e = world.css_find(time_css).first
e._element.send_keys(Keys.TAB)
time.sleep(float(1))
world.browser.click_link_by_text('Save')
@@ -64,13 +67,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 +88,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 +102,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 12:00am')
############ HELPER METHODS ###################
@@ -120,10 +123,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)

View File

@@ -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')

View File

@@ -1,30 +1,30 @@
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
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
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

View File

@@ -1,5 +1,7 @@
#pylint: disable=C0111
#pylint: disable=W0621
from lettuce import world, step
from terrain.factories import *
from common import *
from nose.tools import assert_true, assert_false, assert_equal
@@ -9,16 +11,16 @@ logger = getLogger(__name__)
@step(u'I have a course with no sections$')
def have_a_course(step):
clear_courses()
course = CourseFactory.create()
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()
course = CourseFactory.create()
section = ItemFactory.create(parent_location=course.location)
subsection1 = ItemFactory.create(
world.clear_courses()
course = world.CourseFactory.create()
section = world.ItemFactory.create(parent_location=course.location)
subsection1 = world.ItemFactory.create(
parent_location=section.location,
template='i4x://edx/templates/sequential/Empty',
display_name='Subsection One',)
@@ -26,21 +28,21 @@ 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()
course = CourseFactory.create()
section = ItemFactory.create(parent_location=course.location)
subsection1 = ItemFactory.create(
world.clear_courses()
course = world.CourseFactory.create()
section = world.ItemFactory.create(parent_location=course.location)
subsection1 = world.ItemFactory.create(
parent_location=section.location,
template='i4x://edx/templates/sequential/Empty',
display_name='Subsection One',)
section2 = ItemFactory.create(
section2 = world.ItemFactory.create(
parent_location=course.location,
display_name='Section Two',)
subsection2 = ItemFactory.create(
subsection2 = world.ItemFactory.create(
parent_location=section2.location,
template='i4x://edx/templates/sequential/Empty',
display_name='Subsection Alpha',)
subsection3 = ItemFactory.create(
subsection3 = world.ItemFactory.create(
parent_location=section2.location,
template='i4x://edx/templates/sequential/Empty',
display_name='Subsection Beta',)
@@ -50,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')
@@ -67,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)
@@ -112,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)

View File

@@ -17,6 +17,14 @@ Feature: Create Subsection
And I click to edit the subsection name
Then I see the complete subsection name with a quote in the editor
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: Delete a subsection
Given I have opened a new course section in Studio

View File

@@ -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,7 +10,7 @@ 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()
@@ -15,8 +18,7 @@ def i_have_opened_a_new_course_section(step):
@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,14 +33,14 @@ 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$')
@@ -46,6 +48,17 @@ def i_have_added_a_new_subsection(step):
add_subsection()
@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 +83,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)

View File

@@ -7,7 +7,7 @@ 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
from .prompt import query_yes_no
from auth.authz import _delete_course_group

View File

@@ -15,10 +15,10 @@ def get_module_info(store, location, parent_location=None, rewrite_static_links=
template_location = Location(['i4x', 'edx', 'templates', location.category, 'Empty'])
module = store.clone_item(template_location, location)
data = module.definition['data']
data = module.data
if rewrite_static_links:
data = replace_static_urls(
module.definition['data'],
module.data,
None,
course_namespace=Location([
module.location.tag,
@@ -32,7 +32,8 @@ def get_module_info(store, location, parent_location=None, rewrite_static_links=
return {
'id': module.location.url(),
'data': data,
'metadata': module.metadata
# TODO (cpennington): This really shouldn't have to do this much reaching in to get the metadata
'metadata': module._model_data._kvs._metadata
}
@@ -70,23 +71,23 @@ def set_module_info(store, location, post_data):
# 'apply' the submitted metadata, so we don't end up deleting system metadata
if post_data.get('metadata') is not None:
posted_metadata = post_data['metadata']
# update existing metadata with submitted metadata (which can be partial)
# IMPORTANT NOTE: if the client passed pack 'null' (None) for a piece of metadata that means 'remove it'
for metadata_key in posted_metadata.keys():
for metadata_key, value in posted_metadata.items():
# let's strip out any metadata fields from the postback which have been identified as system metadata
# and therefore should not be user-editable, so we should accept them back from the client
if metadata_key in module.system_metadata_fields:
del posted_metadata[metadata_key]
elif posted_metadata[metadata_key] is None:
# remove both from passed in collection as well as the collection read in from the modulestore
if metadata_key in module.metadata:
del module.metadata[metadata_key]
if metadata_key in module._model_data:
del module._model_data[metadata_key]
del posted_metadata[metadata_key]
# overlay the new metadata over the modulestore sourced collection to support partial updates
module.metadata.update(posted_metadata)
else:
module._model_data[metadata_key] = value
# commit to datastore
store.update_metadata(location, module.metadata)
# TODO (cpennington): This really shouldn't have to do this much reaching in to get the metadata
store.update_metadata(location, module._model_data._kvs._metadata)

View File

@@ -0,0 +1,96 @@
""" Unit tests for checklist methods in views.py. """
from contentstore.utils import get_modulestore, get_url_reverse
from contentstore.tests.test_course_settings import CourseTestCase
from xmodule.modulestore.inheritance import own_metadata
from xmodule.modulestore.tests.factories import CourseFactory
from django.core.urlresolvers import reverse
import json
class ChecklistTestCase(CourseTestCase):
""" Test for checklist get and put methods. """
def setUp(self):
""" Creates the test course. """
super(ChecklistTestCase, self).setUp()
self.course = CourseFactory.create(org='mitX', number='333', display_name='Checklists Course')
def get_persisted_checklists(self):
""" Returns the checklists as persisted in the modulestore. """
modulestore = get_modulestore(self.course.location)
return modulestore.get_item(self.course.location).checklists
def test_get_checklists(self):
""" Tests the get checklists method. """
checklists_url = get_url_reverse('Checklists', self.course)
response = self.client.get(checklists_url)
self.assertContains(response, "Getting Started With Studio")
payload = response.content
# Now delete the checklists from the course and verify they get repopulated (for courses
# created before checklists were introduced).
self.course.checklists = None
modulestore = get_modulestore(self.course.location)
modulestore.update_metadata(self.course.location, own_metadata(self.course))
self.assertEquals(self.get_persisted_checklists(), None)
response = self.client.get(checklists_url)
self.assertEquals(payload, response.content)
def test_update_checklists_no_index(self):
""" No checklist index, should return all of them. """
update_url = reverse('checklists_updates', kwargs={
'org': self.course.location.org,
'course': self.course.location.course,
'name': self.course.location.name})
returned_checklists = json.loads(self.client.get(update_url).content)
self.assertListEqual(self.get_persisted_checklists(), returned_checklists)
def test_update_checklists_index_ignored_on_get(self):
""" Checklist index ignored on get. """
update_url = reverse('checklists_updates', kwargs={'org': self.course.location.org,
'course': self.course.location.course,
'name': self.course.location.name,
'checklist_index': 1})
returned_checklists = json.loads(self.client.get(update_url).content)
self.assertListEqual(self.get_persisted_checklists(), returned_checklists)
def test_update_checklists_post_no_index(self):
""" No checklist index, will error on post. """
update_url = reverse('checklists_updates', kwargs={'org': self.course.location.org,
'course': self.course.location.course,
'name': self.course.location.name})
response = self.client.post(update_url)
self.assertContains(response, 'Could not save checklist', status_code=400)
def test_update_checklists_index_out_of_range(self):
""" Checklist index out of range, will error on post. """
update_url = reverse('checklists_updates', kwargs={'org': self.course.location.org,
'course': self.course.location.course,
'name': self.course.location.name,
'checklist_index': 100})
response = self.client.post(update_url)
self.assertContains(response, 'Could not save checklist', status_code=400)
def test_update_checklists_index(self):
""" Check that an update of a particular checklist works. """
update_url = reverse('checklists_updates', kwargs={'org': self.course.location.org,
'course': self.course.location.course,
'name': self.course.location.name,
'checklist_index': 2})
payload = self.course.checklists[2]
self.assertFalse(payload.get('is_checked'))
payload['is_checked'] = True
returned_checklist = json.loads(self.client.post(update_url, json.dumps(payload), "application/json").content)
self.assertTrue(returned_checklist.get('is_checked'))
self.assertEqual(self.get_persisted_checklists()[2], returned_checklist)
def test_update_checklists_delete_unsupported(self):
""" Delete operation is not supported. """
update_url = reverse('checklists_updates', kwargs={'org': self.course.location.org,
'course': self.course.location.course,
'name': self.course.location.name,
'checklist_index': 100})
response = self.client.delete(update_url)
self.assertContains(response, 'Unsupported request', status_code=400)

View File

@@ -6,15 +6,16 @@ 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 django.contrib.auth.models import User
from cms.djangoapps.contentstore.utils import get_modulestore
from contentstore.utils import get_modulestore
from utils import ModuleStoreTestCase, parse_json
from .utils import ModuleStoreTestCase, parse_json
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from xmodule.modulestore import Location
@@ -25,6 +26,7 @@ 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.inheritance import own_metadata
from xmodule.capa_module import CapaDescriptor
from xmodule.course_module import CourseDescriptor
@@ -35,6 +37,14 @@ TEST_DATA_MODULESTORE = copy.deepcopy(settings.MODULESTORE)
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):
@@ -99,6 +109,20 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
self.assertEqual(reverse_tabs, course_tabs)
def test_import_polls(self):
import_from_xml(modulestore(), 'common/test/data/', ['full'])
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)
def test_delete(self):
import_from_xml(modulestore(), 'common/test/data/', ['full'])
@@ -109,10 +133,10 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
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.definition['children'])
self.assertTrue(sequential.location.url() in chapter.children)
self.client.post(reverse('delete_item'),
json.dumps({'id': sequential.location.url(), 'delete_children':'true', 'delete_all_versions':'true'}),
self.client.post(reverse('delete_item'),
json.dumps({'id': sequential.location.url(), 'delete_children': 'true', 'delete_all_versions': 'true'}),
"application/json")
found = False
@@ -127,9 +151,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
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.definition['children'])
self.assertFalse(sequential.location.url() in chapter.children)
def test_about_overrides(self):
'''
@@ -139,11 +161,11 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
import_from_xml(modulestore(), 'common/test/data/', ['full'])
module_store = modulestore('direct')
effort = module_store.get_item(Location(['i4x', 'edX', 'full', 'about', 'effort', None]))
self.assertEqual(effort.definition['data'], '6 hours')
self.assertEqual(effort.data, '6 hours')
# this one should be in a non-override folder
effort = module_store.get_item(Location(['i4x', 'edX', 'full', 'about', 'end_date', None]))
self.assertEqual(effort.definition['data'], 'TBD')
self.assertEqual(effort.data, 'TBD')
def test_remove_hide_progress_tab(self):
import_from_xml(modulestore(), 'common/test/data/', ['full'])
@@ -153,7 +175,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
source_location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
course = module_store.get_item(source_location)
self.assertNotIn('hide_progress_tab', course.metadata)
self.assertFalse(course.hide_progress_tab)
def test_clone_course(self):
@@ -191,6 +213,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'])
@@ -246,7 +272,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
# compare what's on disk compared to what we have in our course
with fs.open('grading_policy.json', 'r') as grading_policy:
on_disk = loads(grading_policy.read())
self.assertEqual(on_disk, course.definition['data']['grading_policy'])
self.assertEqual(on_disk, course.grading_policy)
#check for policy.json
self.assertTrue(fs.exists('policy.json'))
@@ -255,7 +281,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
with fs.open('policy.json', 'r') as course_policy:
on_disk = loads(course_policy.read())
self.assertIn('course/6.002_Spring_2012', on_disk)
self.assertEqual(on_disk['course/6.002_Spring_2012'], course.metadata)
self.assertEqual(on_disk['course/6.002_Spring_2012'], own_metadata(course))
# remove old course
delete_course(module_store, content_store, location)
@@ -291,6 +317,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()
@@ -302,10 +350,11 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
course = module_store.get_item(location)
metadata = own_metadata(course)
# add a bool piece of unknown metadata so we can verify we don't throw an exception
course.metadata['new_metadata'] = True
metadata['new_metadata'] = True
module_store.update_metadata(location, course.metadata)
module_store.update_metadata(location, metadata)
print 'Exporting to tempdir = {0}'.format(root_dir)
@@ -473,21 +522,20 @@ class ContentStoreTest(ModuleStoreTestCase):
self.assertIsInstance(problem, CapaDescriptor, "New problem is not a CapaDescriptor")
context = problem.get_context()
self.assertIn('markdown', context, "markdown is missing from context")
self.assertIn('markdown', problem.metadata, "markdown is missing from metadata")
self.assertNotIn('markdown', problem.editable_metadata_fields, "Markdown slipped into the editable metadata fields")
def test_import_metadata_with_attempts_empty_string(self):
import_from_xml(modulestore(), 'common/test/data/', ['simple'])
module_store = modulestore('direct')
did_load_item = False
try:
try:
module_store.get_item(Location(['i4x', 'edX', 'simple', 'problem', 'ps01-simple', None]))
did_load_item = True
except ItemNotFoundError:
pass
# make sure we found the item (e.g. it didn't error while loading)
self.assertTrue(did_load_item)
self.assertTrue(did_load_item)
def test_metadata_inheritance(self):
import_from_xml(modulestore(), 'common/test/data/', ['full'])
@@ -499,8 +547,7 @@ class ContentStoreTest(ModuleStoreTestCase):
# let's assert on the metadata_inheritance on an existing vertical
for vertical in verticals:
self.assertIn('xqa_key', vertical.metadata)
self.assertEqual(course.metadata['xqa_key'], vertical.metadata['xqa_key'])
self.assertEqual(course.lms.xqa_key, vertical.lms.xqa_key)
self.assertGreater(len(verticals), 0)
@@ -510,36 +557,33 @@ class ContentStoreTest(ModuleStoreTestCase):
# crate a new module and add it as a child to a vertical
module_store.clone_item(source_template_location, new_component_location)
parent = verticals[0]
module_store.update_children(parent.location, parent.definition.get('children', []) + [new_component_location.url()])
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
self.assertIn('graceperiod', new_module.metadata)
self.assertEqual(parent.lms.graceperiod, new_module.lms.graceperiod)
self.assertEqual(parent.metadata['graceperiod'], new_module.metadata['graceperiod'])
self.assertEqual(course.metadata['xqa_key'], new_module.metadata['xqa_key'])
self.assertEqual(course.lms.xqa_key, new_module.lms.xqa_key)
#
# now let's define an override at the leaf node level
#
new_module.metadata['graceperiod'] = '1 day'
module_store.update_metadata(new_module.location, new_module.metadata)
new_module.lms.graceperiod = timedelta(1)
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.assertIn('graceperiod', new_module.metadata)
self.assertEqual('1 day', new_module.metadata['graceperiod'])
self.assertEqual(timedelta(1), new_module.lms.graceperiod)
class TemplateTestCase(ModuleStoreTestCase):
def test_template_cleanup(self):
def test_template_cleanup(self):
module_store = modulestore('direct')
# insert a bogus template in the store
@@ -562,4 +606,3 @@ class TemplateTestCase(ModuleStoreTestCase):
asserted = True
self.assertTrue(asserted)

View File

@@ -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,38 +8,18 @@ from django.core.urlresolvers import reverse
from django.utils.timezone import UTC
from xmodule.modulestore import Location
from cms.djangoapps.models.settings.course_details import (CourseDetails,
from models.settings.course_details import (CourseDetails,
CourseSettingsEncoder)
from cms.djangoapps.models.settings.course_grading import CourseGradingModel
from cms.djangoapps.contentstore.utils import get_modulestore
from models.settings.course_grading import CourseGradingModel
from contentstore.utils import get_modulestore
from django.test import TestCase
from utils import ModuleStoreTestCase
from .utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
from cms.djangoapps.models.settings.course_metadata import CourseMetadata
from models.settings.course_metadata import CourseMetadata
from xmodule.modulestore.xml_importer import import_from_xml
from xmodule.modulestore.django import modulestore
# 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):
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))
from xmodule.fields import Date
class CourseTestCase(ModuleStoreTestCase):
def setUp(self):
@@ -104,7 +82,7 @@ class CourseDetailsTestCase(CourseTestCase):
self.assertIsNone(jsondetails['effort'], "effort somehow initialized")
def test_update_and_fetch(self):
## NOTE: I couldn't figure out how to validly test time setting w/ all the conversions
# # NOTE: I couldn't figure out how to validly test time setting w/ all the conversions
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
@@ -170,19 +148,26 @@ 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.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_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)
expected_delta = datetime.timedelta(0)
self.assertEqual(dt1 - dt2, expected_delta, str(dt1) + "!=" + str(dt2) + " at " + context)
else:
self.fail(field + " missing from encoded but in details at " + context)
@@ -246,8 +231,9 @@ class CourseGradingTest(CourseTestCase):
altered_grader = CourseGradingModel.update_from_json(test_grader.__dict__)
self.assertDictEqual(test_grader.__dict__, altered_grader.__dict__, "cutoff add D")
test_grader.grace_period = {'hours' : 4, 'minutes' : 5, 'seconds': 0}
test_grader.grace_period = {'hours': 4, 'minutes': 5, 'seconds': 0}
altered_grader = CourseGradingModel.update_from_json(test_grader.__dict__)
print test_grader.grace_period, altered_grader.grace_period
self.assertDictEqual(test_grader.__dict__, altered_grader.__dict__, "4 hour grace period")
def test_update_grader_from_json(self):
@@ -268,7 +254,7 @@ class CourseMetadataEditingTest(CourseTestCase):
CourseTestCase.setUp(self)
# add in the full class too
import_from_xml(modulestore(), 'common/test/data/', ['full'])
self.fullcourse_location = Location(['i4x','edX','full','course','6.002_Spring_2012', None])
self.fullcourse_location = Location(['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None])
def test_fetch_initial_fields(self):
@@ -286,31 +272,31 @@ class CourseMetadataEditingTest(CourseTestCase):
def test_update_from_json(self):
test_model = CourseMetadata.update_from_json(self.course_location,
{ "a" : 1,
"b_a_c_h" : { "c" : "test" },
"test_text" : "a text string"})
{ "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,
{ "a" : 2,
{ "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('a', test_model, 'Missing revised a metadata field')
self.assertEqual(test_model['a'], 2, "a not expected value")
self.assertIn('advertised_start', test_model, 'Missing revised advertised_start metadata field')
self.assertEqual(test_model['advertised_start'], 'start B', "advertised_start not expected value")
def update_check(self, test_model):
self.assertIn('display_name', test_model, 'Missing editable metadata field')
self.assertEqual(test_model['display_name'], 'Robot Super Course', "not expected value")
self.assertIn('a', test_model, 'Missing new a metadata field')
self.assertEqual(test_model['a'], 1, "a not expected value")
self.assertIn('b_a_c_h', test_model, 'Missing b_a_c_h metadata field')
self.assertDictEqual(test_model['b_a_c_h'], { "c" : "test" }, "b_a_c_h not expected value")
self.assertIn('test_text', test_model, 'Missing test_text metadata field')
self.assertEqual(test_model['test_text'], "a text string", "test_text not expected value")
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.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):
@@ -321,5 +307,5 @@ class CourseMetadataEditingTest(CourseTestCase):
self.assertEqual(test_model['display_name'], 'Testing', "not expected value")
self.assertIn('rerandomize', test_model, 'Missing rerandomize metadata field')
# check for deletion effectiveness
self.assertNotIn('showanswer', test_model, 'showanswer field still in')
self.assertNotIn('xqa_key', test_model, 'xqa_key field still in')
self.assertEqual('closed', test_model['showanswer'], 'showanswer field still in')
self.assertEqual(None, test_model['xqa_key'], 'xqa_key field still in')

View File

@@ -1,31 +1,145 @@
from cms.djangoapps.contentstore.tests.test_course_settings import CourseTestCase
'''unit tests for course_info views and models.'''
from contentstore.tests.test_course_settings import CourseTestCase
from django.core.urlresolvers import reverse
import json
class CourseUpdateTest(CourseTestCase):
'''The do all and end all of unit test cases.'''
def test_course_update(self):
'''Go through each interface and ensure it works.'''
# first get the update to force the creation
url = reverse('course_info', kwargs={'org': self.course_location.org, 'course': self.course_location.course,
'name': self.course_location.name})
url = reverse('course_info',
kwargs={'org': self.course_location.org,
'course': self.course_location.course,
'name': self.course_location.name})
self.client.get(url)
content = '<iframe width="560" height="315" src="http://www.youtube.com/embed/RocY-Jd93XU" frameborder="0"></iframe>'
init_content = '<iframe width="560" height="315" src="http://www.youtube.com/embed/RocY-Jd93XU" frameborder="0">'
content = init_content + '</iframe>'
payload = {'content': content,
'date': 'January 8, 2013'}
url = reverse('course_info', kwargs={'org': self.course_location.org, 'course': self.course_location.course,
'provided_id': ''})
url = reverse('course_info_json',
kwargs={'org': self.course_location.org,
'course': self.course_location.course,
'provided_id': ''})
resp = self.client.post(url, json.dumps(payload), "application/json")
payload = json.loads(resp.content)
self.assertHTMLEqual(content, payload['content'], "single iframe")
self.assertHTMLEqual(payload['content'], content)
url = reverse('course_info', kwargs={'org': self.course_location.org, 'course': self.course_location.course,
'provided_id': payload['id']})
content += '<div>div <p>p</p></div>'
first_update_url = reverse('course_info_json',
kwargs={'org': self.course_location.org,
'course': self.course_location.course,
'provided_id': payload['id']})
content += '<div>div <p>p<br/></p></div>'
payload['content'] = content
resp = self.client.post(first_update_url, json.dumps(payload),
"application/json")
self.assertHTMLEqual(content, json.loads(resp.content)['content'],
"iframe w/ div")
# now put in an evil update
content = '<ol/>'
payload = {'content': content,
'date': 'January 11, 2013'}
url = reverse('course_info_json',
kwargs={'org': self.course_location.org,
'course': self.course_location.course,
'provided_id': ''})
resp = self.client.post(url, json.dumps(payload), "application/json")
self.assertHTMLEqual(content, json.loads(resp.content)['content'], "iframe w/ div")
payload = json.loads(resp.content)
self.assertHTMLEqual(content, payload['content'], "self closing ol")
url = reverse('course_info_json',
kwargs={'org': self.course_location.org,
'course': self.course_location.course,
'provided_id': ''})
resp = self.client.get(url)
payload = json.loads(resp.content)
self.assertTrue(len(payload) == 2)
# can't test non-json paylod b/c expect_json throws error
# try json w/o required fields
self.assertContains(
self.client.post(url, json.dumps({'garbage': 1}),
"application/json"),
'Failed to save', status_code=400)
# now try to update a non-existent update
url = reverse('course_info_json',
kwargs={'org': self.course_location.org,
'course': self.course_location.course,
'provided_id': '9'})
content = 'blah blah'
payload = {'content': content,
'date': 'January 21, 2013'}
self.assertContains(
self.client.post(url, json.dumps(payload), "application/json"),
'Failed to save', status_code=400)
# update w/ malformed html
content = '<garbage tag No closing brace to force <span>error</span>'
payload = {'content': content,
'date': 'January 11, 2013'}
url = reverse('course_info_json', kwargs={'org': self.course_location.org,
'course': self.course_location.course,
'provided_id': ''})
self.assertContains(
self.client.post(url, json.dumps(payload), "application/json"),
'<garbage')
# set to valid html which would break an xml parser
content = "<p><br><br></p>"
payload = {'content': content,
'date': 'January 11, 2013'}
url = reverse('course_info_json', kwargs={'org': self.course_location.org,
'course': self.course_location.course,
'provided_id': ''})
resp = self.client.post(url, json.dumps(payload), "application/json")
payload = json.loads(resp.content)
self.assertHTMLEqual(content, json.loads(resp.content)['content'])
# now try to delete a non-existent update
url = reverse('course_info_json', kwargs={'org': self.course_location.org,
'course': self.course_location.course,
'provided_id': '19'})
payload = {'content': content,
'date': 'January 21, 2013'}
self.assertContains(self.client.delete(url), "delete", status_code=400)
# now delete a real update
content = 'blah blah'
payload = {'content': content,
'date': 'January 28, 2013'}
url = reverse('course_info_json', kwargs={'org': self.course_location.org,
'course': self.course_location.course,
'provided_id': ''})
resp = self.client.post(url, json.dumps(payload), "application/json")
payload = json.loads(resp.content)
this_id = payload['id']
self.assertHTMLEqual(content, payload['content'], "single iframe")
# first count the entries
url = reverse('course_info_json',
kwargs={'org': self.course_location.org,
'course': self.course_location.course,
'provided_id': ''})
resp = self.client.get(url)
payload = json.loads(resp.content)
before_delete = len(payload)
url = reverse('course_info_json',
kwargs={'org': self.course_location.org,
'course': self.course_location.course,
'provided_id': this_id})
resp = self.client.delete(url)
payload = json.loads(resp.content)
self.assertTrue(len(payload) == before_delete - 1)

View File

@@ -1,19 +1,72 @@
from cms.djangoapps.contentstore import utils
""" Tests for utils. """
from contentstore import utils
import mock
from django.test import TestCase
from xmodule.modulestore.tests.factories import CourseFactory
from .utils import ModuleStoreTestCase
class LMSLinksTestCase(TestCase):
""" Tests for LMS links. """
def about_page_test(self):
""" Get URL for about page. """
location = 'i4x', 'mitX', '101', 'course', 'test'
utils.get_course_id = mock.Mock(return_value="mitX/101/test")
link = utils.get_lms_link_for_about_page(location)
self.assertEquals(link, "//localhost:8000/courses/mitX/101/test/about")
def ls_link_test(self):
def lms_link_test(self):
""" Tests get_lms_link_for_item. """
location = 'i4x', 'mitX', '101', 'vertical', 'contacting_us'
utils.get_course_id = mock.Mock(return_value="mitX/101/test")
link = utils.get_lms_link_for_item(location, False)
self.assertEquals(link, "//localhost:8000/courses/mitX/101/test/jump_to/i4x://mitX/101/vertical/contacting_us")
link = utils.get_lms_link_for_item(location, True)
self.assertEquals(link, "//preview.localhost:8000/courses/mitX/101/test/jump_to/i4x://mitX/101/vertical/contacting_us")
self.assertEquals(
link,
"//preview.localhost:8000/courses/mitX/101/test/jump_to/i4x://mitX/101/vertical/contacting_us"
)
class UrlReverseTestCase(ModuleStoreTestCase):
""" Tests for get_url_reverse """
def test_CoursePageNames(self):
""" Test the defined course pages. """
course = CourseFactory.create(org='mitX', number='666', display_name='URL Reverse Course')
self.assertEquals(
'/manage_users/i4x://mitX/666/course/URL_Reverse_Course',
utils.get_url_reverse('ManageUsers', course)
)
self.assertEquals(
'/mitX/666/settings-details/URL_Reverse_Course',
utils.get_url_reverse('SettingsDetails', course)
)
self.assertEquals(
'/mitX/666/settings-grading/URL_Reverse_Course',
utils.get_url_reverse('SettingsGrading', course)
)
self.assertEquals(
'/mitX/666/course/URL_Reverse_Course',
utils.get_url_reverse('CourseOutline', course)
)
self.assertEquals(
'/mitX/666/checklists/URL_Reverse_Course',
utils.get_url_reverse('Checklists', course)
)
def test_unknown_passes_through(self):
""" Test that unknown values pass through. """
course = CourseFactory.create(org='mitX', number='666', display_name='URL Reverse Course')
self.assertEquals(
'foobar',
utils.get_url_reverse('foobar', course)
)
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)
)

View File

@@ -8,7 +8,7 @@ import json
from fs.osfs import OSFS
import copy
from cms.djangoapps.contentstore.utils import get_modulestore
from contentstore.utils import get_modulestore
from xmodule.modulestore import Location
from xmodule.modulestore.store_utilities import clone_course
@@ -24,7 +24,7 @@ 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 ModuleStoreTestCase, parse_json, user, registration
class ContentStoreTestCase(ModuleStoreTestCase):

View File

@@ -2,6 +2,7 @@ from django.conf import settings
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']
@@ -40,10 +41,10 @@ def get_course_location_for_item(location):
# make sure we found exactly one match on this above course search
found_cnt = len(courses)
if found_cnt == 0:
raise BaseException('Could not find course at {0}'.format(course_search_location))
raise Exception('Could not find course at {0}'.format(course_search_location))
if found_cnt > 1:
raise BaseException('Found more than one course at {0}. There should only be one!!! Dump = {1}'.format(course_search_location, courses))
raise Exception('Found more than one course at {0}. There should only be one!!! Dump = {1}'.format(course_search_location, courses))
location = courses[0].location
@@ -137,7 +138,7 @@ def compute_unit_state(unit):
'private' content is editabled and not visible in the LMS
"""
if unit.metadata.get('is_draft', False):
if unit.cms.is_draft:
try:
modulestore('direct').get_item(unit.location)
return UnitState.draft
@@ -160,6 +161,38 @@ def update_item(location, value):
else:
get_modulestore(location).update_item(location, value)
def get_url_reverse(course_page_name, course_module):
"""
Returns the course URL link to the specified location. This value is suitable to use as an href link.
course_page_name should correspond to an attribute in CoursePageNames (for example, 'ManageUsers'
or 'SettingsDetails'), or else it will simply be returned. This method passes back unknown values of
course_page_names so that it can also be used for absolute (known) URLs.
course_module is used to obtain the location, org, course, and name properties for a course, if
course_page_name corresponds to an attribute in CoursePageNames.
"""
url_name = getattr(CoursePageNames, course_page_name, None)
ctx_loc = course_module.location
if CoursePageNames.ManageUsers == url_name:
return reverse(url_name, kwargs={"location": ctx_loc})
elif url_name in [CoursePageNames.SettingsDetails, CoursePageNames.SettingsGrading,
CoursePageNames.CourseOutline, CoursePageNames.Checklists]:
return reverse(url_name, kwargs={'org': ctx_loc.org, 'course': ctx_loc.course, 'name': ctx_loc.name})
else:
return course_page_name
class CoursePageNames:
""" Constants for pages that are recognized by get_url_reverse method. """
ManageUsers = "manage_users"
SettingsDetails = "settings_details"
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.
@@ -175,4 +208,3 @@ def add_open_ended_panel_tab(course):
course_tabs.append(OPEN_ENDED_PANEL)
changed = True
return changed, course_tabs

View File

@@ -18,7 +18,8 @@ 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
from django.http import HttpResponse, Http404, HttpResponseBadRequest, HttpResponseForbidden, HttpResponseServerError
from django.http import HttpResponseNotFound
from django.contrib.auth.decorators import login_required
from django.core.exceptions import PermissionDenied
from django.core.context_processors import csrf
@@ -28,11 +29,15 @@ from django.conf import settings
from xmodule.modulestore import Location
from xmodule.modulestore.exceptions import ItemNotFoundError, InvalidLocationError
from xmodule.modulestore.inheritance import own_metadata
from xblock.core import Scope
from xblock.runtime import KeyValueStore, DbModel, InvalidScopeError
from xmodule.x_module import ModuleSystem
from xmodule.error_module import ErrorDescriptor
from xmodule.errortracker import exc_info_to_str
import static_replace
from external_auth.views import ssl_login_shortcut
from xmodule.modulestore.mongo import MongoUsage
from mitxmako.shortcuts import render_to_response, render_to_string
from xmodule.modulestore.django import modulestore
@@ -46,21 +51,20 @@ from xmodule.contentstore.content import StaticContent
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
from .utils import add_open_ended_panel_tab
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, add_open_ended_panel_tab
from xmodule.modulestore.xml_importer import import_from_xml
from contentstore.course_info_model import get_course_updates,\
from contentstore.course_info_model import get_course_updates, \
update_course_updates, delete_course_update
from cache_toolbox.core import del_cached_content
from xmodule.timeparse import stringify_time
from contentstore.module_info_model import get_module_info, set_module_info
from cms.djangoapps.models.settings.course_details import CourseDetails,\
from models.settings.course_details import CourseDetails, \
CourseSettingsEncoder
from cms.djangoapps.models.settings.course_grading import CourseGradingModel
from cms.djangoapps.contentstore.utils import get_modulestore
from models.settings.course_grading import CourseGradingModel
from contentstore.utils import get_modulestore
from django.shortcuts import redirect
from cms.djangoapps.models.settings.course_metadata import CourseMetadata
from models.settings.course_metadata import CourseMetadata
# to install PIL on MacOSX: 'easy_install http://dist.repoze.org/PIL-1.1.6.tar.gz'
@@ -69,8 +73,7 @@ log = logging.getLogger(__name__)
COMPONENT_TYPES = ['customtag', 'discussion', 'html', 'problem', 'video']
OPEN_ENDED_COMPONENT_TYPES = ["combinedopenended", "peergrading"]
ADVANCED_COMPONENT_TYPES = ['annotatable'] + OPEN_ENDED_COMPONENT_TYPES
ADVANCED_COMPONENT_TYPES = ['annotatable', 'combinedopenended', 'peergrading']
ADVANCED_COMPONENT_CATEGORY = 'advanced'
ADVANCED_COMPONENT_POLICY_KEY = 'advanced_modules'
@@ -112,7 +115,7 @@ def login_page(request):
def howitworks(request):
if request.user.is_authenticated():
return index(request)
else:
else:
return render_to_response('howitworks.html', {})
# ==== Views for any logged-in user ==================================
@@ -137,11 +140,8 @@ def index(request):
return render_to_response('index.html', {
'new_course_template': Location('i4x', 'edx', 'templates', 'course', 'Empty'),
'courses': [(course.metadata.get('display_name'),
reverse('course_index', args=[
course.location.org,
course.location.course,
course.location.name]),
'courses': [(course.display_name,
get_url_reverse('CourseOutline', course),
get_lms_link_for_item(course.location, course_id=course.location.course_id))
for course in courses],
'user': request.user,
@@ -178,19 +178,15 @@ def course_index(request, org, course, name):
org, course, name: Attributes of the Location for the item to edit
"""
location = ['i4x', org, course, 'course', name]
# check that logged in user has permissions to this item
if not has_access(request.user, location):
raise PermissionDenied()
location = get_location_and_verify_access(request, org, course, name)
lms_link = get_lms_link_for_item(location)
upload_asset_callback_url = reverse('upload_asset', kwargs={
'org': org,
'course': course,
'coursename': name
})
'org': org,
'course': course,
'coursename': name
})
course = modulestore().get_item(location)
sections = course.get_children()
@@ -241,8 +237,13 @@ def edit_subsection(request, location):
# remove all metadata from the generic dictionary that is presented in a more normalized UI
policy_metadata = dict((key, value) for key, value in item.metadata.iteritems()
if key not in ['display_name', 'start', 'due', 'format'] and key not in item.system_metadata_fields)
policy_metadata = dict(
(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
)
can_view_live = False
subsection_units = item.get_children()
@@ -253,18 +254,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
@@ -295,8 +296,7 @@ def edit_unit(request, location):
# Check if there are any advanced modules specified in the course policy. These modules
# should be specified as a list of strings, where the strings are the names of the modules
# in ADVANCED_COMPONENT_TYPES that should be enabled for the course.
course_metadata = CourseMetadata.fetch(course.location)
course_advanced_keys = course_metadata.get(ADVANCED_COMPONENT_POLICY_KEY, [])
course_advanced_keys = course.advanced_modules
# Set component types according to course policy file
component_types = list(COMPONENT_TYPES)
@@ -310,16 +310,17 @@ def edit_unit(request, location):
templates = modulestore().get_items(Location('i4x', 'edx', 'templates'))
for template in templates:
category = template.location.category
if category in course_advanced_keys:
category = ADVANCED_COMPONENT_CATEGORY
if category in component_types:
#This is a hack to create categories for different xmodules
# This is a hack to create categories for different xmodules
component_templates[category].append((
template.display_name,
template.display_name_with_default,
template.location.url(),
'markdown' in template.metadata,
'empty' in template.metadata
hasattr(template, 'markdown') and template.markdown is not None,
template.cms.empty,
))
components = [
@@ -363,11 +364,6 @@ def edit_unit(request, location):
unit_state = compute_unit_state(item)
try:
published_date = time.strftime('%B %d, %Y', item.metadata.get('published_date'))
except TypeError:
published_date = None
return render_to_response('unit.html', {
'context_course': course,
'active_tab': 'courseware',
@@ -378,11 +374,11 @@ 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.start))) if containing_subsection.start is not None else None,
'release_date': get_date_display(datetime.fromtimestamp(time.mktime(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,
'published_date': published_date,
'published_date': item.cms.published_date.strftime('%B %d, %Y') if item.cms.published_date is not None else None,
})
@@ -414,7 +410,7 @@ def assignment_type_update(request, org, course, category, name):
if request.method == 'GET':
return HttpResponse(json.dumps(CourseGradingModel.get_section_grader_type(location)),
mimetype="application/json")
elif request.method == 'POST': # post or put, doesn't matter.
elif request.method == 'POST': # post or put, doesn't matter.
return HttpResponse(json.dumps(CourseGradingModel.update_section_grader_type(location, request.POST)),
mimetype="application/json")
@@ -447,9 +443,8 @@ def preview_dispatch(request, preview_id, location, dispatch=None):
dispatch: The action to execute
"""
instance_state, shared_state = load_preview_state(request, preview_id, location)
descriptor = modulestore().get_item(location)
instance = load_preview_module(request, preview_id, descriptor, instance_state, shared_state)
instance = load_preview_module(request, preview_id, descriptor)
# Let the module handle the AJAX
try:
ajax_return = instance.handle_ajax(dispatch, request.POST)
@@ -460,46 +455,9 @@ def preview_dispatch(request, preview_id, location, dispatch=None):
log.exception("error processing ajax call")
raise
save_preview_state(request, preview_id, location, instance.get_instance_state(), instance.get_shared_state())
return HttpResponse(ajax_return)
def load_preview_state(request, preview_id, location):
"""
Load the state of a preview module from the request
preview_id (str): An identifier specifying which preview this module is used for
location: The Location of the module to dispatch to
"""
if 'preview_states' not in request.session:
request.session['preview_states'] = defaultdict(dict)
instance_state = request.session['preview_states'][preview_id, location].get('instance')
shared_state = request.session['preview_states'][preview_id, location].get('shared')
return instance_state, shared_state
def save_preview_state(request, preview_id, location, instance_state, shared_state):
"""
Save the state of a preview module to the request
preview_id (str): An identifier specifying which preview this module is used for
location: The Location of the module to dispatch to
instance_state: The instance state to save
shared_state: The shared state to save
"""
if 'preview_states' not in request.session:
request.session['preview_states'] = defaultdict(dict)
# request.session doesn't notice indirect changes; so, must set its dict w/ every change to get
# it to persist: http://www.djangobook.com/en/2.0/chapter14.html
preview_states = request.session['preview_states']
preview_states[preview_id, location]['instance'] = instance_state
preview_states[preview_id, location]['shared'] = shared_state
request.session['preview_states'] = preview_states # make session mgmt notice the update
def render_from_lms(template_name, dictionary, context=None, namespace='main'):
"""
Render a template using the LMS MAKO_TEMPLATES
@@ -507,6 +465,33 @@ def render_from_lms(template_name, dictionary, context=None, namespace='main'):
return render_to_string(template_name, dictionary, context, namespace="lms." + namespace)
class SessionKeyValueStore(KeyValueStore):
def __init__(self, request, model_data):
self._model_data = model_data
self._session = request.session
def get(self, key):
try:
return self._model_data[key.field_name]
except (KeyError, InvalidScopeError):
return self._session[tuple(key)]
def set(self, key, value):
try:
self._model_data[key.field_name] = value
except (KeyError, InvalidScopeError):
self._session[tuple(key)] = value
def delete(self, key):
try:
del self._model_data[key.field_name]
except (KeyError, InvalidScopeError):
del self._session[tuple(key)]
def has(self, key):
return key in self._model_data or key in self._session
def preview_module_system(request, preview_id, descriptor):
"""
Returns a ModuleSystem for the specified descriptor that is specialized for
@@ -517,6 +502,14 @@ def preview_module_system(request, preview_id, descriptor):
descriptor: An XModuleDescriptor
"""
def preview_model_data(descriptor):
return DbModel(
SessionKeyValueStore(request, descriptor._model_data),
descriptor.module_class,
preview_id,
MongoUsage(preview_id, descriptor.location.url()),
)
return ModuleSystem(
ajax_url=reverse('preview_dispatch', args=[preview_id, descriptor.location.url(), '']).rstrip('/'),
# TODO (cpennington): Do we want to track how instructors are using the preview problems?
@@ -527,6 +520,7 @@ def preview_module_system(request, preview_id, descriptor):
debug=True,
replace_urls=partial(static_replace.replace_static_urls, data_directory=None, course_namespace=descriptor.location),
user=request.user,
xblock_model_data=preview_model_data,
)
@@ -539,11 +533,11 @@ def get_preview_module(request, preview_id, descriptor):
preview_id (str): An identifier specifying which preview this module is used for
location: A Location
"""
instance_state, shared_state = descriptor.get_sample_state()[0]
return load_preview_module(request, preview_id, descriptor, instance_state, shared_state)
return load_preview_module(request, preview_id, descriptor)
def load_preview_module(request, preview_id, descriptor, instance_state, shared_state):
def load_preview_module(request, preview_id, descriptor):
"""
Return a preview XModule instantiated from the supplied descriptor, instance_state, and shared_state
@@ -555,12 +549,13 @@ def load_preview_module(request, preview_id, descriptor, instance_state, shared_
"""
system = preview_module_system(request, preview_id, descriptor)
try:
module = descriptor.xmodule_constructor(system)(instance_state, shared_state)
module = descriptor.xmodule(system)
except:
log.debug("Unable to load preview module", exc_info=True)
module = ErrorDescriptor.from_descriptor(
descriptor,
error_msg=exc_info_to_str(sys.exc_info())
).xmodule_constructor(system)(None, None)
).xmodule(system)
# cdodge: Special case
if module.location.category == 'static_tab':
@@ -578,11 +573,9 @@ def load_preview_module(request, preview_id, descriptor, instance_state, shared_
module.get_html = replace_static_urls(
module.get_html,
module.metadata.get('data_dir', module.location.course),
getattr(module, 'data_dir', module.location.course),
course_namespace=Location([module.location.tag, module.location.org, module.location.course, None, None])
)
save_preview_state(request, preview_id, descriptor.location.url(),
module.get_instance_state(), module.get_shared_state())
return module
@@ -596,7 +589,7 @@ def get_module_previews(request, descriptor):
"""
preview_html = []
for idx, (instance_state, shared_state) in enumerate(descriptor.get_sample_state()):
module = load_preview_module(request, str(idx), descriptor, instance_state, shared_state)
module = load_preview_module(request, str(idx), descriptor)
preview_html.append(module.get_html())
return preview_html
@@ -650,9 +643,11 @@ def delete_item(request):
for parent_loc in parent_locs:
parent = modulestore('direct').get_item(parent_loc)
item_url = item_loc.url()
if item_url in parent.definition["children"]:
parent.definition["children"].remove(item_url)
modulestore('direct').update_children(parent.location, parent.definition["children"])
if item_url in parent.children:
children = parent.children
children.remove(item_url)
parent.children = children
modulestore('direct').update_children(parent.location, parent.children)
return HttpResponse()
@@ -691,7 +686,7 @@ def save_item(request):
# update existing metadata with submitted metadata (which can be partial)
# IMPORTANT NOTE: if the client passed pack 'null' (None) for a piece of metadata that means 'remove it'
for metadata_key in posted_metadata.keys():
for metadata_key, value in posted_metadata.items():
# let's strip out any metadata fields from the postback which have been identified as system metadata
# and therefore should not be user-editable, so we should accept them back from the client
@@ -699,15 +694,15 @@ def save_item(request):
del posted_metadata[metadata_key]
elif posted_metadata[metadata_key] is None:
# remove both from passed in collection as well as the collection read in from the modulestore
if metadata_key in existing_item.metadata:
del existing_item.metadata[metadata_key]
if metadata_key in existing_item._model_data:
del existing_item._model_data[metadata_key]
del posted_metadata[metadata_key]
# overlay the new metadata over the modulestore sourced collection to support partial updates
existing_item.metadata.update(posted_metadata)
else:
existing_item._model_data[metadata_key] = value
# commit to datastore
store.update_metadata(item_location, existing_item.metadata)
# TODO (cpennington): This really shouldn't have to do this much reaching in to get the metadata
store.update_metadata(item_location, own_metadata(existing_item))
return HttpResponse()
@@ -774,17 +769,14 @@ def clone_item(request):
new_item = get_modulestore(template).clone_item(template, dest_location)
# TODO: This needs to be deleted when we have proper storage for static content
new_item.metadata['data_dir'] = parent.metadata['data_dir']
# replace the display name with an optional parameter passed in from the caller
if display_name is not None:
new_item.metadata['display_name'] = display_name
new_item.display_name = display_name
get_modulestore(template).update_metadata(new_item.location.url(), new_item.own_metadata)
get_modulestore(template).update_metadata(new_item.location.url(), own_metadata(new_item))
if new_item.location.category not in DETACHED_CATEGORIES:
get_modulestore(parent.location).update_children(parent_location, parent.definition.get('children', []) + [new_item.location.url()])
get_modulestore(parent.location).update_children(parent_location, parent.children + [new_item.location.url()])
return HttpResponse(json.dumps({'id': dest_location.url()}))
@@ -799,9 +791,7 @@ def upload_asset(request, org, course, coursename):
return HttpResponseBadRequest()
# construct a location from the passed in path
location = ['i4x', org, course, 'course', coursename]
if not has_access(request.user, location):
return HttpResponseForbidden()
location = get_location_and_verify_access(request, org, course, coursename)
# Does the course actually exist?!? Get anything from it to prove its existance
@@ -832,7 +822,7 @@ def upload_asset(request, org, course, coursename):
if thumbnail_content is not None:
content.thumbnail_location = thumbnail_location
#then commit the content
# then commit the content
contentstore().save(content)
del_cached_content(content.location)
@@ -875,7 +865,7 @@ def manage_users(request, location):
})
def create_json_response(errmsg = None):
def create_json_response(errmsg=None):
if errmsg is not None:
resp = HttpResponse(json.dumps({'Status': 'Failed', 'ErrMsg': errmsg}))
else:
@@ -953,11 +943,7 @@ def landing(request, org, course, coursename):
@ensure_csrf_cookie
def static_pages(request, org, course, coursename):
location = ['i4x', org, course, 'course', coursename]
# check that logged in user has permissions to this item
if not has_access(request.user, location):
raise PermissionDenied()
location = get_location_and_verify_access(request, org, course, coursename)
course = modulestore().get_item(location)
@@ -1003,7 +989,7 @@ 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].metadata.get('display_name'),
'name': tab_items[static_tab_idx].display_name,
'url_slug': tab_items[static_tab_idx].location.name})
static_tab_idx += 1
else:
@@ -1012,7 +998,7 @@ def reorder_static_tabs(request):
# OK, re-assemble the static tabs in the new order
course.tabs = reordered_tabs
modulestore('direct').update_metadata(course.location, course.metadata)
modulestore('direct').update_metadata(course.location, own_metadata(course))
return HttpResponse()
@@ -1069,11 +1055,7 @@ def course_info(request, org, course, name, provided_id=None):
org, course, name: Attributes of the Location for the item to edit
"""
location = ['i4x', org, course, 'course', name]
# check that logged in user has permissions to this item
if not has_access(request.user, location):
raise PermissionDenied()
location = get_location_and_verify_access(request, org, course, name)
course_module = modulestore().get_item(location)
@@ -1112,21 +1094,25 @@ def course_info_updates(request, org, course, provided_id=None):
if not has_access(request.user, location):
raise PermissionDenied()
# NB: we're setting Backbone.emulateHTTP to true on the client so everything comes as a post!!!
if request.method == 'POST' and 'HTTP_X_HTTP_METHOD_OVERRIDE' in request.META:
real_method = request.META['HTTP_X_HTTP_METHOD_OVERRIDE']
else:
real_method = request.method
real_method = get_request_method(request)
if request.method == 'GET':
return HttpResponse(json.dumps(get_course_updates(location)), mimetype="application/json")
elif real_method == 'DELETE': # coming as POST need to pull from Request Header X-HTTP-Method-Override DELETE
return HttpResponse(json.dumps(delete_course_update(location, request.POST, provided_id)), mimetype="application/json")
return HttpResponse(json.dumps(get_course_updates(location)),
mimetype="application/json")
elif real_method == 'DELETE':
try:
return HttpResponse(json.dumps(delete_course_update(location,
request.POST, provided_id)), mimetype="application/json")
except:
return HttpResponseBadRequest("Failed to delete",
content_type="text/plain")
elif request.method == 'POST':
try:
return HttpResponse(json.dumps(update_course_updates(location, request.POST, provided_id)), mimetype="application/json")
return HttpResponse(json.dumps(update_course_updates(location,
request.POST, provided_id)), mimetype="application/json")
except:
return HttpResponseBadRequest("Failed to save: malformed html", content_type="text/plain")
return HttpResponseBadRequest("Failed to save",
content_type="text/plain")
@expect_json
@@ -1139,11 +1125,7 @@ def module_info(request, module_location):
if not has_access(request.user, location):
raise PermissionDenied()
# NB: we're setting Backbone.emulateHTTP to true on the client so everything comes as a post!!!
if request.method == 'POST' and 'HTTP_X_HTTP_METHOD_OVERRIDE' in request.META:
real_method = request.META['HTTP_X_HTTP_METHOD_OVERRIDE']
else:
real_method = request.method
real_method = get_request_method(request)
rewrite_static_links = request.GET.get('rewrite_url_links', 'True') in ['True', 'true']
logging.debug('rewrite_static_links = {0} {1}'.format(request.GET.get('rewrite_url_links', 'False'), rewrite_static_links))
@@ -1168,11 +1150,7 @@ def get_course_settings(request, org, course, name):
org, course, name: Attributes of the Location for the item to edit
"""
location = ['i4x', org, course, 'course', name]
# check that logged in user has permissions to this item
if not has_access(request.user, location):
raise PermissionDenied()
location = get_location_and_verify_access(request, org, course, name)
course_module = modulestore().get_item(location)
@@ -1195,11 +1173,7 @@ def course_config_graders_page(request, org, course, name):
org, course, name: Attributes of the Location for the item to edit
"""
location = ['i4x', org, course, 'course', name]
# check that logged in user has permissions to this item
if not has_access(request.user, location):
raise PermissionDenied()
location = get_location_and_verify_access(request, org, course, name)
course_module = modulestore().get_item(location)
course_details = CourseGradingModel.fetch(location)
@@ -1219,18 +1193,13 @@ def course_config_advanced_page(request, org, course, name):
org, course, name: Attributes of the Location for the item to edit
"""
location = ['i4x', org, course, 'course', name]
# check that logged in user has permissions to this item
if not has_access(request.user, location):
raise PermissionDenied()
location = get_location_and_verify_access(request, org, course, name)
course_module = modulestore().get_item(location)
return render_to_response('settings_advanced.html', {
'context_course': course_module,
'course_location' : location,
'advanced_blacklist' : json.dumps(CourseMetadata.FILTERED_LIST),
'advanced_dict' : json.dumps(CourseMetadata.fetch(location)),
})
@@ -1246,11 +1215,7 @@ def course_settings_updates(request, org, course, name, section):
org, course: Attributes of the Location for the item to edit
section: one of details, faculty, grading, problems, discussions
"""
location = ['i4x', org, course, 'course', name]
# check that logged in user has permissions to this item
if not has_access(request.user, location):
raise PermissionDenied()
get_location_and_verify_access(request, org, course, name)
if section == 'details':
manager = CourseDetails
@@ -1262,7 +1227,7 @@ def course_settings_updates(request, org, course, name, section):
# Cannot just do a get w/o knowing the course name :-(
return HttpResponse(json.dumps(manager.fetch(Location(['i4x', org, course, 'course', name])), cls=CourseSettingsEncoder),
mimetype="application/json")
elif request.method == 'POST': # post or put, doesn't matter.
elif request.method == 'POST': # post or put, doesn't matter.
return HttpResponse(json.dumps(manager.update_from_json(request.POST), cls=CourseSettingsEncoder),
mimetype="application/json")
@@ -1278,31 +1243,24 @@ def course_grader_updates(request, org, course, name, grader_index=None):
org, course: Attributes of the Location for the item to edit
"""
location = ['i4x', org, course, 'course', name]
location = get_location_and_verify_access(request, org, course, name)
# check that logged in user has permissions to this item
if not has_access(request.user, location):
raise PermissionDenied()
if request.method == 'POST' and 'HTTP_X_HTTP_METHOD_OVERRIDE' in request.META:
real_method = request.META['HTTP_X_HTTP_METHOD_OVERRIDE']
else:
real_method = request.method
real_method = get_request_method(request)
if real_method == 'GET':
# Cannot just do a get w/o knowing the course name :-(
return HttpResponse(json.dumps(CourseGradingModel.fetch_grader(Location(['i4x', org, course, 'course', name]), grader_index)),
return HttpResponse(json.dumps(CourseGradingModel.fetch_grader(Location(location), grader_index)),
mimetype="application/json")
elif real_method == "DELETE":
# ??? Shoudl this return anything? Perhaps success fail?
CourseGradingModel.delete_grader(Location(['i4x', org, course, 'course', name]), grader_index)
# ??? Should this return anything? Perhaps success fail?
CourseGradingModel.delete_grader(Location(location), grader_index)
return HttpResponse()
elif request.method == 'POST': # post or put, doesn't matter.
return HttpResponse(json.dumps(CourseGradingModel.update_grader_from_json(Location(['i4x', org, course, 'course', name]), request.POST)),
return HttpResponse(json.dumps(CourseGradingModel.update_grader_from_json(Location(location), request.POST)),
mimetype="application/json")
## NB: expect_json failed on ["key", "key2"] and json payload
# # NB: expect_json failed on ["key", "key2"] and json payload
@login_required
@ensure_csrf_cookie
def course_advanced_updates(request, org, course, name):
@@ -1312,17 +1270,9 @@ def course_advanced_updates(request, org, course, name):
org, course: Attributes of the Location for the item to edit
"""
location = ['i4x', org, course, 'course', name]
# check that logged in user has permissions to this item
if not has_access(request.user, location):
raise PermissionDenied()
location = get_location_and_verify_access(request, org, course, name)
# NB: we're setting Backbone.emulateHTTP to true on the client so everything comes as a post!!!
if request.method == 'POST' and 'HTTP_X_HTTP_METHOD_OVERRIDE' in request.META:
real_method = request.META['HTTP_X_HTTP_METHOD_OVERRIDE']
else:
real_method = request.method
real_method = get_request_method(request)
if real_method == 'GET':
return HttpResponse(json.dumps(CourseMetadata.fetch(location)), mimetype="application/json")
@@ -1353,6 +1303,94 @@ def course_advanced_updates(request, org, course, name):
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
@login_required
def get_checklists(request, org, course, name):
"""
Send models, views, and html for displaying the course checklists.
org, course, name: Attributes of the Location for the item to edit
"""
location = get_location_and_verify_access(request, org, course, name)
modulestore = get_modulestore(location)
course_module = modulestore.get_item(location)
new_course_template = Location('i4x', 'edx', 'templates', 'course', 'Empty')
template_module = modulestore.get_item(new_course_template)
# If course was created before checklists were introduced, copy them over from the template.
copied = False
if not course_module.checklists:
course_module.checklists = template_module.checklists
copied = True
checklists, modified = expand_checklist_action_urls(course_module)
if copied or modified:
modulestore.update_metadata(location, own_metadata(course_module))
return render_to_response('checklists.html',
{
'context_course': course_module,
'checklists': checklists
})
@ensure_csrf_cookie
@login_required
def update_checklist(request, org, course, name, checklist_index=None):
"""
restful CRUD operations on course checklists. The payload is a json rep of
the modified checklist. For PUT or POST requests, the index of the
checklist being modified must be included; the returned payload will
be just that one checklist. For GET requests, the returned payload
is a json representation of the list of all checklists.
org, course, name: Attributes of the Location for the item to edit
"""
location = get_location_and_verify_access(request, org, course, name)
modulestore = get_modulestore(location)
course_module = modulestore.get_item(location)
real_method = get_request_method(request)
if real_method == 'POST' or real_method == 'PUT':
if checklist_index is not None and 0 <= int(checklist_index) < len(course_module.checklists):
index = int(checklist_index)
course_module.checklists[index] = json.loads(request.body)
checklists, modified = expand_checklist_action_urls(course_module)
modulestore.update_metadata(location, own_metadata(course_module))
return HttpResponse(json.dumps(checklists[index]), mimetype="application/json")
else:
return HttpResponseBadRequest(
"Could not save checklist state because the checklist index was out of range or unspecified.",
content_type="text/plain")
elif request.method == 'GET':
# In the JavaScript view initialize method, we do a fetch to get all the checklists.
checklists, modified = expand_checklist_action_urls(course_module)
if modified:
modulestore.update_metadata(location, own_metadata(course_module))
return HttpResponse(json.dumps(checklists), mimetype="application/json")
else:
return HttpResponseBadRequest("Unsupported request.", content_type="text/plain")
def expand_checklist_action_urls(course_module):
"""
Gets the checklists out of the course module and expands their action urls
if they have not yet been expanded.
Returns the checklists with modified urls, as well as a boolean
indicating whether or not the checklists were modified.
"""
checklists = course_module.checklists
modified = False
for checklist in checklists:
if not checklist.get('action_urls_expanded', False):
for item in checklist.get('items'):
item['action_url'] = get_url_reverse(item.get('action_url'), course_module)
checklist['action_urls_expanded'] = True
modified = True
return checklists, modified
@login_required
@ensure_csrf_cookie
@@ -1362,18 +1400,13 @@ def asset_index(request, org, course, name):
org, course, name: Attributes of the Location for the item to edit
"""
location = ['i4x', org, course, 'course', name]
# check that logged in user has permissions to this item
if not has_access(request.user, location):
raise PermissionDenied()
location = get_location_and_verify_access(request, org, course, name)
upload_asset_callback_url = reverse('upload_asset', kwargs={
'org': org,
'course': course,
'coursename': name
})
'org': org,
'course': course,
'coursename': name
})
course_module = modulestore().get_item(location)
@@ -1455,13 +1488,10 @@ def create_new_course(request):
new_course = modulestore('direct').clone_item(template, dest_location)
if display_name is not None:
new_course.metadata['display_name'] = display_name
# we need a 'data_dir' for legacy reasons
new_course.metadata['data_dir'] = uuid4().hex
new_course.display_name = display_name
# set a default start date to now
new_course.metadata['start'] = stringify_time(time.gmtime())
new_course.start = time.gmtime()
initialize_course_tabs(new_course)
@@ -1480,23 +1510,19 @@ 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": "course_info", "name": "Course Info"},
{"type": "discussion", "name": "Discussion"},
{"type": "wiki", "name": "Wiki"},
{"type": "progress", "name": "Progress"}]
modulestore('direct').update_metadata(course.location.url(), course.own_metadata)
modulestore('direct').update_metadata(course.location.url(), own_metadata(course))
@ensure_csrf_cookie
@login_required
def import_course(request, org, course, name):
location = ['i4x', org, course, 'course', name]
# check that logged in user has permissions to this item
if not has_access(request.user, location):
raise PermissionDenied()
location = get_location_and_verify_access(request, org, course, name)
if request.method == 'POST':
filename = request.FILES['course-data'].name
@@ -1559,20 +1585,14 @@ def import_course(request, org, course, name):
return render_to_response('import.html', {
'context_course': course_module,
'active_tab': 'import',
'successful_import_redirect_url': reverse('course_index', args=[
course_module.location.org,
course_module.location.course,
course_module.location.name])
'successful_import_redirect_url': get_url_reverse('CourseOutline', course_module)
})
@ensure_csrf_cookie
@login_required
def generate_export_course(request, org, course, name):
location = ['i4x', org, course, 'course', name]
# check that logged in user has permissions to this item
if not has_access(request.user, location):
raise PermissionDenied()
location = get_location_and_verify_access(request, org, course, name)
loc = Location(location)
export_file = NamedTemporaryFile(prefix=name + '.', suffix=".tar.gz")
@@ -1584,7 +1604,7 @@ 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'
# 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')
@@ -1605,11 +1625,9 @@ def generate_export_course(request, org, course, name):
@login_required
def export_course(request, org, course, name):
location = ['i4x', org, course, 'course', name]
location = get_location_and_verify_access(request, org, course, name)
course_module = modulestore().get_item(location)
# check that logged in user has permissions to this item
if not has_access(request.user, location):
raise PermissionDenied()
return render_to_response('export.html', {
'context_course': course_module,
@@ -1624,3 +1642,39 @@ def event(request):
console logs don't get distracted :-)
'''
return HttpResponse(True)
def render_404(request):
return HttpResponseNotFound(render_to_string('404.html', {}))
def render_500(request):
return HttpResponseServerError(render_to_string('500.html', {}))
def get_location_and_verify_access(request, org, course, name):
"""
Create the location tuple verify that the user has permissions
to view the location. Returns the location.
"""
location = ['i4x', org, course, 'course', name]
# check that logged in user has permissions to this item
if not has_access(request.user, location):
raise PermissionDenied()
return location
def get_request_method(request):
"""
Using HTTP_X_HTTP_METHOD_OVERRIDE, in the request metadata, determine
what type of request came from the client, and return it.
"""
# NB: we're setting Backbone.emulateHTTP to true on the client so everything comes as a post!!!
if request.method == 'POST' and 'HTTP_X_HTTP_METHOD_OVERRIDE' in request.META:
real_method = request.META['HTTP_X_HTTP_METHOD_OVERRIDE']
else:
real_method = request.method
return real_method

View File

@@ -1,13 +1,13 @@
from xmodule.modulestore.django import modulestore
from xmodule.modulestore import Location
from xmodule.modulestore.exceptions import ItemNotFoundError
from xmodule.modulestore.inheritance import own_metadata
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 cms.djangoapps.models.settings import course_grading
from cms.djangoapps.contentstore.utils import update_item
from models.settings import course_grading
from contentstore.utils import update_item
from xmodule.fields import Date
import re
import logging
@@ -43,25 +43,25 @@ class CourseDetails(object):
temploc = course_location._replace(category='about', name='syllabus')
try:
course.syllabus = get_modulestore(temploc).get_item(temploc).definition['data']
course.syllabus = get_modulestore(temploc).get_item(temploc).data
except ItemNotFoundError:
pass
temploc = temploc._replace(name='overview')
try:
course.overview = get_modulestore(temploc).get_item(temploc).definition['data']
course.overview = get_modulestore(temploc).get_item(temploc).data
except ItemNotFoundError:
pass
temploc = temploc._replace(name='effort')
try:
course.effort = get_modulestore(temploc).get_item(temploc).definition['data']
course.effort = get_modulestore(temploc).get_item(temploc).data
except ItemNotFoundError:
pass
temploc = temploc._replace(name='video')
try:
raw_video = get_modulestore(temploc).get_item(temploc).definition['data']
raw_video = get_modulestore(temploc).get_item(temploc).data
course.intro_video = CourseDetails.parse_video_tag(raw_video)
except ItemNotFoundError:
pass
@@ -80,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:
@@ -89,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
@@ -98,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
@@ -107,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
@@ -116,7 +122,7 @@ class CourseDetails(object):
descriptor.enrollment_end = converted
if dirty:
get_modulestore(course_location).update_metadata(course_location, descriptor.metadata)
get_modulestore(course_location).update_metadata(course_location, own_metadata(descriptor))
# NOTE: below auto writes to the db w/o verifying that any of the fields actually changed
# to make faster, could compare against db or could have client send over a list of which fields changed.
@@ -133,7 +139,6 @@ class CourseDetails(object):
recomposed_video_tag = CourseDetails.recompose_video_tag(jsondict['intro_video'])
update_item(temploc, recomposed_video_tag)
# 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
return CourseDetails.fetch(course_location)
@@ -178,6 +183,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)

View File

@@ -1,7 +1,6 @@
from xmodule.modulestore import Location
from contentstore.utils import get_modulestore
import re
from util import converters
from datetime import timedelta
class CourseGradingModel(object):
@@ -91,7 +90,7 @@ class CourseGradingModel(object):
descriptor.raw_grader = graders_parsed
descriptor.grade_cutoffs = jsondict['grade_cutoffs']
get_modulestore(course_location).update_item(course_location, descriptor.definition['data'])
get_modulestore(course_location).update_item(course_location, descriptor._model_data._kvs._data)
CourseGradingModel.update_grace_period_from_json(course_location, jsondict['grace_period'])
return CourseGradingModel.fetch(course_location)
@@ -119,7 +118,7 @@ class CourseGradingModel(object):
else:
descriptor.raw_grader.append(grader)
get_modulestore(course_location).update_item(course_location, descriptor.definition['data'])
get_modulestore(course_location).update_item(course_location, descriptor._model_data._kvs._data)
return CourseGradingModel.jsonize_grader(index, descriptor.raw_grader[index])
@@ -134,7 +133,7 @@ class CourseGradingModel(object):
descriptor = get_modulestore(course_location).get_item(course_location)
descriptor.grade_cutoffs = cutoffs
get_modulestore(course_location).update_item(course_location, descriptor.definition['data'])
get_modulestore(course_location).update_item(course_location, descriptor._model_data._kvs._data)
return cutoffs
@@ -156,11 +155,11 @@ class CourseGradingModel(object):
graceperiodjson = graceperiodjson['grace_period']
# lms requires these to be in a fixed order
grace_rep = "{0[hours]:d} hours {0[minutes]:d} minutes {0[seconds]:d} seconds".format(graceperiodjson)
grace_timedelta = timedelta(**graceperiodjson)
descriptor = get_modulestore(course_location).get_item(course_location)
descriptor.metadata['graceperiod'] = grace_rep
get_modulestore(course_location).update_metadata(course_location, descriptor.metadata)
descriptor.lms.graceperiod = grace_timedelta
get_modulestore(course_location).update_metadata(course_location, descriptor._model_data._kvs._metadata)
@staticmethod
def delete_grader(course_location, index):
@@ -176,7 +175,7 @@ class CourseGradingModel(object):
del descriptor.raw_grader[index]
# force propagation to definition
descriptor.raw_grader = descriptor.raw_grader
get_modulestore(course_location).update_item(course_location, descriptor.definition['data'])
get_modulestore(course_location).update_item(course_location, descriptor._model_data._kvs._data)
# NOTE cannot delete cutoffs. May be useful to reset
@staticmethod
@@ -189,7 +188,7 @@ class CourseGradingModel(object):
descriptor = get_modulestore(course_location).get_item(course_location)
descriptor.grade_cutoffs = descriptor.defaut_grading_policy['GRADE_CUTOFFS']
get_modulestore(course_location).update_item(course_location, descriptor.definition['data'])
get_modulestore(course_location).update_item(course_location, descriptor._model_data._kvs._data)
return descriptor.grade_cutoffs
@@ -202,8 +201,8 @@ class CourseGradingModel(object):
course_location = Location(course_location)
descriptor = get_modulestore(course_location).get_item(course_location)
if 'graceperiod' in descriptor.metadata: del descriptor.metadata['graceperiod']
get_modulestore(course_location).update_metadata(course_location, descriptor.metadata)
del descriptor.lms.graceperiod
get_modulestore(course_location).update_metadata(course_location, descriptor._model_data._kvs._metadata)
@staticmethod
def get_section_grader_type(location):
@@ -212,7 +211,7 @@ class CourseGradingModel(object):
descriptor = get_modulestore(location).get_item(location)
return {
"graderType": descriptor.metadata.get('format', u"Not Graded"),
"graderType": descriptor.lms.format if descriptor.lms.format is not None else 'Not Graded',
"location": location,
"id": 99 # just an arbitrary value to
}
@@ -224,23 +223,41 @@ class CourseGradingModel(object):
descriptor = get_modulestore(location).get_item(location)
if 'graderType' in jsondict and jsondict['graderType'] != u"Not Graded":
descriptor.metadata['format'] = jsondict.get('graderType')
descriptor.metadata['graded'] = True
descriptor.lms.format = jsondict.get('graderType')
descriptor.lms.graded = True
else:
if 'format' in descriptor.metadata: del descriptor.metadata['format']
if 'graded' in descriptor.metadata: del descriptor.metadata['graded']
del descriptor.lms.format
del descriptor.lms.graded
get_modulestore(location).update_metadata(location, descriptor.metadata)
get_modulestore(location).update_metadata(location, descriptor._model_data._kvs._metadata)
@staticmethod
def convert_set_grace_period(descriptor):
# 5 hours 59 minutes 59 seconds => { hours: 5, minutes : 59, seconds : 59}
rawgrace = descriptor.metadata.get('graceperiod', None)
# 5 hours 59 minutes 59 seconds => converted to iso format
rawgrace = descriptor.lms.graceperiod
if rawgrace:
parsedgrace = {str(key): int(val) for (val, key) in re.findall('\s*(\d+)\s*(\w+)', rawgrace)}
return parsedgrace
else: return None
hours_from_days = rawgrace.days*24
seconds = rawgrace.seconds
hours_from_seconds = int(seconds / 3600)
hours = hours_from_days + hours_from_seconds
seconds -= hours_from_seconds * 3600
minutes = int(seconds / 60)
seconds -= minutes * 60
graceperiod = {'hours': 0, 'minutes': 0, 'seconds': 0}
if hours > 0:
graceperiod['hours'] = hours
if minutes > 0:
graceperiod['minutes'] = minutes
if seconds > 0:
graceperiod['seconds'] = seconds
return graceperiod
else:
return None
@staticmethod
def parse_grader(json_grader):

View File

@@ -1,21 +1,26 @@
from xmodule.modulestore import Location
from contentstore.utils import get_modulestore
from xmodule.x_module import XModuleDescriptor
import copy
from xmodule.modulestore.inheritance import own_metadata
from xblock.core import Scope
from xmodule.course_module import CourseDescriptor
class CourseMetadata(object):
'''
For CRUD operations on metadata fields which do not have specific editors on the other pages including any user generated ones.
The objects have no predefined attrs but instead are obj encodings of the editable metadata.
For CRUD operations on metadata fields which do not have specific editors
on the other pages including any user generated ones.
The objects have no predefined attrs but instead are obj encodings of the
editable metadata.
'''
# __new_advanced_key__ is used by client not server; so, could argue against it being here
FILTERED_LIST = XModuleDescriptor.system_metadata_fields + ['start', 'end', 'enrollment_start', 'enrollment_end', 'tabs', 'graceperiod', '__new_advanced_key__']
FILTERED_LIST = XModuleDescriptor.system_metadata_fields + ['start', 'end',
'enrollment_start', 'enrollment_end', 'tabs', 'graceperiod', 'checklists']
@classmethod
def fetch(cls, course_location):
"""
Fetch the key:value editable course details for the given course from persistence and return a CourseMetadata model.
Fetch the key:value editable course details for the given course from
persistence and return a CourseMetadata model.
"""
if not isinstance(course_location, Location):
course_location = Location(course_location)
@@ -24,23 +29,26 @@ class CourseMetadata(object):
descriptor = get_modulestore(course_location).get_item(course_location)
for k, v in descriptor.metadata.iteritems():
if k not in cls.FILTERED_LIST:
course[k] = v
for field in descriptor.fields + descriptor.lms.fields:
if field.scope != Scope.settings:
continue
if field.name not in cls.FILTERED_LIST:
course[field.name] = field.read_json(descriptor)
return course
@classmethod
def update_from_json(cls, course_location, jsondict, filter_tabs=True):
def update_from_json(cls, course_location, jsondict):
"""
Decode the json into CourseMetadata and save any changed attrs to the db.
Ensures none of the fields are in the blacklist.
"""
descriptor = get_modulestore(course_location).get_item(course_location)
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
@@ -49,29 +57,41 @@ class CourseMetadata(object):
for k, v in jsondict.iteritems():
# should it be an error if one of the filtered list items is in the payload?
if k not in filtered_list and (k not in descriptor.metadata or descriptor.metadata[k] != v):
if k in filtered_list:
continue
if hasattr(descriptor, k) and getattr(descriptor, k) != v:
dirty = True
descriptor.metadata[k] = v
value = getattr(CourseDescriptor, k).from_json(v)
setattr(descriptor, k, value)
elif hasattr(descriptor.lms, k) and getattr(descriptor.lms, k) != k:
dirty = True
value = getattr(CourseDescriptor.lms, k).from_json(v)
setattr(descriptor.lms, k, value)
if dirty:
get_modulestore(course_location).update_metadata(course_location, descriptor.metadata)
get_modulestore(course_location).update_metadata(course_location,
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
# 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
return cls.fetch(course_location)
@classmethod
def delete_key(cls, course_location, payload):
'''
Remove the given metadata key(s) from the course. payload can be a single key or [key..]
Remove the given metadata key(s) from the course. payload can be a
single key or [key..]
'''
descriptor = get_modulestore(course_location).get_item(course_location)
for key in payload['deleteKeys']:
if key in descriptor.metadata:
del descriptor.metadata[key]
if hasattr(descriptor, key):
delattr(descriptor, key)
elif hasattr(descriptor.lms, key):
delattr(descriptor.lms, key)
get_modulestore(course_location).update_metadata(course_location, descriptor.metadata)
get_modulestore(course_location).update_metadata(course_location,
own_metadata(descriptor))
return cls.fetch(course_location)

View File

@@ -113,6 +113,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',

View File

@@ -96,6 +96,13 @@ CACHES = {
'KEY_PREFIX': 'general',
'VERSION': 4,
'KEY_FUNCTION': 'util.memcache.safe_key',
},
'mongo_metadata_inheritance': {
'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache',
'LOCATION': '/var/tmp/mongo_metadata_inheritance',
'TIMEOUT': 300,
'KEY_FUNCTION': 'util.memcache.safe_key',
}
}
@@ -120,8 +127,7 @@ DEBUG_TOOLBAR_PANELS = (
'debug_toolbar.panels.sql.SQLDebugPanel',
'debug_toolbar.panels.signals.SignalDebugPanel',
'debug_toolbar.panels.logger.LoggingPanel',
# This is breaking Mongo updates-- Christina is investigating.
# 'debug_toolbar_mongo.panel.MongoDebugPanel',
'debug_toolbar_mongo.panel.MongoDebugPanel',
# Enabling the profiler has a weird bug as of django-debug-toolbar==0.9.4 and
# Django=1.3.1/1.4 where requests to views get duplicated (your method gets
@@ -136,4 +142,4 @@ 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 = False

View File

@@ -98,6 +98,13 @@ CACHES = {
'KEY_PREFIX': 'general',
'VERSION': 4,
'KEY_FUNCTION': 'util.memcache.safe_key',
},
'mongo_metadata_inheritance': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
'LOCATION': '/var/tmp/mongo_metadata_inheritance',
'TIMEOUT': 300,
'KEY_FUNCTION': 'util.memcache.safe_key',
}
}

View File

@@ -1,5 +1,15 @@
from dogapi import dog_http_api, dog_stats_api
from django.conf import settings
from xmodule.modulestore.django import modulestore
from request_cache.middleware import RequestCache
from django.core.cache import get_cache, InvalidCacheBackendError
cache = get_cache('mongo_metadata_inheritance')
for store_name in settings.MODULESTORE:
store = modulestore(store_name)
store.metadata_inheritance_cache_subsystem = cache
store.request_cache = RequestCache.get_request_cache()
if hasattr(settings, 'DATADOG_API'):
dog_http_api.api_key = settings.DATADOG_API

View File

@@ -1,16 +1,11 @@
<li class="field-group course-advanced-policy-list-item">
<div class="field text key" id="<%= (_.isEmpty(key) ? '__new_advanced_key__' : key) %>">
<div class="field is-not-editable text key" id="<%= key %>">
<label for="<%= keyUniqueId %>">Policy Key:</label>
<input type="text" class="short policy-key" id="<%= keyUniqueId %>" value="<%= key %>" />
<span class="tip tip-stacked">Keys are case sensitive and cannot contain spaces or start with a number</span>
<input readonly title="This field is disabled: policy keys cannot be edited." type="text" class="short policy-key" id="<%= keyUniqueId %>" value="<%= key %>" />
</div>
<div class="field text value">
<label for="<%= valueUniqueId %>">Policy Value:</label>
<textarea class="json text" id="<%= valueUniqueId %>"><%= value %></textarea>
</div>
<div class="actions">
<a href="#" class="button delete-button standard remove-item remove-grading-data"><span class="delete-icon"></span>Delete</a>
</div>
</li>

View File

@@ -0,0 +1,61 @@
<% var allChecked = itemsChecked == items.length; %>
<section
<% if (allChecked) { %>
class="course-checklist is-completed"
<% } else { %>
class="course-checklist"
<% } %>
id="<%= 'course-checklist' + checklistIndex %>">
<% var widthPercentage = 'width:' + percentChecked + '%;'; %>
<span class="viz viz-checklist-status"><span class="viz value viz-checklist-status-value" style="<%= widthPercentage %>">
<span class="int"><%= percentChecked %></span>% of checklist completed</span></span>
<header>
<h3 class="checklist-title title-2 is-selectable" title="Collapse/Expand this Checklist">
<i class="ss-icon ss-symbolicons-standard icon-arrow ui-toggle-expansion">&#x25BE;</i>
<%= checklistShortDescription %></h3>
<span class="checklist-status status">
Tasks Completed: <span class="status-count"><%= itemsChecked %></span>/<span class="status-amount"><%= items.length %></span>
<i class="ss-icon ss-symbolicons-standard icon-confirm">&#x2713;</i>
</span>
</header>
<ul class="list list-tasks">
<% var taskIndex = 0; %>
<% _.each(items, function(item) { %>
<% var checked = item['is_checked']; %>
<li
<% if (checked) { %>
class="task is-completed"
<% } else { %>
class="task"
<% } %>
>
<% var taskId = 'course-checklist' + checklistIndex + '-task' + taskIndex; %>
<input type="checkbox" class="task-input" data-checklist="<%= checklistIndex %>" data-task="<%= taskIndex %>"
name="<%= taskId %>" id="<%= taskId %>"
<% if (checked) { %>
checked="checked"
<% } %>
>
<label class="task-details" for="<%= taskId %>">
<h4 class="task-name title title-3"><%= item['short_description'] %></h4>
<p class="task-description"><%= item['long_description'] %></p>
</label>
<% if (item['action_text'] !== '' && item['action_url'] !== '') { %>
<ul class="list-actions task-actions">
<li>
<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"
<% } %>
><%= item['action_text'] %></a>
</li>
</ul>
<% } %>
</li>
<% taskIndex+=1; }) %>
</ul>
</section>

View File

@@ -1,12 +1,12 @@
{
"js_files": [
"/static/js/vendor/RequireJS.js",
"/static/js/vendor/jquery.min.js",
"/static/js/vendor/jquery-ui.min.js",
"/static/js/vendor/jquery.ui.draggable.js",
"/static/js/vendor/jquery.cookie.js",
"/static/js/vendor/json2.js",
"/static/js/vendor/underscore-min.js",
"/static/js/vendor/backbone-min.js"
"static_files": [
"js/vendor/RequireJS.js",
"js/vendor/jquery.min.js",
"js/vendor/jquery-ui.min.js",
"js/vendor/jquery.ui.draggable.js",
"js/vendor/jquery.cookie.js",
"js/vendor/json2.js",
"js/vendor/underscore-min.js",
"js/vendor/backbone-min.js"
]
}

View File

@@ -34,7 +34,10 @@ class CMS.Views.UnitEdit extends Backbone.View
@$('.components').sortable(
handle: '.drag-handle'
update: (event, ui) => @model.save(children: @components())
update: (event, ui) =>
payload = children : @components()
options = success : => @model.unset('children')
@model.save(payload, options)
helper: 'clone'
opacity: '0.5'
placeholder: 'component-placeholder'
@@ -109,7 +112,14 @@ class CMS.Views.UnitEdit extends Backbone.View
id: $component.data('id')
}, =>
$component.remove()
@model.save(children: @components())
# b/c we don't vigilantly keep children up to date
# get rid of it before it hurts someone
# sorry for the js, i couldn't figure out the coffee equivalent
`_this.model.save({children: _this.components()},
{success: function(model) {
model.unset('children');
}}
);`
)
deleteDraft: (event) ->
@@ -157,7 +167,7 @@ class CMS.Views.UnitEdit extends Backbone.View
class CMS.Views.UnitEdit.NameEdit extends Backbone.View
events:
"keyup .unit-display-name-input": "saveName"
'change .unit-display-name-input': 'saveName'
initialize: =>
@model.on('change:metadata', @render)
@@ -180,29 +190,10 @@ class CMS.Views.UnitEdit.NameEdit extends Backbone.View
# Treat the metadata dictionary as immutable
metadata = $.extend({}, @model.get('metadata'))
metadata.display_name = @$('.unit-display-name-input').val()
@model.save(metadata: metadata)
# Update name shown in the right-hand side location summary.
$('.unit-location .editing .unit-name').html(metadata.display_name)
inputField = this.$el.find('input')
# add a spinner
@$spinner.css({
'position': 'absolute',
'top': Math.floor(inputField.position().top + (inputField.outerHeight() / 2) + 3),
'left': inputField.position().left + inputField.outerWidth() - 24,
'margin-top': '-10px'
});
inputField.after(@$spinner);
@$spinner.fadeIn(10)
# save the name after a slight delay
if @timer
clearTimeout @timer
@timer = setTimeout( =>
@model.save(metadata: metadata)
@timer = null
@$spinner.delay(500).fadeOut(150)
, 500)
class CMS.Views.UnitEdit.LocationState extends Backbone.View
initialize: =>
@model.on('change:state', @render)

View File

@@ -14,10 +14,6 @@ $(document).ready(function () {
// scopes (namely the course-info tab)
window.$modalCover = $modalCover;
// Control whether template caching in local memory occurs (see template_loader.js). Caching screws up development but may
// be a good optimization in production (it works fairly well)
window.cachetemplates = false;
$body.append($modalCover);
$newComponentItem = $('.new-component-item');
$newComponentTypePicker = $('.new-component');
@@ -76,10 +72,7 @@ $(document).ready(function () {
});
// general link management - new window/tab
$('a[rel="external"]').attr('title', 'This link will open in a new browser window/tab').click(function (e) {
window.open($(this).attr('href'));
e.preventDefault();
});
$('a[rel="external"]').attr('title', 'This link will open in a new browser window/tab').bind('click', linkNewWindow);
// general link management - lean modal window
$('a[rel="modal"]').attr('title', 'This link will open in a modal window').leanModal({overlay: 0.50, closeButton: '.action-modal-close' });
@@ -87,6 +80,10 @@ $(document).ready(function () {
(e).preventDefault();
});
// general link management - smooth scrolling page links
$('a[rel*="view"][href^="#"]').bind('click', smoothScrollLink);
// toggling overview section details
$(function () {
if ($('.courseware-section').length > 0) {
@@ -95,9 +92,9 @@ $(document).ready(function () {
});
$('.toggle-button-sections').bind('click', toggleSections);
// autosave when a field is updated on the subsection page
$body.on('keyup', '.subsection-display-name-input, .unit-subtitle, .policy-list-value', checkForNewValue);
$('.subsection-display-name-input, .unit-subtitle, .policy-list-name, .policy-list-value').each(function (i) {
// autosave when leaving input field
$body.on('change', '.subsection-display-name-input', saveSubsection);
$('.subsection-display-name-input').each(function () {
this.val = $(this).val();
});
$("#start_date, #start_time, #due_date, #due_time").bind('change', autosaveInput);
@@ -113,11 +110,6 @@ $(document).ready(function () {
// add new/delete subsection
$('.new-subsection-item').bind('click', addNewSubsection);
$('.delete-subsection-button').bind('click', deleteSubsection);
// add/remove policy metadata button click handlers
$('.add-policy-data').bind('click', addPolicyMetadata);
$('.remove-policy-data').bind('click', removePolicyMetadata);
$body.on('click', '.policy-list-element .save-button', savePolicyMetadata);
$body.on('click', '.policy-list-element .cancel-button', cancelPolicyMetadata);
$('.sync-date').bind('click', syncReleaseDate);
@@ -156,10 +148,27 @@ $(document).ready(function () {
});
});
// function collapseAll(e) {
// $('.branch').addClass('collapsed');
// $('.expand-collapse-icon').removeClass('collapse').addClass('expand');
// }
function smoothScrollLink(e) {
(e).preventDefault();
$.smoothScroll({
offset: -200,
easing: 'swing',
speed: 1000,
scrollElement: null,
scrollTarget: $(this).attr('href')
});
}
function linkNewWindow(e) {
window.open($(e.target).attr('href'));
e.preventDefault();
}
// On AWS instances, 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 the checklists)
window.cmsLinkNewWindow = linkNewWindow;
function toggleSections(e) {
e.preventDefault();
@@ -219,56 +228,6 @@ function syncReleaseDate(e) {
$("#start_time").val("");
}
function addPolicyMetadata(e) {
e.preventDefault();
var template = $('#add-new-policy-element-template > li');
var newNode = template.clone();
var _parent_el = $(this).parent('ol:.policy-list');
newNode.insertBefore('.add-policy-data');
$('.remove-policy-data').bind('click', removePolicyMetadata);
newNode.find('.policy-list-name').focus();
}
function savePolicyMetadata(e) {
e.preventDefault();
var $policyElement = $(this).parents('.policy-list-element');
saveSubsection()
$policyElement.removeClass('new-policy-list-element');
$policyElement.find('.policy-list-name').attr('disabled', 'disabled');
$policyElement.removeClass('editing');
}
function cancelPolicyMetadata(e) {
e.preventDefault();
var $policyElement = $(this).parents('.policy-list-element');
if (!$policyElement.hasClass('editing')) {
$policyElement.remove();
} else {
$policyElement.removeClass('new-policy-list-element');
$policyElement.find('.policy-list-name').val($policyElement.data('currentValues')[0]);
$policyElement.find('.policy-list-value').val($policyElement.data('currentValues')[1]);
}
$policyElement.removeClass('editing');
}
function removePolicyMetadata(e) {
e.preventDefault();
if (!confirm('Are you sure you wish to delete this item. It cannot be reversed!'))
return;
policy_name = $(this).data('policy-name');
var _parent_el = $(this).parent('li:.policy-list-element');
if ($(_parent_el).hasClass("new-policy-list-element")) {
_parent_el.remove();
} else {
_parent_el.appendTo("#policy-to-delete");
}
saveSubsection()
}
function getEdxTimeFromDateTimeVals(date_val, time_val, format) {
var edxTimeStr = null;
@@ -277,7 +236,7 @@ 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);
var date = Date.parse(date_val + " " + time_val);
if (format == null)
format = 'yyyy-MM-ddTHH:mm';
@@ -294,32 +253,8 @@ function getEdxTimeFromDateTimeInputs(date_id, time_id, format) {
return getEdxTimeFromDateTimeVals(input_date, input_time, format);
}
function checkForNewValue(e) {
if ($(this).parents('.new-policy-list-element')[0]) {
return;
}
if (this.val) {
this.hasChanged = this.val != $(this).val();
} else {
this.hasChanged = false;
}
this.val = $(this).val();
if (this.hasChanged) {
if (this.saveTimer) {
clearTimeout(this.saveTimer);
}
this.saveTimer = setTimeout(function () {
$changedInput = $(e.target);
saveSubsection();
this.saveTimer = null;
}, 500);
}
}
function autosaveInput(e) {
var self = this;
if (this.saveTimer) {
clearTimeout(this.saveTimer);
}
@@ -327,11 +262,12 @@ function autosaveInput(e) {
this.saveTimer = setTimeout(function () {
$changedInput = $(e.target);
saveSubsection();
this.saveTimer = null;
self.saveTimer = null;
}, 500);
}
function saveSubsection() {
// Spinner is no longer used by subsection name, but is still used by date and time pickers on the right.
if ($changedInput && !$changedInput.hasClass('no-spinner')) {
$spinner.css({
'position': 'absolute',
@@ -354,20 +290,6 @@ function saveSubsection() {
metadata[$(el).data("metadata-name")] = el.value;
}
// now add 'free-formed' metadata which are presented to the user as dual input fields (name/value)
$('ol.policy-list > li.policy-list-element').each(function (i, element) {
var name = $(element).children('.policy-list-name').val();
metadata[name] = $(element).children('.policy-list-value').val();
});
// now add any 'removed' policy metadata which is stored in a separate hidden div
// 'null' presented to the server means 'remove'
$("#policy-to-delete > li.policy-list-element").each(function (i, element) {
var name = $(element).children('.policy-list-name').val();
if (name != "")
metadata[name] = null;
});
// 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
@@ -382,6 +304,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.');

View File

@@ -0,0 +1,24 @@
// Model for checklists_view.js.
CMS.Models.Checklist = Backbone.Model.extend({
});
CMS.Models.ChecklistCollection = Backbone.Collection.extend({
model : CMS.Models.Checklist,
parse: function(response) {
_.each(response,
function( element, idx ) {
element.id = idx;
});
return response;
},
// Disable caching so the browser back button will work (checklists have links to other
// places within Studio).
fetch: function (options) {
options.cache = false;
return Backbone.Collection.prototype.fetch.call(this, options);
}
});

View File

@@ -1,27 +1,15 @@
if (!CMS.Models['Settings']) CMS.Models.Settings = {};
CMS.Models.Settings.Advanced = Backbone.Model.extend({
// the key for a newly added policy-- before the user has entered a key value
new_key : "__new_advanced_key__",
defaults: {
// the properties are whatever the user types in (in addition to whatever comes originally from the server)
},
// which keys to send as the deleted keys on next save
deleteKeys : [],
blacklistKeys : [], // an array which the controller should populate directly for now [static not instance based]
validate: function (attrs) {
var errors = {};
for (var key in attrs) {
if (key === this.new_key || _.isEmpty(key)) {
errors[key] = "A key must be entered.";
}
else if (_.contains(this.blacklistKeys, key)) {
errors[key] = key + " is a reserved keyword or can be edited on another screen";
}
}
if (!_.isEmpty(errors)) return errors;
// Keys can no longer be edited. We are currently not validating values.
},
save : function (attrs, options) {

View File

@@ -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.";
}

View File

@@ -1,78 +1,79 @@
// <!-- from https://github.com/Gazler/Underscore-Template-Loader/blob/master/index.html -->
// TODO Figure out how to initialize w/ static views from server (don't call .load but instead inject in django as strings)
// so this only loads the lazily loaded ones.
(function() {
if (typeof window.templateLoader == 'function') return;
var templateLoader = {
templateVersion: "0.0.15",
templates: {},
loadRemoteTemplate: function(templateName, filename, callback) {
if (!this.templates[templateName]) {
var self = this;
jQuery.ajax({url : filename,
success : function(data) {
self.addTemplate(templateName, data);
self.saveLocalTemplates();
callback(data);
},
error : function(xhdr, textStatus, errorThrown) {
console.log(textStatus); },
dataType : "html"
})
}
else {
callback(this.templates[templateName]);
}
},
addTemplate: function(templateName, data) {
// is there a reason this doesn't go ahead and compile the template? _.template(data)
// I suppose localstorage use would still req raw string rather than compiled version, but that sd work
// if it maintains a separate cache of uncompiled ones
this.templates[templateName] = data;
},
localStorageAvailable: function() {
try {
// window.cachetemplates is global set in base.js to turn caching on/off
return window.cachetemplates && 'localStorage' in window && window['localStorage'] !== null;
} catch (e) {
return false;
}
},
saveLocalTemplates: function() {
if (this.localStorageAvailable) {
localStorage.setItem("templates", JSON.stringify(this.templates));
localStorage.setItem("templateVersion", this.templateVersion);
}
},
loadLocalTemplates: function() {
if (this.localStorageAvailable) {
var templateVersion = localStorage.getItem("templateVersion");
if (templateVersion && templateVersion == this.templateVersion) {
var templates = localStorage.getItem("templates");
if (templates) {
templates = JSON.parse(templates);
for (var x in templates) {
if (!this.templates[x]) {
this.addTemplate(x, templates[x]);
}
}
}
}
else {
localStorage.removeItem("templates");
localStorage.removeItem("templateVersion");
}
}
}
(function () {
if (typeof window.templateLoader == 'function') return;
var templateLoader = {
templateVersion: "0.0.15",
templates: {},
// Control whether template caching in local memory occurs. Caching screws up development but may
// be a good optimization in production (it works fairly well).
cacheTemplates: false,
loadRemoteTemplate: function (templateName, filename, callback) {
if (!this.templates[templateName]) {
var self = this;
jQuery.ajax({url: filename,
success: function (data) {
self.addTemplate(templateName, data);
self.saveLocalTemplates();
callback(data);
},
error: function (xhdr, textStatus, errorThrown) {
console.log(textStatus);
},
dataType: "html"
})
}
else {
callback(this.templates[templateName]);
}
},
addTemplate: function (templateName, data) {
// is there a reason this doesn't go ahead and compile the template? _.template(data)
// I suppose localstorage use would still req raw string rather than compiled version, but that sd work
// if it maintains a separate cache of uncompiled ones
this.templates[templateName] = data;
},
localStorageAvailable: function () {
try {
return this.cacheTemplates && 'localStorage' in window && window['localStorage'] !== null;
} catch (e) {
return false;
}
},
saveLocalTemplates: function () {
if (this.localStorageAvailable()) {
localStorage.setItem("templates", JSON.stringify(this.templates));
localStorage.setItem("templateVersion", this.templateVersion);
}
},
loadLocalTemplates: function () {
if (this.localStorageAvailable()) {
var templateVersion = localStorage.getItem("templateVersion");
if (templateVersion && templateVersion == this.templateVersion) {
var templates = localStorage.getItem("templates");
if (templates) {
templates = JSON.parse(templates);
for (var x in templates) {
if (!this.templates[x]) {
this.addTemplate(x, templates[x]);
}
}
}
}
else {
localStorage.removeItem("templates");
localStorage.removeItem("templateVersion");
}
}
}
};
templateLoader.loadLocalTemplates();
window.templateLoader = templateLoader;
})();
})();

View File

@@ -0,0 +1,89 @@
if (!CMS.Views['Checklists']) CMS.Views.Checklists = {};
CMS.Views.Checklists = Backbone.View.extend({
// takes CMS.Models.Checklists as model
events : {
'click .course-checklist .checklist-title' : "toggleChecklist",
'click .course-checklist .task input' : "toggleTask",
'click a[rel="external"]' : window.cmsLinkNewWindow
},
initialize : function() {
var self = this;
this.collection.fetch({
complete: function () {
window.templateLoader.loadRemoteTemplate("checklist",
"/static/client_templates/checklist.html",
function (raw_template) {
self.template = _.template(raw_template);
self.render();
}
);
},
error: CMS.ServerError
}
);
},
render: function() {
// catch potential outside call before template loaded
if (!this.template) return this;
this.$el.empty();
var self = this;
_.each(this.collection.models,
function(checklist, index) {
self.$el.append(self.renderTemplate(checklist, index));
});
return this;
},
renderTemplate: function (checklist, index) {
var checklistItems = checklist.attributes['items'];
var itemsChecked = 0;
_.each(checklistItems,
function(checklist) {
if (checklist['is_checked']) {
itemsChecked +=1;
}
});
var percentChecked = Math.round((itemsChecked/checklistItems.length)*100);
return this.template({
checklistIndex : index,
checklistShortDescription : checklist.attributes['short_description'],
items: checklistItems,
itemsChecked: itemsChecked,
percentChecked: percentChecked});
},
toggleChecklist : function(e) {
e.preventDefault();
$(e.target).closest('.course-checklist').toggleClass('is-collapsed');
},
toggleTask : function (e) {
var self = this;
var completed = 'is-completed';
var $checkbox = $(e.target);
var $task = $checkbox.closest('.task');
$task.toggleClass(completed);
var checklist_index = $checkbox.data('checklist');
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);
},
error : CMS.ServerError
});
}
});

View File

@@ -142,8 +142,11 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
onDelete: function(event) {
event.preventDefault();
// TODO ask for confirmation
// remove the dom element and delete the model
if (!confirm('Are you sure you want to delete this update? This action cannot be undone.')) {
return;
}
var targetModel = this.eventModel(event);
this.modelDom(event).remove();
var cacheThis = this;

View File

@@ -6,14 +6,6 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({
// Model class is CMS.Models.Settings.Advanced
events : {
'click .delete-button' : "deleteEntry",
'click .new-button' : "addEntry",
// update model on changes
'change .policy-key' : "updateKey",
// keypress to catch alpha keys and backspace/delete on some browsers
'keypress .policy-key' : "showSaveCancelButtons",
// keyup to catch backspace/delete reliably
'keyup .policy-key' : "showSaveCancelButtons",
'focus :input' : "focusInput",
'blur :input' : "blurInput"
// TODO enable/disable save based on validation (currently enabled whenever there are changes)
@@ -95,16 +87,11 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({
mirror.setValue(stringValue);
} catch(quotedE) {
// TODO: validation error
console.log("Error with JSON, even after converting to String.");
console.log(quotedE);
// console.log("Error with JSON, even after converting to String.");
// console.log(quotedE);
JSONValue = undefined;
}
}
else {
// TODO: validation error
console.log("Error with JSON, but will not convert to String.");
console.log(e);
}
}
if (JSONValue !== undefined) {
self.clearValidationErrors();
@@ -113,7 +100,6 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({
}
});
},
showMessage: function (type) {
this.$el.find(".message-status").removeClass("is-shown");
if (type) {
@@ -128,56 +114,19 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({
else {
// This is the case of the page first rendering, or when Cancel is pressed.
this.hideSaveCancelButtons();
this.toggleNewButton(true);
}
},
showSaveCancelButtons: function(event) {
if (!this.buttonsVisible) {
if (event && (event.type === 'keypress' || event.type === 'keyup')) {
// check whether it's really an altering event: note, String.fromCharCode(keyCode) will
// give positive values for control/command/option-letter combos; so, don't use it
if (!((event.charCode && String.fromCharCode(event.charCode) !== "") ||
// 8 = backspace, 46 = delete
event.keyCode === 8 || event.keyCode === 46)) return;
}
this.$el.find(".message-status").removeClass("is-shown");
$('.wrapper-notification').addClass('is-shown');
this.buttonsVisible = true;
}
},
hideSaveCancelButtons: function() {
$('.wrapper-notification').removeClass('is-shown');
this.buttonsVisible = false;
},
toggleNewButton: function (enable) {
var newButton = this.$el.find(".new-button");
if (enable) {
newButton.removeClass('disabled');
}
else {
newButton.addClass('disabled');
}
},
deleteEntry : function(event) {
event.preventDefault();
// find out which entry
var li$ = $(event.currentTarget).closest('li');
// Not data b/c the validation view uses it for a selector
var key = $('.key', li$).attr('id');
delete this.selectorToField[this.fieldToSelectorMap[key]];
delete this.fieldToSelectorMap[key];
if (key !== this.model.new_key) {
this.model.deleteKeys.push(key);
this.model.unset(key);
}
li$.remove();
this.showSaveCancelButtons();
},
saveView : function(event) {
// TODO one last verification scan:
// call validateKey on each to ensure proper format
@@ -201,102 +150,15 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({
error : CMS.ServerError
});
},
addEntry : function() {
var listEle$ = this.$el.find('.course-advanced-policy-list');
var newEle = this.renderTemplate("", "");
listEle$.append(newEle);
// need to re-find b/c replaceWith seems to copy rather than use the specific ele instance
var policyValueDivs = this.$el.find('#' + this.model.new_key).closest('li').find('.json');
// only 1 but hey, let's take advantage of the context mechanism
_.each(policyValueDivs, this.attachJSONEditor, this);
this.toggleNewButton(false);
},
updateKey : function(event) {
var parentElement = $(event.currentTarget).closest('.key');
// old key: either the key as in the model or new_key.
// That is, it doesn't change as the val changes until val is accepted.
var oldKey = parentElement.attr('id');
// TODO: validation of keys with spaces. For now at least trim strings to remove initial and
// trailing whitespace
var newKey = $.trim($(event.currentTarget).val());
if (oldKey !== newKey) {
// TODO: is it OK to erase other validation messages?
this.clearValidationErrors();
if (!this.validateKey(oldKey, newKey)) return;
if (this.model.has(newKey)) {
var error = {};
error[oldKey] = 'You have already defined "' + newKey + '" in the manual policy definitions.';
error[newKey] = "You tried to enter a duplicate of this key.";
this.model.trigger("invalid", this.model, error);
return false;
}
// explicitly call validate to determine whether to proceed (relying on triggered error means putting continuation in the success
// method which is uglier I think?)
var newEntryModel = {};
// set the new key's value to the old one's
newEntryModel[newKey] = (oldKey === this.model.new_key ? '' : this.model.get(oldKey));
var validation = this.model.validate(newEntryModel);
if (validation) {
if (_.has(validation, newKey)) {
// swap to the key which the map knows about
validation[oldKey] = validation[newKey];
}
this.model.trigger("invalid", this.model, validation);
// abandon update
return;
}
// Now safe to actually do the update
this.model.set(newEntryModel);
// update maps
var selector = this.fieldToSelectorMap[oldKey];
this.selectorToField[selector] = newKey;
this.fieldToSelectorMap[newKey] = selector;
delete this.fieldToSelectorMap[oldKey];
if (oldKey !== this.model.new_key) {
// mark the old key for deletion and delete from field maps
this.model.deleteKeys.push(oldKey);
this.model.unset(oldKey) ;
}
else {
// id for the new entry will now be the key value. Enable new entry button.
this.toggleNewButton(true);
}
// check for newkey being the name of one which was previously deleted in this session
var wasDeleting = this.model.deleteKeys.indexOf(newKey);
if (wasDeleting >= 0) {
this.model.deleteKeys.splice(wasDeleting, 1);
}
// Update the ID to the new value.
parentElement.attr('id', newKey);
}
},
validateKey : function(oldKey, newKey) {
// model validation can't handle malformed keys nor notice if 2 fields have same key; so, need to add that chk here
// TODO ensure there's no spaces or illegal chars (note some checking for spaces currently done in model's
// validate method.
return true;
},
renderTemplate: function (key, value) {
var newKeyId = _.uniqueId('policy_key_'),
newEle = this.template({ key : key, value : JSON.stringify(value, null, 4),
keyUniqueId: newKeyId, valueUniqueId: _.uniqueId('policy_value_')});
this.fieldToSelectorMap[(_.isEmpty(key) ? this.model.new_key : key)] = newKeyId;
this.selectorToField[newKeyId] = (_.isEmpty(key) ? this.model.new_key : key);
this.fieldToSelectorMap[key] = newKeyId;
this.selectorToField[newKeyId] = key;
return newEle;
},
focusInput : function(event) {
$(event.target).prev().addClass("is-focused");
},

View File

@@ -101,6 +101,12 @@ 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

View File

@@ -25,11 +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);
if ($(ele).is('div')) {
// put error on the contained inputs
$(ele).find('input, textarea').addClass('error');
}
else $(ele).addClass('error');
this.getInputElements(ele).addClass('error');
$(ele).parent().append(this.errorTemplate({message : error[field]}));
}
},
@@ -37,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();
}
},
@@ -65,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);
}
}
});

View File

@@ -1,7 +1,7 @@
// studio base styling
// studio - base styling
// ====================
// basic reset
// basic setup
html {
font-size: 62.5%;
overflow-y: scroll;
@@ -9,7 +9,7 @@ html {
body {
@include font-size(16);
min-width: 980px;
min-width: $fg-min-width;
background: $gray-l5;
line-height: 1.6;
color: $baseFontColor;
@@ -214,7 +214,7 @@ h1 {
color: $gray-l2;
}
.title, .title-1 {
.title-1 {
@include font-size(32);
margin: 0;
padding: 0;
@@ -283,8 +283,8 @@ h1 {
.title-3 {
@include font-size(16);
margin: 0 0 ($baseline/4) 0;
font-weight: 500;
margin: 0 0 ($baseline/2) 0;
font-weight: 600;
}
.title-4 {
@@ -327,7 +327,8 @@ h1 {
}
}
.nav-related {
// navigation
.nav-related, .nav-page {
.nav-item {
margin-bottom: ($baseline/4);
@@ -350,10 +351,11 @@ h1 {
// layout - grandfathered
.main-wrapper {
position: relative;
margin: 0 40px;
margin: 40px;
}
.inner-wrapper {
@include clearfix();
position: relative;
max-width: 1280px;
margin: auto;
@@ -363,6 +365,12 @@ h1 {
}
}
.main-column {
clear: both;
float: left;
width: 70%;
}
.sidebar {
float: right;
width: 28%;
@@ -378,94 +386,6 @@ h1 {
// ====================
// forms
input[type="text"],
input[type="email"],
input[type="password"],
textarea.text {
padding: 6px 8px 8px;
@include box-sizing(border-box);
border: 1px solid $mediumGrey;
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);
font-family: 'Open Sans', sans-serif;
font-size: 11px;
color: $baseFontColor;
outline: 0;
&::-webkit-input-placeholder,
&:-moz-placeholder,
&:-ms-input-placeholder {
color: #979faf;
}
&:focus {
@include linear-gradient($paleYellow, tint($paleYellow, 90%));
outline: 0;
}
}
// forms - specific
input.search {
padding: 6px 15px 8px 30px;
@include box-sizing(border-box);
border: 1px solid $darkGrey;
border-radius: 20px;
background: url(../img/search-icon.png) no-repeat 8px 7px #edf1f5;
font-family: 'Open Sans', sans-serif;
color: $baseFontColor;
outline: 0;
&::-webkit-input-placeholder {
color: #979faf;
}
}
label {
font-size: 12px;
}
code {
padding: 0 4px;
border-radius: 3px;
background: #eee;
font-family: Monaco, monospace;
}
.CodeMirror {
font-size: 13px;
border: 1px solid $darkGrey;
background: #fff;
}
.text-editor {
width: 100%;
min-height: 80px;
padding: 10px;
@include box-sizing(border-box);
border: 1px solid $mediumGrey;
@include linear-gradient(top, rgba(255, 255, 255, 0), rgba(255, 255, 255, 0.3));
background-color: #edf1f5;
@include box-shadow(0 1px 2px rgba(0, 0, 0, 0.1) inset);
font-family: Monaco, monospace;
}
// ====================
// UI - chrome
.window {
@include clearfix();
@include border-radius(3px);
@include box-shadow(0 1px 1px $shadow-l1);
margin-bottom: $baseline;
border: 1px solid $gray-l2;
background: $white;
}
// ====================
// UI - actions
.new-unit-item,
.new-subsection-item,
@@ -772,6 +692,10 @@ hr.divide {
word-wrap: break-word;
}
hr.divider {
@extend .sr;
}
// ====================
// js dependant
@@ -846,14 +770,4 @@ body.hide-wip {
.wip-box {
display: none;
}
}
// ====================
// needed fudges for now
body.dashboard {
.my-classes {
margin-top: $baseline;
}
}

View File

@@ -1,367 +0,0 @@
section.cal {
@include box-sizing(border-box);
@include clearfix;
padding: 20px;
> header {
display: none;
@include clearfix;
margin-bottom: 10px;
opacity: .4;
@include transition;
text-shadow: 0 1px 0 #fff;
&:hover {
opacity: 1;
}
h2 {
@include inline-block();
text-transform: uppercase;
letter-spacing: 1px;
font-size: 14px;
padding: 6px 6px 6px 0;
font-size: 12px;
margin: 0;
}
ul {
@include inline-block;
float: right;
margin: 0;
padding: 0;
&.actions {
float: left;
}
li {
@include inline-block;
margin-right: 6px;
border-right: 1px solid #ddd;
padding: 0 6px 0 0;
&:last-child {
border-right: 0;
margin-right: 0;
padding-right: 0;
}
a {
@include inline-block();
font-size: 12px;
@include inline-block;
margin: 0 6px;
font-style: italic;
}
ul {
@include inline-block();
margin: 0;
li {
@include inline-block();
padding: 0;
border-left: 0;
}
}
}
}
}
ol {
list-style: none;
@include clearfix;
border: 1px solid lighten( $dark-blue , 30% );
background: #FFF;
width: 100%;
@include box-sizing(border-box);
margin: 0;
padding: 0;
@include box-shadow(0 0 5px lighten($dark-blue, 45%));
@include border-radius(3px);
overflow: hidden;
> li {
border-right: 1px solid lighten($dark-blue, 40%);
border-bottom: 1px solid lighten($dark-blue, 40%);
@include box-sizing(border-box);
float: left;
width: flex-grid(3) + ((flex-gutter() * 3) / 4);
background-color: $light-blue;
@include box-shadow(inset 0 0 0 1px lighten($light-blue, 8%));
&:hover {
li.create-module {
opacity: 1;
}
}
&:nth-child(4n) {
border-right: 0;
}
header {
border-bottom: 1px solid lighten($dark-blue, 40%);
@include box-shadow(0 2px 2px $light-blue);
display: block;
margin-bottom: 2px;
background: #FFF;
h1 {
font-size: 14px;
text-transform: uppercase;
border-bottom: 1px solid lighten($dark-blue, 60%);
padding: 6px;
color: $bright-blue;
margin: 0;
a {
color: $bright-blue;
display: block;
padding: 6px;
margin: -6px;
&:hover {
color: darken($bright-blue, 10%);
background: lighten($yellow, 10%);
}
}
}
ul {
margin: 0;
padding: 0;
li {
background: #fff;
color: #888;
border-bottom: 0;
font-size: 12px;
@include box-shadow(none);
}
}
}
ul {
list-style: none;
margin: 0 0 1px 0;
padding: 0;
li {
border-bottom: 1px solid darken($light-blue, 6%);
// @include box-shadow(0 1px 0 lighten($light-blue, 4%));
overflow: hidden;
position: relative;
text-shadow: 0 1px 0 #fff;
&:hover {
background-color: lighten($yellow, 14%);
a.draggable {
background-color: lighten($yellow, 14%);
opacity: 1;
}
}
&.editable {
padding: 3px 6px;
}
a {
color: lighten($dark-blue, 10%);
display: block;
padding: 6px 35px 6px 6px;
&:hover {
background-color: lighten($yellow, 10%);
}
&.draggable {
background-color: $light-blue;
opacity: .3;
padding: 0;
&:hover {
background-color: lighten($yellow, 10%);
}
}
}
&.create-module {
position: relative;
opacity: 0;
@include transition(all 3s ease-in-out);
background: darken($light-blue, 2%);
> div {
background: $dark-blue;
@include box-shadow(0 0 5px darken($light-blue, 60%));
@include box-sizing(border-box);
display: none;
margin-left: 3%;
padding: 10px;
@include position(absolute, 30px 0 0 0);
width: 90%;
z-index: 99;
ul {
li {
border-bottom: 0;
background: none;
input {
@include box-sizing(border-box);
width: 100%;
}
select {
@include box-sizing(border-box);
width: 100%;
option {
font-size: 14px;
}
}
div {
margin-top: 10px;
}
a {
color: $light-blue;
float: right;
&:first-child {
float: left;
}
&:hover {
color: #fff;
}
}
}
}
}
}
}
}
}
}
section.new-section {
margin: 10px 0 40px;
@include inline-block();
position: relative;
> a {
@extend .button;
display: block;
}
section {
display: none;
@include position(absolute, 30px 0 0 0);
background: rgba(#000, .8);
min-width: 300px;
padding: 10px;
@include box-sizing(border-box);
@include border-radius(3px);
z-index: 99;
&:before {
content: " ";
display: block;
background: rgba(#000, .8);
width: 10px;
height: 10px;
@include position(absolute, -5px 0 0 20%);
@include transform(rotate(45deg));
}
form {
ul {
list-style: none;
li {
border-bottom: 0;
background: none;
margin-bottom: 6px;
input {
width: 100%;
@include box-sizing(border-box);
border-color: #000;
padding: 6px;
}
select {
width: 100%;
@include box-sizing(border-box);
option {
font-size: 14px;
}
}
a {
float: right;
&:first-child {
float: left;
}
}
}
}
}
}
}
}
body.content
section.cal {
width: flex-grid(3);
float: left;
overflow: scroll;
@include box-sizing(border-box);
opacity: .4;
@include transition();
&:hover {
opacity: 1;
}
> header {
@include transition;
overflow: hidden;
> a {
display: none;
}
ul {
float: none;
display: block;
li {
ul {
display: inline;
}
}
}
}
ol {
li {
@include box-sizing(border-box);
width: 100%;
border-right: 0;
&.create-module {
display: none;
}
}
}
}

View File

@@ -1,3 +1,6 @@
// studio - utilities - mixins and extends
// ====================
@mixin clearfix {
&:after {
content: '';

View File

@@ -1,689 +0,0 @@
input.courseware-unit-search-input {
float: left;
width: 260px;
background-color: #fff;
}
.branch {
.section-item {
@include clearfix();
.details {
display: block;
float: left;
margin-bottom: 0;
width: 650px;
}
.gradable-status {
float: right;
position: relative;
top: -4px;
right: 50px;
width: 145px;
.status-label {
position: absolute;
top: 2px;
right: -5px;
display: none;
width: 110px;
padding: 5px 40px 5px 10px;
@include border-radius(3px);
color: $lightGrey;
text-align: right;
font-size: 12px;
font-weight: bold;
line-height: 16px;
}
.menu-toggle {
z-index: 10;
position: absolute;
top: 0;
right: 5px;
padding: 5px;
color: $mediumGrey;
&:hover, &.is-active {
color: $blue;
}
}
.menu {
z-index: 1;
display: none;
opacity: 0.0;
position: absolute;
top: -1px;
left: 5px;
margin: 0;
padding: 8px 12px;
background: $white;
border: 1px solid $mediumGrey;
font-size: 12px;
@include border-radius(4px);
@include box-shadow(0 1px 2px rgba(0, 0, 0, .2));
@include transition(opacity .15s);
li {
width: 115px;
margin-bottom: 3px;
padding-bottom: 3px;
border-bottom: 1px solid $lightGrey;
&:last-child {
margin-bottom: 0;
padding-bottom: 0;
border: none;
a {
color: $darkGrey;
}
}
}
a {
color: $blue;
&.is-selected {
font-weight: bold;
}
}
}
// dropdown state
&.is-active {
.menu {
z-index: 1000;
display: block;
opacity: 1.0;
}
.menu-toggle {
z-index: 10000;
}
}
// set state
&.is-set {
.menu-toggle {
color: $blue;
}
.status-label {
display: block;
color: $blue;
}
}
}
}
}
.courseware-section {
position: relative;
background: #fff;
border-radius: 3px;
border: 1px solid $mediumGrey;
margin-top: 15px;
padding-bottom: 12px;
@include box-shadow(0 1px 1px rgba(0, 0, 0, 0.1));
&:first-child {
margin-top: 0;
}
&.collapsed {
padding-bottom: 0;
}
label {
float: left;
line-height: 29px;
}
.datepair {
float: left;
margin-left: 10px;
}
.section-published-date {
position: absolute;
top: 19px;
right: 90px;
padding: 4px 10px;
border-radius: 3px;
background: $lightGrey;
text-align: right;
.published-status {
font-size: 12px;
margin-right: 15px;
strong {
font-weight: bold;
}
}
.schedule-button {
@include blue-button;
}
.edit-button {
@include blue-button;
}
.schedule-button,
.edit-button {
font-size: 11px;
padding: 3px 15px 5px;
}
}
.datepair .date,
.datepair .time {
padding-left: 0;
padding-right: 0;
border: none;
background: none;
@include box-shadow(none);
font-size: 13px;
font-weight: bold;
color: $blue;
cursor: pointer;
}
.datepair .date {
width: 80px;
}
.datepair .time {
width: 65px;
}
&.collapsed .subsection-list,
.collapsed .subsection-list,
.collapsed > ol {
display: none !important;
}
header {
min-height: 75px;
@include clearfix();
.item-details, .section-published-date {
}
.item-details {
display: inline-block;
padding: 20px 0 10px 0;
@include clearfix();
.section-name {
float: left;
margin-right: 10px;
width: 350px;
font-size: 19px;
font-weight: bold;
color: $blue;
}
.section-name-span {
cursor: pointer;
@include transition(color .15s);
&:hover {
color: $orange;
}
}
.section-name-edit {
position: relative;
width: 400px;
background: $white;
input {
font-size: 16px;
}
.save-button {
@include blue-button;
padding: 7px 20px 7px;
margin-right: 5px;
}
.cancel-button {
@include white-button;
padding: 7px 20px 7px;
}
}
.section-published-date {
float: right;
width: 265px;
margin-right: 220px;
@include border-radius(3px);
background: $lightGrey;
.published-status {
font-size: 12px;
margin-right: 15px;
strong {
font-weight: bold;
}
}
.schedule-button {
@include blue-button;
}
.edit-button {
@include blue-button;
}
.schedule-button,
.edit-button {
font-size: 11px;
padding: 3px 15px 5px;
}
}
.gradable-status {
position: absolute;
top: 20px;
right: 70px;
width: 145px;
.status-label {
position: absolute;
top: 0;
right: 2px;
display: none;
width: 100px;
padding: 10px 35px 10px 10px;
@include border-radius(3px);
background: $lightGrey;
color: $lightGrey;
text-align: right;
font-size: 12px;
font-weight: bold;
line-height: 16px;
}
.menu-toggle {
z-index: 10;
position: absolute;
top: 2px;
right: 5px;
padding: 5px;
color: $lightGrey;
&:hover, &.is-active {
color: $blue;
}
}
.menu {
z-index: 1;
display: none;
opacity: 0.0;
position: absolute;
top: -1px;
left: 2px;
margin: 0;
padding: 8px 12px;
background: $white;
border: 1px solid $mediumGrey;
font-size: 12px;
@include border-radius(4px);
@include box-shadow(0 1px 2px rgba(0, 0, 0, .2));
@include transition(opacity .15s);
@include transition(display .15s);
li {
width: 115px;
margin-bottom: 3px;
padding-bottom: 3px;
border-bottom: 1px solid $lightGrey;
&:last-child {
margin-bottom: 0;
padding-bottom: 0;
border: none;
a {
color: $darkGrey;
}
}
}
a {
&.is-selected {
font-weight: bold;
}
}
}
// dropdown state
&.is-active {
.menu {
z-index: 1000;
display: block;
opacity: 1.0;
}
.menu-toggle {
z-index: 10000;
}
}
// set state
&.is-set {
.menu-toggle {
color: $blue;
}
.status-label {
display: block;
color: $blue;
}
}
float: left;
padding: 21px 0 0;
}
}
.item-actions {
margin-top: 21px;
margin-right: 12px;
.edit-button,
.delete-button {
margin-top: -3px;
}
}
.expand-collapse-icon {
float: left;
margin: 29px 6px 16px 16px;
@include transition(none);
&.expand {
background-position: 0 0;
}
&.collapsed {
}
}
.drag-handle {
margin-left: 11px;
}
}
h3 {
font-size: 19px;
font-weight: 700;
color: $blue;
}
.section-name-span {
cursor: pointer;
@include transition(color .15s);
&:hover {
color: $orange;
}
}
.section-name-form {
margin-bottom: 15px;
}
.section-name-edit {
input {
font-size: 16px;
}
.save-button {
@include blue-button;
padding: 7px 20px 7px;
margin-right: 5px;
}
.cancel-button {
@include white-button;
padding: 7px 20px 7px;
}
}
h4 {
font-size: 12px;
color: #878e9d;
strong {
font-weight: bold;
}
}
.list-header {
@include linear-gradient(top, transparent, rgba(0, 0, 0, .1));
background-color: #ced2db;
border-radius: 3px 3px 0 0;
}
.subsection-list {
margin: 0 12px;
> ol {
@include tree-view;
border-top-width: 0;
}
}
&.new-section {
header {
height: auto;
@include clearfix();
}
.expand-collapse-icon {
visibility: hidden;
}
.item-details {
padding: 25px 0 0 0;
.section-name {
float: none;
width: 100%;
}
}
}
}
.toggle-button-sections {
display: none;
position: relative;
float: right;
margin-top: 10px;
font-size: 13px;
color: $darkGrey;
&.is-shown {
display: block;
}
.ss-icon {
@include border-radius(20px);
position: relative;
top: -1px;
display: inline-block;
margin-right: 2px;
line-height: 5px;
font-size: 11px;
}
.label {
display: inline-block;
}
}
.new-section-name,
.new-subsection-name-input {
width: 515px;
}
.new-section-name-save,
.new-subsection-name-save {
@include blue-button;
padding: 4px 20px 7px;
margin: 0 5px;
color: #fff !important;
}
.new-section-name-cancel,
.new-subsection-name-cancel {
@include white-button;
padding: 4px 20px 7px;
color: #8891a1 !important;
}
.dummy-calendar {
display: none;
position: absolute;
top: 55px;
left: 110px;
z-index: 9999;
border: 1px solid #3C3C3C;
@include box-shadow(0 1px 15px rgba(0, 0, 0, .2));
}
.unit-name-input {
padding: 20px 40px;
label {
display: block;
}
input {
width: 100%;
font-size: 20px;
}
}
.preview {
background: url(../img/preview.jpg) center top no-repeat;
}
.edit-subsection-publish-settings {
display: none;
position: fixed;
top: 100px;
left: 50%;
z-index: 99999;
width: 600px;
margin-left: -300px;
background: #fff;
text-align: center;
.settings {
padding: 40px;
}
h3 {
font-size: 34px;
font-weight: 300;
}
.picker {
margin: 30px 0 65px;
}
.description {
margin-top: 30px;
font-size: 14px;
line-height: 20px;
}
strong {
font-weight: 700;
}
.start-date,
.start-time {
font-size: 19px;
}
.save-button {
@include blue-button;
margin-right: 10px;
}
.cancel-button {
@include white-button;
}
.save-button,
.cancel-button {
font-size: 16px;
}
}
.collapse-all-button {
float: right;
margin-top: 10px;
font-size: 13px;
color: $darkGrey;
}
// sort/drag and drop
.ui-droppable {
@include transition (padding 0.5s ease-in-out 0s);
min-height: 20px;
padding: 0;
&.dropover {
padding: 15px 0;
}
}
.ui-draggable-dragging {
@include box-shadow(0 1px 2px rgba(0, 0, 0, .3));
border: 1px solid $darkGrey;
opacity : 0.2;
&:hover {
opacity : 1.0;
.section-item {
background: $yellow !important;
}
}
// hiding unit button - temporary fix until this semantically corrected
.new-unit-item {
display: none;
}
}
ol.ui-droppable .branch:first-child .section-item {
border-top: none;
}

View File

@@ -1,114 +0,0 @@
.class-list {
margin-top: 20px;
border-radius: 3px;
border: 1px solid $darkGrey;
background: #fff;
@include box-shadow(0 1px 2px rgba(0, 0, 0, .1));
li {
position: relative;
border-bottom: 1px solid $mediumGrey;
&:last-child {
border-bottom: none;
}
.class-link {
z-index: 100;
display: block;
padding: 20px 25px;
line-height: 1.3;
&:hover {
background: $paleYellow;
+ .view-live-button {
opacity: 1.0;
pointer-events: auto;
}
}
}
}
.class-name {
display: block;
font-size: 19px;
font-weight: 300;
}
.detail {
font-size: 14px;
font-weight: 400;
margin-right: 20px;
color: #3c3c3c;
}
// view live button
.view-live-button {
z-index: 10000;
position: absolute;
top: 15px;
right: $baseline;
padding: ($baseline/4) ($baseline/2);
opacity: 0;
pointer-events: none;
&:hover {
opacity: 1.0;
pointer-events: auto;
}
}
}
.new-course {
padding: 15px 25px;
margin-top: 20px;
border-radius: 3px;
border: 1px solid $darkGrey;
background: #fff;
box-shadow: 0 1px 2px rgba(0, 0, 0, .1);
@include clearfix;
.row {
margin-bottom: 15px;
@include clearfix;
}
.column {
float: left;
width: 48%;
}
.column:first-child {
margin-right: 4%;
}
.course-info {
width: 600px;
}
label {
display: block;
font-size: 13px;
font-weight: 700;
}
.new-course-org,
.new-course-number,
.new-course-name {
width: 100%;
}
.new-course-name {
font-size: 19px;
font-weight: 300;
}
.new-course-save {
@include blue-button;
}
.new-course-cancel {
@include white-button;
}
}

View File

@@ -1,78 +0,0 @@
.faded-hr-divider {
@include background-image(linear-gradient(180deg, rgba(200,200,200, 0) 0%,
rgba(200,200,200, 1) 50%,
rgba(200,200,200, 0)));
height: 1px;
width: 100%;
}
.faded-hr-divider-medium {
@include background-image(linear-gradient(180deg, rgba(240,240,240, 0) 0%,
rgba(240,240,240, 1) 50%,
rgba(240,240,240, 0)));
height: 1px;
width: 100%;
}
.faded-hr-divider-light {
@include background-image(linear-gradient(180deg, rgba(255,255,255, 0) 0%,
rgba(255,255,255, 0.8) 50%,
rgba(255,255,255, 0)));
height: 1px;
width: 100%;
}
.faded-vertical-divider {
@include background-image(linear-gradient(90deg, rgba(200,200,200, 0) 0%,
rgba(200,200,200, 1) 50%,
rgba(200,200,200, 0)));
height: 100%;
width: 1px;
}
.faded-vertical-divider-light {
@include background-image(linear-gradient(90deg, rgba(255,255,255, 0) 0%,
rgba(255,255,255, 0.6) 50%,
rgba(255,255,255, 0)));
height: 100%;
width: 1px;
}
.vertical-divider {
@extend .faded-vertical-divider;
position: relative;
&::after {
@extend .faded-vertical-divider-light;
content: "";
display: block;
position: absolute;
left: 1px;
}
}
.horizontal-divider {
border: none;
@extend .faded-hr-divider;
position: relative;
&::after {
@extend .faded-hr-divider-light;
content: "";
display: block;
position: absolute;
top: 1px;
}
}
.fade-right-hr-divider {
@include background-image(linear-gradient(180deg, rgba(200,200,200, 0) 0%,
rgba(200,200,200, 1)));
border: none;
}
.fade-left-hr-divider {
@include background-image(linear-gradient(180deg, rgba(200,200,200, 1) 0%,
rgba(200,200,200, 0)));
border: none;
}

View File

@@ -1,126 +0,0 @@
// This is a temporary page, which will be replaced once we have a more extensive course catalog and marketing site for edX labs.
.class-landing {
.main-wrapper {
width: 700px !important;
margin: 100px auto;
}
.class-info {
padding: 30px 40px 40px;
@extend .window;
hgroup {
padding-bottom: 26px;
border-bottom: 1px solid $mediumGrey;
}
h1 {
float: none;
font-size: 30px;
font-weight: 300;
margin: 0;
}
h2 {
color: #5d6779;
}
.class-actions {
@include clearfix;
padding: 15px 0;
margin-bottom: 18px;
border-bottom: 1px solid $mediumGrey;
}
.log-in-form {
@include clearfix;
padding: 15px 0 20px;
margin-bottom: 18px;
border-bottom: 1px solid $mediumGrey;
.log-in-submit-button {
@include blue-button;
padding: 6px 20px 8px;
margin: 24px 0 0;
}
.column {
float: left;
width: 41%;
margin-right: 1%;
&.submit {
width: 16%;
margin-right: 0;
}
label {
float: left;
}
}
input {
width: 100%;
font-family: $sans-serif;
font-size: 13px;
}
.forgot-button {
float: right;
margin-bottom: 6px;
font-size: 12px;
}
}
.sign-up-button {
@include blue-button;
display: block;
width: 250px;
margin: auto;
}
.log-in-button {
@include white-button;
float: right;
}
.sign-up-button,
.log-in-button {
padding: 8px 0 12px;
font-size: 18px;
font-weight: 300;
text-align: center;
}
.class-description {
margin-top: 30px;
font-size: 14px;
}
p + p {
margin-top: 22px;
}
}
.edx-labs-logo-small {
display: block;
width: 124px;
height: 30px;
margin: auto;
background: url(../img/edx-labs-logo-small.png) no-repeat;
text-indent: -9999px;
overflow: hidden;
}
.edge-logo {
display: block;
width: 143px;
height: 39px;
margin: auto;
background: url(../images/edge-logo-small.png) no-repeat;
text-indent: -9999px;
overflow: hidden;
}
}

View File

@@ -1,125 +0,0 @@
body {
@include clearfix();
height: 100%;
font: 14px $body-font-family;
background-color: lighten($dark-blue, 62%);
background-image: url('/static/img/noise.png');
> section {
display: table;
table-layout: fixed;
width: 100%;
}
> header {
background: $dark-blue;
@include background-image(url('/static/img/noise.png'), linear-gradient(lighten($dark-blue, 10%), $dark-blue));
border-bottom: 1px solid darken($dark-blue, 15%);
@include box-shadow(inset 0 -1px 0 lighten($dark-blue, 10%));
@include box-sizing(border-box);
color: #fff;
display: block;
float: none;
padding: 0 20px;
text-shadow: 0 -1px 0 darken($dark-blue, 15%);
width: 100%;
nav {
@include clearfix;
> a {
@include hide-text;
background: url('/static/img/menu.png') 0 center no-repeat;
border-right: 1px solid darken($dark-blue, 10%);
@include box-shadow(1px 0 0 lighten($dark-blue, 10%));
display: block;
float: left;
height: 19px;
padding: 8px 10px 8px 0;
width: 14px;
&:hover, &:focus {
opacity: .7;
}
}
h2 {
border-right: 1px solid darken($dark-blue, 10%);
@include box-shadow(1px 0 0 lighten($dark-blue, 10%));
float: left;
font-size: 14px;
margin: 0;
text-transform: uppercase;
-webkit-font-smoothing: antialiased;
a {
color: #fff;
padding: 8px 20px;
display: block;
&:hover {
background-color: rgba(darken($dark-blue, 15%), .5);
color: $yellow;
}
}
}
a {
color: rgba(#fff, .8);
&:hover {
color: rgba(#fff, .6);
}
}
ul {
float: left;
margin: 0;
padding: 0;
@include clearfix;
&.user-nav {
float: right;
border-left: 1px solid darken($dark-blue, 10%);
}
li {
border-right: 1px solid darken($dark-blue, 10%);
float: left;
@include box-shadow(1px 0 0 lighten($dark-blue, 10%));
a {
padding: 8px 20px;
display: block;
&:hover {
background-color: rgba(darken($dark-blue, 15%), .5);
color: $yellow;
}
&.new-module {
&:before {
@include inline-block;
content: "+";
font-weight: bold;
margin-right: 10px;
}
}
}
}
}
}
}
&.content {
section.main-content {
border-left: 2px solid $dark-blue;
@include box-sizing(border-box);
width: flex-grid(9) + flex-gutter();
float: left;
@include box-shadow( -2px 0 0 lighten($dark-blue, 55%));
@include transition();
background: #FFF;
}
}
}

View File

@@ -1,69 +0,0 @@
.component {
font-family: 'Open Sans', Verdana, Arial, Helvetica, sans-serif;
font-size: 16px;
line-height: 1.6;
color: #3c3c3c;
a {
color: #1d9dd9;
text-decoration: none;
}
p {
font-size: 16px;
line-height: 1.6;
}
h1 {
float: none;
}
h2 {
color: #646464;
font-size: 19px;
font-weight: 300;
letter-spacing: 1px;
margin-bottom: 15px;
margin-left: 0;
text-transform: uppercase;
}
h3 {
font-size: 19px;
font-weight: 400;
}
h4 {
background: none;
padding: 0;
border: none;
@include box-shadow(none);
font-size: 16px;
font-weight: 400;
}
code {
margin: 0 2px;
padding: 0px 5px;
border-radius: 3px;
border: 1px solid #eaeaea;
white-space: nowrap;
font-family: Monaco, monospace;
font-size: 14px;
background-color: #f8f8f8;
}
p + h2, ul + h2, ol + h2, p + h3 {
margin-top: 40px;
}
p + p, ul + p, ol + p {
margin-top: 20px;
}
p {
color: #3c3c3c;
font: normal 1em/1.6em;
margin: 0px;
}
}

View File

@@ -1,139 +0,0 @@
.edx-studio-logo-large {
display: block;
width: 224px;
height: 45px;
margin: 100px auto 30px;
background: url(../img/edx-studio-large.png) no-repeat;
}
.sign-up-box,
.log-in-box {
width: 500px;
margin: auto;
border-radius: 3px;
header {
height: 36px;
border-radius: 3px 3px 0 0;
border: 1px solid #2c2e33;
@include linear-gradient(top, #686b76, #54565e);
color: #fff;
@include box-shadow(0 1px 1px rgba(0, 0, 0, 0.2), 0 -1px 0px rgba(255, 255, 255, 0.05) inset, 0 1px 0 rgba(255, 255, 255, .25) inset);
h1 {
float: none;
margin: 5px 0;
font-size: 15px;
font-weight: 300;
text-align: center;
}
}
form {
padding: 40px;
border: 1px solid $darkGrey;
border-top-width: 0;
border-radius: 0 0 3px 3px;
background: #fff;
@include box-shadow(0 1px 2px rgba(0, 0, 0, .1));
}
label {
display: block;
margin-bottom: 5px;
font-size: 13px;
font-weight: 700;
}
input[type="text"],
input[type="email"],
input[type="password"] {
width: 100%;
font-size: 20px;
font-weight: 300;
}
.row {
@include clearfix;
margin-bottom: 24px;
.split {
float: left;
width: 48%;
&:first-child {
margin-right: 4%;
}
}
}
.form-actions {
@include clearfix;
margin-top: 32px;
margin-bottom: 5px;
text-align: center;
}
.log-in-button,
.create-account-button {
@include blue-button;
padding: 8px 0 10px;
font-family: $sans-serif;
@include transition(all .15s);
}
.create-account-button {
padding: 10px 40px 12px;
margin-bottom: 10px;
}
.enrolled {
font-size: 14px;
}
.sign-up-button {
@include white-button;
padding: 7px 0 9px;
}
.log-in-button,
.sign-up-button {
@include box-sizing(border-box);
float: left;
width: 45%;
}
.or {
float: left;
display: inline-block;
width: 10%;
font-size: 15px;
line-height: 36px;
color: $darkGrey;
text-align: center;
}
.forgot-button {
float: right;
font-size: 11px;
font-weight: 400;
line-height: 21px;
}
.log-in-extra {
margin-top: 10px;
text-align: right;
font-size: 13px;
}
#login_error,
#register_error {
display: none;
margin-bottom: 30px;
padding: 5px 10px;
border-radius: 3px;
background: $error-red;
font-size: 14px;
color: #fff;
}
}

View File

@@ -1,128 +0,0 @@
section.video-new, section.video-edit, section.problem-new, section.problem-edit {
position: absolute;
top: 72px;
right: 0;
background: #fff;
width: flex-grid(6);
@include box-shadow(0 0 6px #666);
border: 1px solid #333;
border-right: 0;
z-index: 4;
> header {
background: #666;
@include clearfix;
color: #fff;
padding: 6px;
border-bottom: 1px solid #333;
-webkit-font-smoothing: antialiased;
h2 {
float: left;
font-size: 14px;
}
a {
color: #fff;
&.save-update {
float: right;
}
&.cancel {
float: left;
}
}
}
> section {
padding: 20px;
> header {
h1 {
font-size: 24px;
margin: 12px 0;
}
section {
&.status-settings {
ul {
list-style: none;
@include border-radius(2px);
border: 1px solid #999;
@include inline-block();
li {
@include inline-block();
border-right: 1px solid #999;
padding: 6px;
&:last-child {
border-right: 0;
}
&.current {
background: #eee;
}
}
}
a.settings {
@include inline-block();
margin: 0 20px;
border: 1px solid #999;
padding: 6px;
}
select {
float: right;
}
}
&.meta {
background: #eee;
padding: 10px;
margin: 20px 0;
@include clearfix();
div {
float: left;
margin-right: 20px;
h2 {
font-size: 14px;
@include inline-block();
}
p {
@include inline-block();
}
}
}
}
}
section.notes {
margin-top: 20px;
padding: 6px;
background: #eee;
border: 1px solid #ccc;
textarea {
@include box-sizing(border-box);
display: block;
width: 100%;
}
h2 {
font-size: 14px;
margin-bottom: 6px;
}
input[type="submit"]{
margin-top: 10px;
}
}
}
}

View File

@@ -1,24 +0,0 @@
section.problem-new, section.problem-edit {
> section {
textarea {
@include box-sizing(border-box);
display: block;
width: 100%;
}
div.preview {
background: #eee;
@include box-sizing(border-box);
height: 40px;
padding: 10px;
width: 100%;
}
a.save {
@extend .button;
@include inline-block();
margin-top: 20px;
}
}
}

View File

@@ -1,3 +1,10 @@
// studio - utilities - reset
// ====================
// * {
// @include box-sizing(border-box);
// }
html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
@@ -18,7 +25,7 @@ time, mark, audio, video {
font: inherit;
vertical-align: baseline;
}
/* HTML5 display-role reset for older browsers */
article, aside, details, figcaption, figure,
footer, header, hgroup, menu, nav, section {
display: block;
@@ -38,12 +45,6 @@ q:before, q:after {
content: none;
}
/* remember to define visible focus styles!
:focus {
outline: ?????;
} */
/* remember to highlight inserts somehow! */
ins {
text-decoration: none;
}
@@ -56,10 +57,11 @@ table {
border-spacing: 0;
}
/* Reset styles to remove ui-lightness jquery ui theme
from the tabs component (used in the add component problem tab menu)
*/
// ====================
// grandfathered styles
// reset styles to remove ui-lightness jquery ui theme from the tabs component (used in the add component problem tab menu)
.ui-tabs {
padding: 0;
white-space: normal;
@@ -118,10 +120,7 @@ from the tabs component (used in the add component problem tab menu)
padding: 0;
}
/* reapplying the tab styles from unit.scss after
removing jquery ui ui-lightness styling
*/
// reapplying the tab styles from unit.scss after removing jquery ui ui-lightness styling
.problem-type-tabs {
border:none;
list-style-type: none;
@@ -146,26 +145,4 @@ removing jquery ui ui-lightness styling
border: 0px;
}
}
/*
li {
float:left;
display:inline-block;
text-align:center;
width: auto;
//@include linear-gradient(top, rgba(255, 255, 255, .4), rgba(255, 255, 255, 0));
//background-color: tint($lightBluishGrey, 20%);
//@include box-shadow(0 1px 0 rgba(255, 255, 255, 0.2) inset, 0 -1px 0 rgba(0, 0, 0, 0.2) inset);
opacity:.8;
&:hover {
opacity:1;
}
&.current {
border: 0px;
//@include active;
opacity:1;
}
}
*/
}

View File

@@ -1,239 +0,0 @@
section#unit-wrapper {
section.filters {
@include clearfix;
display: none;
opacity: .4;
margin-bottom: 10px;
@include transition;
&:hover {
opacity: 1;
}
h2 {
@include inline-block();
text-transform: uppercase;
letter-spacing: 1px;
font-size: 14px;
padding: 6px 6px 6px 0;
font-size: 12px;
margin: 0;
}
ul {
@include clearfix();
list-style: none;
margin: 0;
padding: 0;
li {
@include inline-block;
margin-right: 6px;
border-right: 1px solid #ddd;
padding-right: 6px;
&.search {
float: right;
border: 0;
}
a {
&.more {
font-size: 12px;
@include inline-block;
margin: 0 6px;
font-style: italic;
}
}
}
}
}
div.content {
display: table;
border: 1px solid lighten($dark-blue, 40%);
width: 100%;
@include border-radius(3px);
@include box-shadow(0 0 4px lighten($dark-blue, 50%));
section {
header {
background: #fff;
padding: 6px;
border-bottom: 1px solid lighten($dark-blue, 60%);
@include clearfix;
h2 {
color: $bright-blue;
// float: left;
font-size: 14px;
letter-spacing: 1px;
// line-height: 20px;
text-transform: uppercase;
margin: 0;
}
}
&.modules {
@include box-sizing(border-box);
display: table-cell;
width: flex-grid(6, 9);
border-right: 1px solid lighten($dark-blue, 40%);
&.empty {
text-align: center;
vertical-align: middle;
a {
@extend .button;
@include inline-block();
margin-top: 10px;
}
}
ol {
list-style: none;
margin: 0;
padding: 0;
li {
border-bottom: 1px solid lighten($dark-blue, 60%);
a {
color: #000;
}
ol {
list-style: none;
margin: 0;
padding: 0;
li {
padding: 6px;
position: relative;
&:last-child {
border-bottom: 0;
}
&:hover {
background-color: lighten($yellow, 10%);
a.draggable {
opacity: 1;
}
}
a.draggable {
float: right;
opacity: .4;
}
&.group {
padding: 0;
header {
padding: 6px;
background: none;
h3 {
font-size: 14px;
margin: 0;
}
}
ol {
border-left: 4px solid #999;
border-bottom: 0;
margin: 0;
padding: 0;
li {
&:last-child {
border-bottom: 0;
}
}
}
}
}
}
}
}
}
&.scratch-pad {
@include box-sizing(border-box);
display: table-cell;
width: flex-grid(3, 9) + flex-gutter(9);
vertical-align: top;
ol {
list-style: none;
margin: 0;
padding: 0;
li {
background: $light-blue;
&:last-child {
border-bottom: 0;
}
&.new-module a {
background-color: darken($light-blue, 2%);
border-bottom: 1px solid darken($light-blue, 8%);
&:hover {
background-color: lighten($yellow, 10%);
}
}
a {
color: $dark-blue;
}
ul {
list-style: none;
margin: 0;
padding: 0;
li {
padding: 6px;
border-collapse: collapse;
border-bottom: 1px solid darken($light-blue, 8%);
position: relative;
&:last-child {
border-bottom: 1px solid darken($light-blue, 8%);
}
&:hover {
background-color: lighten($yellow, 10%);
a.draggable {
opacity: 1;
}
}
&.empty {
padding: 12px;
a {
@extend .button;
display: block;
text-align: center;
}
}
a.draggable {
opacity: .3;
}
}
}
}
}
}
}
}
}

View File

@@ -1,295 +0,0 @@
.subsection .main-wrapper {
margin: 40px;
}
.subsection .inner-wrapper {
@include clearfix();
}
.subsection-body {
padding: 32px 40px;
@include clearfix;
> div {
margin-bottom: 40px;
}
input {
font-size: 14px;
}
.unit-subtitle {
display: block;
width: 100%;
}
.sortable-unit-list {
ol {
@include tree-view;
}
}
.policy-list {
input[disabled] {
border: none;
@include box-shadow(none);
}
.policy-list-name {
margin-right: 5px;
margin-bottom: 10px;
}
.policy-list-value {
width: 320px;
margin-right: 10px;
}
}
.policy-list-element {
.save-button,
.cancel-button {
display: none;
}
.edit-icon {
margin-right: 8px;
}
&.editing,
&.new-policy-list-element {
.policy-list-name,
.policy-list-value {
border: 1px solid #b0b6c2;
@include linear-gradient(top, rgba(255, 255, 255, 0), rgba(255, 255, 255, .3));
background-color: #edf1f5;
@include box-shadow(0 1px 2px rgba(0, 0, 0, .1) inset);
}
}
}
.new-policy-list-element {
padding: 10px 10px 0;
margin: 0 -10px 10px;
border-radius: 3px;
background: $mediumGrey;
.save-button {
@include blue-button;
margin-bottom: 10px;
}
.cancel-button {
@include white-button;
}
.edit-icon {
display: none;
}
.delete-icon {
display: none;
}
}
.new-policy-item {
margin: 10px 0;
.plus-icon-small {
position: relative;
top: -1px;
vertical-align: middle;
}
}
}
.subsection-name-input {
label {
display: block;
}
input {
width: 100%;
font-size: 20px;
}
}
.scheduled-date-input,
.due-date-input {
@include clearfix;
.date-input,
.time-input {
display: inline-block;
width: 100px;
}
.inherits-check {
label {
font-size: 13px;
}
}
.notice {
margin-top: 6px;
font-size: 11px;
color: #999;
}
}
.due-date-input {
label {
display: inline-block !important;
margin-right: 10px;
}
a {
font-size: 11px;
font-weight: bold;
text-transform: uppercase;
}
.date-setter {
@include clearfix;
display: none;
}
.remove-date {
display: block;
}
}
.row.visibility {
label {
display: inline-block !important;
margin-right: 10px;
line-height: 21px;
}
a {
display: inline-block;
height: 31px;
margin-right: 8px;
vertical-align: middle;
font-size: 11px;
font-weight: 700;
line-height: 31px;
text-transform: uppercase;
}
.large-toggle {
width: 41px;
background: url(../img/large-toggles.png) no-repeat;
background-position: 0 -50px;
.hidden {
background-position: 0 -5px;
}
}
}
.gradable {
label {
display: inline-block;
vertical-align: top;
}
.gradable-status {
position: relative;
top: -4px;
display: inline-block;
margin-left: 10px;
width: 65%;
.status-label {
margin: 0;
padding: 0;
background: transparent;
color: $blue;
border: none;
font-size: 11px;
font-weight: bold;
text-transform: uppercase;
}
.menu-toggle {
z-index: 100;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 20px;
background: transparent;
&:hover, &.is-active {
color: $blue;
}
}
.menu {
z-index: 1;
position: absolute;
top: -12px;
left: -7px;
display: none;
width: 100%;
margin: 0;
padding: 8px 12px;
opacity: 0.0;
background: $white;
border: 1px solid $mediumGrey;
font-size: 12px;
@include border-radius(4px);
@include box-shadow(0 1px 2px rgba(0, 0, 0, .2));
@include transition(opacity .15s);
li {
margin-bottom: 3px;
padding-bottom: 3px;
border-bottom: 1px solid $lightGrey;
&:last-child {
margin-bottom: 0;
padding-bottom: 0;
border: none;
}
}
a {
&.is-selected {
font-weight: bold;
}
}
}
// dropdown state
&.is-active {
.menu {
z-index: 10000;
display: block;
opacity: 1.0;
}
.menu-toggle {
z-index: 1000;
}
}
// set state
&.is-set {
.menu-toggle {
color: $blue;
}
.status-label {
display: block;
color: $blue;
}
}
}
}

View File

@@ -1,667 +0,0 @@
.unit .main-wrapper {
@include clearfix();
margin: 40px;
}
//Problem Selector tab menu requirements
.js .tabs .tab {
display: none;
}
//end problem selector reqs
.main-column {
clear: both;
float: left;
width: 70%;
}
.unit-body.published {
.components > li {
border: none;
.rendered-component {
padding: 0 20px;
}
}
}
.unit-body {
.breadcrumbs {
border-radius: 3px 3px 0 0;
border-bottom: 1px solid #cbd1db;
@include linear-gradient(top, rgba(255, 255, 255, .3), rgba(255, 255, 255, 0) 70%);
background-color: #edf1f5;
@include box-shadow(0 1px 0 rgba(255, 255, 255, .7) inset);
@include clearfix;
li {
float: left;
}
a,
.current-page {
display: block;
padding: 15px 35px 15px 30px;
font-size: 14px;
background: url(../img/breadcrumb-arrow.png) no-repeat right center;
}
}
h2 {
margin: 30px 40px 30px 0;
color: #646464;
font-size: 19px;
font-weight: 300;
letter-spacing: 1px;
text-transform: uppercase;
}
.components {
> li {
position: relative;
z-index: 10;
margin: 20px 40px;
.title {
margin: 0 0 15px 0;
color: $mediumGrey;
.value {
}
}
&.new-component-item {
margin: 20px 0px;
border-top: 1px solid $mediumGrey;
box-shadow: 0 2px 1px rgba(182, 182, 182, 0.75) inset;
background-color: $lightGrey;
margin-bottom: 0px;
padding-bottom: 20px;
.new-component-button {
display: block;
padding: 20px;
text-align: center;
color: #edf1f5;
}
h5 {
margin: 20px 0px;
color: #fff;
font-weight: 600;
font-size: 18px;
}
.rendered-component {
display: none;
background: #fff;
border-radius: 3px 3px 0 0;
}
.new-component-type {
a,
li {
display: inline-block;
}
a {
border: 1px solid $mediumGrey;
width: 100px;
height: 100px;
color: #fff;
margin-right: 15px;
margin-bottom: 20px;
border-radius: 8px;
font-size: 15px;
line-height: 14px;
text-align: center;
@include box-shadow(0 1px 1px rgba(0, 0, 0, .2), 0 1px 0 rgba(255, 255, 255, .4) inset);
.name {
position: absolute;
bottom: 5px;
left: 0;
width: 100%;
padding: 10px;
@include box-sizing(border-box);
color: #fff;
}
}
}
.new-component-templates {
display: none;
margin: 20px 40px 20px 40px;
border-radius: 3px;
border: 1px solid $mediumGrey;
background-color: #fff;
@include box-shadow(0 1px 1px rgba(0, 0, 0, .2), 0 1px 0 rgba(255, 255, 255, .4) inset);
@include clearfix;
.cancel-button {
margin: 20px 0px 10px 10px;
@include white-button;
}
.problem-type-tabs {
display: none;
}
// specific menu types
&.new-component-problem {
padding-bottom:10px;
.ss-icon, .editor-indicator {
display: inline-block;
}
.problem-type-tabs {
display: inline-block;
}
}
}
.new-component-type,
.new-component-template {
@include clearfix;
a {
position: relative;
border: 1px solid $darkGreen;
background: tint($green,20%);
color: #fff;
&:hover {
background: $brightGreen;
}
}
}
.problem-type-tabs {
list-style-type: none;
border-radius: 0;
width: 100%;
@include linear-gradient(top, rgba(255, 255, 255, .4), rgba(255, 255, 255, 0));
background-color: $lightBluishGrey;
@include box-shadow(0 1px 0 rgba(255, 255, 255, 0.2) inset, 0 -1px 0 rgba(0, 0, 0, 0.2) inset);
li:first-child {
margin-left: 20px;
}
li {
float:left;
display:inline-block;
text-align:center;
width: auto;
@include linear-gradient(top, rgba(255, 255, 255, .4), rgba(255, 255, 255, 0));
background-color: tint($lightBluishGrey, 10%);
@include box-shadow(0 1px 0 rgba(255, 255, 255, 0.2) inset, 0 -1px 0 rgba(0, 0, 0, 0.2) inset);
opacity:.8;
&:hover {
opacity:1;
background-color: tint($lightBluishGrey, 20%);
}
&.ui-state-active {
border: 0px;
@include active;
opacity:1;
}
}
a{
display: block;
padding: 15px 25px;
font-size: 15px;
line-height: 16px;
text-align: center;
color: #3c3c3c;
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.3);
}
}
.new-component-template {
a {
background: #fff;
border: 0px;
color: #3c3c3c;
@include transition (none);
&:hover {
background: tint($green,30%);
color: #fff;
@include transition(background-color .15s);
}
}
li {
border:none;
border-bottom: 1px dashed $lightGrey;
color: #fff;
}
li:first-child {
a {
border-top: 0px;
}
}
li:nth-child(2) {
a {
border-radius: 0px;
}
}
a {
@include clearfix();
display: block;
padding: 7px 20px;
border-bottom: none;
font-weight: 500;
.name {
float: left;
.ss-icon {
@include transition(opacity .15s);
display: inline-block;
top: 1px;
margin-right: 5px;
opacity: 0.5;
width: 17;
height: 21px;
vertical-align: middle;
}
}
.editor-indicator {
@include transition(opacity .15s);
float: right;
position: relative;
top: 3px;
font-size: 12px;
opacity: 0.3;
}
.ss-icon, .editor-indicator {
display: none;
}
&:hover {
color: #fff;
.ss-icon {
opacity: 1.0;
}
.editor-indicator {
opacity: 1.0;
}
}
}
// specific editor types
.empty {
a {
line-height: 1.4;
font-weight: 400;
background: #fff;
color: #3c3c3c;
&:hover {
background: tint($green,30%);
color: #fff;
}
}
}
}
.new-component {
text-align: center;
h5 {
color: $darkGreen;
}
}
}
}
}
.component {
border: 1px solid $lightBluishGrey2;
border-radius: 3px;
background: #fff;
@include transition(none);
&:hover {
border-color: #6696d7;
.drag-handle {
background-color: $blue;
border-color: $blue;
}
}
&.editing {
border: 1px solid $lightBluishGrey2;
z-index: auto;
.drag-handle,
.component-actions {
display: none;
}
}
&.component-placeholder {
border-color: #6696d7;
}
.component-actions {
position: absolute;
top: 7px;
right: 9px;
}
.drag-handle {
position: absolute;
display: block;
top: -1px;
right: -16px;
z-index: 10;
width: 15px;
height: 100%;
border-radius: 0 3px 3px 0;
border: 1px solid $lightBluishGrey2;
background: url(../img/white-drag-handles.png) center no-repeat $lightBluishGrey2;
cursor: move;
@include transition(none);
}
}
.xmodule_display {
padding: 40px 20px 20px;
overflow-x: auto;
h1 {
float: none;
margin-left: 0;
}
}
.wrapper-component-editor {
z-index: 9999;
position: relative;
background: $lightBluishGrey2;
}
.component-editor {
@include edit-box;
@include box-shadow(none);
display: none;
padding: 20px;
border-radius: 2px 2px 0 0;
.metadata_edit {
margin-bottom: 20px;
font-size: 13px;
li {
margin-bottom: 10px;
}
label {
display: inline-block;
margin-right: 10px;
}
}
h3 {
margin-bottom: 10px;
font-size: 18px;
font-weight: 700;
}
h5 {
margin-bottom: 8px;
color: #fff;
font-weight: 700;
}
.save-button {
margin-top: 10px;
margin: 15px 8px 0 0;
}
}
}
.unit-settings {
.window-contents {
padding: 10px 20px;
}
.unit-actions {
border-bottom: none;
padding-bottom: 0;
}
.published-alert {
display: none;
padding: 10px;
border: 1px solid #edbd3c;
border-radius: 3px;
background: #fbf6e1;
font-size: 14px;
line-height: 1.4;
div {
margin-top: 15px;
}
}
input[type="radio"] {
margin-right: 7px;
}
.status {
font-size: 12px;
strong {
font-weight: 700;
}
}
.preview-button, .view-button {
@include white-button;
margin-bottom: 10px;
}
.publish-button {
@include orange-button;
}
.delete-button {
@include blue-button;
}
.delete-draft {
display: inline-block;
}
.delete-button,
.preview-button,
.publish-button,
.view-button {
font-size: 11px;
margin-top: 10px;
padding: 6px 15px 8px;
}
}
.unit-history {
&.collapsed {
h4 {
border-bottom: none;
border-radius: 3px;
}
.window-contents {
display: none;
}
}
ol {
border: 1px solid #ced2db;
li {
display: block;
padding: 6px 8px 8px 10px;
background: #edf1f5;
font-size: 12px;
&:hover {
background: #fffcf1;
.item-actions {
display: block;
}
}
&.checked {
background: #d1dae3;
}
.item-actions {
display: none;
}
input[type="radio"] {
margin-right: 7px;
}
}
}
}
.unit-location {
.url {
width: 100%;
margin-bottom: 10px;
@include box-shadow(none);
}
.draft-tag,
.hidden-tag,
.private-tag,
.has-new-draft-tag {
font-size: 8px;
}
.window-contents > ol {
@include tree-view;
.section-item {
display: inline-block;
width: 100%;
font-size: 11px;
padding: 2px 8px 4px;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
@include box-sizing(border-box);
}
ol {
.section-item {
padding-left: 20px;
}
.new-unit-item {
margin-left: 20px;
}
}
ol ol {
.section-item {
padding-left: 34px;
}
.new-unit-item {
margin: 0 0 10px 41px;
}
}
}
}
.edit-state-draft {
.visibility,
.edit-draft-message,
.view-button {
display: none;
}
.published-alert {
display: block;
}
}
.edit-state-public {
.delete-draft,
.component-actions,
.new-component-item,
.editing-draft-alert,
.publish-draft-message,
.preview-button {
display: none;
}
.published-alert {
display: block;
}
.drag-handle {
display: none !important;
}
}
.edit-state-private {
.delete-draft,
.publish-draft,
.editing-draft-alert,
.create-draft,
.view-button {
display: none;
}
}
// editing units from courseware
body.unit {
.component {
padding-top: 30px;
.component-actions {
@include box-sizing(border-box);
position: absolute;
width: 100%;
padding: 15px;
top: 0;
left: 0;
border-bottom: 1px solid $lightBluishGrey2;
background: $lightGrey;
}
&.editing {
padding-top: 0;
}
}
}

View File

@@ -1,3 +1,6 @@
// studio - utilities - variables
// ====================
$baseline: 20px;
// grid
@@ -12,11 +15,18 @@ $fg-min-width: 900px;
// type
$sans-serif: 'Open Sans', $verdana;
$body-line-height: golden-ratio(.875em, 1);
$error-red: rgb(253, 87, 87);
// colors - new for re-org
$black: rgb(0,0,0);
$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);
$white-t2: rgba(255,255,255,0.50);
$white-t3: rgba(255,255,255,0.75);
$gray: rgb(127,127,127);
$gray-l1: tint($gray,20%);
@@ -24,6 +34,7 @@ $gray-l2: tint($gray,40%);
$gray-l3: tint($gray,60%);
$gray-l4: tint($gray,80%);
$gray-l5: tint($gray,90%);
$gray-l6: tint($gray,95%);
$gray-d1: shade($gray,20%);
$gray-d2: shade($gray,40%);
$gray-d3: shade($gray,60%);
@@ -39,6 +50,12 @@ $blue-d1: shade($blue,20%);
$blue-d2: shade($blue,40%);
$blue-d3: shade($blue,60%);
$blue-d4: shade($blue,80%);
$blue-s1: saturate($blue,15%);
$blue-s2: saturate($blue,30%);
$blue-s3: saturate($blue,45%);
$blue-u1: desaturate($blue,15%);
$blue-u2: desaturate($blue,30%);
$blue-u3: desaturate($blue,45%);
$pink: rgb(183, 37, 103);
$pink-l1: tint($pink,20%);
@@ -50,6 +67,29 @@ $pink-d1: shade($pink,20%);
$pink-d2: shade($pink,40%);
$pink-d3: shade($pink,60%);
$pink-d4: shade($pink,80%);
$pink-s1: saturate($pink,15%);
$pink-s2: saturate($pink,30%);
$pink-s3: saturate($pink,45%);
$pink-u1: desaturate($pink,15%);
$pink-u2: desaturate($pink,30%);
$pink-u3: desaturate($pink,45%);
$red: rgb(178, 6, 16);
$red-l1: tint($red,20%);
$red-l2: tint($red,40%);
$red-l3: tint($red,60%);
$red-l4: tint($red,80%);
$red-l5: tint($red,90%);
$red-d1: shade($red,20%);
$red-d2: shade($red,40%);
$red-d3: shade($red,60%);
$red-d4: shade($red,80%);
$red-s1: saturate($red,15%);
$red-s2: saturate($red,30%);
$red-s3: saturate($red,45%);
$red-u1: desaturate($red,15%);
$red-u2: desaturate($red,30%);
$red-u3: desaturate($red,45%);
$green: rgb(37, 184, 90);
$green-l1: tint($green,20%);
@@ -61,6 +101,12 @@ $green-d1: shade($green,20%);
$green-d2: shade($green,40%);
$green-d3: shade($green,60%);
$green-d4: shade($green,80%);
$green-s1: saturate($green,15%);
$green-s2: saturate($green,30%);
$green-s3: saturate($green,45%);
$green-u1: desaturate($green,15%);
$green-u2: desaturate($green,30%);
$green-u3: desaturate($green,45%);
$yellow: rgb(231, 214, 143);
$yellow-l1: tint($yellow,20%);
@@ -72,6 +118,29 @@ $yellow-d1: shade($yellow,20%);
$yellow-d2: shade($yellow,40%);
$yellow-d3: shade($yellow,60%);
$yellow-d4: shade($yellow,80%);
$yellow-s1: saturate($yellow,15%);
$yellow-s2: saturate($yellow,30%);
$yellow-s3: saturate($yellow,45%);
$yellow-u1: desaturate($yellow,15%);
$yellow-u2: desaturate($yellow,30%);
$yellow-u3: desaturate($yellow,45%);
$orange: rgb(237, 189, 60);
$orange-l1: tint($orange,20%);
$orange-l2: tint($orange,40%);
$orange-l3: tint($orange,60%);
$orange-l4: tint($orange,80%);
$orange-l5: tint($orange,90%);
$orange-d1: shade($orange,20%);
$orange-d2: shade($orange,40%);
$orange-d3: shade($orange,60%);
$orange-d4: shade($orange,80%);
$orange-s1: saturate($orange,15%);
$orange-s2: saturate($orange,30%);
$orange-s3: saturate($orange,45%);
$orange-u1: desaturate($orange,15%);
$orange-u2: desaturate($orange,30%);
$orange-u3: desaturate($orange,45%);
$shadow: rgba(0,0,0,0.2);
$shadow-l1: rgba(0,0,0,0.1);
@@ -80,8 +149,6 @@ $shadow-d1: rgba(0,0,0,0.4);
// colors - inherited
$baseFontColor: #3c3c3c;
$offBlack: #3c3c3c;
$orange: #edbd3c;
$red: #b20610;
$green: #108614;
$lightGrey: #edf1f5;
$mediumGrey: #b0b6c2;
@@ -94,4 +161,5 @@ $brightGreen: rgb(22, 202, 87);
$disabledGreen: rgb(124, 206, 153);
$darkGreen: rgb(52, 133, 76);
$lightBluishGrey: rgb(197, 207, 223);
$lightBluishGrey2: rgb(213, 220, 228);
$lightBluishGrey2: rgb(213, 220, 228);
$error-red: rgb(253, 87, 87);

View File

@@ -1,33 +0,0 @@
section.video-new, section.video-edit {
> section {
section.upload {
padding: 6px;
margin-bottom: 10px;
border: 1px solid #ddd;
a.upload-button {
@extend .button;
@include inline-block();
}
}
section.in-use {
h2 {
font-size: 14px;
}
div {
background: #eee;
text-align: center;
padding: 6px;
}
}
a.save-update {
@extend .button;
@include inline-block();
margin-top: 20px;
}
}
}

View File

@@ -1,256 +0,0 @@
section.week-edit,
section.week-new,
section.sequence-edit {
> header {
border-bottom: 2px solid #333;
@include clearfix();
div {
@include clearfix();
padding: 6px 20px;
h1 {
font-size: 18px;
text-transform: uppercase;
letter-spacing: 1px;
float: left;
}
p {
float: right;
}
&.week {
background: #eee;
font-size: 12px;
border-bottom: 1px solid #ccc;
h2 {
font-size: 12px;
@include inline-block();
margin-right: 20px;
}
ul {
list-style: none;
@include inline-block();
li {
@include inline-block();
margin-right: 10px;
p {
float: none;
}
}
}
}
}
section.goals {
background: #eee;
padding: 6px 20px;
border-top: 1px solid #ccc;
ul {
list-style: none;
color: #999;
li {
margin-bottom: 6px;
&:last-child {
margin-bottom: 0;
}
}
}
}
}
> section.content {
@include box-sizing(border-box);
padding: 20px;
section.filters {
@include clearfix;
margin-bottom: 10px;
background: #efefef;
border: 1px solid #ddd;
ul {
@include clearfix();
list-style: none;
padding: 6px;
li {
@include inline-block();
&.advanced {
float: right;
}
}
}
}
> div {
display: table;
border: 1px solid;
width: 100%;
section {
header {
background: #eee;
padding: 6px;
border-bottom: 1px solid #ccc;
@include clearfix;
h2 {
text-transform: uppercase;
letter-spacing: 1px;
font-size: 12px;
float: left;
}
}
&.modules {
@include box-sizing(border-box);
display: table-cell;
width: flex-grid(6, 9);
border-right: 1px solid #333;
&.empty {
text-align: center;
vertical-align: middle;
a {
@extend .button;
@include inline-block();
margin-top: 10px;
}
}
ol {
list-style: none;
border-bottom: 1px solid #333;
li {
border-bottom: 1px solid #333;
&:last-child{
border-bottom: 0;
}
a {
color: #000;
}
ol {
list-style: none;
li {
padding: 6px;
&:hover {
a.draggable {
opacity: 1;
}
}
a.draggable {
float: right;
opacity: .5;
}
&.group {
padding: 0;
header {
padding: 6px;
background: none;
h3 {
font-size: 14px;
}
}
ol {
border-left: 4px solid #999;
border-bottom: 0;
li {
&:last-child {
border-bottom: 0;
}
}
}
}
}
}
}
}
}
&.scratch-pad {
@include box-sizing(border-box);
display: table-cell;
width: flex-grid(3, 9) + flex-gutter(9);
vertical-align: top;
ol {
list-style: none;
border-bottom: 1px solid #999;
li {
border-bottom: 1px solid #999;
background: #f9f9f9;
&:last-child {
border-bottom: 0;
}
ul {
list-style: none;
li {
padding: 6px;
&:last-child {
border-bottom: 0;
}
&:hover {
a.draggable {
opacity: 1;
}
}
&.empty {
padding: 12px;
a {
@extend .button;
display: block;
text-align: center;
}
}
a.draggable {
float: right;
opacity: .3;
}
a {
color: #000;
}
}
}
}
}
}
}
}
}
}

View File

@@ -1,39 +1,52 @@
// studio - css architecture
// ====================
// bourbon libs and resets
@import 'bourbon/bourbon';
@import 'bourbon/addons/button';
@import 'vendor/normalize';
@import 'keyframes';
@import 'reset';
// utilities
@import 'variables';
@import 'mixins';
@import 'cms_mixins';
@import "fonts";
@import "variables";
@import "cms_mixins";
@import "extends";
@import "base";
@import "header";
@import "footer";
@import "dashboard";
@import "courseware";
@import "subsection";
@import "unit";
@import "assets";
@import "static-pages";
@import "users";
@import "import";
@import "export";
@import "settings";
@import "course-info";
@import "landing";
@import "graphics";
@import "modal";
@import "alerts";
@import "login";
@import "account";
@import "index";
@import 'jquery-ui-calendar';
// assets
@import 'assets/fonts';
@import 'assets/graphics';
@import 'assets/keyframes';
@import 'content-types';
// base
@import 'base';
// elements
@import 'elements/header';
@import 'elements/footer';
@import 'elements/navigation';
@import 'elements/forms';
@import 'elements/modal';
@import 'elements/alerts';
@import 'elements/jquery-ui-calendar';
// specific views
@import 'views/account';
@import 'views/assets';
@import 'views/updates';
@import 'views/dashboard';
@import 'views/export';
@import 'views/index';
@import 'views/import';
@import 'views/outline';
@import 'views/settings';
@import 'views/static-pages';
@import 'views/subsection';
@import 'views/unit';
@import 'views/users';
@import 'views/checklists';
@import 'assets/content-types';
// xblock-related
@import 'module/module-styles.scss';
@import 'descriptor/module-styles.scss';

View File

@@ -1,3 +1,6 @@
// studio - elements - alerts, notifications, prompts
// ====================
// notifications
.wrapper-notification {
@include clearfix();

View File

@@ -1,4 +1,6 @@
//studio global footer
// studio - elements - global footer
// ====================
.wrapper-footer {
margin: ($baseline*1.5) 0 $baseline 0;
padding: $baseline;

View File

@@ -0,0 +1,76 @@
// studio - elements - forms
// ====================
// forms - general
input[type="text"],
input[type="email"],
input[type="password"],
textarea.text {
padding: 6px 8px 8px;
@include box-sizing(border-box);
border: 1px solid $mediumGrey;
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);
font-family: 'Open Sans', sans-serif;
font-size: 11px;
color: $baseFontColor;
outline: 0;
&::-webkit-input-placeholder,
&:-moz-placeholder,
&:-ms-input-placeholder {
color: #979faf;
}
&:focus {
@include linear-gradient($paleYellow, tint($paleYellow, 90%));
outline: 0;
}
}
// forms - specific
input.search {
padding: 6px 15px 8px 30px;
@include box-sizing(border-box);
border: 1px solid $darkGrey;
border-radius: 20px;
background: url(../img/search-icon.png) no-repeat 8px 7px #edf1f5;
font-family: 'Open Sans', sans-serif;
color: $baseFontColor;
outline: 0;
&::-webkit-input-placeholder {
color: #979faf;
}
}
label {
font-size: 12px;
}
code {
padding: 0 4px;
border-radius: 3px;
background: #eee;
font-family: Monaco, monospace;
}
.CodeMirror {
font-size: 13px;
border: 1px solid $darkGrey;
background: #fff;
}
.text-editor {
width: 100%;
min-height: 80px;
padding: 10px;
@include box-sizing(border-box);
border: 1px solid $mediumGrey;
@include linear-gradient(top, rgba(255, 255, 255, 0), rgba(255, 255, 255, 0.3));
background-color: #edf1f5;
@include box-shadow(0 1px 2px rgba(0, 0, 0, 0.1) inset);
font-family: Monaco, monospace;
}

View File

@@ -1,4 +1,4 @@
// studio global header and navigation
// studio - elements - global header
// ====================
.wrapper-header {

View File

@@ -1,3 +1,6 @@
// studio - elements - JQUI calendar
// ====================
.ui-datepicker {
border-color: $darkGrey;
border-radius: 2px;

View File

@@ -1,3 +1,6 @@
// studio - elements - modal windows
// ====================
.modal-cover {
display: none;
position: fixed;

View File

@@ -0,0 +1,24 @@
// studio - elements - navigation
// ====================
// common
// ====================
// primary
// ====================
// right hand side
// ====================
// tabs
// ====================
// dropdown
// ====================
//

View File

@@ -1,5 +1,6 @@
// Studio - Sign In/Up
// studio - views - sign up/in
// ====================
body.signup, body.signin {
.wrapper-content {

View File

@@ -1,4 +1,8 @@
.uploads {
// studio - views - assets
// ====================
body.course.uploads {
input.asset-search-input {
float: left;
width: 260px;

View File

@@ -0,0 +1,347 @@
// Studio - Course Settings
// ====================
body.course.checklists {
.content-primary, .content-supplementary {
@include box-sizing(border-box);
float: left;
}
.content-primary {
width: flex-grid(9, 12);
margin-right: flex-gutter();
}
// checklists - general
.course-checklist {
@extend .window;
margin: 0 0 ($baseline*2) 0;
&:last-child {
margin-bottom: 0;
}
// visual status
.viz-checklist-status {
@include text-hide();
@include size(100%,($baseline/4));
position: relative;
display: block;
margin: 0;
background: $gray-l4;
.viz-checklist-status-value {
@include transition(width 2s ease-in-out .25s);
position: absolute;
top: 0;
left: 0;
width: 0%;
height: ($baseline/4);
background: $green;
.int {
@include text-sr();
}
}
}
// <span class="viz viz-checklist-status"><span class="viz value viz-checklist-status-value"><span class="int">0</span>% of checklist completed</span></span>
// header/title
header {
@include clearfix();
@include box-shadow(inset 0 -1px 1px $shadow-l1);
margin-bottom: 0;
border-bottom: 1px solid $gray-l3;
padding: $baseline ($baseline*1.5);
.checklist-title {
@include transition(color .15s .25s ease-in-out);
width: flex-grid(6, 9);
margin: 0 flex-gutter() 0 0;
float: left;
.ui-toggle-expansion {
@include transition(rotate .15s ease-in-out .25s);
@include font-size(14);
display: inline-block;
vertical-align: middle;
margin-right: ($baseline/2);
color: $gray-l4;
}
&.is-selectable {
cursor: pointer;
&:hover {
color: $blue;
.ui-toggle-expansion {
color: $blue;
}
}
}
}
.checklist-status {
@include font-size(13);
width: flex-grid(3, 9);
float: right;
margin-top: ($baseline/2);
text-align: right;
color: $gray-l2;
.icon-confirm {
@include font-size(20);
display: inline-block;
vertical-align: middle;
margin-left: ($baseline/2);
color: $gray-l4;
}
.status-count {
@include font-size(16);
margin-left: ($baseline/4);
margin-right: ($baseline/4);
color: $gray-d3;
font-weight: 600;
}
.status-amount {
@include font-size(16);
margin-left: ($baseline/4);
color: $gray-d3;
font-weight: 600;
}
}
}
// checklist actions
.course-checklist-actions {
@include clearfix();
@include box-shadow(inset 0 1px 1px $shadow-l1);
@include transition(border .15s ease-in-out .25s);
border-top: 1px solid $gray-l2;
padding: $baseline ($baseline*1.5);
background: $gray-l4;
.action-primary {
@include green-button();
float: left;
.icon-add {
@include font-size(12);
display: inline-block;
vertical-align: middle;
margin-right: ($baseline/4);
}
}
.action-secondary {
@include font-size(14);
@include grey-button();
font-weight: 400;
float: right;
.icon-delete {
@include font-size(12);
display: inline-block;
vertical-align: middle;
margin-right: ($baseline/4);
}
}
}
// state - collapsed
&.is-collapsed {
header {
@include box-shadow(none);
.checklist-title {
.ui-toggle-expansion {
@include transform(rotate(-90deg));
@include transform-origin(50% 50%);
}
}
}
.list-tasks {
height: 0;
}
}
// state - completed
&.is-completed {
.viz-checklist-status {
.viz-checklist-status-value {
width: 100%;
}
}
header {
.checklist-title, .icon-confirm {
color: $green;
}
.checklist-status {
.status-count, .status-amount, .icon-confirm {
color: $green;
}
}
}
}
// state - not available
.is-not-available {
}
}
// list of tasks
.list-tasks {
height: auto;
overflow: hidden;
.task {
@include transition(background .15s ease-in-out .25s);
@include transition(border .15s ease-in-out .25s);
@include clearfix();
position: relative;
border-top: 1px solid $white;
border-bottom: 1px solid $gray-l5;
padding: $baseline ($baseline*1.5);
background: $white;
opacity: 1.0;
&:last-child {
margin-bottom: 0;
border-bottom: none;
}
.task-input {
display: inline-block;
vertical-align: text-top;
float: left;
margin: ($baseline/2) flex-gutter() 0 0;
}
.task-details {
display: inline-block;
vertical-align: text-top;
float: left;
width: flex-grid(6,9);
font-weight: 500;
.task-name {
@include transition(color .15s .25s ease-in-out);
vertical-align: baseline;
cursor: pointer;
margin-bottom: 0;
}
.task-description {
@include transition(color .15s .25s ease-in-out);
@include font-size(14);
color: $gray-l2;
}
.task-support {
@include transition(opacity .15s .25s ease-in-out);
@include font-size(12);
opacity: 0;
pointer-events: none;
}
}
.task-actions {
@include transition(opacity .15s .25s ease-in-out);
@include clearfix();
display: inline-block;
vertical-align: middle;
float: right;
width: flex-grid(2,9);
margin: ($baseline/2) 0 0 flex-gutter();
opacity: 0;
pointer-events: none;
text-align: right;
.action-primary {
@include blue-button;
@include transition(all .15s);
@include font-size(12);
font-weight: 600;
text-align: center;
}
.action-secondary {
@include font-size(13);
margin-top: ($baseline/2);
}
}
// state - hover
&:hover {
background: $blue-l5;
border-bottom-color: $blue-l4;
border-top-color: $blue-l4;
opacity: 1.0;
.task-details {
.task-support {
opacity: 1.0;
pointer-events: auto;
}
}
.task-actions {
opacity: 1.0;
pointer-events: auto;
}
}
// state - completed
&.is-completed {
background: $gray-l6;
border-top-color: $gray-l5;
border-bottom-color: $gray-l5;
.task-name {
color: $gray-l2;
}
.task-actions {
.action-primary {
@include grey-button;
@include font-size(12);
font-weight: 600;
text-align: center;
}
}
&:hover {
background: $gray-l5;
border-bottom-color: $gray-l4;
border-top-color: $gray-l4;
.task-details {
opacity:1.0;
}
}
}
}
}
.content-supplementary {
width: flex-grid(3, 12);
}
}

View File

@@ -0,0 +1,124 @@
// studio - views - user dashboard
// ====================
body.dashboard {
.my-classes {
margin-top: $baseline;
}
.class-list {
margin-top: 20px;
border-radius: 3px;
border: 1px solid $darkGrey;
background: #fff;
@include box-shadow(0 1px 2px rgba(0, 0, 0, .1));
li {
position: relative;
border-bottom: 1px solid $mediumGrey;
&:last-child {
border-bottom: none;
}
.class-link {
z-index: 100;
display: block;
padding: 20px 25px;
line-height: 1.3;
&:hover {
background: $paleYellow;
+ .view-live-button {
opacity: 1.0;
pointer-events: auto;
}
}
}
}
.class-name {
display: block;
font-size: 19px;
font-weight: 300;
}
.detail {
font-size: 14px;
font-weight: 400;
margin-right: 20px;
color: #3c3c3c;
}
// view live button
.view-live-button {
z-index: 10000;
position: absolute;
top: 15px;
right: $baseline;
padding: ($baseline/4) ($baseline/2);
opacity: 0;
pointer-events: none;
&:hover {
opacity: 1.0;
pointer-events: auto;
}
}
}
.new-course {
padding: 15px 25px;
margin-top: 20px;
border-radius: 3px;
border: 1px solid $darkGrey;
background: #fff;
box-shadow: 0 1px 2px rgba(0, 0, 0, .1);
@include clearfix;
.row {
margin-bottom: 15px;
@include clearfix;
}
.column {
float: left;
width: 48%;
}
.column:first-child {
margin-right: 4%;
}
.course-info {
width: 600px;
}
label {
display: block;
font-size: 13px;
font-weight: 700;
}
.new-course-org,
.new-course-number,
.new-course-name {
width: 100%;
}
.new-course-name {
font-size: 19px;
font-weight: 300;
}
.new-course-save {
@include blue-button;
}
.new-course-cancel {
@include white-button;
}
}
}

View File

@@ -1,4 +1,8 @@
.export {
// studio - views - course export
// ====================
body.course.export {
.export-overview {
@extend .window;
@include clearfix;
@@ -118,6 +122,4 @@
}
}
}
}

View File

@@ -1,4 +1,8 @@
.import {
// studio - views - course import
// ====================
body.course.import {
.import-overview {
@extend .window;
@include clearfix;

View File

@@ -1,5 +1,7 @@
// how it works/not signed in index
.index {
// studio - views - how it works
// ====================
body.index {
&.not-signedin {

View File

@@ -0,0 +1,680 @@
// studio - views - course outline
// ====================
body.course.outline {
input.courseware-unit-search-input {
float: left;
width: 260px;
background-color: #fff;
}
.branch {
.section-item {
@include clearfix();
.details {
display: block;
float: left;
margin-bottom: 0;
width: 650px;
}
.gradable-status {
float: right;
position: relative;
top: -4px;
right: 50px;
width: 145px;
.status-label {
position: absolute;
top: 2px;
right: -5px;
display: none;
width: 110px;
padding: 5px 40px 5px 10px;
@include border-radius(3px);
color: $lightGrey;
text-align: right;
font-size: 12px;
font-weight: bold;
line-height: 16px;
}
.menu-toggle {
z-index: 10;
position: absolute;
top: 0;
right: 5px;
padding: 5px;
color: $mediumGrey;
&:hover, &.is-active {
color: $blue;
}
}
.menu {
z-index: 1;
display: none;
opacity: 0.0;
position: absolute;
top: -1px;
left: 5px;
margin: 0;
padding: 8px 12px;
background: $white;
border: 1px solid $mediumGrey;
font-size: 12px;
@include border-radius(4px);
@include box-shadow(0 1px 2px rgba(0, 0, 0, .2));
@include transition(opacity .15s);
li {
width: 115px;
margin-bottom: 3px;
padding-bottom: 3px;
border-bottom: 1px solid $lightGrey;
&:last-child {
margin-bottom: 0;
padding-bottom: 0;
border: none;
a {
color: $darkGrey;
}
}
}
a {
color: $blue;
&.is-selected {
font-weight: bold;
}
}
}
// dropdown state
&.is-active {
.menu {
z-index: 1000;
display: block;
opacity: 1.0;
}
.menu-toggle {
z-index: 10000;
}
}
// set state
&.is-set {
.menu-toggle {
color: $blue;
}
.status-label {
display: block;
color: $blue;
}
}
}
}
}
.courseware-section {
position: relative;
background: #fff;
border-radius: 3px;
border: 1px solid $mediumGrey;
margin-top: 15px;
padding-bottom: 12px;
@include box-shadow(0 1px 1px rgba(0, 0, 0, 0.1));
&:first-child {
margin-top: 0;
}
&.collapsed {
padding-bottom: 0;
}
label {
float: left;
line-height: 29px;
}
.datepair {
float: left;
margin-left: 10px;
}
.section-published-date {
position: absolute;
top: 19px;
right: 90px;
padding: 4px 10px;
border-radius: 3px;
background: $lightGrey;
text-align: right;
.published-status {
font-size: 12px;
margin-right: 15px;
strong {
font-weight: bold;
}
}
.schedule-button {
@include blue-button;
}
.edit-button {
@include blue-button;
}
.schedule-button,
.edit-button {
font-size: 11px;
padding: 3px 15px 5px;
}
}
.datepair .date,
.datepair .time {
padding-left: 0;
padding-right: 0;
border: none;
background: none;
@include box-shadow(none);
font-size: 13px;
font-weight: bold;
color: $blue;
cursor: pointer;
}
.datepair .date {
width: 80px;
}
.datepair .time {
width: 65px;
}
&.collapsed .subsection-list,
.collapsed .subsection-list,
.collapsed > ol {
display: none !important;
}
header {
min-height: 75px;
@include clearfix();
.item-details, .section-published-date {
}
.item-details {
display: inline-block;
padding: 20px 0 10px 0;
@include clearfix();
.section-name {
float: left;
margin-right: 10px;
width: 350px;
font-size: 19px;
font-weight: bold;
color: $blue;
}
.section-name-span {
cursor: pointer;
@include transition(color .15s);
&:hover {
color: $orange;
}
}
.section-name-edit {
position: relative;
width: 400px;
background: $white;
input {
font-size: 16px;
}
.save-button {
@include blue-button;
padding: 7px 20px 7px;
margin-right: 5px;
}
.cancel-button {
@include white-button;
padding: 7px 20px 7px;
}
}
.section-published-date {
float: right;
width: 265px;
margin-right: 220px;
@include border-radius(3px);
background: $lightGrey;
.published-status {
font-size: 12px;
margin-right: 15px;
strong {
font-weight: bold;
}
}
.schedule-button {
@include blue-button;
}
.edit-button {
@include blue-button;
}
.schedule-button,
.edit-button {
font-size: 11px;
padding: 3px 15px 5px;
}
}
.gradable-status {
position: absolute;
top: 20px;
right: 70px;
width: 145px;
.status-label {
position: absolute;
top: 0;
right: 2px;
display: none;
width: 100px;
padding: 10px 35px 10px 10px;
@include border-radius(3px);
background: $lightGrey;
color: $lightGrey;
text-align: right;
font-size: 12px;
font-weight: bold;
line-height: 16px;
}
.menu-toggle {
z-index: 10;
position: absolute;
top: 2px;
right: 5px;
padding: 5px;
color: $lightGrey;
&:hover, &.is-active {
color: $blue;
}
}
.menu {
z-index: 1;
display: none;
opacity: 0.0;
position: absolute;
top: -1px;
left: 2px;
margin: 0;
padding: 8px 12px;
background: $white;
border: 1px solid $mediumGrey;
font-size: 12px;
@include border-radius(4px);
@include box-shadow(0 1px 2px rgba(0, 0, 0, .2));
@include transition(opacity .15s);
@include transition(display .15s);
li {
width: 115px;
margin-bottom: 3px;
padding-bottom: 3px;
border-bottom: 1px solid $lightGrey;
&:last-child {
margin-bottom: 0;
padding-bottom: 0;
border: none;
a {
color: $darkGrey;
}
}
}
a {
&.is-selected {
font-weight: bold;
}
}
}
// dropdown state
&.is-active {
.menu {
z-index: 1000;
display: block;
opacity: 1.0;
}
.menu-toggle {
z-index: 10000;
}
}
// set state
&.is-set {
.menu-toggle {
color: $blue;
}
.status-label {
display: block;
color: $blue;
}
}
float: left;
padding: 21px 0 0;
}
}
.item-actions {
margin-top: 21px;
margin-right: 12px;
.edit-button,
.delete-button {
margin-top: -3px;
}
}
.expand-collapse-icon {
float: left;
margin: 29px 6px 16px 16px;
@include transition(none);
&.expand {
background-position: 0 0;
}
&.collapsed {
}
}
.drag-handle {
margin-left: 11px;
}
}
h3 {
font-size: 19px;
font-weight: 700;
color: $blue;
}
.section-name-span {
cursor: pointer;
@include transition(color .15s);
&:hover {
color: $orange;
}
}
.section-name-form {
margin-bottom: 15px;
}
.section-name-edit {
input {
font-size: 16px;
}
.save-button {
@include blue-button;
padding: 7px 20px 7px;
margin-right: 5px;
}
.cancel-button {
@include white-button;
padding: 7px 20px 7px;
}
}
h4 {
font-size: 12px;
color: #878e9d;
strong {
font-weight: bold;
}
}
.list-header {
@include linear-gradient(top, transparent, rgba(0, 0, 0, .1));
background-color: #ced2db;
border-radius: 3px 3px 0 0;
}
.subsection-list {
margin: 0 12px;
> ol {
@include tree-view;
border-top-width: 0;
}
}
&.new-section {
header {
height: auto;
@include clearfix();
}
.expand-collapse-icon {
visibility: hidden;
}
.item-details {
padding: 25px 0 0 0;
.section-name {
float: none;
width: 100%;
}
}
}
}
.toggle-button-sections {
display: none;
position: relative;
float: right;
margin-top: 10px;
font-size: 13px;
color: $darkGrey;
&.is-shown {
display: block;
}
.ss-icon {
@include border-radius(20px);
position: relative;
top: -1px;
display: inline-block;
margin-right: 2px;
line-height: 5px;
font-size: 11px;
}
.label {
display: inline-block;
}
}
.new-section-name,
.new-subsection-name-input {
width: 515px;
}
.new-section-name-save,
.new-subsection-name-save {
@include blue-button;
padding: 4px 20px 7px;
margin: 0 5px;
color: #fff !important;
}
.new-section-name-cancel,
.new-subsection-name-cancel {
@include white-button;
padding: 4px 20px 7px;
color: #8891a1 !important;
}
.dummy-calendar {
display: none;
position: absolute;
top: 55px;
left: 110px;
z-index: 9999;
border: 1px solid #3C3C3C;
@include box-shadow(0 1px 15px rgba(0, 0, 0, .2));
}
.preview {
background: url(../img/preview.jpg) center top no-repeat;
}
.edit-subsection-publish-settings {
display: none;
position: fixed;
top: 100px;
left: 50%;
z-index: 99999;
width: 600px;
margin-left: -300px;
background: #fff;
text-align: center;
.settings {
padding: 40px;
}
h3 {
font-size: 34px;
font-weight: 300;
}
.picker {
margin: 30px 0 65px;
}
.description {
margin-top: 30px;
font-size: 14px;
line-height: 20px;
}
strong {
font-weight: 700;
}
.start-date,
.start-time {
font-size: 19px;
}
.save-button {
@include blue-button;
margin-right: 10px;
}
.cancel-button {
@include white-button;
}
.save-button,
.cancel-button {
font-size: 16px;
}
}
.collapse-all-button {
float: right;
margin-top: 10px;
font-size: 13px;
color: $darkGrey;
}
// sort/drag and drop
.ui-droppable {
@include transition (padding 0.5s ease-in-out 0s);
min-height: 20px;
padding: 0;
&.dropover {
padding: 15px 0;
}
}
.ui-draggable-dragging {
@include box-shadow(0 1px 2px rgba(0, 0, 0, .3));
border: 1px solid $darkGrey;
opacity : 0.2;
&:hover {
opacity : 1.0;
.section-item {
background: $yellow !important;
}
}
// hiding unit button - temporary fix until this semantically corrected
.new-unit-item {
display: none;
}
}
ol.ui-droppable .branch:first-child .section-item {
border-top: none;
}
}

View File

@@ -1,5 +1,6 @@
// Studio - Course Settings
// studio - views - course settings
// ====================
body.course.settings {
.content-primary, .content-supplementary {
@@ -239,13 +240,9 @@ body.course.settings {
// not editable fields
.field.is-not-editable {
label, .label {
color: $gray-l3;
}
input {
opacity: 0.25;
& label.is-focused {
color: $gray-d2;
}
}

View File

@@ -1,4 +1,8 @@
.static-pages {
// studio - views - course static pages
// ====================
body.course.static-pages {
.new-static-page-button {
@include grey-button;
display: block;
@@ -16,6 +20,51 @@
margin: 0 0 5px 0;
}
}
.wrapper-component-editor {
z-index: 9999;
position: relative;
background: $lightBluishGrey2;
}
.component-editor {
@include edit-box;
@include box-shadow(none);
display: none;
padding: 20px;
border-radius: 2px 2px 0 0;
.metadata_edit {
margin-bottom: 20px;
font-size: 13px;
li {
margin-bottom: 10px;
}
label {
display: inline-block;
margin-right: 10px;
}
}
h3 {
margin-bottom: 10px;
font-size: 18px;
font-weight: 700;
}
h5 {
margin-bottom: 8px;
color: #fff;
font-weight: 700;
}
.save-button {
margin-top: 10px;
margin: 15px 8px 0 0;
}
}
}
.component-editor {
@@ -35,6 +84,7 @@
}
.component {
position: relative;
border: 1px solid $mediumGrey;
border-top: none;
@@ -56,10 +106,13 @@
}
.drag-handle {
position: absolute;
display: block;
top: 0;
right: 0;
z-index: 11;
width: 35px;
height: 100%;
border: none;
background: url(../img/drag-handles.png) center no-repeat #fff;
@@ -69,6 +122,7 @@
}
.component-actions {
position: absolute;
top: 26px;
right: 44px;
}

View File

@@ -0,0 +1,372 @@
// studio - views - course subsection
// ====================
body.course.subsection {
.unit-settings {
.window-contents {
padding: 10px 20px;
}
.unit-actions {
border-bottom: none;
padding-bottom: 0;
}
.published-alert {
display: none;
padding: 10px;
border: 1px solid #edbd3c;
border-radius: 3px;
background: #fbf6e1;
font-size: 14px;
line-height: 1.4;
div {
margin-top: 15px;
}
}
input[type="radio"] {
margin-right: 7px;
}
.status {
font-size: 12px;
strong {
font-weight: 700;
}
}
.preview-button, .view-button {
@include white-button;
margin-bottom: 10px;
}
.publish-button {
@include orange-button;
}
.delete-button {
@include blue-button;
}
.delete-draft {
display: inline-block;
}
.delete-button,
.preview-button,
.publish-button,
.view-button {
font-size: 11px;
margin-top: 10px;
padding: 6px 15px 8px;
}
}
.unit-history {
&.collapsed {
h4 {
border-bottom: none;
border-radius: 3px;
}
.window-contents {
display: none;
}
}
ol {
border: 1px solid #ced2db;
li {
display: block;
padding: 6px 8px 8px 10px;
background: #edf1f5;
font-size: 12px;
&:hover {
background: #fffcf1;
.item-actions {
display: block;
}
}
&.checked {
background: #d1dae3;
}
.item-actions {
display: none;
}
input[type="radio"] {
margin-right: 7px;
}
}
}
}
.unit-location {
.url {
width: 100%;
margin-bottom: 10px;
@include box-shadow(none);
}
.draft-tag,
.hidden-tag,
.private-tag,
.has-new-draft-tag {
font-size: 8px;
}
.window-contents > ol {
@include tree-view;
.section-item {
display: inline-block;
width: 100%;
font-size: 11px;
padding: 2px 8px 4px;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
@include box-sizing(border-box);
}
ol {
.section-item {
padding-left: 20px;
}
.new-unit-item {
margin-left: 20px;
}
}
ol ol {
.section-item {
padding-left: 34px;
}
.new-unit-item {
margin: 0 0 10px 41px;
}
}
}
}
.subsection-body {
padding: 32px 40px;
@include clearfix;
> div {
margin-bottom: 40px;
}
input {
font-size: 14px;
}
.sortable-unit-list {
ol {
@include tree-view;
}
}
}
.subsection-name-input {
label {
display: block;
}
input {
width: 100%;
font-size: 20px;
}
}
.scheduled-date-input,
.due-date-input {
@include clearfix;
.date-input,
.time-input {
display: inline-block;
width: 100px;
}
.inherits-check {
label {
font-size: 13px;
}
}
.notice {
margin-top: 6px;
font-size: 11px;
color: #999;
}
}
.due-date-input {
label {
display: inline-block !important;
margin-right: 10px;
}
a {
font-size: 11px;
font-weight: bold;
text-transform: uppercase;
}
.date-setter {
@include clearfix;
display: none;
}
.remove-date {
display: block;
}
}
.row.visibility {
label {
display: inline-block !important;
margin-right: 10px;
line-height: 21px;
}
a {
display: inline-block;
height: 31px;
margin-right: 8px;
vertical-align: middle;
font-size: 11px;
font-weight: 700;
line-height: 31px;
text-transform: uppercase;
}
.large-toggle {
width: 41px;
background: url(../img/large-toggles.png) no-repeat;
background-position: 0 -50px;
.hidden {
background-position: 0 -5px;
}
}
}
.gradable {
label {
display: inline-block;
vertical-align: top;
}
.gradable-status {
position: relative;
top: -4px;
display: inline-block;
margin-left: 10px;
width: 65%;
.status-label {
margin: 0;
padding: 0;
background: transparent;
color: $blue;
border: none;
font-size: 11px;
font-weight: bold;
text-transform: uppercase;
}
.menu-toggle {
z-index: 100;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 20px;
background: transparent;
&:hover, &.is-active {
color: $blue;
}
}
.menu {
z-index: 1;
position: absolute;
top: -12px;
left: -7px;
display: none;
width: 100%;
margin: 0;
padding: 8px 12px;
opacity: 0.0;
background: $white;
border: 1px solid $mediumGrey;
font-size: 12px;
@include border-radius(4px);
@include box-shadow(0 1px 2px rgba(0, 0, 0, .2));
@include transition(opacity .15s);
li {
margin-bottom: 3px;
padding-bottom: 3px;
border-bottom: 1px solid $lightGrey;
&:last-child {
margin-bottom: 0;
padding-bottom: 0;
border: none;
}
}
a {
&.is-selected {
font-weight: bold;
}
}
}
// dropdown state
&.is-active {
.menu {
z-index: 10000;
display: block;
opacity: 1.0;
}
.menu-toggle {
z-index: 1000;
}
}
// set state
&.is-set {
.menu-toggle {
color: $blue;
}
.status-label {
display: block;
color: $blue;
}
}
}
}
}

View File

@@ -0,0 +1,681 @@
// studio - views - unit
// ====================
body.course.unit {
.unit .main-wrapper {
@include clearfix();
margin: 40px;
}
//Problem Selector tab menu requirements
.js .tabs .tab {
display: none;
}
//end problem selector reqs
.main-column {
clear: both;
float: left;
width: 70%;
}
.unit-body.published {
.components > li {
border: none;
.rendered-component {
padding: 0 20px;
}
}
}
.unit-body {
.unit-name-input {
padding: 20px 40px;
label {
display: block;
}
input {
width: 100%;
font-size: 20px;
}
}
.breadcrumbs {
border-radius: 3px 3px 0 0;
border-bottom: 1px solid #cbd1db;
@include linear-gradient(top, rgba(255, 255, 255, .3), rgba(255, 255, 255, 0) 70%);
background-color: #edf1f5;
@include box-shadow(0 1px 0 rgba(255, 255, 255, .7) inset);
@include clearfix;
li {
float: left;
}
a,
.current-page {
display: block;
padding: 15px 35px 15px 30px;
font-size: 14px;
background: url(../img/breadcrumb-arrow.png) no-repeat right center;
}
}
h2 {
margin: 30px 40px 30px 0;
color: #646464;
font-size: 19px;
font-weight: 300;
letter-spacing: 1px;
text-transform: uppercase;
}
.components {
> li {
position: relative;
z-index: 10;
margin: 20px 40px;
.title {
margin: 0 0 15px 0;
color: $mediumGrey;
.value {
}
}
&.new-component-item {
margin: 20px 0px;
border-top: 1px solid $mediumGrey;
box-shadow: 0 2px 1px rgba(182, 182, 182, 0.75) inset;
background-color: $lightGrey;
margin-bottom: 0px;
padding-bottom: 20px;
.new-component-button {
display: block;
padding: 20px;
text-align: center;
color: #edf1f5;
}
h5 {
margin: 20px 0px;
color: #fff;
font-weight: 600;
font-size: 18px;
}
.rendered-component {
display: none;
background: #fff;
border-radius: 3px 3px 0 0;
}
.new-component-type {
a,
li {
display: inline-block;
}
a {
border: 1px solid $mediumGrey;
width: 100px;
height: 100px;
color: #fff;
margin-right: 15px;
margin-bottom: 20px;
border-radius: 8px;
font-size: 15px;
line-height: 14px;
text-align: center;
@include box-shadow(0 1px 1px rgba(0, 0, 0, .2), 0 1px 0 rgba(255, 255, 255, .4) inset);
.name {
position: absolute;
bottom: 5px;
left: 0;
width: 100%;
padding: 10px;
@include box-sizing(border-box);
color: #fff;
}
}
}
.new-component-templates {
display: none;
margin: 20px 40px 20px 40px;
border-radius: 3px;
border: 1px solid $mediumGrey;
background-color: #fff;
@include box-shadow(0 1px 1px rgba(0, 0, 0, .2), 0 1px 0 rgba(255, 255, 255, .4) inset);
@include clearfix;
.cancel-button {
margin: 20px 0px 10px 10px;
@include white-button;
}
.problem-type-tabs {
display: none;
}
// specific menu types
&.new-component-problem {
padding-bottom:10px;
.ss-icon, .editor-indicator {
display: inline-block;
}
.problem-type-tabs {
display: inline-block;
}
}
}
.new-component-type,
.new-component-template {
@include clearfix;
a {
position: relative;
border: 1px solid $darkGreen;
background: tint($green,20%);
color: #fff;
&:hover {
background: $brightGreen;
}
}
}
.problem-type-tabs {
list-style-type: none;
border-radius: 0;
width: 100%;
@include linear-gradient(top, rgba(255, 255, 255, .4), rgba(255, 255, 255, 0));
background-color: $lightBluishGrey;
@include box-shadow(0 1px 0 rgba(255, 255, 255, 0.2) inset, 0 -1px 0 rgba(0, 0, 0, 0.2) inset);
li:first-child {
margin-left: 20px;
}
li {
float:left;
display:inline-block;
text-align:center;
width: auto;
@include linear-gradient(top, rgba(255, 255, 255, .4), rgba(255, 255, 255, 0));
background-color: tint($lightBluishGrey, 10%);
@include box-shadow(0 1px 0 rgba(255, 255, 255, 0.2) inset, 0 -1px 0 rgba(0, 0, 0, 0.2) inset);
opacity:.8;
&:hover {
opacity:1;
background-color: tint($lightBluishGrey, 20%);
}
&.ui-state-active {
border: 0px;
@include active;
opacity:1;
}
}
a{
display: block;
padding: 15px 25px;
font-size: 15px;
line-height: 16px;
text-align: center;
color: #3c3c3c;
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.3);
}
}
.new-component-template {
a {
background: #fff;
border: 0px;
color: #3c3c3c;
@include transition (none);
&:hover {
background: tint($green,30%);
color: #fff;
@include transition(background-color .15s);
}
}
li {
border:none;
border-bottom: 1px dashed $lightGrey;
color: #fff;
}
li:first-child {
a {
border-top: 0px;
}
}
li:nth-child(2) {
a {
border-radius: 0px;
}
}
a {
@include clearfix();
display: block;
padding: 7px 20px;
border-bottom: none;
font-weight: 500;
.name {
float: left;
.ss-icon {
@include transition(opacity .15s);
display: inline-block;
top: 1px;
margin-right: 5px;
opacity: 0.5;
width: 17;
height: 21px;
vertical-align: middle;
}
}
.editor-indicator {
@include transition(opacity .15s);
float: right;
position: relative;
top: 3px;
font-size: 12px;
opacity: 0.3;
}
.ss-icon, .editor-indicator {
display: none;
}
&:hover {
color: #fff;
.ss-icon {
opacity: 1.0;
}
.editor-indicator {
opacity: 1.0;
}
}
}
// specific editor types
.empty {
a {
line-height: 1.4;
font-weight: 400;
background: #fff;
color: #3c3c3c;
&:hover {
background: tint($green,30%);
color: #fff;
}
}
}
}
.new-component {
text-align: center;
h5 {
color: $darkGreen;
}
}
}
}
}
.component {
border: 1px solid $lightBluishGrey2;
border-radius: 3px;
background: #fff;
@include transition(none);
&:hover {
border-color: #6696d7;
.drag-handle {
background-color: $blue;
border-color: $blue;
}
}
&.editing {
border: 1px solid $lightBluishGrey2;
z-index: auto;
.drag-handle,
.component-actions {
display: none;
}
}
&.component-placeholder {
border-color: #6696d7;
}
.drag-handle {
position: absolute;
display: block;
top: -1px;
right: -16px;
z-index: 10;
width: 15px;
height: 100%;
border-radius: 0 3px 3px 0;
border: 1px solid $lightBluishGrey2;
background: url(../img/white-drag-handles.png) center no-repeat $lightBluishGrey2;
cursor: move;
@include transition(none);
}
}
.xmodule_display {
padding: 40px 20px 20px;
overflow-x: auto;
h1 {
float: none;
margin-left: 0;
}
}
.wrapper-component-editor {
z-index: 9999;
position: relative;
background: $lightBluishGrey2;
}
.component-editor {
@include edit-box;
@include box-shadow(none);
display: none;
padding: 20px;
border-radius: 2px 2px 0 0;
.metadata_edit {
margin-bottom: 20px;
font-size: 13px;
li {
margin-bottom: 10px;
}
label {
display: inline-block;
margin-right: 10px;
}
}
h3 {
margin-bottom: 10px;
font-size: 18px;
font-weight: 700;
}
h5 {
margin-bottom: 8px;
color: #fff;
font-weight: 700;
}
.save-button {
margin-top: 10px;
margin: 15px 8px 0 0;
}
}
}
.unit-settings {
.window-contents {
padding: 10px 20px;
}
.unit-actions {
border-bottom: none;
padding-bottom: 0;
}
.published-alert {
display: none;
padding: 10px;
border: 1px solid #edbd3c;
border-radius: 3px;
background: #fbf6e1;
font-size: 14px;
line-height: 1.4;
div {
margin-top: 15px;
}
}
input[type="radio"] {
margin-right: 7px;
}
.status {
font-size: 12px;
strong {
font-weight: 700;
}
}
.preview-button, .view-button {
@include white-button;
margin-bottom: 10px;
}
.publish-button {
@include orange-button;
}
.delete-button {
@include blue-button;
}
.delete-draft {
display: inline-block;
}
.delete-button,
.preview-button,
.publish-button,
.view-button {
font-size: 11px;
margin-top: 10px;
padding: 6px 15px 8px;
}
}
.unit-history {
&.collapsed {
h4 {
border-bottom: none;
border-radius: 3px;
}
.window-contents {
display: none;
}
}
ol {
border: 1px solid #ced2db;
li {
display: block;
padding: 6px 8px 8px 10px;
background: #edf1f5;
font-size: 12px;
&:hover {
background: #fffcf1;
.item-actions {
display: block;
}
}
&.checked {
background: #d1dae3;
}
.item-actions {
display: none;
}
input[type="radio"] {
margin-right: 7px;
}
}
}
}
.unit-location {
.url {
width: 100%;
margin-bottom: 10px;
@include box-shadow(none);
}
.draft-tag,
.hidden-tag,
.private-tag,
.has-new-draft-tag {
font-size: 8px;
}
.window-contents > ol {
@include tree-view;
.section-item {
display: inline-block;
width: 100%;
font-size: 11px;
padding: 2px 8px 4px;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
@include box-sizing(border-box);
}
ol {
.section-item {
padding-left: 20px;
}
.new-unit-item {
margin-left: 20px;
}
}
ol ol {
.section-item {
padding-left: 34px;
}
.new-unit-item {
margin: 0 0 10px 41px;
}
}
}
}
.edit-state-draft {
.visibility,
.edit-draft-message,
.view-button {
display: none;
}
.published-alert {
display: block;
}
}
.edit-state-public {
.delete-draft,
.component-actions,
.new-component-item,
.editing-draft-alert,
.publish-draft-message,
.preview-button {
display: none;
}
.published-alert {
display: block;
}
.drag-handle {
display: none !important;
}
}
.edit-state-private {
.delete-draft,
.publish-draft,
.editing-draft-alert,
.create-draft,
.view-button {
display: none;
}
}
}
// editing units from courseware
body.unit {
.component {
padding-top: 30px;
.component-actions {
@include box-sizing(border-box);
position: absolute;
width: 100%;
padding: 15px;
top: 0;
left: 0;
border-bottom: 1px solid $lightBluishGrey2;
background: $lightGrey;
}
&.editing {
padding-top: 0;
}
}
}

View File

@@ -1,4 +1,8 @@
.course-info {
// studio - views - course updates
// ====================
body.course.updates {
h2 {
margin-bottom: 24px;
font-size: 22px;

View File

@@ -1,4 +1,8 @@
.users {
// studio - views - course users
// ====================
body.course.users {
.new-user-form {
display: none;
padding: 15px 20px;

14
cms/templates/404.html Normal file
View File

@@ -0,0 +1,14 @@
<%inherit file="base.html" />
<%block name="title">Page Not Found</%block>
<%block name="content">
<div class="wrapper-content wrapper">
<section class="content">
<h1>Page not found</h1>
<p>The page that you were looking for was not found. Go back to the <a href="/">homepage</a> or let us know about any pages that may have been moved at <a href="mailto:technical@edx.org">technical@edx.org</a>.</p>
</section>
</div>
</%block>

13
cms/templates/500.html Normal file
View File

@@ -0,0 +1,13 @@
<%inherit file="base.html" />
<%block name="title">Server Error</%block>
<%block name="content">
<div class="wrapper-content wrapper">
<section class="content">
<h1>Currently the <em>edX</em> servers are down</h1>
<p>Our staff is currently working to get the site back up as soon as possible. Please email us at <a href="mailto:technical@edx.org">technical@edx.org</a> to report any problems or downtime.</p>
</section>
</div>
</%block>

View File

@@ -1,7 +1,7 @@
<%inherit file="base.html" />
<%! from django.core.urlresolvers import reverse %>
<%block name="bodyclass">is-signedin course uploads</%block>
<%block name="title">Uploads &amp; Files</%block>
<%block name="title">Files &amp; Uploads</%block>
<%namespace name='static' file='static_content.html'/>

View File

@@ -6,10 +6,10 @@
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title>
<%block name="title"></%block> |
<%block name="title"></%block> |
% if context_course:
<% ctx_loc = context_course.location %>
${context_course.display_name} |
${context_course.display_name_with_default} |
% endif
edX Studio
</title>
@@ -22,7 +22,7 @@
<link rel="stylesheet" type="text/css" href="${static.url('js/vendor/markitup/sets/wiki/style.css')}" />
<link rel="stylesheet" type="text/css" href="${static.url('css/vendor/symbolset.ss-symbolicons-block.css')}" />
<link rel="stylesheet" type="text/css" href="${static.url('css/vendor/symbolset.ss-standard.css')}" />
<%block name="header_extras"></%block>
</head>
@@ -45,6 +45,7 @@
<script src="${static.url('js/vendor/jquery.leanModal.min.js')}"></script>
<script src="${static.url('js/vendor/jquery.tablednd.js')}"></script>
<script src="${static.url('js/vendor/jquery.form.js')}"></script>
<script src="${static.url('js/vendor/jquery.smooth-scroll.min.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/CodeMirror/htmlmixed.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/CodeMirror/css.js')}"></script>
<script type="text/javascript">

View File

@@ -0,0 +1,74 @@
<%inherit file="base.html" />
<%! from django.core.urlresolvers import reverse %>
<%block name="title">Course Checklists</%block>
<%block name="bodyclass">is-signedin course uxdesign checklists</%block>
<%namespace name='static' file='static_content.html'/>
<%block name="jsextra">
<script type="text/javascript" src="${static.url('js/template_loader.js')}"></script>
<script type="text/javascript" src="${static.url('js/views/checklists_view.js')}"></script>
<script type="text/javascript" src="${static.url('js/models/checklists.js')}"></script>
<script type="text/javascript" src="${static.url('js/views/server_error.js')}"></script>
<script type="text/javascript">
$(document).ready(function () {
var checklistCollection = new CMS.Models.ChecklistCollection();
checklistCollection.url = "${reverse('checklists_updates', kwargs=dict(org=context_course.location.org, course=context_course.location.course, name=context_course.location.name))}";
var editor = new CMS.Views.Checklists({
el: $('.course-checklists'),
collection: checklistCollection
});
});
</script>
</%block>
<%block name="content">
<div class="wrapper-mast wrapper">
<header class="mast has-actions has-subtitle">
<div class="title">
<span class="title-sub">Tools</span>
<h1 class="title-1">Course Checklists</h1>
</div>
</header>
</div>
<div class="wrapper-content wrapper">
<section class="content">
<article class="content-primary" role="main">
<form id="course-checklists" class="course-checklists" method="post" action="">
<h2 class="title title-3 sr">Current Checklists</h2>
</form>
</article>
<aside class="content-supplementary" role="complimentary">
<div class="bit">
<h3 class="title title-3">What are checklists?</h3>
<p>
Running a course on edX is a complex undertaking. Course checklists are designed to help you understand and keep track of all the steps necessary to get your course ready for students.
</p>
<p>
These checklists are shared among your course team, and any changes you make are immediately visible to other members of the team and saved automatically.
</p>
</div>
<div class="bit">
<h3 class="title title-3">Studio checklists</h3>
<nav class="nav-page checklists-current">
<ol>
% for checklist in checklists:
<li class="nav-item">
<a rel="view" href="${'#course-checklist' + str(loop.index)}">${checklist['short_description']}</a>
</li>
% endfor
</ol>
</nav>
</div>
</aside>
</section>
</div>
</%block>

View File

@@ -1,14 +0,0 @@
<%inherit file="base.html" />
<%include file="widgets/header.html"/>
<%block name="content">
<section class="main-container">
<%include file="widgets/navigation.html"/>
<section class="main-content">
</section>
</section>
</%block>

Some files were not shown because too many files have changed in this diff Show More