Merge branch 'master' into feature/deena/testing
Conflicts: cms/djangoapps/contentstore/tests/factories.py lms/djangoapps/courseware/tests/test_access.py
1
.gitignore
vendored
@@ -27,4 +27,5 @@ lms/lib/comment_client/python
|
||||
nosetests.xml
|
||||
cover_html/
|
||||
.idea/
|
||||
.redcar/
|
||||
chromedriver.log
|
||||
@@ -1 +1 @@
|
||||
1.8.7-p371
|
||||
1.8.7-p371
|
||||
@@ -9,6 +9,7 @@ gfortran
|
||||
liblapack-dev
|
||||
libfreetype6-dev
|
||||
libpng12-dev
|
||||
libjpeg-dev
|
||||
libxml2-dev
|
||||
libxslt-dev
|
||||
yui-compressor
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# .coveragerc for cms
|
||||
[run]
|
||||
data_file = reports/cms/.coverage
|
||||
source = cms
|
||||
omit = cms/envs/*, cms/manage.py
|
||||
source = cms,common/djangoapps
|
||||
omit = cms/envs/*, cms/manage.py, common/djangoapps/terrain/*, common/djangoapps/*/migrations/*
|
||||
|
||||
[report]
|
||||
ignore_errors = True
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
|
||||
@@ -18,6 +18,8 @@ STAFF_ROLE_NAME = 'staff'
|
||||
# we're just making a Django group for each location/role combo
|
||||
# to do this we're just creating a Group name which is a formatted string
|
||||
# of those two variables
|
||||
|
||||
|
||||
def get_course_groupname_for_role(location, role):
|
||||
loc = Location(location)
|
||||
# hack: check for existence of a group name in the legacy LMS format <role>_<course>
|
||||
@@ -25,11 +27,12 @@ def get_course_groupname_for_role(location, role):
|
||||
# more information
|
||||
groupname = '{0}_{1}'.format(role, loc.course)
|
||||
|
||||
if len(Group.objects.filter(name = groupname)) == 0:
|
||||
groupname = '{0}_{1}'.format(role,loc.course_id)
|
||||
if len(Group.objects.filter(name=groupname)) == 0:
|
||||
groupname = '{0}_{1}'.format(role, loc.course_id)
|
||||
|
||||
return groupname
|
||||
|
||||
|
||||
def get_users_in_course_group_by_role(location, role):
|
||||
groupname = get_course_groupname_for_role(location, role)
|
||||
(group, created) = Group.objects.get_or_create(name=groupname)
|
||||
@@ -39,6 +42,8 @@ def get_users_in_course_group_by_role(location, role):
|
||||
'''
|
||||
Create all permission groups for a new course and subscribe the caller into those roles
|
||||
'''
|
||||
|
||||
|
||||
def create_all_course_groups(creator, location):
|
||||
create_new_course_group(creator, location, INSTRUCTOR_ROLE_NAME)
|
||||
create_new_course_group(creator, location, STAFF_ROLE_NAME)
|
||||
@@ -46,7 +51,7 @@ def create_all_course_groups(creator, location):
|
||||
|
||||
def create_new_course_group(creator, location, role):
|
||||
groupname = get_course_groupname_for_role(location, role)
|
||||
(group, created) =Group.objects.get_or_create(name=groupname)
|
||||
(group, created) = Group.objects.get_or_create(name=groupname)
|
||||
if created:
|
||||
group.save()
|
||||
|
||||
@@ -59,6 +64,8 @@ def create_new_course_group(creator, location, role):
|
||||
This is to be called only by either a command line code path or through a app which has already
|
||||
asserted permissions
|
||||
'''
|
||||
|
||||
|
||||
def _delete_course_group(location):
|
||||
# remove all memberships
|
||||
instructors = Group.objects.get(name=get_course_groupname_for_role(location, INSTRUCTOR_ROLE_NAME))
|
||||
@@ -75,6 +82,8 @@ def _delete_course_group(location):
|
||||
This is to be called only by either a command line code path or through an app which has already
|
||||
asserted permissions to do this action
|
||||
'''
|
||||
|
||||
|
||||
def _copy_course_group(source, dest):
|
||||
instructors = Group.objects.get(name=get_course_groupname_for_role(source, INSTRUCTOR_ROLE_NAME))
|
||||
new_instructors_group = Group.objects.get(name=get_course_groupname_for_role(dest, INSTRUCTOR_ROLE_NAME))
|
||||
@@ -86,7 +95,7 @@ def _copy_course_group(source, dest):
|
||||
new_staff_group = Group.objects.get(name=get_course_groupname_for_role(dest, STAFF_ROLE_NAME))
|
||||
for user in staff.user_set.all():
|
||||
user.groups.add(new_staff_group)
|
||||
user.save()
|
||||
user.save()
|
||||
|
||||
|
||||
def add_user_to_course_group(caller, user, location, role):
|
||||
@@ -133,8 +142,6 @@ def remove_user_from_course_group(caller, user, location, role):
|
||||
def is_user_in_course_group_role(user, location, role):
|
||||
if user.is_active and user.is_authenticated:
|
||||
# all "is_staff" flagged accounts belong to all groups
|
||||
return user.is_staff or user.groups.filter(name=get_course_groupname_for_role(location,role)).count() > 0
|
||||
return user.is_staff or user.groups.filter(name=get_course_groupname_for_role(location, role)).count() > 0
|
||||
|
||||
return False
|
||||
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@ import logging
|
||||
|
||||
## 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
|
||||
|
||||
|
||||
def get_course_updates(location):
|
||||
"""
|
||||
Retrieve the relevant course_info updates and unpack into the model which the client expects:
|
||||
@@ -21,13 +23,13 @@ def get_course_updates(location):
|
||||
|
||||
# current db rep: {"_id" : locationjson, "definition" : { "data" : "<ol>[<li><h2>date</h2>content</li>]</ol>"} "metadata" : ignored}
|
||||
location_base = course_updates.location.url()
|
||||
|
||||
|
||||
# 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'])
|
||||
except:
|
||||
course_html_parsed = html.fromstring("<ol></ol>")
|
||||
|
||||
|
||||
# Confirm that root is <ol>, iterate over <li>, pull out <h2> subs and then rest of val
|
||||
course_upd_collection = []
|
||||
if course_html_parsed.tag == 'ol':
|
||||
@@ -40,25 +42,26 @@ def get_course_updates(location):
|
||||
content = update[0].tail
|
||||
else:
|
||||
content = "\n".join([html.tostring(ele) for ele in update[1:]])
|
||||
|
||||
|
||||
# make the id on the client be 1..len w/ 1 being the oldest and len being the newest
|
||||
course_upd_collection.append({"id" : location_base + "/" + str(len(course_html_parsed) - idx),
|
||||
"date" : update.findtext("h2"),
|
||||
"content" : content})
|
||||
|
||||
course_upd_collection.append({"id": location_base + "/" + str(len(course_html_parsed) - idx),
|
||||
"date": update.findtext("h2"),
|
||||
"content": content})
|
||||
|
||||
return course_upd_collection
|
||||
|
||||
|
||||
def update_course_updates(location, update, passed_id=None):
|
||||
"""
|
||||
Either add or update the given course update. It will add it if the passed_id is absent or None. It will update it if
|
||||
it has an passed_id which has a valid value. Until updates have distinct values, the passed_id is the location url + an index
|
||||
into the html structure.
|
||||
into the html structure.
|
||||
"""
|
||||
try:
|
||||
course_updates = modulestore('direct').get_item(location)
|
||||
except ItemNotFoundError:
|
||||
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'])
|
||||
@@ -67,7 +70,7 @@ def update_course_updates(location, update, passed_id=None):
|
||||
|
||||
# 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>')
|
||||
|
||||
|
||||
# Confirm that root is <ol>, iterate over <li>, pull out <h2> subs and then rest of val
|
||||
if course_html_parsed.tag == 'ol':
|
||||
# ??? Should this use the id in the json or in the url or does it matter?
|
||||
@@ -80,14 +83,15 @@ def update_course_updates(location, update, passed_id=None):
|
||||
|
||||
idx = len(course_html_parsed)
|
||||
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'])
|
||||
|
||||
return {"id" : passed_id,
|
||||
"date" : update['date'],
|
||||
"content" :update['content']}
|
||||
|
||||
return {"id": passed_id,
|
||||
"date": update['date'],
|
||||
"content": update['content']}
|
||||
|
||||
|
||||
def delete_course_update(location, update, passed_id):
|
||||
"""
|
||||
@@ -96,19 +100,19 @@ def delete_course_update(location, update, passed_id):
|
||||
"""
|
||||
if not passed_id:
|
||||
return HttpResponseBadRequest
|
||||
|
||||
|
||||
try:
|
||||
course_updates = modulestore('direct').get_item(location)
|
||||
except ItemNotFoundError:
|
||||
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'])
|
||||
except:
|
||||
course_html_parsed = html.fromstring("<ol></ol>")
|
||||
|
||||
|
||||
if course_html_parsed.tag == 'ol':
|
||||
# ??? Should this use the id in the json or in the url or does it matter?
|
||||
idx = get_idx(passed_id)
|
||||
@@ -120,10 +124,11 @@ def delete_course_update(location, update, passed_id):
|
||||
# update db record
|
||||
course_updates.definition['data'] = html.tostring(course_html_parsed)
|
||||
store = modulestore('direct')
|
||||
store.update_item(location, course_updates.definition['data'])
|
||||
|
||||
store.update_item(location, course_updates.definition['data'])
|
||||
|
||||
return get_course_updates(location)
|
||||
|
||||
|
||||
|
||||
def get_idx(passed_id):
|
||||
"""
|
||||
From the url w/ idx appended, get the idx.
|
||||
@@ -131,4 +136,4 @@ def get_idx(passed_id):
|
||||
# TODO compile this regex into a class static and reuse for each call
|
||||
idx_matcher = re.search(r'.*/(\d+)$', passed_id)
|
||||
if idx_matcher:
|
||||
return int(idx_matcher.group(1))
|
||||
return int(idx_matcher.group(1))
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
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
|
||||
|
||||
Scenario: A course author sees only display_name on a newly created course
|
||||
Given I have opened a new course in Studio
|
||||
When I select the Advanced Settings
|
||||
Then I see only the display name
|
||||
|
||||
Scenario: Test if there are no policy settings without existing UI controls
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
Scenario: Test how multi-line input appears
|
||||
Given I am on the Advanced Course Settings page in Studio
|
||||
When I create a JSON object
|
||||
Then it is displayed as formatted
|
||||
182
cms/djangoapps/contentstore/features/advanced-settings.py
Normal file
@@ -0,0 +1,182 @@
|
||||
from lettuce import world, step
|
||||
from common import *
|
||||
import time
|
||||
|
||||
from nose.tools import assert_equal
|
||||
from nose.tools import assert_true
|
||||
|
||||
"""
|
||||
http://selenium.googlecode.com/svn/trunk/docs/api/py/webdriver/selenium.webdriver.common.keys.html
|
||||
"""
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
|
||||
|
||||
############### 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)
|
||||
link_css = 'li.nav-course-settings-advanced a'
|
||||
css_click(link_css)
|
||||
|
||||
|
||||
@step('I am on the Advanced Course Settings page in Studio$')
|
||||
def i_am_on_advanced_course_settings(step):
|
||||
step.given('I have opened a new course in Studio')
|
||||
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.fill('new')
|
||||
|
||||
|
||||
@step(u'I press the "([^"]*)" notification button$')
|
||||
def press_the_notification_button(step, name):
|
||||
world.browser.click_link_by_text(name)
|
||||
|
||||
|
||||
@step(u'I edit the value of a policy key$')
|
||||
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._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$')
|
||||
def create_JSON_object(step):
|
||||
create_entry("json", '{"key": "value", "key_2": "value_2"}')
|
||||
click_save()
|
||||
|
||||
|
||||
############### RESULTS ####################
|
||||
@step('I see only the display name$')
|
||||
def i_see_only_display_name(step):
|
||||
assert_policy_entries(["display_name"], ['"Robot Super Course"'])
|
||||
|
||||
|
||||
@step('there are no advanced policy settings$')
|
||||
def no_policy_settings(step):
|
||||
assert_policy_entries([], [])
|
||||
|
||||
|
||||
@step('they are alphabetized$')
|
||||
def they_are_alphabetized(step):
|
||||
assert_policy_entries(["a", "display_name", "z"], ['"zebra"', '"Robot Super Course"', '"apple"'])
|
||||
|
||||
|
||||
@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}'])
|
||||
|
||||
|
||||
@step(u'the policy key name is unchanged$')
|
||||
def the_policy_key_name_is_unchanged(step):
|
||||
policy_key_css = 'input.policy-key'
|
||||
e = css_find(policy_key_css).first
|
||||
assert_equal(e.value, 'display_name')
|
||||
|
||||
|
||||
@step(u'the policy key name is changed$')
|
||||
def the_policy_key_name_is_changed(step):
|
||||
policy_key_css = 'input.policy-key'
|
||||
e = css_find(policy_key_css).first
|
||||
assert_equal(e.value, 'new')
|
||||
|
||||
|
||||
@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'
|
||||
e = css_find(policy_value_css).first
|
||||
assert_equal(e.value, '"Robot Super Course"')
|
||||
|
||||
|
||||
@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'
|
||||
e = css_find(policy_value_css).first
|
||||
assert_equal(e.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 has a bug that 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)
|
||||
|
||||
|
||||
def delete_entry(index):
|
||||
"""
|
||||
Delete the nth entry where index is 0-based
|
||||
"""
|
||||
css = '.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', expected_keys)
|
||||
assert_entries('.json', expected_values)
|
||||
|
||||
|
||||
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 click_save():
|
||||
css = ".save-button"
|
||||
|
||||
def is_shown(driver):
|
||||
visible = css_find(css).first.visible
|
||||
if visible:
|
||||
# Even when waiting for visible, this fails sporadically. Adding in a small wait.
|
||||
time.sleep(float(1))
|
||||
return visible
|
||||
wait_for(is_shown)
|
||||
css_click(css)
|
||||
|
||||
|
||||
def fill_last_field(value):
|
||||
newValue = css_find('#__new_advanced_key__ input').first
|
||||
newValue.fill(value)
|
||||
@@ -1,51 +1,67 @@
|
||||
from lettuce import world, step
|
||||
from factories import *
|
||||
from django.core.management import call_command
|
||||
from lettuce.django import django_url
|
||||
from django.conf import settings
|
||||
from django.core.management import call_command
|
||||
from nose.tools import assert_true
|
||||
from nose.tools import assert_equal
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
|
||||
from terrain.factories import UserFactory, RegistrationFactory, UserProfileFactory
|
||||
from terrain.factories import CourseFactory, GroupFactory
|
||||
import xmodule.modulestore.django
|
||||
from auth.authz import get_user_by_email
|
||||
|
||||
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('/'))
|
||||
assert world.browser.is_element_present_by_css('body.no-header', 10)
|
||||
signin_css = 'a.action-signin'
|
||||
assert world.browser.is_element_present_by_css(signin_css, 10)
|
||||
|
||||
|
||||
@step('I am logged into Studio$')
|
||||
def i_am_logged_into_studio(step):
|
||||
log_into_studio()
|
||||
|
||||
|
||||
@step('I confirm the alert$')
|
||||
def i_confirm_with_ok(step):
|
||||
world.browser.get_alert().accept()
|
||||
|
||||
|
||||
@step(u'I press the "([^"]*)" delete icon$')
|
||||
def i_press_the_category_delete_icon(step, category):
|
||||
if category == 'section':
|
||||
css = 'a.delete-button.delete-section-button span.delete-icon'
|
||||
elif category == 'subsection':
|
||||
css='a.delete-button.delete-subsection-button span.delete-icon'
|
||||
css = 'a.delete-button.delete-subsection-button span.delete-icon'
|
||||
else:
|
||||
assert False, 'Invalid category: %s' % category
|
||||
css_click(css)
|
||||
|
||||
|
||||
@step('I have opened a new course in Studio$')
|
||||
def i_have_opened_a_new_course(step):
|
||||
clear_courses()
|
||||
log_into_studio()
|
||||
create_a_course()
|
||||
|
||||
####### HELPER FUNCTIONS ##############
|
||||
|
||||
|
||||
def create_studio_user(
|
||||
uname='robot',
|
||||
email='robot+studio@edx.org',
|
||||
password='test',
|
||||
is_staff=False):
|
||||
is_staff=False):
|
||||
studio_user = UserFactory.build(
|
||||
username=uname,
|
||||
username=uname,
|
||||
email=email,
|
||||
password=password,
|
||||
is_staff=is_staff)
|
||||
@@ -58,6 +74,7 @@ def create_studio_user(
|
||||
|
||||
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
|
||||
@@ -70,26 +87,57 @@ def flush_xmodule_store():
|
||||
xmodule.modulestore.django.modulestore().collection.drop()
|
||||
xmodule.templates.update_templates()
|
||||
|
||||
def assert_css_with_text(css,text):
|
||||
|
||||
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):
|
||||
assert_true(world.browser.is_element_present_by_css(css, 5))
|
||||
world.browser.find_by_css(css).first.click()
|
||||
|
||||
|
||||
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
|
||||
'''
|
||||
assert_true(world.browser.is_element_present_by_css(css, 5))
|
||||
e = world.browser.find_by_css(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):
|
||||
return world.browser.find_by_css(css)
|
||||
|
||||
|
||||
def wait_for(func):
|
||||
WebDriverWait(world.browser.driver, 10).until(func)
|
||||
|
||||
|
||||
def id_find(id):
|
||||
return world.browser.find_by_id(id)
|
||||
|
||||
|
||||
def clear_courses():
|
||||
flush_xmodule_store()
|
||||
|
||||
|
||||
def fill_in_course_info(
|
||||
name='Robot Super Course',
|
||||
org='MITx',
|
||||
num='101'):
|
||||
css_fill('.new-course-name',name)
|
||||
css_fill('.new-course-org',org)
|
||||
css_fill('.new-course-number',num)
|
||||
css_fill('.new-course-name', name)
|
||||
css_fill('.new-course-org', org)
|
||||
css_fill('.new-course-number', num)
|
||||
|
||||
|
||||
def log_into_studio(
|
||||
uname='robot',
|
||||
@@ -99,7 +147,11 @@ def log_into_studio(
|
||||
create_studio_user(uname=uname, email=email, is_staff=is_staff)
|
||||
world.browser.cookies.delete()
|
||||
world.browser.visit(django_url('/'))
|
||||
world.browser.is_element_present_by_css('body.no-header', 10)
|
||||
signin_css = 'a.action-signin'
|
||||
world.browser.is_element_present_by_css(signin_css, 10)
|
||||
|
||||
# click the signin button
|
||||
css_click(signin_css)
|
||||
|
||||
login_form = world.browser.find_by_css('form#login_form')
|
||||
login_form.find_by_name('email').fill(email)
|
||||
@@ -108,19 +160,34 @@ def log_into_studio(
|
||||
|
||||
assert_true(world.browser.is_element_present_by_css('.new-course-button', 5))
|
||||
|
||||
|
||||
def create_a_course():
|
||||
css_click('a.new-course-button')
|
||||
fill_in_course_info()
|
||||
css_click('input.new-course-save')
|
||||
assert_true(world.browser.is_element_present_by_css('a#courseware-tab', 5))
|
||||
c = 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')
|
||||
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)
|
||||
course_title_css = 'span.course-title'
|
||||
assert_true(world.browser.is_element_present_by_css(course_title_css, 5))
|
||||
|
||||
|
||||
def add_section(name='My Section'):
|
||||
link_css = 'a.new-courseware-section-button'
|
||||
css_click(link_css)
|
||||
name_css = '.new-section-name'
|
||||
save_css = '.new-section-name-save'
|
||||
css_fill(name_css,name)
|
||||
name_css = 'input.new-section-name'
|
||||
save_css = 'input.new-section-name-save'
|
||||
css_fill(name_css, name)
|
||||
css_click(save_css)
|
||||
span_css = 'span.section-name-span'
|
||||
assert_true(world.browser.is_element_present_by_css(span_css, 5))
|
||||
|
||||
|
||||
def add_subsection(name='Subsection One'):
|
||||
css = 'a.new-subsection-item'
|
||||
@@ -128,4 +195,4 @@ def add_subsection(name='Subsection One'):
|
||||
name_css = 'input.new-subsection-name-input'
|
||||
save_css = 'input.new-subsection-name-save'
|
||||
css_fill(name_css, name)
|
||||
css_click(save_css)
|
||||
css_click(save_css)
|
||||
|
||||
@@ -2,49 +2,61 @@ from lettuce import world, step
|
||||
from common import *
|
||||
|
||||
############### ACTIONS ####################
|
||||
|
||||
|
||||
@step('There are no courses$')
|
||||
def no_courses(step):
|
||||
clear_courses()
|
||||
|
||||
|
||||
@step('I click the New Course button$')
|
||||
def i_click_new_course(step):
|
||||
css_click('.new-course-button')
|
||||
|
||||
|
||||
@step('I fill in the new course information$')
|
||||
def i_fill_in_a_new_course_information(step):
|
||||
fill_in_course_info()
|
||||
|
||||
|
||||
@step('I create a new course$')
|
||||
def i_create_a_course(step):
|
||||
create_a_course()
|
||||
|
||||
|
||||
@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)
|
||||
|
||||
############ ASSERTIONS ###################
|
||||
|
||||
|
||||
@step('the Courseware page has loaded in Studio$')
|
||||
def courseware_page_has_loaded_in_studio(step):
|
||||
courseware_css = 'a#courseware-tab'
|
||||
assert world.browser.is_element_present_by_css(courseware_css)
|
||||
course_title_css = 'span.course-title'
|
||||
assert world.browser.is_element_present_by_css(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_css_with_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_css_with_text(class_css, 'Robot Super Course')
|
||||
|
||||
|
||||
@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_css_with_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_css_with_text(link_css, '+ New Section')
|
||||
|
||||
@@ -3,6 +3,7 @@ from student.models import User, UserProfile, Registration
|
||||
from datetime import datetime
|
||||
import uuid
|
||||
|
||||
|
||||
class UserProfileFactory(factory.Factory):
|
||||
FACTORY_FOR = UserProfile
|
||||
|
||||
@@ -10,12 +11,14 @@ class UserProfileFactory(factory.Factory):
|
||||
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
|
||||
|
||||
@@ -28,4 +31,4 @@ class UserFactory(factory.Factory):
|
||||
is_active = True
|
||||
is_superuser = False
|
||||
last_login = datetime.now()
|
||||
date_joined = datetime.now()
|
||||
date_joined = datetime.now()
|
||||
|
||||
@@ -2,54 +2,59 @@ from lettuce import world, step
|
||||
from common import *
|
||||
|
||||
############### ACTIONS ####################
|
||||
@step('I have opened a new course in Studio$')
|
||||
def i_have_opened_a_new_course(step):
|
||||
clear_courses()
|
||||
log_into_studio()
|
||||
create_a_course()
|
||||
|
||||
|
||||
@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)
|
||||
|
||||
|
||||
@step('I enter the section name and click save$')
|
||||
def i_save_section_name(step):
|
||||
name_css = '.new-section-name'
|
||||
save_css = '.new-section-name-save'
|
||||
css_fill(name_css,'My Section')
|
||||
css_fill(name_css, 'My Section')
|
||||
css_click(save_css)
|
||||
|
||||
|
||||
@step('I have added a new section$')
|
||||
def i_have_added_new_section(step):
|
||||
add_section()
|
||||
|
||||
|
||||
|
||||
@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)
|
||||
|
||||
|
||||
@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')
|
||||
css_fill(date_css, '12/25/2013')
|
||||
# click here to make the calendar go away
|
||||
css_click(time_css)
|
||||
css_fill(time_css,'12:00am')
|
||||
css_fill(time_css, '12:00am')
|
||||
css_click('a.save-button')
|
||||
|
||||
|
||||
############ ASSERTIONS ###################
|
||||
|
||||
|
||||
@step('I see my section on the Courseware page$')
|
||||
def i_see_my_section_on_the_courseware_page(step):
|
||||
section_css = 'span.section-name-span'
|
||||
assert_css_with_text(section_css,'My Section')
|
||||
assert_css_with_text(section_css, 'My Section')
|
||||
|
||||
|
||||
@step('the section does not exist$')
|
||||
def section_does_not_exist(step):
|
||||
css = 'span.section-name-span'
|
||||
assert world.browser.is_element_not_present_by_css(css)
|
||||
|
||||
|
||||
@step('I see a release date for my section$')
|
||||
def i_see_a_release_date_for_my_section(step):
|
||||
import re
|
||||
@@ -63,18 +68,21 @@ def i_see_a_release_date_for_my_section(step):
|
||||
date_regex = '[01][0-9]\/[0-3][0-9]\/[12][0-9][0-9][0-9]'
|
||||
time_regex = '[0-2][0-9]:[0-5][0-9]'
|
||||
match_string = '%s %s at %s' % (msg, date_regex, time_regex)
|
||||
assert re.match(match_string,status_text)
|
||||
assert re.match(match_string, status_text)
|
||||
|
||||
|
||||
@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)
|
||||
|
||||
|
||||
@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
|
||||
|
||||
|
||||
@step('the section release date is updated$')
|
||||
def the_section_release_date_is_updated(step):
|
||||
css = 'span.published-status'
|
||||
|
||||
@@ -5,8 +5,8 @@ Feature: Sign in
|
||||
|
||||
Scenario: Sign up from the homepage
|
||||
Given I visit the Studio homepage
|
||||
When I click the link with the text "Sign up"
|
||||
When I click the link with the text "Sign Up"
|
||||
And I fill in the registration form
|
||||
And I press the "Create My Account" button on the registration form
|
||||
And I press the Create My Account button on the registration form
|
||||
Then I should see be on the studio home page
|
||||
And I should see the message "please click on the activation link in your email."
|
||||
And I should see the message "please click on the activation link in your email."
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from lettuce import world, step
|
||||
|
||||
|
||||
@step('I fill in the registration form$')
|
||||
def i_fill_in_the_registration_form(step):
|
||||
register_form = world.browser.find_by_css('form#register_form')
|
||||
@@ -9,15 +10,19 @@ def i_fill_in_the_registration_form(step):
|
||||
register_form.find_by_name('name').fill('Robot Studio')
|
||||
register_form.find_by_name('terms_of_service').check()
|
||||
|
||||
@step('I press the "([^"]*)" button on the registration form$')
|
||||
def i_press_the_button_on_the_registration_form(step, button):
|
||||
|
||||
@step('I press the Create My Account button on the registration form$')
|
||||
def i_press_the_button_on_the_registration_form(step):
|
||||
register_form = world.browser.find_by_css('form#register_form')
|
||||
register_form.find_by_value(button).click()
|
||||
submit_css = 'button#submit'
|
||||
register_form.find_by_css(submit_css).click()
|
||||
|
||||
|
||||
@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')
|
||||
|
||||
|
||||
@step(u'I should see the message "([^"]*)"$')
|
||||
def i_should_see_the_message(step, msg):
|
||||
assert world.browser.is_text_present(msg, 5)
|
||||
assert world.browser.is_text_present(msg, 5)
|
||||
|
||||
@@ -6,11 +6,13 @@ from nose.tools import assert_true, assert_false, assert_equal
|
||||
from logging import getLogger
|
||||
logger = getLogger(__name__)
|
||||
|
||||
|
||||
@step(u'I have a course with no sections$')
|
||||
def have_a_course(step):
|
||||
clear_courses()
|
||||
course = CourseFactory.create()
|
||||
|
||||
|
||||
@step(u'I have a course with 1 section$')
|
||||
def have_a_course_with_1_section(step):
|
||||
clear_courses()
|
||||
@@ -18,8 +20,9 @@ def have_a_course_with_1_section(step):
|
||||
section = ItemFactory.create(parent_location=course.location)
|
||||
subsection1 = ItemFactory.create(
|
||||
parent_location=section.location,
|
||||
template = 'i4x://edx/templates/sequential/Empty',
|
||||
display_name = 'Subsection One',)
|
||||
template='i4x://edx/templates/sequential/Empty',
|
||||
display_name='Subsection One',)
|
||||
|
||||
|
||||
@step(u'I have a course with multiple sections$')
|
||||
def have_a_course_with_two_sections(step):
|
||||
@@ -28,19 +31,20 @@ def have_a_course_with_two_sections(step):
|
||||
section = ItemFactory.create(parent_location=course.location)
|
||||
subsection1 = ItemFactory.create(
|
||||
parent_location=section.location,
|
||||
template = 'i4x://edx/templates/sequential/Empty',
|
||||
display_name = 'Subsection One',)
|
||||
template='i4x://edx/templates/sequential/Empty',
|
||||
display_name='Subsection One',)
|
||||
section2 = ItemFactory.create(
|
||||
parent_location=course.location,
|
||||
display_name='Section Two',)
|
||||
subsection2 = ItemFactory.create(
|
||||
parent_location=section2.location,
|
||||
template = 'i4x://edx/templates/sequential/Empty',
|
||||
display_name = 'Subsection Alpha',)
|
||||
template='i4x://edx/templates/sequential/Empty',
|
||||
display_name='Subsection Alpha',)
|
||||
subsection3 = ItemFactory.create(
|
||||
parent_location=section2.location,
|
||||
template = 'i4x://edx/templates/sequential/Empty',
|
||||
display_name = 'Subsection Beta',)
|
||||
template='i4x://edx/templates/sequential/Empty',
|
||||
display_name='Subsection Beta',)
|
||||
|
||||
|
||||
@step(u'I navigate to the course overview page$')
|
||||
def navigate_to_the_course_overview_page(step):
|
||||
@@ -48,15 +52,18 @@ def navigate_to_the_course_overview_page(step):
|
||||
course_locator = '.class-name'
|
||||
css_click(course_locator)
|
||||
|
||||
|
||||
@step(u'I navigate to the courseware page of a course with multiple sections')
|
||||
def nav_to_the_courseware_page_of_a_course_with_multiple_sections(step):
|
||||
step.given('I have a course with multiple sections')
|
||||
step.given('I navigate to the course overview page')
|
||||
|
||||
|
||||
@step(u'I add a section')
|
||||
def i_add_a_section(step):
|
||||
add_section(name='My New Section That I Just Added')
|
||||
|
||||
|
||||
@step(u'I click the "([^"]*)" link$')
|
||||
def i_click_the_text_span(step, text):
|
||||
span_locator = '.toggle-button-sections span'
|
||||
@@ -65,16 +72,19 @@ def i_click_the_text_span(step, text):
|
||||
assert_equal(world.browser.find_by_css(span_locator).value, text)
|
||||
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)
|
||||
|
||||
|
||||
@step(u'I expand the first section$')
|
||||
def i_expand_a_section(step):
|
||||
expand_locator = 'section.courseware-section a.expand'
|
||||
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'
|
||||
@@ -82,6 +92,7 @@ def i_see_the_span_with_text(step, text):
|
||||
assert_equal(world.browser.find_by_css(span_locator).value, text)
|
||||
assert_true(world.browser.find_by_css(span_locator).visible)
|
||||
|
||||
|
||||
@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
|
||||
@@ -89,6 +100,7 @@ def i_do_not_see_the_span_with_text(step, text):
|
||||
assert_true(world.browser.is_element_present_by_css(span_locator))
|
||||
assert_false(world.browser.find_by_css(span_locator).visible)
|
||||
|
||||
|
||||
@step(u'all sections are expanded$')
|
||||
def all_sections_are_expanded(step):
|
||||
subsection_locator = 'div.subsection-list'
|
||||
@@ -96,9 +108,10 @@ def all_sections_are_expanded(step):
|
||||
for s in subsections:
|
||||
assert_true(s.visible)
|
||||
|
||||
|
||||
@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)
|
||||
for s in subsections:
|
||||
assert_false(s.visible)
|
||||
assert_false(s.visible)
|
||||
|
||||
@@ -2,6 +2,8 @@ from lettuce import world, step
|
||||
from common import *
|
||||
|
||||
############### ACTIONS ####################
|
||||
|
||||
|
||||
@step('I have opened a new course section in Studio$')
|
||||
def i_have_opened_a_new_course_section(step):
|
||||
clear_courses()
|
||||
@@ -9,31 +11,37 @@ def i_have_opened_a_new_course_section(step):
|
||||
create_a_course()
|
||||
add_section()
|
||||
|
||||
|
||||
@step('I click the New Subsection link')
|
||||
def i_click_the_new_subsection_link(step):
|
||||
css = 'a.new-subsection-item'
|
||||
css_click(css)
|
||||
|
||||
|
||||
@step('I enter the subsection name and click save$')
|
||||
def i_save_subsection_name(step):
|
||||
name_css = 'input.new-subsection-name-input'
|
||||
save_css = 'input.new-subsection-name-save'
|
||||
css_fill(name_css,'Subsection One')
|
||||
css_fill(name_css, 'Subsection One')
|
||||
css_click(save_css)
|
||||
|
||||
|
||||
@step('I have added a new subsection$')
|
||||
def i_have_added_a_new_subsection(step):
|
||||
add_subsection()
|
||||
|
||||
############ ASSERTIONS ###################
|
||||
|
||||
|
||||
@step('I see my subsection on the Courseware page$')
|
||||
def i_see_my_subsection_on_the_courseware_page(step):
|
||||
css = 'span.subsection-name'
|
||||
assert world.browser.is_element_present_by_css(css)
|
||||
assert world.browser.is_element_present_by_css(css)
|
||||
css = 'span.subsection-name-value'
|
||||
assert_css_with_text(css,'Subsection One')
|
||||
assert_css_with_text(css, 'Subsection One')
|
||||
|
||||
|
||||
@step('the subsection does not exist$')
|
||||
def the_subsection_does_not_exist(step):
|
||||
css = 'span.subsection-name'
|
||||
assert world.browser.is_element_not_present_by_css(css)
|
||||
assert world.browser.is_element_not_present_by_css(css)
|
||||
|
||||
@@ -14,6 +14,7 @@ from auth.authz import _copy_course_group
|
||||
# To run from command line: rake cms:clone SOURCE_LOC=MITx/111/Foo1 DEST_LOC=MITx/135/Foo3
|
||||
#
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = \
|
||||
'''Clone a MongoDB backed course to another location'''
|
||||
|
||||
@@ -15,26 +15,32 @@ from auth.authz import _delete_course_group
|
||||
# To run from command line: rake cms:delete_course LOC=MITx/111/Foo1
|
||||
#
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = \
|
||||
'''Delete a MongoDB backed course'''
|
||||
|
||||
def handle(self, *args, **options):
|
||||
if len(args) != 1:
|
||||
raise CommandError("delete_course requires one argument: <location>")
|
||||
if len(args) != 1 and len(args) != 2:
|
||||
raise CommandError("delete_course requires one or more arguments: <location> |commit|")
|
||||
|
||||
loc_str = args[0]
|
||||
|
||||
commit = False
|
||||
if len(args) == 2:
|
||||
commit = args[1] == 'commit'
|
||||
|
||||
if commit:
|
||||
print 'Actually going to delete the course from DB....'
|
||||
|
||||
ms = modulestore('direct')
|
||||
cs = contentstore()
|
||||
|
||||
if query_yes_no("Deleting course {0}. Confirm?".format(loc_str), default="no"):
|
||||
if query_yes_no("Are you sure. This action cannot be undone!", default="no"):
|
||||
loc = CourseDescriptor.id_to_location(loc_str)
|
||||
if delete_course(ms, cs, loc) == True:
|
||||
if delete_course(ms, cs, loc, commit) == True:
|
||||
print 'removing User permissions from course....'
|
||||
# in the django layer, we need to remove all the user permissions groups associated with this course
|
||||
_delete_course_group(loc)
|
||||
|
||||
|
||||
|
||||
if commit:
|
||||
_delete_course_group(loc)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import sys
|
||||
|
||||
|
||||
def query_yes_no(question, default="yes"):
|
||||
"""Ask a yes/no question via raw_input() and return their answer.
|
||||
|
||||
@@ -30,4 +31,4 @@ def query_yes_no(question, default="yes"):
|
||||
return valid[choice]
|
||||
else:
|
||||
sys.stdout.write("Please respond with 'yes' or 'no' "\
|
||||
"(or 'y' or 'n').\n")
|
||||
"(or 'y' or 'n').\n")
|
||||
|
||||
@@ -1,84 +1,92 @@
|
||||
import logging
|
||||
from static_replace import replace_urls
|
||||
from static_replace import replace_static_urls
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from lxml import etree
|
||||
import re
|
||||
from django.http import HttpResponseBadRequest, Http404
|
||||
from django.http import Http404
|
||||
|
||||
def get_module_info(store, location, parent_location = None, rewrite_static_links = False):
|
||||
try:
|
||||
if location.revision is None:
|
||||
module = store.get_item(location)
|
||||
else:
|
||||
module = store.get_item(location)
|
||||
except ItemNotFoundError:
|
||||
raise Http404
|
||||
|
||||
data = module.definition['data']
|
||||
if rewrite_static_links:
|
||||
data = replace_urls(module.definition['data'], course_namespace = Location([module.location.tag, module.location.org, module.location.course, None, None]))
|
||||
def get_module_info(store, location, parent_location=None, rewrite_static_links=False):
|
||||
try:
|
||||
if location.revision is None:
|
||||
module = store.get_item(location)
|
||||
else:
|
||||
module = store.get_item(location)
|
||||
except ItemNotFoundError:
|
||||
# create a new one
|
||||
template_location = Location(['i4x', 'edx', 'templates', location.category, 'Empty'])
|
||||
module = store.clone_item(template_location, location)
|
||||
|
||||
return {
|
||||
data = module.definition['data']
|
||||
if rewrite_static_links:
|
||||
data = replace_static_urls(
|
||||
module.definition['data'],
|
||||
None,
|
||||
course_namespace=Location([
|
||||
module.location.tag,
|
||||
module.location.org,
|
||||
module.location.course,
|
||||
None,
|
||||
None
|
||||
])
|
||||
)
|
||||
|
||||
return {
|
||||
'id': module.location.url(),
|
||||
'data': data,
|
||||
'metadata': module.metadata
|
||||
}
|
||||
|
||||
|
||||
def set_module_info(store, location, post_data):
|
||||
module = None
|
||||
isNew = False
|
||||
try:
|
||||
if location.revision is None:
|
||||
module = store.get_item(location)
|
||||
else:
|
||||
module = store.get_item(location)
|
||||
except:
|
||||
pass
|
||||
module = None
|
||||
try:
|
||||
if location.revision is None:
|
||||
module = store.get_item(location)
|
||||
else:
|
||||
module = store.get_item(location)
|
||||
except:
|
||||
pass
|
||||
|
||||
if module is None:
|
||||
# new module at this location
|
||||
# presume that we have an 'Empty' template
|
||||
template_location = Location(['i4x', 'edx', 'templates', location.category, 'Empty'])
|
||||
module = store.clone_item(template_location, location)
|
||||
isNew = True
|
||||
if module is None:
|
||||
# new module at this location
|
||||
# presume that we have an 'Empty' template
|
||||
template_location = Location(['i4x', 'edx', 'templates', location.category, 'Empty'])
|
||||
module = store.clone_item(template_location, location)
|
||||
|
||||
if post_data.get('data') is not None:
|
||||
data = post_data['data']
|
||||
store.update_item(location, data)
|
||||
|
||||
# cdodge: note calling request.POST.get('children') will return None if children is an empty array
|
||||
# so it lead to a bug whereby the last component to be deleted in the UI was not actually
|
||||
# deleting the children object from the children collection
|
||||
if 'children' in post_data and post_data['children'] is not None:
|
||||
children = post_data['children']
|
||||
store.update_children(location, children)
|
||||
if post_data.get('data') is not None:
|
||||
data = post_data['data']
|
||||
store.update_item(location, data)
|
||||
|
||||
# cdodge: also commit any metadata which might have been passed along in the
|
||||
# POST from the client, if it is there
|
||||
# NOTE, that the postback is not the complete metadata, as there's system metadata which is
|
||||
# not presented to the end-user for editing. So let's fetch the original and
|
||||
# 'apply' the submitted metadata, so we don't end up deleting system metadata
|
||||
if post_data.get('metadata') is not None:
|
||||
posted_metadata = post_data['metadata']
|
||||
# cdodge: note calling request.POST.get('children') will return None if children is an empty array
|
||||
# so it lead to a bug whereby the last component to be deleted in the UI was not actually
|
||||
# deleting the children object from the children collection
|
||||
if 'children' in post_data and post_data['children'] is not None:
|
||||
children = post_data['children']
|
||||
store.update_children(location, children)
|
||||
|
||||
# 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():
|
||||
|
||||
# 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]
|
||||
del posted_metadata[metadata_key]
|
||||
|
||||
# overlay the new metadata over the modulestore sourced collection to support partial updates
|
||||
module.metadata.update(posted_metadata)
|
||||
|
||||
# commit to datastore
|
||||
store.update_metadata(location, module.metadata)
|
||||
# cdodge: also commit any metadata which might have been passed along in the
|
||||
# POST from the client, if it is there
|
||||
# NOTE, that the postback is not the complete metadata, as there's system metadata which is
|
||||
# not presented to the end-user for editing. So let's fetch the original and
|
||||
# 'apply' the submitted metadata, so we don't end up deleting system metadata
|
||||
if 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():
|
||||
|
||||
# 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]
|
||||
del posted_metadata[metadata_key]
|
||||
|
||||
# overlay the new metadata over the modulestore sourced collection to support partial updates
|
||||
module.metadata.update(posted_metadata)
|
||||
|
||||
# commit to datastore
|
||||
store.update_metadata(location, module.metadata)
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
from factory import Factory
|
||||
from datetime import datetime
|
||||
import uuid
|
||||
from time import gmtime
|
||||
from uuid import uuid4
|
||||
from student.models import (User, UserProfile, Registration,
|
||||
CourseEnrollmentAllowed)
|
||||
from django.contrib.auth.models import Group
|
||||
|
||||
from django.contrib.auth.models import Group
|
||||
|
||||
@@ -13,54 +14,44 @@ from student.models import (User, UserProfile, Registration,
|
||||
CourseEnrollmentAllowed)
|
||||
from django.contrib.auth.models import Group
|
||||
|
||||
def XMODULE_COURSE_CREATION(class_to_create, **kwargs):
|
||||
return XModuleCourseFactory._create(class_to_create, **kwargs)
|
||||
class UserProfileFactory(Factory):
|
||||
FACTORY_FOR = UserProfile
|
||||
|
||||
def XMODULE_ITEM_CREATION(class_to_create, **kwargs):
|
||||
return XModuleItemFactory._create(class_to_create, **kwargs)
|
||||
user = None
|
||||
name = 'Robot Studio'
|
||||
courseware = 'course.xml'
|
||||
|
||||
class XModuleCourseFactory(Factory):
|
||||
"""
|
||||
Factory for XModule courses.
|
||||
"""
|
||||
|
||||
ABSTRACT_FACTORY = True
|
||||
_creation_function = (XMODULE_COURSE_CREATION,)
|
||||
class RegistrationFactory(Factory):
|
||||
FACTORY_FOR = Registration
|
||||
|
||||
@classmethod
|
||||
def _create(cls, target_class, *args, **kwargs):
|
||||
user = None
|
||||
activation_key = uuid4().hex
|
||||
|
||||
template = Location('i4x', 'edx', 'templates', 'course', 'Empty')
|
||||
org = kwargs.get('org')
|
||||
number = kwargs.get('number')
|
||||
display_name = kwargs.get('display_name')
|
||||
location = Location('i4x', org, number,
|
||||
'course', Location.clean(display_name))
|
||||
|
||||
store = modulestore('direct')
|
||||
class UserFactory(Factory):
|
||||
FACTORY_FOR = User
|
||||
|
||||
# Write the data to the mongo datastore
|
||||
new_course = store.clone_item(template, location)
|
||||
username = 'robot'
|
||||
email = 'robot@edx.org'
|
||||
password = 'test'
|
||||
first_name = 'Robot'
|
||||
last_name = 'Tester'
|
||||
is_staff = False
|
||||
is_active = True
|
||||
is_superuser = False
|
||||
last_login = datetime.now()
|
||||
date_joined = datetime.now()
|
||||
|
||||
# This metadata code was copied from cms/djangoapps/contentstore/views.py
|
||||
if display_name is not None:
|
||||
new_course.metadata['display_name'] = display_name
|
||||
|
||||
new_course.metadata['data_dir'] = uuid4().hex
|
||||
new_course.metadata['start'] = stringify_time(gmtime())
|
||||
new_course.tabs = [{"type": "courseware"},
|
||||
{"type": "course_info", "name": "Course Info"},
|
||||
{"type": "discussion", "name": "Discussion"},
|
||||
{"type": "wiki", "name": "Wiki"},
|
||||
{"type": "progress", "name": "Progress"}]
|
||||
class GroupFactory(Factory):
|
||||
FACTORY_FOR = Group
|
||||
|
||||
# Update the data in the mongo datastore
|
||||
store.update_metadata(new_course.location.url(), new_course.own_metadata)
|
||||
name = 'test_group'
|
||||
|
||||
return new_course
|
||||
|
||||
class Course:
|
||||
pass
|
||||
class CourseEnrollmentAllowedFactory(Factory):
|
||||
FACTORY_FOR = CourseEnrollmentAllowed
|
||||
|
||||
class CourseFactory(XModuleCourseFactory):
|
||||
FACTORY_FOR = Course
|
||||
@@ -157,6 +148,3 @@ class GroupFactory(Factory):
|
||||
|
||||
class CourseEnrollmentAllowedFactory(Factory):
|
||||
FACTORY_FOR = CourseEnrollmentAllowed
|
||||
|
||||
email = 'test@edx.org'
|
||||
course_id = 'edX/test/2012_Fall'
|
||||
|
||||
512
cms/djangoapps/contentstore/tests/test_contentstore.py
Normal file
@@ -0,0 +1,512 @@
|
||||
import json
|
||||
import shutil
|
||||
from django.test.client import Client
|
||||
from django.test.utils import override_settings
|
||||
from django.conf import settings
|
||||
from django.core.urlresolvers import reverse
|
||||
from path import path
|
||||
from tempdir import mkdtemp_clean
|
||||
import json
|
||||
from fs.osfs import OSFS
|
||||
import copy
|
||||
from mock import Mock
|
||||
from json import dumps, loads
|
||||
|
||||
from student.models import Registration
|
||||
from django.contrib.auth.models import User
|
||||
from cms.djangoapps.contentstore.utils import get_modulestore
|
||||
|
||||
from utils import ModuleStoreTestCase, parse_json
|
||||
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
|
||||
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.store_utilities import clone_course
|
||||
from xmodule.modulestore.store_utilities import delete_course
|
||||
from xmodule.modulestore.django import modulestore, _MODULESTORES
|
||||
from xmodule.contentstore.django import contentstore
|
||||
from xmodule.templates import update_templates
|
||||
from xmodule.modulestore.xml_exporter import export_to_xml
|
||||
from xmodule.modulestore.xml_importer import import_from_xml
|
||||
from xmodule.templates import update_templates
|
||||
|
||||
from xmodule.capa_module import CapaDescriptor
|
||||
from xmodule.course_module import CourseDescriptor
|
||||
from xmodule.seq_module import SequenceDescriptor
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
|
||||
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')
|
||||
|
||||
|
||||
@override_settings(MODULESTORE=TEST_DATA_MODULESTORE)
|
||||
class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
"""
|
||||
Tests that rely on the toy courses.
|
||||
TODO: refactor using CourseFactory so they do not.
|
||||
"""
|
||||
def setUp(self):
|
||||
uname = 'testuser'
|
||||
email = 'test+courses@edx.org'
|
||||
password = 'foo'
|
||||
|
||||
# Create the use so we can log them in.
|
||||
self.user = User.objects.create_user(uname, email, password)
|
||||
|
||||
# Note that we do not actually need to do anything
|
||||
# for registration if we directly mark them active.
|
||||
self.user.is_active = True
|
||||
# Staff has access to view all courses
|
||||
self.user.is_staff = True
|
||||
self.user.save()
|
||||
|
||||
self.client = Client()
|
||||
self.client.login(username=uname, password=password)
|
||||
|
||||
|
||||
def check_edit_unit(self, test_course_name):
|
||||
import_from_xml(modulestore(), 'common/test/data/', [test_course_name])
|
||||
|
||||
for descriptor in modulestore().get_items(Location(None, None, 'vertical', None, None)):
|
||||
print "Checking ", descriptor.location.url()
|
||||
print descriptor.__class__, descriptor.location
|
||||
resp = self.client.get(reverse('edit_unit', kwargs={'location': descriptor.location.url()}))
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
def test_edit_unit_toy(self):
|
||||
self.check_edit_unit('toy')
|
||||
|
||||
def test_edit_unit_full(self):
|
||||
self.check_edit_unit('full')
|
||||
|
||||
def test_static_tab_reordering(self):
|
||||
import_from_xml(modulestore(), 'common/test/data/', ['full'])
|
||||
|
||||
ms = modulestore('direct')
|
||||
course = ms.get_item(Location(['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None]))
|
||||
|
||||
# reverse the ordering
|
||||
reverse_tabs = []
|
||||
for tab in course.tabs:
|
||||
if tab['type'] == 'static_tab':
|
||||
reverse_tabs.insert(0, 'i4x://edX/full/static_tab/{0}'.format(tab['url_slug']))
|
||||
|
||||
resp = self.client.post(reverse('reorder_static_tabs'), json.dumps({'tabs': reverse_tabs}), "application/json")
|
||||
|
||||
course = ms.get_item(Location(['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None]))
|
||||
|
||||
# compare to make sure that the tabs information is in the expected order after the server call
|
||||
course_tabs = []
|
||||
for tab in course.tabs:
|
||||
if tab['type'] == 'static_tab':
|
||||
course_tabs.append('i4x://edX/full/static_tab/{0}'.format(tab['url_slug']))
|
||||
|
||||
self.assertEqual(reverse_tabs, course_tabs)
|
||||
|
||||
def test_about_overrides(self):
|
||||
'''
|
||||
This test case verifies that a course can use specialized override for about data, e.g. /about/Fall_2012/effort.html
|
||||
while there is a base definition in /about/effort.html
|
||||
'''
|
||||
import_from_xml(modulestore(), 'common/test/data/', ['full'])
|
||||
ms = modulestore('direct')
|
||||
effort = ms.get_item(Location(['i4x', 'edX', 'full', 'about', 'effort', None]))
|
||||
self.assertEqual(effort.definition['data'], '6 hours')
|
||||
|
||||
# this one should be in a non-override folder
|
||||
effort = ms.get_item(Location(['i4x', 'edX', 'full', 'about', 'end_date', None]))
|
||||
self.assertEqual(effort.definition['data'], 'TBD')
|
||||
|
||||
def test_remove_hide_progress_tab(self):
|
||||
import_from_xml(modulestore(), 'common/test/data/', ['full'])
|
||||
|
||||
ms = modulestore('direct')
|
||||
cs = contentstore()
|
||||
|
||||
source_location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
|
||||
course = ms.get_item(source_location)
|
||||
self.assertNotIn('hide_progress_tab', course.metadata)
|
||||
|
||||
def test_clone_course(self):
|
||||
|
||||
course_data = {
|
||||
'template': 'i4x://edx/templates/course/Empty',
|
||||
'org': 'MITx',
|
||||
'number': '999',
|
||||
'display_name': 'Robot Super Course',
|
||||
}
|
||||
|
||||
import_from_xml(modulestore(), 'common/test/data/', ['full'])
|
||||
|
||||
resp = self.client.post(reverse('create_new_course'), course_data)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
data = parse_json(resp)
|
||||
self.assertEqual(data['id'], 'i4x://MITx/999/course/Robot_Super_Course')
|
||||
|
||||
ms = modulestore('direct')
|
||||
cs = contentstore()
|
||||
|
||||
source_location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
|
||||
dest_location = CourseDescriptor.id_to_location('MITx/999/Robot_Super_Course')
|
||||
|
||||
clone_course(ms, cs, source_location, dest_location)
|
||||
|
||||
# now loop through all the units in the course and verify that the clone can render them, which
|
||||
# means the objects are at least present
|
||||
items = ms.get_items(Location(['i4x', 'edX', 'full', 'vertical', None]))
|
||||
self.assertGreater(len(items), 0)
|
||||
clone_items = ms.get_items(Location(['i4x', 'MITx', '999', 'vertical', None]))
|
||||
self.assertGreater(len(clone_items), 0)
|
||||
for descriptor in items:
|
||||
new_loc = descriptor.location._replace(org='MITx', course='999')
|
||||
print "Checking {0} should now also be at {1}".format(descriptor.location.url(), new_loc.url())
|
||||
resp = self.client.get(reverse('edit_unit', kwargs={'location': new_loc.url()}))
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
def test_delete_course(self):
|
||||
import_from_xml(modulestore(), 'common/test/data/', ['full'])
|
||||
|
||||
ms = modulestore('direct')
|
||||
cs = contentstore()
|
||||
|
||||
location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
|
||||
|
||||
delete_course(ms, cs, location, commit=True)
|
||||
|
||||
items = ms.get_items(Location(['i4x', 'edX', 'full', 'vertical', None]))
|
||||
self.assertEqual(len(items), 0)
|
||||
|
||||
def verify_content_existence(self, modulestore, root_dir, location, dirname, category_name, filename_suffix=''):
|
||||
fs = OSFS(root_dir / 'test_export')
|
||||
self.assertTrue(fs.exists(dirname))
|
||||
|
||||
query_loc = Location('i4x', location.org, location.course, category_name, None)
|
||||
items = modulestore.get_items(query_loc)
|
||||
|
||||
for item in items:
|
||||
fs = OSFS(root_dir / ('test_export/' + dirname))
|
||||
self.assertTrue(fs.exists(item.location.name + filename_suffix))
|
||||
|
||||
def test_export_course(self):
|
||||
ms = modulestore('direct')
|
||||
cs = contentstore()
|
||||
|
||||
import_from_xml(ms, 'common/test/data/', ['full'])
|
||||
location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
|
||||
|
||||
root_dir = path(mkdtemp_clean())
|
||||
|
||||
print 'Exporting to tempdir = {0}'.format(root_dir)
|
||||
|
||||
# export out to a tempdir
|
||||
export_to_xml(ms, cs, location, root_dir, 'test_export')
|
||||
|
||||
# check for static tabs
|
||||
self.verify_content_existence(ms, root_dir, location, 'tabs', 'static_tab', '.html')
|
||||
|
||||
# check for custom_tags
|
||||
self.verify_content_existence(ms, root_dir, location, 'info', 'course_info', '.html')
|
||||
|
||||
# check for custom_tags
|
||||
self.verify_content_existence(ms, root_dir, location, 'custom_tags', 'custom_tag_template')
|
||||
|
||||
# check for graiding_policy.json
|
||||
fs = OSFS(root_dir / 'test_export/policies/6.002_Spring_2012')
|
||||
self.assertTrue(fs.exists('grading_policy.json'))
|
||||
|
||||
course = ms.get_item(location)
|
||||
# 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'])
|
||||
|
||||
#check for policy.json
|
||||
self.assertTrue(fs.exists('policy.json'))
|
||||
|
||||
# compare what's on disk to what we have in the course module
|
||||
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)
|
||||
|
||||
# remove old course
|
||||
delete_course(ms, cs, location)
|
||||
|
||||
# reimport
|
||||
import_from_xml(ms, root_dir, ['test_export'])
|
||||
|
||||
items = ms.get_items(Location(['i4x', 'edX', 'full', 'vertical', None]))
|
||||
self.assertGreater(len(items), 0)
|
||||
for descriptor in items:
|
||||
print "Checking {0}....".format(descriptor.location.url())
|
||||
resp = self.client.get(reverse('edit_unit', kwargs={'location': descriptor.location.url()}))
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
shutil.rmtree(root_dir)
|
||||
|
||||
def test_course_handouts_rewrites(self):
|
||||
ms = modulestore('direct')
|
||||
cs = contentstore()
|
||||
|
||||
# import a test course
|
||||
import_from_xml(ms, 'common/test/data/', ['full'])
|
||||
|
||||
handout_location = Location(['i4x', 'edX', 'full', 'course_info', 'handouts'])
|
||||
|
||||
# get module info
|
||||
resp = self.client.get(reverse('module_info', kwargs={'module_location': handout_location}))
|
||||
|
||||
# make sure we got a successful response
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
# check that /static/ has been converted to the full path
|
||||
# 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')
|
||||
|
||||
|
||||
|
||||
class ContentStoreTest(ModuleStoreTestCase):
|
||||
"""
|
||||
Tests for the CMS ContentStore application.
|
||||
"""
|
||||
def setUp(self):
|
||||
"""
|
||||
These tests need a user in the DB so that the django Test Client
|
||||
can log them in.
|
||||
They inherit from the ModuleStoreTestCase class so that the mongodb collection
|
||||
will be cleared out before each test case execution and deleted
|
||||
afterwards.
|
||||
"""
|
||||
uname = 'testuser'
|
||||
email = 'test+courses@edx.org'
|
||||
password = 'foo'
|
||||
|
||||
# Create the use so we can log them in.
|
||||
self.user = User.objects.create_user(uname, email, password)
|
||||
|
||||
# Note that we do not actually need to do anything
|
||||
# for registration if we directly mark them active.
|
||||
self.user.is_active = True
|
||||
# Staff has access to view all courses
|
||||
self.user.is_staff = True
|
||||
self.user.save()
|
||||
|
||||
self.client = Client()
|
||||
self.client.login(username=uname, password=password)
|
||||
|
||||
self.course_data = {
|
||||
'template': 'i4x://edx/templates/course/Empty',
|
||||
'org': 'MITx',
|
||||
'number': '999',
|
||||
'display_name': 'Robot Super Course',
|
||||
}
|
||||
|
||||
def test_create_course(self):
|
||||
"""Test new course creation - happy path"""
|
||||
resp = self.client.post(reverse('create_new_course'), self.course_data)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
data = parse_json(resp)
|
||||
self.assertEqual(data['id'], 'i4x://MITx/999/course/Robot_Super_Course')
|
||||
|
||||
def test_create_course_duplicate_course(self):
|
||||
"""Test new course creation - error path"""
|
||||
resp = self.client.post(reverse('create_new_course'), self.course_data)
|
||||
resp = self.client.post(reverse('create_new_course'), self.course_data)
|
||||
data = parse_json(resp)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertEqual(data['ErrMsg'], 'There is already a course defined with this name.')
|
||||
|
||||
def test_create_course_duplicate_number(self):
|
||||
"""Test new course creation - error path"""
|
||||
resp = self.client.post(reverse('create_new_course'), self.course_data)
|
||||
self.course_data['display_name'] = 'Robot Super Course Two'
|
||||
|
||||
resp = self.client.post(reverse('create_new_course'), self.course_data)
|
||||
data = parse_json(resp)
|
||||
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertEqual(data['ErrMsg'],
|
||||
'There is already a course defined with the same organization and course number.')
|
||||
|
||||
def test_create_course_with_bad_organization(self):
|
||||
"""Test new course creation - error path for bad organization name"""
|
||||
self.course_data['org'] = 'University of California, Berkeley'
|
||||
resp = self.client.post(reverse('create_new_course'), self.course_data)
|
||||
data = parse_json(resp)
|
||||
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertEqual(data['ErrMsg'],
|
||||
"Unable to create course 'Robot Super Course'.\n\nInvalid characters in 'University of California, Berkeley'.")
|
||||
|
||||
def test_course_index_view_with_no_courses(self):
|
||||
"""Test viewing the index page with no courses"""
|
||||
# Create a course so there is something to view
|
||||
resp = self.client.get(reverse('index'))
|
||||
self.assertContains(resp,
|
||||
'<h1 class="title-1">My Courses</h1>',
|
||||
status_code=200,
|
||||
html=True)
|
||||
|
||||
def test_course_factory(self):
|
||||
"""Test that the course factory works correctly."""
|
||||
course = CourseFactory.create()
|
||||
self.assertIsInstance(course, CourseDescriptor)
|
||||
|
||||
def test_item_factory(self):
|
||||
"""Test that the item factory works correctly."""
|
||||
course = CourseFactory.create()
|
||||
item = ItemFactory.create(parent_location=course.location)
|
||||
self.assertIsInstance(item, SequenceDescriptor)
|
||||
|
||||
def test_course_index_view_with_course(self):
|
||||
"""Test viewing the index page with an existing course"""
|
||||
CourseFactory.create(display_name='Robot Super Educational Course')
|
||||
resp = self.client.get(reverse('index'))
|
||||
self.assertContains(resp,
|
||||
'<span class="class-name">Robot Super Educational Course</span>',
|
||||
status_code=200,
|
||||
html=True)
|
||||
|
||||
def test_course_overview_view_with_course(self):
|
||||
"""Test viewing the course overview page with an existing course"""
|
||||
CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course')
|
||||
|
||||
data = {
|
||||
'org': 'MITx',
|
||||
'course': '999',
|
||||
'name': Location.clean('Robot Super Course'),
|
||||
}
|
||||
|
||||
resp = self.client.get(reverse('course_index', kwargs=data))
|
||||
self.assertContains(resp,
|
||||
'<article class="courseware-overview" data-course-id="i4x://MITx/999/course/Robot_Super_Course">',
|
||||
status_code=200,
|
||||
html=True)
|
||||
|
||||
def test_clone_item(self):
|
||||
"""Test cloning an item. E.g. creating a new section"""
|
||||
CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course')
|
||||
|
||||
section_data = {
|
||||
'parent_location': 'i4x://MITx/999/course/Robot_Super_Course',
|
||||
'template': 'i4x://edx/templates/chapter/Empty',
|
||||
'display_name': 'Section One',
|
||||
}
|
||||
|
||||
resp = self.client.post(reverse('clone_item'), section_data)
|
||||
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
data = parse_json(resp)
|
||||
self.assertRegexpMatches(data['id'],
|
||||
'^i4x:\/\/MITx\/999\/chapter\/([0-9]|[a-f]){32}$')
|
||||
|
||||
def test_capa_module(self):
|
||||
"""Test that a problem treats markdown specially."""
|
||||
course = CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course')
|
||||
|
||||
problem_data = {
|
||||
'parent_location': 'i4x://MITx/999/course/Robot_Super_Course',
|
||||
'template': 'i4x://edx/templates/problem/Blank_Common_Problem'
|
||||
}
|
||||
|
||||
resp = self.client.post(reverse('clone_item'), problem_data)
|
||||
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
payload = parse_json(resp)
|
||||
problem_loc = payload['id']
|
||||
problem = get_modulestore(problem_loc).get_item(problem_loc)
|
||||
# should be a CapaDescriptor
|
||||
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'])
|
||||
ms = modulestore('direct')
|
||||
did_load_item = False
|
||||
try:
|
||||
ms.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)
|
||||
|
||||
def test_metadata_inheritance(self):
|
||||
import_from_xml(modulestore(), 'common/test/data/', ['full'])
|
||||
|
||||
ms = modulestore('direct')
|
||||
course = ms.get_item(Location(['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None]))
|
||||
|
||||
verticals = ms.get_items(['i4x', 'edX', 'full', 'vertical', None, None])
|
||||
|
||||
# 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.assertGreater(len(verticals), 0)
|
||||
|
||||
new_component_location = Location('i4x', 'edX', 'full', 'html', 'new_component')
|
||||
source_template_location = Location('i4x', 'edx', 'templates', 'html', 'Blank_HTML_Page')
|
||||
|
||||
# crate a new module and add it as a child to a vertical
|
||||
ms.clone_item(source_template_location, new_component_location)
|
||||
parent = verticals[0]
|
||||
ms.update_children(parent.location, parent.definition.get('children', []) + [new_component_location.url()])
|
||||
|
||||
# flush the cache
|
||||
ms.get_cached_metadata_inheritance_tree(new_component_location, -1)
|
||||
new_module = ms.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.metadata['graceperiod'], new_module.metadata['graceperiod'])
|
||||
|
||||
self.assertEqual(course.metadata['xqa_key'], new_module.metadata['xqa_key'])
|
||||
|
||||
#
|
||||
# now let's define an override at the leaf node level
|
||||
#
|
||||
new_module.metadata['graceperiod'] = '1 day'
|
||||
ms.update_metadata(new_module.location, new_module.metadata)
|
||||
|
||||
# flush the cache and refetch
|
||||
ms.get_cached_metadata_inheritance_tree(new_component_location, -1)
|
||||
new_module = ms.get_item(new_component_location)
|
||||
|
||||
self.assertIn('graceperiod', new_module.metadata)
|
||||
self.assertEqual('1 day', new_module.metadata['graceperiod'])
|
||||
|
||||
|
||||
class TemplateTestCase(ModuleStoreTestCase):
|
||||
|
||||
def test_template_cleanup(self):
|
||||
ms = modulestore('direct')
|
||||
|
||||
# insert a bogus template in the store
|
||||
bogus_template_location = Location('i4x', 'edx', 'templates', 'html', 'bogus')
|
||||
source_template_location = Location('i4x', 'edx', 'templates', 'html', 'Blank_HTML_Page')
|
||||
|
||||
ms.clone_item(source_template_location, bogus_template_location)
|
||||
|
||||
verify_create = ms.get_item(bogus_template_location)
|
||||
self.assertIsNotNone(verify_create)
|
||||
|
||||
# now run cleanup
|
||||
update_templates()
|
||||
|
||||
# now try to find dangling template, it should not be in DB any longer
|
||||
asserted = False
|
||||
try:
|
||||
verify_create = ms.get_item(bogus_template_location)
|
||||
except ItemNotFoundError:
|
||||
asserted = True
|
||||
|
||||
self.assertTrue(asserted)
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
from django.test.testcases import TestCase
|
||||
from cache_toolbox.core import get_cached_content, set_cached_content, del_cached_content
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.contentstore.content import StaticContent
|
||||
from django.test import TestCase
|
||||
|
||||
|
||||
class Content:
|
||||
def __init__(self, location, content):
|
||||
@@ -11,6 +12,7 @@ class Content:
|
||||
def get_id(self):
|
||||
return StaticContent.get_id_from_location(self.location)
|
||||
|
||||
|
||||
class CachingTestCase(TestCase):
|
||||
# Tests for https://edx.lighthouseapp.com/projects/102637/tickets/112-updating-asset-does-not-refresh-the-cached-copy
|
||||
unicodeLocation = Location(u'c4x', u'mitX', u'800', u'thumbnail', u'monsters.jpg')
|
||||
@@ -32,7 +34,3 @@ class CachingTestCase(TestCase):
|
||||
'should not be stored in cache with unicodeLocation')
|
||||
self.assertEqual(None, get_cached_content(self.nonUnicodeLocation),
|
||||
'should not be stored in cache with nonUnicodeLocation')
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,43 +1,57 @@
|
||||
from django.test.testcases import TestCase
|
||||
import datetime
|
||||
import time
|
||||
import json
|
||||
import copy
|
||||
from util import converters
|
||||
from util.converters import jsdate_to_time
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
import xmodule
|
||||
from django.test.client import Client
|
||||
from django.core.urlresolvers import reverse
|
||||
from xmodule.modulestore import Location
|
||||
from cms.djangoapps.models.settings.course_details import CourseDetails,\
|
||||
CourseSettingsEncoder
|
||||
import json
|
||||
from util import converters
|
||||
import calendar
|
||||
from util.converters import jsdate_to_time
|
||||
from django.utils.timezone import UTC
|
||||
|
||||
from xmodule.modulestore import Location
|
||||
from cms.djangoapps.models.settings.course_details import (CourseDetails,
|
||||
CourseSettingsEncoder)
|
||||
from cms.djangoapps.models.settings.course_grading import CourseGradingModel
|
||||
from cms.djangoapps.contentstore.utils import get_modulestore
|
||||
import copy
|
||||
|
||||
from django.test import TestCase
|
||||
from utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
|
||||
from cms.djangoapps.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())
|
||||
|
||||
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))
|
||||
|
||||
|
||||
class CourseTestCase(TestCase):
|
||||
|
||||
|
||||
class CourseTestCase(ModuleStoreTestCase):
|
||||
def setUp(self):
|
||||
"""
|
||||
These tests need a user in the DB so that the django Test Client
|
||||
can log them in.
|
||||
They inherit from the ModuleStoreTestCase class so that the mongodb collection
|
||||
will be cleared out before each test case execution and deleted
|
||||
afterwards.
|
||||
"""
|
||||
uname = 'testuser'
|
||||
email = 'test+courses@edx.org'
|
||||
password = 'foo'
|
||||
@@ -52,36 +66,16 @@ class CourseTestCase(TestCase):
|
||||
self.user.is_staff = True
|
||||
self.user.save()
|
||||
|
||||
# Flush and initialize the module store
|
||||
# It needs the templates because it creates new records
|
||||
# by cloning from the template.
|
||||
# Note that if your test module gets in some weird state
|
||||
# (though it shouldn't), do this manually
|
||||
# from the bash shell to drop it:
|
||||
# $ mongo test_xmodule --eval "db.dropDatabase()"
|
||||
xmodule.modulestore.django._MODULESTORES = {}
|
||||
xmodule.modulestore.django.modulestore().collection.drop()
|
||||
xmodule.templates.update_templates()
|
||||
|
||||
self.client = Client()
|
||||
self.client.login(username=uname, password=password)
|
||||
|
||||
self.course_data = {
|
||||
'template': 'i4x://edx/templates/course/Empty',
|
||||
'org': 'MITx',
|
||||
'number': '999',
|
||||
'display_name': 'Robot Super Course',
|
||||
}
|
||||
self.course_location = Location('i4x', 'MITx', '999', 'course', 'Robot_Super_Course')
|
||||
self.create_course()
|
||||
t = 'i4x://edx/templates/course/Empty'
|
||||
o = 'MITx'
|
||||
n = '999'
|
||||
dn = 'Robot Super Course'
|
||||
self.course_location = Location('i4x', o, n, 'course', 'Robot_Super_Course')
|
||||
CourseFactory.create(template=t, org=o, number=n, display_name=dn)
|
||||
|
||||
def tearDown(self):
|
||||
xmodule.modulestore.django._MODULESTORES = {}
|
||||
xmodule.modulestore.django.modulestore().collection.drop()
|
||||
|
||||
def create_course(self):
|
||||
"""Create new course"""
|
||||
self.client.post(reverse('create_new_course'), self.course_data)
|
||||
|
||||
class CourseDetailsTestCase(CourseTestCase):
|
||||
def test_virgin_fetch(self):
|
||||
@@ -94,7 +88,7 @@ class CourseDetailsTestCase(CourseTestCase):
|
||||
self.assertEqual(details.overview, "", "overview somehow initialized" + details.overview)
|
||||
self.assertIsNone(details.intro_video, "intro_video somehow initialized" + str(details.intro_video))
|
||||
self.assertIsNone(details.effort, "effort somehow initialized" + str(details.effort))
|
||||
|
||||
|
||||
def test_encoder(self):
|
||||
details = CourseDetails.fetch(self.course_location)
|
||||
jsondetails = json.dumps(details, cls=CourseSettingsEncoder)
|
||||
@@ -108,7 +102,7 @@ class CourseDetailsTestCase(CourseTestCase):
|
||||
self.assertEqual(jsondetails['overview'], "", "overview somehow initialized")
|
||||
self.assertIsNone(jsondetails['intro_video'], "intro_video somehow initialized")
|
||||
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
|
||||
jsondetails = CourseDetails.fetch(self.course_location)
|
||||
@@ -126,6 +120,7 @@ class CourseDetailsTestCase(CourseTestCase):
|
||||
self.assertEqual(CourseDetails.update_from_json(jsondetails.__dict__).effort,
|
||||
jsondetails.effort, "After set effort")
|
||||
|
||||
|
||||
class CourseDetailsViewTest(CourseTestCase):
|
||||
def alter_field(self, url, details, field, val):
|
||||
setattr(details, field, val)
|
||||
@@ -136,9 +131,9 @@ class CourseDetailsViewTest(CourseTestCase):
|
||||
payload['end_date'] = CourseDetailsViewTest.convert_datetime_to_iso(details.end_date)
|
||||
payload['enrollment_start'] = CourseDetailsViewTest.convert_datetime_to_iso(details.enrollment_start)
|
||||
payload['enrollment_end'] = CourseDetailsViewTest.convert_datetime_to_iso(details.enrollment_end)
|
||||
resp = self.client.post(url, json.dumps(payload), "application/json")
|
||||
resp = self.client.post(url, json.dumps(payload), "application/json")
|
||||
self.compare_details_with_encoding(json.loads(resp.content), details.__dict__, field + str(val))
|
||||
|
||||
|
||||
@staticmethod
|
||||
def convert_datetime_to_iso(datetime):
|
||||
if datetime is not None:
|
||||
@@ -146,27 +141,22 @@ class CourseDetailsViewTest(CourseTestCase):
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def test_update_and_fetch(self):
|
||||
details = CourseDetails.fetch(self.course_location)
|
||||
|
||||
resp = self.client.get(reverse('course_settings', kwargs={'org' : self.course_location.org, 'course' : self.course_location.course,
|
||||
'name' : self.course_location.name }))
|
||||
self.assertContains(resp, '<li><a href="#" class="is-shown" data-section="details">Course Details</a></li>', status_code=200, html=True)
|
||||
|
||||
# resp s/b json from here on
|
||||
url = reverse('course_settings', kwargs={'org' : self.course_location.org, 'course' : self.course_location.course,
|
||||
'name' : self.course_location.name, 'section' : 'details' })
|
||||
# resp s/b json from here on
|
||||
url = reverse('course_settings', kwargs={'org': self.course_location.org, 'course': self.course_location.course,
|
||||
'name': self.course_location.name, 'section': 'details'})
|
||||
resp = self.client.get(url)
|
||||
self.compare_details_with_encoding(json.loads(resp.content), details.__dict__, "virgin get")
|
||||
|
||||
utc = UTC()
|
||||
self.alter_field(url, details, 'start_date', datetime.datetime(2012,11,12,1,30, tzinfo=utc))
|
||||
self.alter_field(url, details, 'start_date', datetime.datetime(2012,11,1,13,30, tzinfo=utc))
|
||||
self.alter_field(url, details, 'end_date', datetime.datetime(2013,2,12,1,30, tzinfo=utc))
|
||||
self.alter_field(url, details, 'enrollment_start', datetime.datetime(2012,10,12,1,30, tzinfo=utc))
|
||||
self.alter_field(url, details, 'start_date', datetime.datetime(2012, 11, 12, 1, 30, tzinfo=utc))
|
||||
self.alter_field(url, details, 'start_date', datetime.datetime(2012, 11, 1, 13, 30, tzinfo=utc))
|
||||
self.alter_field(url, details, 'end_date', datetime.datetime(2013, 2, 12, 1, 30, tzinfo=utc))
|
||||
self.alter_field(url, details, 'enrollment_start', datetime.datetime(2012, 10, 12, 1, 30, tzinfo=utc))
|
||||
|
||||
self.alter_field(url, details, 'enrollment_end', datetime.datetime(2012,11,15,1,30, tzinfo=utc))
|
||||
self.alter_field(url, details, 'enrollment_end', datetime.datetime(2012, 11, 15, 1, 30, tzinfo=utc))
|
||||
self.alter_field(url, details, 'overview', "Overview")
|
||||
self.alter_field(url, details, 'intro_video', "intro_video")
|
||||
self.alter_field(url, details, 'effort', "effort")
|
||||
@@ -179,7 +169,7 @@ class CourseDetailsViewTest(CourseTestCase):
|
||||
self.assertEqual(details['overview'], encoded['overview'], context + " overviews not ==")
|
||||
self.assertEqual(details['intro_video'], encoded.get('intro_video', None), context + " intro_video not ==")
|
||||
self.assertEqual(details['effort'], encoded['effort'], context + " efforts not ==")
|
||||
|
||||
|
||||
def compare_date_fields(self, details, encoded, context, field):
|
||||
if details[field] is not None:
|
||||
if field in encoded and encoded[field] is not None:
|
||||
@@ -191,14 +181,15 @@ class CourseDetailsViewTest(CourseTestCase):
|
||||
else:
|
||||
details_encoded = jsdate_to_time(details[field])
|
||||
dt2 = ConvertersTestCase.struct_to_datetime(details_encoded)
|
||||
|
||||
|
||||
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)
|
||||
elif field in encoded and encoded[field] is not None:
|
||||
self.fail(field + " included in encoding but missing from details at " + context)
|
||||
|
||||
|
||||
|
||||
class CourseGradingTest(CourseTestCase):
|
||||
def test_initial_grader(self):
|
||||
descriptor = get_modulestore(self.course_location).get_item(self.course_location)
|
||||
@@ -218,58 +209,117 @@ class CourseGradingTest(CourseTestCase):
|
||||
self.assertEqual(self.course_location, test_grader.course_location, "Course locations")
|
||||
self.assertIsNotNone(test_grader.graders, "No graders")
|
||||
self.assertIsNotNone(test_grader.grade_cutoffs, "No cutoffs")
|
||||
|
||||
|
||||
for i, grader in enumerate(test_grader.graders):
|
||||
subgrader = CourseGradingModel.fetch_grader(self.course_location, i)
|
||||
self.assertDictEqual(grader, subgrader, str(i) + "th graders not equal")
|
||||
|
||||
|
||||
subgrader = CourseGradingModel.fetch_grader(self.course_location.list(), 0)
|
||||
self.assertDictEqual(test_grader.graders[0], subgrader, "failed with location as list")
|
||||
|
||||
|
||||
def test_fetch_cutoffs(self):
|
||||
test_grader = CourseGradingModel.fetch_cutoffs(self.course_location)
|
||||
# ??? should this check that it's at least a dict? (expected is { "pass" : 0.5 } I think)
|
||||
self.assertIsNotNone(test_grader, "No cutoffs via fetch")
|
||||
|
||||
|
||||
test_grader = CourseGradingModel.fetch_cutoffs(self.course_location.url())
|
||||
self.assertIsNotNone(test_grader, "No cutoffs via fetch with url")
|
||||
|
||||
|
||||
def test_fetch_grace(self):
|
||||
test_grader = CourseGradingModel.fetch_grace_period(self.course_location)
|
||||
# almost a worthless test
|
||||
self.assertIn('grace_period', test_grader, "No grace via fetch")
|
||||
|
||||
|
||||
test_grader = CourseGradingModel.fetch_grace_period(self.course_location.url())
|
||||
self.assertIn('grace_period', test_grader, "No cutoffs via fetch with url")
|
||||
|
||||
|
||||
def test_update_from_json(self):
|
||||
test_grader = CourseGradingModel.fetch(self.course_location)
|
||||
altered_grader = CourseGradingModel.update_from_json(test_grader.__dict__)
|
||||
self.assertDictEqual(test_grader.__dict__, altered_grader.__dict__, "Noop update")
|
||||
|
||||
|
||||
test_grader.graders[0]['weight'] = test_grader.graders[0].get('weight') * 2
|
||||
altered_grader = CourseGradingModel.update_from_json(test_grader.__dict__)
|
||||
self.assertDictEqual(test_grader.__dict__, altered_grader.__dict__, "Weight[0] * 2")
|
||||
|
||||
|
||||
test_grader.grade_cutoffs['D'] = 0.3
|
||||
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'}
|
||||
|
||||
test_grader.grace_period = {'hours' : 4, 'minutes' : 5, 'seconds': 0}
|
||||
altered_grader = CourseGradingModel.update_from_json(test_grader.__dict__)
|
||||
self.assertDictEqual(test_grader.__dict__, altered_grader.__dict__, "4 hour grace period")
|
||||
|
||||
|
||||
def test_update_grader_from_json(self):
|
||||
test_grader = CourseGradingModel.fetch(self.course_location)
|
||||
altered_grader = CourseGradingModel.update_grader_from_json(test_grader.course_location, test_grader.graders[1])
|
||||
self.assertDictEqual(test_grader.graders[1], altered_grader, "Noop update")
|
||||
|
||||
|
||||
test_grader.graders[1]['min_count'] = test_grader.graders[1].get('min_count') + 2
|
||||
altered_grader = CourseGradingModel.update_grader_from_json(test_grader.course_location, test_grader.graders[1])
|
||||
self.assertDictEqual(test_grader.graders[1], altered_grader, "min_count[1] + 2")
|
||||
|
||||
|
||||
test_grader.graders[1]['drop_count'] = test_grader.graders[1].get('drop_count') + 1
|
||||
altered_grader = CourseGradingModel.update_grader_from_json(test_grader.course_location, test_grader.graders[1])
|
||||
self.assertDictEqual(test_grader.graders[1], altered_grader, "drop_count[1] + 2")
|
||||
|
||||
|
||||
|
||||
class CourseMetadataEditingTest(CourseTestCase):
|
||||
def setUp(self):
|
||||
CourseTestCase.setUp(self)
|
||||
# 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])
|
||||
|
||||
|
||||
def test_fetch_initial_fields(self):
|
||||
test_model = CourseMetadata.fetch(self.course_location)
|
||||
self.assertIn('display_name', test_model, 'Missing editable metadata field')
|
||||
self.assertEqual(test_model['display_name'], 'Robot Super Course', "not expected value")
|
||||
|
||||
test_model = CourseMetadata.fetch(self.fullcourse_location)
|
||||
self.assertNotIn('graceperiod', test_model, 'blacklisted field leaked in')
|
||||
self.assertIn('display_name', test_model, 'full missing editable metadata field')
|
||||
self.assertEqual(test_model['display_name'], 'Testing', "not expected value")
|
||||
self.assertIn('rerandomize', test_model, 'Missing rerandomize metadata field')
|
||||
self.assertIn('showanswer', test_model, 'showanswer field ')
|
||||
self.assertIn('xqa_key', test_model, 'xqa_key field ')
|
||||
|
||||
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"})
|
||||
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,
|
||||
"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")
|
||||
|
||||
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")
|
||||
|
||||
|
||||
def test_delete_key(self):
|
||||
test_model = CourseMetadata.delete_key(self.fullcourse_location, { 'deleteKeys' : ['doesnt_exist', 'showanswer', 'xqa_key']})
|
||||
# ensure no harm
|
||||
self.assertNotIn('graceperiod', test_model, 'blacklisted field leaked in')
|
||||
self.assertIn('display_name', test_model, 'full missing editable metadata field')
|
||||
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')
|
||||
@@ -2,29 +2,30 @@ from cms.djangoapps.contentstore.tests.test_course_settings import CourseTestCas
|
||||
from django.core.urlresolvers import reverse
|
||||
import json
|
||||
|
||||
|
||||
class CourseUpdateTest(CourseTestCase):
|
||||
def test_course_update(self):
|
||||
# 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>'
|
||||
payload = { 'content' : content,
|
||||
'date' : 'January 8, 2013'}
|
||||
url = reverse('course_info', kwargs={'org' : self.course_location.org, 'course' : self.course_location.course,
|
||||
'provided_id' : ''})
|
||||
|
||||
payload = {'content': content,
|
||||
'date': 'January 8, 2013'}
|
||||
url = reverse('course_info', 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)
|
||||
|
||||
|
||||
payload = json.loads(resp.content)
|
||||
|
||||
self.assertHTMLEqual(content, payload['content'], "single iframe")
|
||||
|
||||
url = reverse('course_info', kwargs={'org' : self.course_location.org, 'course' : self.course_location.course,
|
||||
'provided_id' : payload['id']})
|
||||
|
||||
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>'
|
||||
payload['content'] = content
|
||||
resp = self.client.post(url, json.dumps(payload), "application/json")
|
||||
|
||||
|
||||
self.assertHTMLEqual(content, json.loads(resp.content)['content'], "iframe w/ div")
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
from django.test.testcases import TestCase
|
||||
from cms.djangoapps.contentstore import utils
|
||||
import mock
|
||||
from django.test import TestCase
|
||||
|
||||
|
||||
class LMSLinksTestCase(TestCase):
|
||||
def about_page_test(self):
|
||||
location = 'i4x','mitX','101','course', 'test'
|
||||
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):
|
||||
location = 'i4x','mitX','101','vertical', 'contacting_us'
|
||||
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")
|
||||
|
||||
@@ -1,48 +1,33 @@
|
||||
import json
|
||||
import shutil
|
||||
from django.test import TestCase
|
||||
from django.test.client import Client
|
||||
from override_settings import override_settings
|
||||
from django.conf import settings
|
||||
from django.core.urlresolvers import reverse
|
||||
from path import path
|
||||
from tempfile import mkdtemp
|
||||
import json
|
||||
from fs.osfs import OSFS
|
||||
|
||||
|
||||
from student.models import Registration
|
||||
from django.contrib.auth.models import User
|
||||
import xmodule.modulestore.django
|
||||
from xmodule.modulestore.xml_importer import import_from_xml
|
||||
import copy
|
||||
from factories import *
|
||||
|
||||
from cms.djangoapps.contentstore.utils import get_modulestore
|
||||
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.store_utilities import clone_course
|
||||
from xmodule.modulestore.store_utilities import delete_course
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.django import modulestore, _MODULESTORES
|
||||
from xmodule.contentstore.django import contentstore
|
||||
from xmodule.course_module import CourseDescriptor
|
||||
from xmodule.templates import update_templates
|
||||
from xmodule.modulestore.xml_exporter import export_to_xml
|
||||
from cms.djangoapps.contentstore.utils import get_modulestore
|
||||
from xmodule.modulestore.xml_importer import import_from_xml
|
||||
|
||||
from xmodule.capa_module import CapaDescriptor
|
||||
from xmodule.course_module import CourseDescriptor
|
||||
from xmodule.seq_module import SequenceDescriptor
|
||||
|
||||
def parse_json(response):
|
||||
"""Parse response, which is assumed to be json"""
|
||||
return json.loads(response.content)
|
||||
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
|
||||
from utils import ModuleStoreTestCase, parse_json, user, registration
|
||||
|
||||
|
||||
def user(email):
|
||||
"""look up a user by email"""
|
||||
return User.objects.get(email=email)
|
||||
|
||||
|
||||
def registration(email):
|
||||
"""look up registration object by email"""
|
||||
return Registration.objects.get(user__email=email)
|
||||
|
||||
|
||||
class ContentStoreTestCase(TestCase):
|
||||
class ContentStoreTestCase(ModuleStoreTestCase):
|
||||
def _login(self, email, pw):
|
||||
"""Login. View should always return 200. The success/fail is in the
|
||||
returned json"""
|
||||
@@ -99,7 +84,6 @@ class ContentStoreTestCase(TestCase):
|
||||
# Now make sure that the user is now actually activated
|
||||
self.assertTrue(user(email).is_active)
|
||||
|
||||
|
||||
class AuthTestCase(ContentStoreTestCase):
|
||||
"""Check that various permissions-related things work"""
|
||||
|
||||
@@ -187,353 +171,3 @@ class AuthTestCase(ContentStoreTestCase):
|
||||
self.assertEqual(resp.status_code, 302)
|
||||
|
||||
# Logged in should work.
|
||||
|
||||
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')
|
||||
|
||||
@override_settings(MODULESTORE=TEST_DATA_MODULESTORE)
|
||||
class ContentStoreTest(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
uname = 'testuser'
|
||||
email = 'test+courses@edx.org'
|
||||
password = 'foo'
|
||||
|
||||
# Create the use so we can log them in.
|
||||
self.user = User.objects.create_user(uname, email, password)
|
||||
|
||||
# Note that we do not actually need to do anything
|
||||
# for registration if we directly mark them active.
|
||||
self.user.is_active = True
|
||||
# Staff has access to view all courses
|
||||
self.user.is_staff = True
|
||||
self.user.save()
|
||||
|
||||
# Flush and initialize the module store
|
||||
# It needs the templates because it creates new records
|
||||
# by cloning from the template.
|
||||
# Note that if your test module gets in some weird state
|
||||
# (though it shouldn't), do this manually
|
||||
# from the bash shell to drop it:
|
||||
# $ mongo test_xmodule --eval "db.dropDatabase()"
|
||||
xmodule.modulestore.django._MODULESTORES = {}
|
||||
xmodule.modulestore.django.modulestore().collection.drop()
|
||||
xmodule.templates.update_templates()
|
||||
|
||||
self.client = Client()
|
||||
self.client.login(username=uname, password=password)
|
||||
|
||||
self.course_data = {
|
||||
'template': 'i4x://edx/templates/course/Empty',
|
||||
'org': 'MITx',
|
||||
'number': '999',
|
||||
'display_name': 'Robot Super Course',
|
||||
}
|
||||
|
||||
def tearDown(self):
|
||||
# Make sure you flush out the test modulestore after the end
|
||||
# of the last test because otherwise on the next run
|
||||
# cms/djangoapps/contentstore/__init__.py
|
||||
# update_templates() will try to update the templates
|
||||
# via upsert and it sometimes seems to be messing things up.
|
||||
xmodule.modulestore.django._MODULESTORES = {}
|
||||
xmodule.modulestore.django.modulestore().collection.drop()
|
||||
|
||||
def test_create_course(self):
|
||||
"""Test new course creation - happy path"""
|
||||
resp = self.client.post(reverse('create_new_course'), self.course_data)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
data = parse_json(resp)
|
||||
self.assertEqual(data['id'], 'i4x://MITx/999/course/Robot_Super_Course')
|
||||
|
||||
def test_create_course_duplicate_course(self):
|
||||
"""Test new course creation - error path"""
|
||||
resp = self.client.post(reverse('create_new_course'), self.course_data)
|
||||
resp = self.client.post(reverse('create_new_course'), self.course_data)
|
||||
data = parse_json(resp)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertEqual(data['ErrMsg'], 'There is already a course defined with this name.')
|
||||
|
||||
def test_create_course_duplicate_number(self):
|
||||
"""Test new course creation - error path"""
|
||||
resp = self.client.post(reverse('create_new_course'), self.course_data)
|
||||
self.course_data['display_name'] = 'Robot Super Course Two'
|
||||
|
||||
resp = self.client.post(reverse('create_new_course'), self.course_data)
|
||||
data = parse_json(resp)
|
||||
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertEqual(data['ErrMsg'],
|
||||
'There is already a course defined with the same organization and course number.')
|
||||
|
||||
def test_create_course_with_bad_organization(self):
|
||||
"""Test new course creation - error path for bad organization name"""
|
||||
self.course_data['org'] = 'University of California, Berkeley'
|
||||
resp = self.client.post(reverse('create_new_course'), self.course_data)
|
||||
data = parse_json(resp)
|
||||
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertEqual(data['ErrMsg'],
|
||||
"Unable to create course 'Robot Super Course'.\n\nInvalid characters in 'University of California, Berkeley'.")
|
||||
|
||||
def test_course_index_view_with_no_courses(self):
|
||||
"""Test viewing the index page with no courses"""
|
||||
# Create a course so there is something to view
|
||||
resp = self.client.get(reverse('index'))
|
||||
self.assertContains(resp,
|
||||
'<h1>My Courses</h1>',
|
||||
status_code=200,
|
||||
html=True)
|
||||
|
||||
def test_course_factory(self):
|
||||
course = CourseFactory.create()
|
||||
self.assertIsInstance(course, xmodule.course_module.CourseDescriptor)
|
||||
|
||||
def test_item_factory(self):
|
||||
course = CourseFactory.create()
|
||||
item = ItemFactory.create(parent_location=course.location)
|
||||
self.assertIsInstance(item, xmodule.seq_module.SequenceDescriptor)
|
||||
|
||||
def test_course_index_view_with_course(self):
|
||||
"""Test viewing the index page with an existing course"""
|
||||
CourseFactory.create(display_name='Robot Super Educational Course')
|
||||
resp = self.client.get(reverse('index'))
|
||||
self.assertContains(resp,
|
||||
'<span class="class-name">Robot Super Educational Course</span>',
|
||||
status_code=200,
|
||||
html=True)
|
||||
|
||||
def test_course_overview_view_with_course(self):
|
||||
"""Test viewing the course overview page with an existing course"""
|
||||
CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course')
|
||||
|
||||
data = {
|
||||
'org': 'MITx',
|
||||
'course': '999',
|
||||
'name': Location.clean('Robot Super Course'),
|
||||
}
|
||||
|
||||
resp = self.client.get(reverse('course_index', kwargs=data))
|
||||
self.assertContains(resp,
|
||||
'<a href="/MITx/999/course/Robot_Super_Course" class="class-name">Robot Super Course</a>',
|
||||
status_code=200,
|
||||
html=True)
|
||||
|
||||
def test_clone_item(self):
|
||||
"""Test cloning an item. E.g. creating a new section"""
|
||||
CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course')
|
||||
|
||||
section_data = {
|
||||
'parent_location' : 'i4x://MITx/999/course/Robot_Super_Course',
|
||||
'template' : 'i4x://edx/templates/chapter/Empty',
|
||||
'display_name': 'Section One',
|
||||
}
|
||||
|
||||
resp = self.client.post(reverse('clone_item'), section_data)
|
||||
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
data = parse_json(resp)
|
||||
self.assertRegexpMatches(data['id'],
|
||||
'^i4x:\/\/MITx\/999\/chapter\/([0-9]|[a-f]){32}$')
|
||||
|
||||
def check_edit_unit(self, test_course_name):
|
||||
import_from_xml(modulestore(), 'common/test/data/', [test_course_name])
|
||||
|
||||
for descriptor in modulestore().get_items(Location(None, None, 'vertical', None, None)):
|
||||
print "Checking ", descriptor.location.url()
|
||||
print descriptor.__class__, descriptor.location
|
||||
resp = self.client.get(reverse('edit_unit', kwargs={'location': descriptor.location.url()}))
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
def test_edit_unit_toy(self):
|
||||
self.check_edit_unit('toy')
|
||||
|
||||
def test_edit_unit_full(self):
|
||||
self.check_edit_unit('full')
|
||||
|
||||
def test_static_tab_reordering(self):
|
||||
import_from_xml(modulestore(), 'common/test/data/', ['full'])
|
||||
|
||||
ms = modulestore('direct')
|
||||
course = ms.get_item(Location(['i4x','edX','full','course','6.002_Spring_2012', None]))
|
||||
|
||||
# reverse the ordering
|
||||
reverse_tabs = []
|
||||
for tab in course.tabs:
|
||||
if tab['type'] == 'static_tab':
|
||||
reverse_tabs.insert(0, 'i4x://edX/full/static_tab/{0}'.format(tab['url_slug']))
|
||||
|
||||
resp = self.client.post(reverse('reorder_static_tabs'), json.dumps({'tabs':reverse_tabs}), "application/json")
|
||||
|
||||
course = ms.get_item(Location(['i4x','edX','full','course','6.002_Spring_2012', None]))
|
||||
|
||||
# compare to make sure that the tabs information is in the expected order after the server call
|
||||
course_tabs = []
|
||||
for tab in course.tabs:
|
||||
if tab['type'] == 'static_tab':
|
||||
course_tabs.append('i4x://edX/full/static_tab/{0}'.format(tab['url_slug']))
|
||||
|
||||
self.assertEqual(reverse_tabs, course_tabs)
|
||||
|
||||
|
||||
|
||||
|
||||
def test_about_overrides(self):
|
||||
'''
|
||||
This test case verifies that a course can use specialized override for about data, e.g. /about/Fall_2012/effort.html
|
||||
while there is a base definition in /about/effort.html
|
||||
'''
|
||||
import_from_xml(modulestore(), 'common/test/data/', ['full'])
|
||||
ms = modulestore('direct')
|
||||
effort = ms.get_item(Location(['i4x','edX','full','about','effort', None]))
|
||||
self.assertEqual(effort.definition['data'],'6 hours')
|
||||
|
||||
# this one should be in a non-override folder
|
||||
effort = ms.get_item(Location(['i4x','edX','full','about','end_date', None]))
|
||||
self.assertEqual(effort.definition['data'],'TBD')
|
||||
|
||||
def test_remove_hide_progress_tab(self):
|
||||
import_from_xml(modulestore(), 'common/test/data/', ['full'])
|
||||
|
||||
ms = modulestore('direct')
|
||||
cs = contentstore()
|
||||
|
||||
source_location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
|
||||
course = ms.get_item(source_location)
|
||||
self.assertNotIn('hide_progress_tab', course.metadata)
|
||||
|
||||
|
||||
def test_clone_course(self):
|
||||
import_from_xml(modulestore(), 'common/test/data/', ['full'])
|
||||
|
||||
resp = self.client.post(reverse('create_new_course'), self.course_data)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
data = parse_json(resp)
|
||||
self.assertEqual(data['id'], 'i4x://MITx/999/course/Robot_Super_Course')
|
||||
|
||||
ms = modulestore('direct')
|
||||
cs = contentstore()
|
||||
|
||||
source_location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
|
||||
dest_location = CourseDescriptor.id_to_location('MITx/999/Robot_Super_Course')
|
||||
|
||||
clone_course(ms, cs, source_location, dest_location)
|
||||
|
||||
# now loop through all the units in the course and verify that the clone can render them, which
|
||||
# means the objects are at least present
|
||||
items = ms.get_items(Location(['i4x','edX', 'full', 'vertical', None]))
|
||||
self.assertGreater(len(items), 0)
|
||||
clone_items = ms.get_items(Location(['i4x', 'MITx','999','vertical', None]))
|
||||
self.assertGreater(len(clone_items), 0)
|
||||
for descriptor in items:
|
||||
new_loc = descriptor.location._replace(org = 'MITx', course='999')
|
||||
print "Checking {0} should now also be at {1}".format(descriptor.location.url(), new_loc.url())
|
||||
resp = self.client.get(reverse('edit_unit', kwargs={'location': new_loc.url()}))
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
def test_delete_course(self):
|
||||
import_from_xml(modulestore(), 'common/test/data/', ['full'])
|
||||
|
||||
ms = modulestore('direct')
|
||||
cs = contentstore()
|
||||
|
||||
location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
|
||||
|
||||
delete_course(ms, cs, location)
|
||||
|
||||
items = ms.get_items(Location(['i4x','edX', 'full', 'vertical', None]))
|
||||
self.assertEqual(len(items), 0)
|
||||
|
||||
def verify_content_existence(self, modulestore, root_dir, location, dirname, category_name, filename_suffix=''):
|
||||
fs = OSFS(root_dir / 'test_export')
|
||||
self.assertTrue(fs.exists(dirname))
|
||||
|
||||
query_loc = Location('i4x', location.org, location.course, category_name, None)
|
||||
items = modulestore.get_items(query_loc)
|
||||
|
||||
for item in items:
|
||||
fs = OSFS(root_dir / ('test_export/' + dirname))
|
||||
self.assertTrue(fs.exists(item.location.name + filename_suffix))
|
||||
|
||||
def test_export_course(self):
|
||||
ms = modulestore('direct')
|
||||
cs = contentstore()
|
||||
|
||||
import_from_xml(ms, 'common/test/data/', ['full'])
|
||||
location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
|
||||
|
||||
root_dir = path(mkdtemp())
|
||||
|
||||
print 'Exporting to tempdir = {0}'.format(root_dir)
|
||||
|
||||
# export out to a tempdir
|
||||
export_to_xml(ms, cs, location, root_dir, 'test_export')
|
||||
|
||||
# check for static tabs
|
||||
self.verify_content_existence(ms, root_dir, location, 'tabs', 'static_tab', '.html')
|
||||
|
||||
# check for custom_tags
|
||||
self.verify_content_existence(ms, root_dir, location, 'info', 'course_info', '.html')
|
||||
|
||||
# check for custom_tags
|
||||
self.verify_content_existence(ms, root_dir, location, 'custom_tags', 'custom_tag_template')
|
||||
|
||||
|
||||
# remove old course
|
||||
delete_course(ms, cs, location)
|
||||
|
||||
# reimport
|
||||
import_from_xml(ms, root_dir, ['test_export'])
|
||||
|
||||
items = ms.get_items(Location(['i4x','edX', 'full', 'vertical', None]))
|
||||
self.assertGreater(len(items), 0)
|
||||
for descriptor in items:
|
||||
print "Checking {0}....".format(descriptor.location.url())
|
||||
resp = self.client.get(reverse('edit_unit', kwargs={'location': descriptor.location.url()}))
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
shutil.rmtree(root_dir)
|
||||
|
||||
|
||||
def test_course_handouts_rewrites(self):
|
||||
ms = modulestore('direct')
|
||||
cs = contentstore()
|
||||
|
||||
# import a test course
|
||||
import_from_xml(ms, 'common/test/data/', ['full'])
|
||||
|
||||
handout_location= Location(['i4x', 'edX', 'full', 'course_info', 'handouts'])
|
||||
|
||||
# get module info
|
||||
resp = self.client.get(reverse('module_info', kwargs={'module_location': handout_location}))
|
||||
|
||||
# make sure we got a successful response
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
# check that /static/ has been converted to the full path
|
||||
# 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_capa_module(self):
|
||||
"""Test that a problem treats markdown specially."""
|
||||
CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course')
|
||||
|
||||
problem_data = {
|
||||
'parent_location' : 'i4x://MITx/999/course/Robot_Super_Course',
|
||||
'template' : 'i4x://edx/templates/problem/Empty'
|
||||
}
|
||||
|
||||
resp = self.client.post(reverse('clone_item'), problem_data)
|
||||
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
payload = parse_json(resp)
|
||||
problem_loc = payload['id']
|
||||
problem = get_modulestore(problem_loc).get_item(problem_loc)
|
||||
# should be a CapaDescriptor
|
||||
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")
|
||||
65
cms/djangoapps/contentstore/tests/utils.py
Normal file
@@ -0,0 +1,65 @@
|
||||
import json
|
||||
import copy
|
||||
from uuid import uuid4
|
||||
from django.test import TestCase
|
||||
from django.conf import settings
|
||||
|
||||
from student.models import Registration
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
import xmodule.modulestore.django
|
||||
from xmodule.templates import update_templates
|
||||
|
||||
|
||||
class ModuleStoreTestCase(TestCase):
|
||||
""" Subclass for any test case that uses the mongodb
|
||||
module store. This populates a uniquely named modulestore
|
||||
collection with templates before running the TestCase
|
||||
and drops it they are finished. """
|
||||
|
||||
def _pre_setup(self):
|
||||
super(ModuleStoreTestCase, self)._pre_setup()
|
||||
|
||||
# Use a uuid to differentiate
|
||||
# the mongo collections on jenkins.
|
||||
self.orig_MODULESTORE = copy.deepcopy(settings.MODULESTORE)
|
||||
self.test_MODULESTORE = self.orig_MODULESTORE
|
||||
self.test_MODULESTORE['default']['OPTIONS']['collection'] = 'modulestore_%s' % uuid4().hex
|
||||
self.test_MODULESTORE['direct']['OPTIONS']['collection'] = 'modulestore_%s' % uuid4().hex
|
||||
settings.MODULESTORE = self.test_MODULESTORE
|
||||
|
||||
# Flush and initialize the module store
|
||||
# It needs the templates because it creates new records
|
||||
# by cloning from the template.
|
||||
# Note that if your test module gets in some weird state
|
||||
# (though it shouldn't), do this manually
|
||||
# from the bash shell to drop it:
|
||||
# $ mongo test_xmodule --eval "db.dropDatabase()"
|
||||
xmodule.modulestore.django._MODULESTORES = {}
|
||||
update_templates()
|
||||
|
||||
def _post_teardown(self):
|
||||
# Make sure you flush out the modulestore.
|
||||
# Drop the collection at the end of the test,
|
||||
# otherwise there will be lingering collections leftover
|
||||
# from executing the tests.
|
||||
xmodule.modulestore.django._MODULESTORES = {}
|
||||
xmodule.modulestore.django.modulestore().collection.drop()
|
||||
settings.MODULESTORE = self.orig_MODULESTORE
|
||||
|
||||
super(ModuleStoreTestCase, self)._post_teardown()
|
||||
|
||||
|
||||
def parse_json(response):
|
||||
"""Parse response, which is assumed to be json"""
|
||||
return json.loads(response.content)
|
||||
|
||||
|
||||
def user(email):
|
||||
"""look up a user by email"""
|
||||
return User.objects.get(email=email)
|
||||
|
||||
|
||||
def registration(email):
|
||||
"""look up registration object by email"""
|
||||
return Registration.objects.get(user__email=email)
|
||||
@@ -5,18 +5,20 @@ from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
|
||||
DIRECT_ONLY_CATEGORIES = ['course', 'chapter', 'sequential', 'about', 'static_tab', 'course_info']
|
||||
|
||||
|
||||
def get_modulestore(location):
|
||||
"""
|
||||
Returns the correct modulestore to use for modifying the specified location
|
||||
"""
|
||||
if not isinstance(location, Location):
|
||||
location = Location(location)
|
||||
|
||||
|
||||
if location.category in DIRECT_ONLY_CATEGORIES:
|
||||
return modulestore('direct')
|
||||
else:
|
||||
return modulestore()
|
||||
|
||||
|
||||
def get_course_location_for_item(location):
|
||||
'''
|
||||
cdodge: for a given Xmodule, return the course that it belongs to
|
||||
@@ -46,6 +48,7 @@ def get_course_location_for_item(location):
|
||||
|
||||
return location
|
||||
|
||||
|
||||
def get_course_for_item(location):
|
||||
'''
|
||||
cdodge: for a given Xmodule, return the course that it belongs to
|
||||
@@ -72,12 +75,20 @@ def get_course_for_item(location):
|
||||
return courses[0]
|
||||
|
||||
|
||||
def get_lms_link_for_item(location, preview=False):
|
||||
def get_lms_link_for_item(location, preview=False, course_id=None):
|
||||
if course_id is None:
|
||||
course_id = get_course_id(location)
|
||||
|
||||
if settings.LMS_BASE is not None:
|
||||
lms_link = "//{preview}{lms_base}/courses/{course_id}/jump_to/{location}".format(
|
||||
preview='preview.' if preview else '',
|
||||
lms_base=settings.LMS_BASE,
|
||||
course_id=get_course_id(location),
|
||||
if preview:
|
||||
lms_base = settings.MITX_FEATURES.get('PREVIEW_LMS_BASE',
|
||||
'preview.' + settings.LMS_BASE)
|
||||
else:
|
||||
lms_base = settings.LMS_BASE
|
||||
|
||||
lms_link = "//{lms_base}/courses/{course_id}/jump_to/{location}".format(
|
||||
lms_base=lms_base,
|
||||
course_id=course_id,
|
||||
location=Location(location)
|
||||
)
|
||||
else:
|
||||
@@ -85,6 +96,7 @@ def get_lms_link_for_item(location, preview=False):
|
||||
|
||||
return lms_link
|
||||
|
||||
|
||||
def get_lms_link_for_about_page(location):
|
||||
"""
|
||||
Returns the url to the course about page from the location tuple.
|
||||
@@ -99,6 +111,7 @@ def get_lms_link_for_about_page(location):
|
||||
|
||||
return lms_link
|
||||
|
||||
|
||||
def get_course_id(location):
|
||||
"""
|
||||
Returns the course_id from a given the location tuple.
|
||||
@@ -106,6 +119,7 @@ def get_course_id(location):
|
||||
# TODO: These will need to be changed to point to the particular instance of this problem in the particular course
|
||||
return modulestore().get_containing_courses(Location(location))[0].id
|
||||
|
||||
|
||||
class UnitState(object):
|
||||
draft = 'draft'
|
||||
private = 'private'
|
||||
@@ -135,6 +149,7 @@ def compute_unit_state(unit):
|
||||
def get_date_display(date):
|
||||
return date.strftime("%d %B, %Y at %I:%M %p")
|
||||
|
||||
|
||||
def update_item(location, value):
|
||||
"""
|
||||
If value is None, delete the db entry. Otherwise, update it using the correct modulestore.
|
||||
@@ -142,4 +157,4 @@ def update_item(location, value):
|
||||
if value is None:
|
||||
get_modulestore(location).delete_item(location)
|
||||
else:
|
||||
get_modulestore(location).update_item(location, value)
|
||||
get_modulestore(location).update_item(location, value)
|
||||
|
||||
@@ -31,7 +31,7 @@ from xmodule.modulestore.exceptions import ItemNotFoundError, InvalidLocationErr
|
||||
from xmodule.x_module import ModuleSystem
|
||||
from xmodule.error_module import ErrorDescriptor
|
||||
from xmodule.errortracker import exc_info_to_str
|
||||
from static_replace import replace_urls
|
||||
import static_replace
|
||||
from external_auth.views import ssl_login_shortcut
|
||||
|
||||
from mitxmako.shortcuts import render_to_response, render_to_string
|
||||
@@ -58,7 +58,8 @@ from cms.djangoapps.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 lxml import etree
|
||||
from django.shortcuts import redirect
|
||||
from cms.djangoapps.models.settings.course_metadata import CourseMetadata
|
||||
|
||||
# to install PIL on MacOSX: 'easy_install http://dist.repoze.org/PIL-1.1.6.tar.gz'
|
||||
|
||||
@@ -81,6 +82,12 @@ def signup(request):
|
||||
csrf_token = csrf(request)['csrf_token']
|
||||
return render_to_response('signup.html', {'csrf': csrf_token})
|
||||
|
||||
def old_login_redirect(request):
|
||||
'''
|
||||
Redirect to the active login url.
|
||||
'''
|
||||
return redirect('login', permanent=True)
|
||||
|
||||
@ssl_login_shortcut
|
||||
@ensure_csrf_cookie
|
||||
def login_page(request):
|
||||
@@ -93,6 +100,11 @@ def login_page(request):
|
||||
'forgot_password_link': "//{base}/#forgot-password-modal".format(base=settings.LMS_BASE),
|
||||
})
|
||||
|
||||
def howitworks(request):
|
||||
if request.user.is_authenticated():
|
||||
return index(request)
|
||||
else:
|
||||
return render_to_response('howitworks.html', {})
|
||||
|
||||
# ==== Views for any logged-in user ==================================
|
||||
|
||||
@@ -102,7 +114,7 @@ def index(request):
|
||||
"""
|
||||
List all courses available to the logged in user
|
||||
"""
|
||||
courses = modulestore().get_items(['i4x', None, None, 'course', None])
|
||||
courses = modulestore('direct').get_items(['i4x', None, None, 'course', None])
|
||||
|
||||
# filter out courses that we don't have access too
|
||||
def course_filter(course):
|
||||
@@ -114,14 +126,16 @@ def index(request):
|
||||
courses = filter(course_filter, courses)
|
||||
|
||||
return render_to_response('index.html', {
|
||||
'new_course_template' : Location('i4x', 'edx', 'templates', 'course', 'Empty'),
|
||||
'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]))
|
||||
course.location.name]),
|
||||
get_lms_link_for_item(course.location, course_id=course.location.course_id))
|
||||
for course in courses],
|
||||
'user': request.user
|
||||
'user': request.user,
|
||||
'disable_course_creation': settings.MITX_FEATURES.get('DISABLE_COURSE_CREATION', False) and not request.user.is_staff
|
||||
})
|
||||
|
||||
|
||||
@@ -132,7 +146,7 @@ def has_access(user, location, role=STAFF_ROLE_NAME):
|
||||
Return True if user allowed to access this piece of data
|
||||
Note that the CMS permissions model is with respect to courses
|
||||
There is a super-admin permissions if user.is_staff is set
|
||||
Also, since we're unifying the user database between LMS and CAS,
|
||||
Also, since we're unifying the user database between LMS and CAS,
|
||||
I'm presuming that the course instructor (formally known as admin)
|
||||
will not be in both INSTRUCTOR and STAFF groups, so we have to cascade our queries here as INSTRUCTOR
|
||||
has all the rights that STAFF do
|
||||
@@ -154,15 +168,17 @@ 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()
|
||||
|
||||
upload_asset_callback_url = reverse('upload_asset', kwargs = {
|
||||
'org' : org,
|
||||
'course' : course,
|
||||
'coursename' : name
|
||||
lms_link = get_lms_link_for_item(location)
|
||||
|
||||
upload_asset_callback_url = reverse('upload_asset', kwargs={
|
||||
'org': org,
|
||||
'course': course,
|
||||
'coursename': name
|
||||
})
|
||||
|
||||
course = modulestore().get_item(location)
|
||||
@@ -171,6 +187,7 @@ def course_index(request, org, course, name):
|
||||
return render_to_response('overview.html', {
|
||||
'active_tab': 'courseware',
|
||||
'context_course': course,
|
||||
'lms_link': lms_link,
|
||||
'sections': sections,
|
||||
'course_graders': json.dumps(CourseGradingModel.fetch(course.location).graders),
|
||||
'parent_location': course.location,
|
||||
@@ -213,7 +230,7 @@ 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()
|
||||
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)
|
||||
|
||||
can_view_live = False
|
||||
@@ -233,9 +250,9 @@ def edit_subsection(request, location):
|
||||
'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
|
||||
'policy_metadata': policy_metadata,
|
||||
'subsection_units': subsection_units,
|
||||
'can_view_live': can_view_live
|
||||
})
|
||||
|
||||
|
||||
@@ -261,7 +278,6 @@ def edit_unit(request, location):
|
||||
break
|
||||
|
||||
lms_link = get_lms_link_for_item(item.location)
|
||||
preview_lms_link = get_lms_link_for_item(item.location, preview=True)
|
||||
|
||||
component_templates = defaultdict(list)
|
||||
|
||||
@@ -272,7 +288,7 @@ def edit_unit(request, location):
|
||||
template.display_name,
|
||||
template.location.url(),
|
||||
'markdown' in template.metadata,
|
||||
template.location.name == 'Empty'
|
||||
'empty' in template.metadata
|
||||
))
|
||||
|
||||
components = [
|
||||
@@ -292,23 +308,26 @@ def edit_unit(request, location):
|
||||
containing_section = modulestore().get_item(containing_section_locs[0])
|
||||
|
||||
# cdodge hack. We're having trouble previewing drafts via jump_to redirect
|
||||
# so let's generate the link url here
|
||||
# so let's generate the link url here
|
||||
|
||||
# need to figure out where this item is in the list of children as the preview will need this
|
||||
index =1
|
||||
index = 1
|
||||
for child in containing_subsection.get_children():
|
||||
if child.location == item.location:
|
||||
break
|
||||
index = index + 1
|
||||
|
||||
preview_lms_link = '//{preview}{lms_base}/courses/{org}/{course}/{course_name}/courseware/{section}/{subsection}/{index}'.format(
|
||||
preview='preview.',
|
||||
lms_base=settings.LMS_BASE,
|
||||
preview_lms_base = settings.MITX_FEATURES.get('PREVIEW_LMS_BASE',
|
||||
'preview.' + settings.LMS_BASE)
|
||||
|
||||
preview_lms_link = '//{preview_lms_base}/courses/{org}/{course}/{course_name}/courseware/{section}/{subsection}/{index}'.format(
|
||||
preview_lms_base=preview_lms_base,
|
||||
lms_base=settings.LMS_BASE,
|
||||
org=course.location.org,
|
||||
course=course.location.course,
|
||||
course_name=course.location.name,
|
||||
section=containing_section.location.name,
|
||||
subsection=containing_subsection.location.name,
|
||||
course=course.location.course,
|
||||
course_name=course.location.name,
|
||||
section=containing_section.location.name,
|
||||
subsection=containing_subsection.location.name,
|
||||
index=index)
|
||||
|
||||
unit_state = compute_unit_state(item)
|
||||
@@ -359,14 +378,14 @@ def assignment_type_update(request, org, course, category, name):
|
||||
location = Location(['i4x', org, course, category, name])
|
||||
if not has_access(request.user, location):
|
||||
raise HttpResponseForbidden()
|
||||
|
||||
|
||||
if request.method == 'GET':
|
||||
return HttpResponse(json.dumps(CourseGradingModel.get_section_grader_type(location)),
|
||||
return HttpResponse(json.dumps(CourseGradingModel.get_section_grader_type(location)),
|
||||
mimetype="application/json")
|
||||
elif request.method == 'POST': # post or put, doesn't matter.
|
||||
return HttpResponse(json.dumps(CourseGradingModel.update_section_grader_type(location, request.POST)),
|
||||
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")
|
||||
|
||||
|
||||
|
||||
def user_author_string(user):
|
||||
'''Get an author string for commits by this user. Format:
|
||||
@@ -474,7 +493,7 @@ def preview_module_system(request, preview_id, descriptor):
|
||||
get_module=partial(get_preview_module, request, preview_id),
|
||||
render_template=render_from_lms,
|
||||
debug=True,
|
||||
replace_urls=replace_urls,
|
||||
replace_urls=partial(static_replace.replace_static_urls, data_directory=None, course_namespace=descriptor.location),
|
||||
user=request.user,
|
||||
)
|
||||
|
||||
@@ -511,24 +530,24 @@ def load_preview_module(request, preview_id, descriptor, instance_state, shared_
|
||||
error_msg=exc_info_to_str(sys.exc_info())
|
||||
).xmodule_constructor(system)(None, None)
|
||||
|
||||
# cdodge: Special case
|
||||
# cdodge: Special case
|
||||
if module.location.category == 'static_tab':
|
||||
module.get_html = wrap_xmodule(
|
||||
module.get_html,
|
||||
module,
|
||||
"xmodule_tab_display.html",
|
||||
)
|
||||
else:
|
||||
else:
|
||||
module.get_html = wrap_xmodule(
|
||||
module.get_html,
|
||||
module,
|
||||
"xmodule_display.html",
|
||||
)
|
||||
|
||||
|
||||
module.get_html = replace_static_urls(
|
||||
module.get_html,
|
||||
module.metadata.get('data_dir', module.location.course),
|
||||
course_namespace = Location([module.location.tag, module.location.org, module.location.course, None, None])
|
||||
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())
|
||||
@@ -555,7 +574,7 @@ def _xmodule_recurse(item, action):
|
||||
_xmodule_recurse(child, action)
|
||||
|
||||
action(item)
|
||||
|
||||
|
||||
|
||||
@login_required
|
||||
@expect_json
|
||||
@@ -589,8 +608,8 @@ def delete_item(request):
|
||||
# semantics of delete_item whereby the store is draft aware. Right now calling
|
||||
# delete_item on a vertical tries to delete the draft version leaving the
|
||||
# requested delete to never occur
|
||||
if item.location.revision is None and item.location.category=='vertical' and delete_all_versions:
|
||||
modulestore('direct').delete_item(item.location)
|
||||
if item.location.revision is None and item.location.category == 'vertical' and delete_all_versions:
|
||||
modulestore('direct').delete_item(item.location)
|
||||
|
||||
return HttpResponse()
|
||||
|
||||
@@ -609,7 +628,7 @@ def save_item(request):
|
||||
if request.POST.get('data') is not None:
|
||||
data = request.POST['data']
|
||||
store.update_item(item_location, data)
|
||||
|
||||
|
||||
# cdodge: note calling request.POST.get('children') will return None if children is an empty array
|
||||
# so it lead to a bug whereby the last component to be deleted in the UI was not actually
|
||||
# deleting the children object from the children collection
|
||||
@@ -699,7 +718,7 @@ def unpublish_unit(request):
|
||||
def clone_item(request):
|
||||
parent_location = Location(request.POST['parent_location'])
|
||||
template = Location(request.POST['template'])
|
||||
|
||||
|
||||
display_name = request.POST.get('display_name')
|
||||
|
||||
if not has_access(request.user, parent_location):
|
||||
@@ -739,9 +758,9 @@ def upload_asset(request, org, course, coursename):
|
||||
location = ['i4x', org, course, 'course', coursename]
|
||||
if not has_access(request.user, location):
|
||||
return HttpResponseForbidden()
|
||||
|
||||
|
||||
# Does the course actually exist?!? Get anything from it to prove its existance
|
||||
|
||||
|
||||
try:
|
||||
item = modulestore().get_item(location)
|
||||
except:
|
||||
@@ -775,12 +794,12 @@ def upload_asset(request, org, course, coursename):
|
||||
|
||||
# readback the saved content - we need the database timestamp
|
||||
readback = contentstore().find(content.location)
|
||||
|
||||
response_payload = {'displayname' : content.name,
|
||||
'uploadDate' : get_date_display(readback.last_modified_at),
|
||||
'url' : StaticContent.get_url_path_from_location(content.location),
|
||||
'thumb_url' : StaticContent.get_url_path_from_location(thumbnail_location) if thumbnail_content is not None else None,
|
||||
'msg' : 'Upload completed'
|
||||
|
||||
response_payload = {'displayname': content.name,
|
||||
'uploadDate': get_date_display(readback.last_modified_at),
|
||||
'url': StaticContent.get_url_path_from_location(content.location),
|
||||
'thumb_url': StaticContent.get_url_path_from_location(thumbnail_location) if thumbnail_content is not None else None,
|
||||
'msg': 'Upload completed'
|
||||
}
|
||||
|
||||
response = HttpResponse(json.dumps(response_payload))
|
||||
@@ -793,7 +812,7 @@ This view will return all CMS users who are editors for the specified course
|
||||
@login_required
|
||||
@ensure_csrf_cookie
|
||||
def manage_users(request, location):
|
||||
|
||||
|
||||
# check that logged in user has permissions to this item
|
||||
if not has_access(request.user, location, role=INSTRUCTOR_ROLE_NAME) and not has_access(request.user, location, role=STAFF_ROLE_NAME):
|
||||
raise PermissionDenied()
|
||||
@@ -804,16 +823,16 @@ def manage_users(request, location):
|
||||
'active_tab': 'users',
|
||||
'context_course': course_module,
|
||||
'staff': get_users_in_course_group_by_role(location, STAFF_ROLE_NAME),
|
||||
'add_user_postback_url' : reverse('add_user', args=[location]).rstrip('/'),
|
||||
'remove_user_postback_url' : reverse('remove_user', args=[location]).rstrip('/'),
|
||||
'allow_actions' : has_access(request.user, location, role=INSTRUCTOR_ROLE_NAME),
|
||||
'request_user_id' : request.user.id
|
||||
'add_user_postback_url': reverse('add_user', args=[location]).rstrip('/'),
|
||||
'remove_user_postback_url': reverse('remove_user', args=[location]).rstrip('/'),
|
||||
'allow_actions': has_access(request.user, location, role=INSTRUCTOR_ROLE_NAME),
|
||||
'request_user_id': request.user.id
|
||||
})
|
||||
|
||||
|
||||
|
||||
def create_json_response(errmsg = None):
|
||||
if errmsg is not None:
|
||||
resp = HttpResponse(json.dumps({'Status': 'Failed', 'ErrMsg' : errmsg}))
|
||||
resp = HttpResponse(json.dumps({'Status': 'Failed', 'ErrMsg': errmsg}))
|
||||
else:
|
||||
resp = HttpResponse(json.dumps({'Status': 'OK'}))
|
||||
|
||||
@@ -829,15 +848,15 @@ the specified course
|
||||
def add_user(request, location):
|
||||
email = request.POST["email"]
|
||||
|
||||
if email=='':
|
||||
if email == '':
|
||||
return create_json_response('Please specify an email address.')
|
||||
|
||||
|
||||
# check that logged in user has admin permissions to this course
|
||||
if not has_access(request.user, location, role=INSTRUCTOR_ROLE_NAME):
|
||||
raise PermissionDenied()
|
||||
|
||||
|
||||
user = get_user_by_email(email)
|
||||
|
||||
|
||||
# user doesn't exist?!? Return error.
|
||||
if user is None:
|
||||
return create_json_response('Could not find user by email address \'{0}\'.'.format(email))
|
||||
@@ -860,7 +879,7 @@ the specified course
|
||||
@ensure_csrf_cookie
|
||||
def remove_user(request, location):
|
||||
email = request.POST["email"]
|
||||
|
||||
|
||||
# check that logged in user has admin permissions on this course
|
||||
if not has_access(request.user, location, role=INSTRUCTOR_ROLE_NAME):
|
||||
raise PermissionDenied()
|
||||
@@ -887,7 +906,7 @@ def landing(request, org, course, coursename):
|
||||
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()
|
||||
@@ -916,13 +935,13 @@ def reorder_static_tabs(request):
|
||||
# get list of existing static tabs in course
|
||||
# make sure they are the same lengths (i.e. the number of passed in tabs equals the number
|
||||
# that we know about) otherwise we can drop some!
|
||||
|
||||
|
||||
existing_static_tabs = [t for t in course.tabs if t['type'] == 'static_tab']
|
||||
if len(existing_static_tabs) != len(tabs):
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
# load all reference tabs, return BadRequest if we can't find any of them
|
||||
tab_items =[]
|
||||
tab_items = []
|
||||
for tab in tabs:
|
||||
item = modulestore('direct').get_item(Location(tab))
|
||||
if item is None:
|
||||
@@ -935,15 +954,15 @@ def reorder_static_tabs(request):
|
||||
static_tab_idx = 0
|
||||
for tab in course.tabs:
|
||||
if tab['type'] == 'static_tab':
|
||||
reordered_tabs.append({'type': 'static_tab',
|
||||
'name' : tab_items[static_tab_idx].metadata.get('display_name'),
|
||||
'url_slug' : tab_items[static_tab_idx].location.name})
|
||||
reordered_tabs.append({'type': 'static_tab',
|
||||
'name': tab_items[static_tab_idx].metadata.get('display_name'),
|
||||
'url_slug': tab_items[static_tab_idx].location.name})
|
||||
static_tab_idx += 1
|
||||
else:
|
||||
reordered_tabs.append(tab)
|
||||
|
||||
|
||||
# OK, re-assemble the static tabs in the new order
|
||||
# OK, re-assemble the static tabs in the new order
|
||||
course.tabs = reordered_tabs
|
||||
modulestore('direct').update_metadata(course.location, course.metadata)
|
||||
return HttpResponse()
|
||||
@@ -952,7 +971,7 @@ def reorder_static_tabs(request):
|
||||
@login_required
|
||||
@ensure_csrf_cookie
|
||||
def edit_tabs(request, org, course, coursename):
|
||||
location = ['i4x', org, course, 'course', coursename]
|
||||
location = ['i4x', org, course, 'course', coursename]
|
||||
course_item = modulestore().get_item(location)
|
||||
static_tabs_loc = Location('i4x', org, course, 'static_tab', None)
|
||||
|
||||
@@ -981,7 +1000,7 @@ def edit_tabs(request, org, course, coursename):
|
||||
|
||||
return render_to_response('edit-tabs.html', {
|
||||
'active_tab': 'pages',
|
||||
'context_course':course_item,
|
||||
'context_course': course_item,
|
||||
'components': components
|
||||
})
|
||||
|
||||
@@ -1002,24 +1021,24 @@ 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()
|
||||
|
||||
|
||||
course_module = modulestore().get_item(location)
|
||||
|
||||
|
||||
# get current updates
|
||||
location = ['i4x', org, course, 'course_info', "updates"]
|
||||
|
||||
return render_to_response('course_info.html', {
|
||||
'active_tab': 'courseinfo-tab',
|
||||
'context_course': course_module,
|
||||
'url_base' : "/" + org + "/" + course + "/",
|
||||
'course_updates' : json.dumps(get_course_updates(location)),
|
||||
'url_base': "/" + org + "/" + course + "/",
|
||||
'course_updates': json.dumps(get_course_updates(location)),
|
||||
'handouts_location': Location(['i4x', org, course, 'course_info', 'handouts']).url()
|
||||
})
|
||||
|
||||
|
||||
@expect_json
|
||||
@login_required
|
||||
@ensure_csrf_cookie
|
||||
@@ -1033,7 +1052,7 @@ def course_info_updates(request, org, course, provided_id=None):
|
||||
# ??? No way to check for access permission afaik
|
||||
# get current updates
|
||||
location = ['i4x', org, course, 'course_info', "updates"]
|
||||
|
||||
|
||||
# Hmmm, provided_id is coming as empty string on create whereas I believe it used to be None :-(
|
||||
# Possibly due to my removing the seemingly redundant pattern in urls.py
|
||||
if provided_id == '':
|
||||
@@ -1048,7 +1067,7 @@ def course_info_updates(request, org, course, provided_id=None):
|
||||
real_method = request.META['HTTP_X_HTTP_METHOD_OVERRIDE']
|
||||
else:
|
||||
real_method = request.method
|
||||
|
||||
|
||||
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
|
||||
@@ -1065,7 +1084,7 @@ def course_info_updates(request, org, course, provided_id=None):
|
||||
@ensure_csrf_cookie
|
||||
def module_info(request, module_location):
|
||||
location = Location(module_location)
|
||||
|
||||
|
||||
# check that logged in user has permissions to this item
|
||||
if not has_access(request.user, location):
|
||||
raise PermissionDenied()
|
||||
@@ -1076,12 +1095,12 @@ def module_info(request, module_location):
|
||||
else:
|
||||
real_method = request.method
|
||||
|
||||
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))
|
||||
|
||||
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))
|
||||
|
||||
# check that logged in user has permissions to this item
|
||||
if not has_access(request.user, location):
|
||||
raise PermissionDenied()
|
||||
raise PermissionDenied()
|
||||
|
||||
if real_method == 'GET':
|
||||
return HttpResponse(json.dumps(get_module_info(get_modulestore(location), location, rewrite_static_links=rewrite_static_links)), mimetype="application/json")
|
||||
@@ -1099,20 +1118,66 @@ 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()
|
||||
|
||||
|
||||
course_module = modulestore().get_item(location)
|
||||
course_details = CourseDetails.fetch(location)
|
||||
|
||||
|
||||
return render_to_response('settings.html', {
|
||||
'active_tab': 'settings',
|
||||
'context_course': course_module,
|
||||
'course_location' : location,
|
||||
'course_details' : json.dumps(course_details, cls=CourseSettingsEncoder)
|
||||
})
|
||||
|
||||
|
||||
@login_required
|
||||
@ensure_csrf_cookie
|
||||
def course_config_graders_page(request, org, course, name):
|
||||
"""
|
||||
Send models and views as well as html for editing the course settings to the client.
|
||||
|
||||
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()
|
||||
|
||||
course_module = modulestore().get_item(location)
|
||||
course_details = CourseGradingModel.fetch(location)
|
||||
|
||||
return render_to_response('settings_graders.html', {
|
||||
'context_course': course_module,
|
||||
'course_location' : location,
|
||||
'course_details': json.dumps(course_details, cls=CourseSettingsEncoder)
|
||||
})
|
||||
|
||||
@login_required
|
||||
@ensure_csrf_cookie
|
||||
def course_config_advanced_page(request, org, course, name):
|
||||
"""
|
||||
Send models and views as well as html for editing the advanced course settings to the client.
|
||||
|
||||
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()
|
||||
|
||||
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)),
|
||||
})
|
||||
|
||||
@expect_json
|
||||
@login_required
|
||||
@ensure_csrf_cookie
|
||||
@@ -1135,13 +1200,13 @@ def course_settings_updates(request, org, course, name, section):
|
||||
elif section == 'grading':
|
||||
manager = CourseGradingModel
|
||||
else: return
|
||||
|
||||
|
||||
if request.method == 'GET':
|
||||
# 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),
|
||||
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.
|
||||
return HttpResponse(json.dumps(manager.update_from_json(request.POST), cls=CourseSettingsEncoder),
|
||||
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")
|
||||
|
||||
@expect_json
|
||||
@@ -1154,7 +1219,7 @@ 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]
|
||||
|
||||
# check that logged in user has permissions to this item
|
||||
@@ -1165,19 +1230,50 @@ def course_grader_updates(request, org, course, name, grader_index=None):
|
||||
real_method = request.META['HTTP_X_HTTP_METHOD_OVERRIDE']
|
||||
else:
|
||||
real_method = request.method
|
||||
|
||||
|
||||
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(['i4x', org, course, 'course', name]), 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)
|
||||
# ??? Shoudl this return anything? Perhaps success fail?
|
||||
CourseGradingModel.delete_grader(Location(['i4x', org, course, 'course', name]), 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)),
|
||||
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)),
|
||||
mimetype="application/json")
|
||||
|
||||
|
||||
## NB: expect_json failed on ["key", "key2"] and json payload
|
||||
@login_required
|
||||
@ensure_csrf_cookie
|
||||
def course_advanced_updates(request, org, course, name):
|
||||
"""
|
||||
restful CRUD operations on metadata. The payload is a json rep of the metadata dicts. For delete, otoh,
|
||||
the payload is either a key or a list of keys to delete.
|
||||
|
||||
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()
|
||||
|
||||
# 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
|
||||
|
||||
if real_method == 'GET':
|
||||
return HttpResponse(json.dumps(CourseMetadata.fetch(location)), mimetype="application/json")
|
||||
elif real_method == 'DELETE':
|
||||
return HttpResponse(json.dumps(CourseMetadata.delete_key(location, json.loads(request.body))), mimetype="application/json")
|
||||
elif real_method == 'POST' or real_method == 'PUT':
|
||||
# NOTE: request.POST is messed up because expect_json cloned_request.POST.copy() is creating a defective entry w/ the whole payload as the key
|
||||
return HttpResponse(json.dumps(CourseMetadata.update_from_json(location, json.loads(request.body))), mimetype="application/json")
|
||||
|
||||
|
||||
@login_required
|
||||
@ensure_csrf_cookie
|
||||
@@ -1188,20 +1284,20 @@ 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()
|
||||
|
||||
|
||||
upload_asset_callback_url = reverse('upload_asset', kwargs = {
|
||||
'org' : org,
|
||||
'course' : course,
|
||||
'coursename' : name
|
||||
upload_asset_callback_url = reverse('upload_asset', kwargs={
|
||||
'org': org,
|
||||
'course': course,
|
||||
'coursename': name
|
||||
})
|
||||
|
||||
course_module = modulestore().get_item(location)
|
||||
|
||||
|
||||
course_reference = StaticContent.compute_location(org, course, name)
|
||||
assets = contentstore().get_all_content_for_course(course_reference)
|
||||
|
||||
@@ -1215,15 +1311,15 @@ def asset_index(request, org, course, name):
|
||||
display_info = {}
|
||||
display_info['displayname'] = asset['displayname']
|
||||
display_info['uploadDate'] = get_date_display(asset['uploadDate'])
|
||||
|
||||
|
||||
asset_location = StaticContent.compute_location(id['org'], id['course'], id['name'])
|
||||
display_info['url'] = StaticContent.get_url_path_from_location(asset_location)
|
||||
|
||||
|
||||
# note, due to the schema change we may not have a 'thumbnail_location' in the result set
|
||||
_thumbnail_location = asset.get('thumbnail_location', None)
|
||||
thumbnail_location = Location(_thumbnail_location) if _thumbnail_location is not None else None
|
||||
display_info['thumb_url'] = StaticContent.get_url_path_from_location(thumbnail_location) if thumbnail_location is not None else None
|
||||
|
||||
|
||||
asset_display.append(display_info)
|
||||
|
||||
return render_to_response('asset_index.html', {
|
||||
@@ -1241,10 +1337,19 @@ def edge(request):
|
||||
@login_required
|
||||
@expect_json
|
||||
def create_new_course(request):
|
||||
|
||||
if settings.MITX_FEATURES.get('DISABLE_COURSE_CREATION', False) and not request.user.is_staff:
|
||||
raise PermissionDenied()
|
||||
|
||||
# This logic is repeated in xmodule/modulestore/tests/factories.py
|
||||
# so if you change anything here, you need to also change it there.
|
||||
# TODO: write a test that creates two courses, one with the factory and
|
||||
# the other with this method, then compare them to make sure they are
|
||||
# equivalent.
|
||||
template = Location(request.POST['template'])
|
||||
org = request.POST.get('org')
|
||||
number = request.POST.get('number')
|
||||
display_name = request.POST.get('display_name')
|
||||
org = request.POST.get('org')
|
||||
number = request.POST.get('number')
|
||||
display_name = request.POST.get('display_name')
|
||||
|
||||
try:
|
||||
dest_location = Location('i4x', org, number, 'course', Location.clean(display_name))
|
||||
@@ -1290,13 +1395,16 @@ def initialize_course_tabs(course):
|
||||
# at least a list populated with the minimal times
|
||||
# @TODO: I don't like the fact that the presentation tier is away of these data related constraints, let's find a better
|
||||
# place for this. Also rather than using a simple list of dictionaries a nice class model would be helpful here
|
||||
course.tabs = [{"type": "courseware"},
|
||||
|
||||
# This logic is repeated in xmodule/modulestore/tests/factories.py
|
||||
# so if you change anything here, you need to also change it there.
|
||||
course.tabs = [{"type": "courseware"},
|
||||
{"type": "course_info", "name": "Course Info"},
|
||||
{"type": "discussion", "name": "Discussion"},
|
||||
{"type": "wiki", "name": "Wiki"},
|
||||
{"type": "progress", "name": "Progress"}]
|
||||
|
||||
modulestore('direct').update_metadata(course.location.url(), course.own_metadata)
|
||||
modulestore('direct').update_metadata(course.location.url(), course.own_metadata)
|
||||
|
||||
@ensure_csrf_cookie
|
||||
@login_required
|
||||
@@ -1336,7 +1444,7 @@ def import_course(request, org, course, name):
|
||||
|
||||
# find the 'course.xml' file
|
||||
|
||||
for r,d,f in os.walk(course_dir):
|
||||
for r, d, f in os.walk(course_dir):
|
||||
for files in f:
|
||||
if files == 'course.xml':
|
||||
break
|
||||
@@ -1350,10 +1458,10 @@ def import_course(request, org, course, name):
|
||||
|
||||
if r != course_dir:
|
||||
for fname in os.listdir(r):
|
||||
shutil.move(r/fname, course_dir)
|
||||
shutil.move(r / fname, course_dir)
|
||||
|
||||
module_store, course_items = import_from_xml(modulestore('direct'), settings.GITHUB_REPO_ROOT,
|
||||
[course_subdir], load_error_modules=False, static_content_store=contentstore(), target_location_namespace = Location(location))
|
||||
[course_subdir], load_error_modules=False, static_content_store=contentstore(), target_location_namespace=Location(location))
|
||||
|
||||
# we can blow this away when we're done importing.
|
||||
shutil.rmtree(course_dir)
|
||||
@@ -1369,7 +1477,7 @@ 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=[
|
||||
'successful_import_redirect_url': reverse('course_index', args=[
|
||||
course_module.location.org,
|
||||
course_module.location.course,
|
||||
course_module.location.name])
|
||||
@@ -1384,12 +1492,12 @@ def generate_export_course(request, org, course, name):
|
||||
raise PermissionDenied()
|
||||
|
||||
loc = Location(location)
|
||||
export_file = NamedTemporaryFile(prefix=name+'.', suffix=".tar.gz")
|
||||
export_file = NamedTemporaryFile(prefix=name + '.', suffix=".tar.gz")
|
||||
|
||||
root_dir = path(mkdtemp())
|
||||
|
||||
# export out to a tempdir
|
||||
|
||||
|
||||
logging.debug('root = {0}'.format(root_dir))
|
||||
|
||||
export_to_xml(modulestore('direct'), contentstore(), loc, root_dir, name)
|
||||
@@ -1397,11 +1505,11 @@ def generate_export_course(request, org, course, name):
|
||||
|
||||
logging.debug('tar file being generated at {0}'.format(export_file.name))
|
||||
tf = tarfile.open(name=export_file.name, mode='w:gz')
|
||||
tf.add(root_dir/name, arcname=name)
|
||||
tf.add(root_dir / name, arcname=name)
|
||||
tf.close()
|
||||
|
||||
# remove temp dir
|
||||
shutil.rmtree(root_dir/name)
|
||||
shutil.rmtree(root_dir / name)
|
||||
|
||||
wrapper = FileWrapper(export_file)
|
||||
response = HttpResponse(wrapper, content_type='application/x-tgz')
|
||||
@@ -1423,7 +1531,7 @@ def export_course(request, org, course, name):
|
||||
return render_to_response('export.html', {
|
||||
'context_course': course_module,
|
||||
'active_tab': 'export',
|
||||
'successful_import_redirect_url' : ''
|
||||
'successful_import_redirect_url': ''
|
||||
})
|
||||
|
||||
def event(request):
|
||||
@@ -1431,4 +1539,4 @@ def event(request):
|
||||
A noop to swallow the analytics call so that cms methods don't spook and poor developers looking at
|
||||
console logs don't get distracted :-)
|
||||
'''
|
||||
return HttpResponse(True)
|
||||
return HttpResponse(True)
|
||||
|
||||
@@ -31,16 +31,16 @@ class CourseDetails(object):
|
||||
"""
|
||||
if not isinstance(course_location, Location):
|
||||
course_location = Location(course_location)
|
||||
|
||||
|
||||
course = cls(course_location)
|
||||
|
||||
|
||||
descriptor = get_modulestore(course_location).get_item(course_location)
|
||||
|
||||
|
||||
course.start_date = descriptor.start
|
||||
course.end_date = descriptor.end
|
||||
course.enrollment_start = descriptor.enrollment_start
|
||||
course.enrollment_end = descriptor.enrollment_end
|
||||
|
||||
|
||||
temploc = course_location._replace(category='about', name='syllabus')
|
||||
try:
|
||||
course.syllabus = get_modulestore(temploc).get_item(temploc).definition['data']
|
||||
@@ -52,32 +52,32 @@ class CourseDetails(object):
|
||||
course.overview = get_modulestore(temploc).get_item(temploc).definition['data']
|
||||
except ItemNotFoundError:
|
||||
pass
|
||||
|
||||
|
||||
temploc = temploc._replace(name='effort')
|
||||
try:
|
||||
course.effort = get_modulestore(temploc).get_item(temploc).definition['data']
|
||||
except ItemNotFoundError:
|
||||
pass
|
||||
|
||||
|
||||
temploc = temploc._replace(name='video')
|
||||
try:
|
||||
raw_video = get_modulestore(temploc).get_item(temploc).definition['data']
|
||||
course.intro_video = CourseDetails.parse_video_tag(raw_video)
|
||||
course.intro_video = CourseDetails.parse_video_tag(raw_video)
|
||||
except ItemNotFoundError:
|
||||
pass
|
||||
|
||||
|
||||
return course
|
||||
|
||||
|
||||
@classmethod
|
||||
def update_from_json(cls, jsondict):
|
||||
"""
|
||||
Decode the json into CourseDetails and save any changed attrs to the db
|
||||
"""
|
||||
## TODO make it an error for this to be undefined & for it to not be retrievable from modulestore
|
||||
## TODO make it an error for this to be undefined & for it to not be retrievable from modulestore
|
||||
course_location = jsondict['course_location']
|
||||
## Will probably want to cache the inflight courses because every blur generates an update
|
||||
descriptor = get_modulestore(course_location).get_item(course_location)
|
||||
|
||||
|
||||
dirty = False
|
||||
|
||||
if 'start_date' in jsondict:
|
||||
@@ -87,7 +87,7 @@ class CourseDetails(object):
|
||||
if converted != descriptor.start:
|
||||
dirty = True
|
||||
descriptor.start = converted
|
||||
|
||||
|
||||
if 'end_date' in jsondict:
|
||||
converted = jsdate_to_time(jsondict['end_date'])
|
||||
else:
|
||||
@@ -96,7 +96,7 @@ class CourseDetails(object):
|
||||
if converted != descriptor.end:
|
||||
dirty = True
|
||||
descriptor.end = converted
|
||||
|
||||
|
||||
if 'enrollment_start' in jsondict:
|
||||
converted = jsdate_to_time(jsondict['enrollment_start'])
|
||||
else:
|
||||
@@ -105,7 +105,7 @@ class CourseDetails(object):
|
||||
if converted != descriptor.enrollment_start:
|
||||
dirty = True
|
||||
descriptor.enrollment_start = converted
|
||||
|
||||
|
||||
if 'enrollment_end' in jsondict:
|
||||
converted = jsdate_to_time(jsondict['enrollment_end'])
|
||||
else:
|
||||
@@ -114,10 +114,10 @@ class CourseDetails(object):
|
||||
if converted != descriptor.enrollment_end:
|
||||
dirty = True
|
||||
descriptor.enrollment_end = converted
|
||||
|
||||
|
||||
if dirty:
|
||||
get_modulestore(course_location).update_metadata(course_location, descriptor.metadata)
|
||||
|
||||
|
||||
# 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.
|
||||
temploc = Location(course_location)._replace(category='about', name='syllabus')
|
||||
@@ -125,19 +125,19 @@ class CourseDetails(object):
|
||||
|
||||
temploc = temploc._replace(name='overview')
|
||||
update_item(temploc, jsondict['overview'])
|
||||
|
||||
|
||||
temploc = temploc._replace(name='effort')
|
||||
update_item(temploc, jsondict['effort'])
|
||||
|
||||
|
||||
temploc = temploc._replace(name='video')
|
||||
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)
|
||||
|
||||
|
||||
@staticmethod
|
||||
def parse_video_tag(raw_video):
|
||||
"""
|
||||
@@ -147,17 +147,17 @@ class CourseDetails(object):
|
||||
"""
|
||||
if not raw_video:
|
||||
return None
|
||||
|
||||
|
||||
keystring_matcher = re.search('(?<=embed/)[a-zA-Z0-9_-]+', raw_video)
|
||||
if keystring_matcher is None:
|
||||
keystring_matcher = re.search('<?=\d+:[a-zA-Z0-9_-]+', raw_video)
|
||||
|
||||
|
||||
if keystring_matcher:
|
||||
return keystring_matcher.group(0)
|
||||
else:
|
||||
logging.warn("ignoring the content because it doesn't not conform to expected pattern: " + raw_video)
|
||||
return None
|
||||
|
||||
|
||||
@staticmethod
|
||||
def recompose_video_tag(video_key):
|
||||
# TODO should this use a mako template? Of course, my hope is that this is a short-term workaround for the db not storing
|
||||
@@ -168,7 +168,7 @@ class CourseDetails(object):
|
||||
video_key + '?autoplay=1&rel=0" frameborder="0" allowfullscreen=""></iframe>'
|
||||
return result
|
||||
|
||||
|
||||
|
||||
|
||||
# TODO move to a more general util? Is there a better way to do the isinstance model check?
|
||||
class CourseSettingsEncoder(json.JSONEncoder):
|
||||
|
||||
@@ -6,55 +6,55 @@ from util import converters
|
||||
|
||||
class CourseGradingModel(object):
|
||||
"""
|
||||
Basically a DAO and Model combo for CRUD operations pertaining to grading policy.
|
||||
Basically a DAO and Model combo for CRUD operations pertaining to grading policy.
|
||||
"""
|
||||
def __init__(self, course_descriptor):
|
||||
self.course_location = course_descriptor.location
|
||||
self.graders = [CourseGradingModel.jsonize_grader(i, grader) for i, grader in enumerate(course_descriptor.raw_grader)] # weights transformed to ints [0..100]
|
||||
self.graders = [CourseGradingModel.jsonize_grader(i, grader) for i, grader in enumerate(course_descriptor.raw_grader)] # weights transformed to ints [0..100]
|
||||
self.grade_cutoffs = course_descriptor.grade_cutoffs
|
||||
self.grace_period = CourseGradingModel.convert_set_grace_period(course_descriptor)
|
||||
|
||||
@classmethod
|
||||
|
||||
@classmethod
|
||||
def fetch(cls, course_location):
|
||||
"""
|
||||
Fetch the course details for the given course from persistence and return a CourseDetails model.
|
||||
"""
|
||||
if not isinstance(course_location, Location):
|
||||
course_location = Location(course_location)
|
||||
|
||||
|
||||
descriptor = get_modulestore(course_location).get_item(course_location)
|
||||
|
||||
model = cls(descriptor)
|
||||
return model
|
||||
|
||||
|
||||
@staticmethod
|
||||
def fetch_grader(course_location, index):
|
||||
"""
|
||||
Fetch the course's nth grader
|
||||
Fetch the course's nth grader
|
||||
Returns an empty dict if there's no such grader.
|
||||
"""
|
||||
if not isinstance(course_location, Location):
|
||||
course_location = Location(course_location)
|
||||
|
||||
|
||||
descriptor = get_modulestore(course_location).get_item(course_location)
|
||||
# # ??? it would be good if these had the course_location in them so that they stand alone sufficiently
|
||||
# # but that would require not using CourseDescriptor's field directly. Opinions?
|
||||
|
||||
index = int(index)
|
||||
if len(descriptor.raw_grader) > index:
|
||||
index = int(index)
|
||||
if len(descriptor.raw_grader) > index:
|
||||
return CourseGradingModel.jsonize_grader(index, descriptor.raw_grader[index])
|
||||
|
||||
|
||||
# return empty model
|
||||
else:
|
||||
return {
|
||||
"id" : index,
|
||||
"type" : "",
|
||||
"min_count" : 0,
|
||||
"drop_count" : 0,
|
||||
"short_label" : None,
|
||||
"weight" : 0
|
||||
"id": index,
|
||||
"type": "",
|
||||
"min_count": 0,
|
||||
"drop_count": 0,
|
||||
"short_label": None,
|
||||
"weight": 0
|
||||
}
|
||||
|
||||
|
||||
@staticmethod
|
||||
def fetch_cutoffs(course_location):
|
||||
"""
|
||||
@@ -62,7 +62,7 @@ class CourseGradingModel(object):
|
||||
"""
|
||||
if not isinstance(course_location, Location):
|
||||
course_location = Location(course_location)
|
||||
|
||||
|
||||
descriptor = get_modulestore(course_location).get_item(course_location)
|
||||
return descriptor.grade_cutoffs
|
||||
|
||||
@@ -73,10 +73,10 @@ class CourseGradingModel(object):
|
||||
"""
|
||||
if not isinstance(course_location, Location):
|
||||
course_location = Location(course_location)
|
||||
|
||||
|
||||
descriptor = get_modulestore(course_location).get_item(course_location)
|
||||
return {'grace_period' : CourseGradingModel.convert_set_grace_period(descriptor) }
|
||||
|
||||
return {'grace_period': CourseGradingModel.convert_set_grace_period(descriptor)}
|
||||
|
||||
@staticmethod
|
||||
def update_from_json(jsondict):
|
||||
"""
|
||||
@@ -85,32 +85,32 @@ class CourseGradingModel(object):
|
||||
"""
|
||||
course_location = jsondict['course_location']
|
||||
descriptor = get_modulestore(course_location).get_item(course_location)
|
||||
|
||||
|
||||
graders_parsed = [CourseGradingModel.parse_grader(jsonele) for jsonele in jsondict['graders']]
|
||||
|
||||
|
||||
descriptor.raw_grader = graders_parsed
|
||||
descriptor.grade_cutoffs = jsondict['grade_cutoffs']
|
||||
|
||||
|
||||
get_modulestore(course_location).update_item(course_location, descriptor.definition['data'])
|
||||
CourseGradingModel.update_grace_period_from_json(course_location, jsondict['grace_period'])
|
||||
|
||||
|
||||
return CourseGradingModel.fetch(course_location)
|
||||
|
||||
|
||||
|
||||
|
||||
@staticmethod
|
||||
def update_grader_from_json(course_location, grader):
|
||||
"""
|
||||
Create or update the grader of the given type (string key) for the given course. Returns the modified
|
||||
Create or update the grader of the given type (string key) for the given course. Returns the modified
|
||||
grader which is a full model on the client but not on the server (just a dict)
|
||||
"""
|
||||
if not isinstance(course_location, Location):
|
||||
course_location = Location(course_location)
|
||||
|
||||
|
||||
descriptor = get_modulestore(course_location).get_item(course_location)
|
||||
# # ??? it would be good if these had the course_location in them so that they stand alone sufficiently
|
||||
# # but that would require not using CourseDescriptor's field directly. Opinions?
|
||||
|
||||
# parse removes the id; so, grab it before parse
|
||||
# parse removes the id; so, grab it before parse
|
||||
index = int(grader.get('id', len(descriptor.raw_grader)))
|
||||
grader = CourseGradingModel.parse_grader(grader)
|
||||
|
||||
@@ -118,11 +118,11 @@ class CourseGradingModel(object):
|
||||
descriptor.raw_grader[index] = grader
|
||||
else:
|
||||
descriptor.raw_grader.append(grader)
|
||||
|
||||
|
||||
get_modulestore(course_location).update_item(course_location, descriptor.definition['data'])
|
||||
|
||||
|
||||
return CourseGradingModel.jsonize_grader(index, descriptor.raw_grader[index])
|
||||
|
||||
|
||||
@staticmethod
|
||||
def update_cutoffs_from_json(course_location, cutoffs):
|
||||
"""
|
||||
@@ -131,18 +131,18 @@ class CourseGradingModel(object):
|
||||
"""
|
||||
if not isinstance(course_location, Location):
|
||||
course_location = Location(course_location)
|
||||
|
||||
|
||||
descriptor = get_modulestore(course_location).get_item(course_location)
|
||||
descriptor.grade_cutoffs = cutoffs
|
||||
get_modulestore(course_location).update_item(course_location, descriptor.definition['data'])
|
||||
|
||||
|
||||
return cutoffs
|
||||
|
||||
|
||||
|
||||
|
||||
@staticmethod
|
||||
def update_grace_period_from_json(course_location, graceperiodjson):
|
||||
"""
|
||||
Update the course's default grace period. Incoming dict is {hours: h, minutes: m} possibly as a
|
||||
Update the course's default grace period. Incoming dict is {hours: h, minutes: m} possibly as a
|
||||
grace_period entry in an enclosing dict. It is also safe to call this method with a value of
|
||||
None for graceperiodjson.
|
||||
"""
|
||||
@@ -155,12 +155,13 @@ class CourseGradingModel(object):
|
||||
if 'grace_period' in graceperiodjson:
|
||||
graceperiodjson = graceperiodjson['grace_period']
|
||||
|
||||
grace_rep = " ".join(["%s %s" % (value, key) for (key, value) in graceperiodjson.iteritems()])
|
||||
# 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)
|
||||
|
||||
descriptor = get_modulestore(course_location).get_item(course_location)
|
||||
descriptor.metadata['graceperiod'] = grace_rep
|
||||
get_modulestore(course_location).update_metadata(course_location, descriptor.metadata)
|
||||
|
||||
|
||||
@staticmethod
|
||||
def delete_grader(course_location, index):
|
||||
"""
|
||||
@@ -168,16 +169,16 @@ class CourseGradingModel(object):
|
||||
"""
|
||||
if not isinstance(course_location, Location):
|
||||
course_location = Location(course_location)
|
||||
|
||||
|
||||
descriptor = get_modulestore(course_location).get_item(course_location)
|
||||
index = int(index)
|
||||
index = int(index)
|
||||
if index < len(descriptor.raw_grader):
|
||||
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'])
|
||||
|
||||
# NOTE cannot delete cutoffs. May be useful to reset
|
||||
|
||||
# NOTE cannot delete cutoffs. May be useful to reset
|
||||
@staticmethod
|
||||
def delete_cutoffs(course_location, cutoffs):
|
||||
"""
|
||||
@@ -185,13 +186,13 @@ class CourseGradingModel(object):
|
||||
"""
|
||||
if not isinstance(course_location, Location):
|
||||
course_location = Location(course_location)
|
||||
|
||||
|
||||
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'])
|
||||
|
||||
|
||||
return descriptor.grade_cutoffs
|
||||
|
||||
|
||||
@staticmethod
|
||||
def delete_grace_period(course_location):
|
||||
"""
|
||||
@@ -199,28 +200,28 @@ class CourseGradingModel(object):
|
||||
"""
|
||||
if not isinstance(course_location, Location):
|
||||
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)
|
||||
|
||||
|
||||
@staticmethod
|
||||
def get_section_grader_type(location):
|
||||
if not isinstance(location, Location):
|
||||
location = Location(location)
|
||||
|
||||
|
||||
descriptor = get_modulestore(location).get_item(location)
|
||||
return {
|
||||
"graderType" : descriptor.metadata.get('format', u"Not Graded"),
|
||||
"location" : location,
|
||||
"id" : 99 # just an arbitrary value to
|
||||
"graderType": descriptor.metadata.get('format', u"Not Graded"),
|
||||
"location": location,
|
||||
"id": 99 # just an arbitrary value to
|
||||
}
|
||||
|
||||
|
||||
@staticmethod
|
||||
def update_section_grader_type(location, jsondict):
|
||||
if not isinstance(location, Location):
|
||||
location = Location(location)
|
||||
|
||||
|
||||
descriptor = get_modulestore(location).get_item(location)
|
||||
if 'graderType' in jsondict and jsondict['graderType'] != u"Not Graded":
|
||||
descriptor.metadata['format'] = jsondict.get('graderType')
|
||||
@@ -228,16 +229,16 @@ class CourseGradingModel(object):
|
||||
else:
|
||||
if 'format' in descriptor.metadata: del descriptor.metadata['format']
|
||||
if 'graded' in descriptor.metadata: del descriptor.metadata['graded']
|
||||
|
||||
get_modulestore(location).update_metadata(location, descriptor.metadata)
|
||||
|
||||
|
||||
|
||||
get_modulestore(location).update_metadata(location, descriptor.metadata)
|
||||
|
||||
|
||||
@staticmethod
|
||||
def convert_set_grace_period(descriptor):
|
||||
# 5 hours 59 minutes 59 seconds => converted to iso format
|
||||
# 5 hours 59 minutes 59 seconds => { hours: 5, minutes : 59, seconds : 59}
|
||||
rawgrace = descriptor.metadata.get('graceperiod', None)
|
||||
if rawgrace:
|
||||
parsedgrace = {str(key): val for (val, key) in re.findall('\s*(\d+)\s*(\w+)', rawgrace)}
|
||||
parsedgrace = {str(key): int(val) for (val, key) in re.findall('\s*(\d+)\s*(\w+)', rawgrace)}
|
||||
return parsedgrace
|
||||
else: return None
|
||||
|
||||
@@ -245,13 +246,13 @@ class CourseGradingModel(object):
|
||||
def parse_grader(json_grader):
|
||||
# manual to clear out kruft
|
||||
result = {
|
||||
"type" : json_grader["type"],
|
||||
"min_count" : int(json_grader.get('min_count', 0)),
|
||||
"drop_count" : int(json_grader.get('drop_count', 0)),
|
||||
"short_label" : json_grader.get('short_label', None),
|
||||
"weight" : float(json_grader.get('weight', 0)) / 100.0
|
||||
"type": json_grader["type"],
|
||||
"min_count": int(json_grader.get('min_count', 0)),
|
||||
"drop_count": int(json_grader.get('drop_count', 0)),
|
||||
"short_label": json_grader.get('short_label', None),
|
||||
"weight": float(json_grader.get('weight', 0)) / 100.0
|
||||
}
|
||||
|
||||
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
@@ -260,6 +261,6 @@ class CourseGradingModel(object):
|
||||
if grader['weight']:
|
||||
grader['weight'] *= 100
|
||||
if not 'short_label' in grader:
|
||||
grader['short_label'] = ""
|
||||
|
||||
grader['short_label'] = ""
|
||||
|
||||
return grader
|
||||
|
||||
70
cms/djangoapps/models/settings/course_metadata.py
Normal file
@@ -0,0 +1,70 @@
|
||||
from xmodule.modulestore import Location
|
||||
from contentstore.utils import get_modulestore
|
||||
from xmodule.x_module import XModuleDescriptor
|
||||
|
||||
|
||||
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.
|
||||
'''
|
||||
# __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__']
|
||||
|
||||
@classmethod
|
||||
def fetch(cls, course_location):
|
||||
"""
|
||||
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)
|
||||
|
||||
course = {}
|
||||
|
||||
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
|
||||
|
||||
return course
|
||||
|
||||
@classmethod
|
||||
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
|
||||
|
||||
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 cls.FILTERED_LIST and (k not in descriptor.metadata or descriptor.metadata[k] != v):
|
||||
dirty = True
|
||||
descriptor.metadata[k] = v
|
||||
|
||||
if dirty:
|
||||
get_modulestore(course_location).update_metadata(course_location, descriptor.metadata)
|
||||
|
||||
# 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..]
|
||||
'''
|
||||
descriptor = get_modulestore(course_location).get_item(course_location)
|
||||
|
||||
for key in payload['deleteKeys']:
|
||||
if key in descriptor.metadata:
|
||||
del descriptor.metadata[key]
|
||||
|
||||
get_modulestore(course_location).update_metadata(course_location, descriptor.metadata)
|
||||
|
||||
return cls.fetch(course_location)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
This config file extends the test environment configuration
|
||||
This config file extends the test environment configuration
|
||||
so that we can run the lettuce acceptance tests.
|
||||
"""
|
||||
from .test import *
|
||||
@@ -21,14 +21,14 @@ DATA_DIR = COURSES_ROOT
|
||||
# }
|
||||
# }
|
||||
|
||||
# Set this up so that rake lms[acceptance] and running the
|
||||
# Set this up so that rake lms[acceptance] and running the
|
||||
# harvest command both use the same (test) database
|
||||
# which they can flush without messing up your dev db
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': ENV_ROOT / "db" / "test_mitx.db",
|
||||
'TEST_NAME': ENV_ROOT / "db" / "test_mitx.db",
|
||||
'TEST_NAME': ENV_ROOT / "db" / "test_mitx.db",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ CONFIG_PREFIX = ""
|
||||
if SERVICE_VARIANT:
|
||||
CONFIG_PREFIX = SERVICE_VARIANT + "."
|
||||
|
||||
############################### ALWAYS THE SAME ################################
|
||||
############### ALWAYS THE SAME ################################
|
||||
DEBUG = False
|
||||
TEMPLATE_DEBUG = False
|
||||
|
||||
@@ -28,7 +28,7 @@ EMAIL_BACKEND = 'django_ses.SESBackend'
|
||||
SESSION_ENGINE = 'django.contrib.sessions.backends.cache'
|
||||
DEFAULT_FILE_STORAGE = 'storages.backends.s3boto.S3BotoStorage'
|
||||
|
||||
########################### NON-SECURE ENV CONFIG ##############################
|
||||
############# NON-SECURE ENV CONFIG ##############################
|
||||
# Things like server locations, ports, etc.
|
||||
with open(ENV_ROOT / CONFIG_PREFIX + "env.json") as env_file:
|
||||
ENV_TOKENS = json.load(env_file)
|
||||
@@ -49,13 +49,10 @@ for feature, value in ENV_TOKENS.get('MITX_FEATURES', {}).items():
|
||||
LOGGING = get_logger_config(LOG_DIR,
|
||||
logging_env=ENV_TOKENS['LOGGING_ENV'],
|
||||
syslog_addr=(ENV_TOKENS['SYSLOG_SERVER'], 514),
|
||||
debug=False)
|
||||
debug=False,
|
||||
service_variant=SERVICE_VARIANT)
|
||||
|
||||
with open(ENV_ROOT / "repos.json") as repos_file:
|
||||
REPOS = json.load(repos_file)
|
||||
|
||||
|
||||
############################## SECURE AUTH ITEMS ###############################
|
||||
################ SECURE AUTH ITEMS ###############################
|
||||
# Secret things: passwords, access keys, etc.
|
||||
with open(ENV_ROOT / CONFIG_PREFIX + "auth.json") as auth_file:
|
||||
AUTH_TOKENS = json.load(auth_file)
|
||||
|
||||
@@ -20,7 +20,6 @@ Longer TODO:
|
||||
"""
|
||||
|
||||
import sys
|
||||
import tempfile
|
||||
import os.path
|
||||
import os
|
||||
import lms.envs.common
|
||||
@@ -33,8 +32,8 @@ MITX_FEATURES = {
|
||||
'USE_DJANGO_PIPELINE': True,
|
||||
'GITHUB_PUSH': False,
|
||||
'ENABLE_DISCUSSION_SERVICE': False,
|
||||
'AUTH_USE_MIT_CERTIFICATES' : False,
|
||||
'STUB_VIDEO_FOR_TESTING': False, # do not display video when running automated acceptance tests
|
||||
'AUTH_USE_MIT_CERTIFICATES': False,
|
||||
'STUB_VIDEO_FOR_TESTING': False, # do not display video when running automated acceptance tests
|
||||
}
|
||||
ENABLE_JASMINE = False
|
||||
|
||||
@@ -59,7 +58,8 @@ sys.path.append(COMMON_ROOT / 'lib')
|
||||
|
||||
############################# WEB CONFIGURATION #############################
|
||||
# This is where we stick our compiled template files.
|
||||
MAKO_MODULE_DIR = tempfile.mkdtemp('mako')
|
||||
from tempdir import mkdtemp_clean
|
||||
MAKO_MODULE_DIR = mkdtemp_clean('mako')
|
||||
MAKO_TEMPLATES = {}
|
||||
MAKO_TEMPLATES['main'] = [
|
||||
PROJECT_ROOT / 'templates',
|
||||
@@ -74,8 +74,8 @@ TEMPLATE_DIRS = MAKO_TEMPLATES['main']
|
||||
|
||||
MITX_ROOT_URL = ''
|
||||
|
||||
LOGIN_REDIRECT_URL = MITX_ROOT_URL + '/login'
|
||||
LOGIN_URL = MITX_ROOT_URL + '/login'
|
||||
LOGIN_REDIRECT_URL = MITX_ROOT_URL + '/signin'
|
||||
LOGIN_URL = MITX_ROOT_URL + '/signin'
|
||||
|
||||
|
||||
TEMPLATE_CONTEXT_PROCESSORS = (
|
||||
@@ -165,13 +165,6 @@ STATICFILES_DIRS = [
|
||||
# This is how you would use the textbook images locally
|
||||
# ("book", ENV_ROOT / "book_images")
|
||||
]
|
||||
if os.path.isdir(GITHUB_REPO_ROOT):
|
||||
STATICFILES_DIRS += [
|
||||
# TODO (cpennington): When courses aren't loaded from github, remove this
|
||||
(course_dir, GITHUB_REPO_ROOT / course_dir)
|
||||
for course_dir in os.listdir(GITHUB_REPO_ROOT)
|
||||
if os.path.isdir(GITHUB_REPO_ROOT / course_dir)
|
||||
]
|
||||
|
||||
# Locale/Internationalization
|
||||
TIME_ZONE = 'America/New_York' # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
|
||||
@@ -179,6 +172,9 @@ LANGUAGE_CODE = 'en' # http://www.i18nguy.com/unicode/language-identi
|
||||
USE_I18N = True
|
||||
USE_L10N = True
|
||||
|
||||
# Tracking
|
||||
TRACK_MAX_EVENT = 10000
|
||||
|
||||
# Messages
|
||||
MESSAGE_STORAGE = 'django.contrib.messages.storage.session.SessionStorage'
|
||||
|
||||
@@ -229,7 +225,7 @@ PIPELINE_JS = {
|
||||
'source_filenames': sorted(
|
||||
rooted_glob(COMMON_ROOT / 'static/', 'coffee/src/**/*.coffee') +
|
||||
rooted_glob(PROJECT_ROOT / 'static/', 'coffee/src/**/*.coffee')
|
||||
) + [ 'js/hesitate.js', 'js/base.js'],
|
||||
) + ['js/hesitate.js', 'js/base.js'],
|
||||
'output_filename': 'js/cms-application.js',
|
||||
},
|
||||
'module-js': {
|
||||
@@ -282,7 +278,12 @@ INSTALLED_APPS = (
|
||||
'auth',
|
||||
'student', # misleading name due to sharing with lms
|
||||
'course_groups', # not used in cms (yet), but tests run
|
||||
|
||||
# tracking
|
||||
'track',
|
||||
|
||||
# For asset pipelining
|
||||
'pipeline',
|
||||
'staticfiles',
|
||||
'static_replace',
|
||||
)
|
||||
|
||||
@@ -12,7 +12,7 @@ TEMPLATE_DEBUG = DEBUG
|
||||
LOGGING = get_logger_config(ENV_ROOT / "log",
|
||||
logging_env="dev",
|
||||
tracking_filename="tracking.log",
|
||||
dev_env = True,
|
||||
dev_env=True,
|
||||
debug=True)
|
||||
|
||||
modulestore_options = {
|
||||
@@ -41,7 +41,7 @@ CONTENTSTORE = {
|
||||
'ENGINE': 'xmodule.contentstore.mongo.MongoContentStore',
|
||||
'OPTIONS': {
|
||||
'host': 'localhost',
|
||||
'db' : 'xcontent',
|
||||
'db': 'xcontent',
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,8 +9,6 @@ import socket
|
||||
|
||||
MITX_FEATURES['AUTH_USE_MIT_CERTIFICATES'] = True
|
||||
|
||||
MITX_FEATURES['USE_DJANGO_PIPELINE']=False # don't recompile scss
|
||||
|
||||
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTOCOL', 'https') # django 1.4 for nginx ssl proxy
|
||||
|
||||
MITX_FEATURES['USE_DJANGO_PIPELINE'] = False # don't recompile scss
|
||||
|
||||
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTOCOL', 'https') # django 1.4 for nginx ssl proxy
|
||||
|
||||
@@ -11,7 +11,6 @@ from .common import *
|
||||
import os
|
||||
from path import path
|
||||
|
||||
|
||||
# Nose Test Runner
|
||||
INSTALLED_APPS += ('django_nose',)
|
||||
NOSE_ARGS = ['--with-xunit']
|
||||
@@ -20,7 +19,7 @@ TEST_RUNNER = 'django_nose.NoseTestSuiteRunner'
|
||||
TEST_ROOT = path('test_root')
|
||||
|
||||
# Makes the tests run much faster...
|
||||
SOUTH_TESTS_MIGRATE = False # To disable migrations and use syncdb instead
|
||||
SOUTH_TESTS_MIGRATE = False # To disable migrations and use syncdb instead
|
||||
|
||||
# Want static files in the same dir for running on jenkins.
|
||||
STATIC_ROOT = TEST_ROOT / "staticfiles"
|
||||
@@ -66,7 +65,7 @@ CONTENTSTORE = {
|
||||
'ENGINE': 'xmodule.contentstore.mongo.MongoContentStore',
|
||||
'OPTIONS': {
|
||||
'host': 'localhost',
|
||||
'db' : 'xcontent',
|
||||
'db': 'xcontent',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,23 +74,12 @@ DATABASES = {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': ENV_ROOT / "db" / "cms.db",
|
||||
},
|
||||
|
||||
# The following are for testing purposes...
|
||||
'edX/toy/2012_Fall': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': ENV_ROOT / "db" / "course1.db",
|
||||
},
|
||||
|
||||
'edx/full/6.002_Spring_2012': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': ENV_ROOT / "db" / "course2.db",
|
||||
}
|
||||
}
|
||||
|
||||
LMS_BASE = "localhost:8000"
|
||||
|
||||
CACHES = {
|
||||
# This is the cache used for most things. Askbot will not work without a
|
||||
# This is the cache used for most things. Askbot will not work without a
|
||||
# functioning cache -- it relies on caching to load its settings in places.
|
||||
# In staging/prod envs, the sessions also live here.
|
||||
'default': {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
from django.core.management import execute_manager
|
||||
import imp
|
||||
try:
|
||||
imp.find_module('settings') # Assumed to be in the same directory.
|
||||
imp.find_module('settings') # Assumed to be in the same directory.
|
||||
except ImportError:
|
||||
import sys
|
||||
sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. "
|
||||
|
||||
16
cms/static/client_templates/advanced_entry.html
Normal file
@@ -0,0 +1,16 @@
|
||||
<li class="field-group course-advanced-policy-list-item">
|
||||
<div class="field text key" id="<%= (_.isEmpty(key) ? '__new_advanced_key__' : 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>
|
||||
</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>
|
||||
@@ -1,69 +1,37 @@
|
||||
<li class="input input-existing multi course-grading-assignment-list-item">
|
||||
<div class="row row-col2">
|
||||
<label for="course-grading-assignment-name">Assignment Type Name:</label>
|
||||
<li class="field-group course-grading-assignment-list-item">
|
||||
<div class="field text" id="field-course-grading-assignment-name">
|
||||
<label for="course-grading-assignment-name">Assignment Type Name</label>
|
||||
<input type="text" class="long" id="course-grading-assignment-name" value="<%= model.get('type') %>" />
|
||||
<span class="tip tip-stacked">e.g. Homework, Midterm Exams</span>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<div class="input course-grading-assignment-name">
|
||||
<input type="text" class="long"
|
||||
id="course-grading-assignment-name" value="<%= model.get('type') %>">
|
||||
<span class="tip tip-stacked">e.g. Homework, Labs, Midterm Exams, Final Exam</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row row-col2">
|
||||
<label for="course-grading-shortname">Abbreviation:</label>
|
||||
|
||||
<div class="field">
|
||||
<div class="input course-grading-shortname">
|
||||
<input type="text" class="short"
|
||||
id="course-grading-assignment-shortname"
|
||||
value="<%= model.get('short_label') %>">
|
||||
<span class="tip tip-inline">e.g. HW, Midterm, Final</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row row-col2">
|
||||
<label for="course-grading-gradeweight">Weight of Total
|
||||
Grade:</label>
|
||||
|
||||
<div class="field">
|
||||
<div class="input course-grading-gradeweight">
|
||||
<input type="text" class="short"
|
||||
id="course-grading-assignment-gradeweight"
|
||||
value = "<%= model.get('weight') %>">
|
||||
<span class="tip tip-inline">e.g. 25%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row row-col2">
|
||||
<label for="course-grading-assignment-totalassignments">Total
|
||||
Number:</label>
|
||||
|
||||
<div class="field">
|
||||
<div class="input course-grading-totalassignments">
|
||||
<input type="text" class="short"
|
||||
id="course-grading-assignment-totalassignments"
|
||||
value = "<%= model.get('min_count') %>">
|
||||
<span class="tip tip-inline">total exercises assigned</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row row-col2">
|
||||
<label for="course-grading-assignment-droppable">Number of
|
||||
Droppable:</label>
|
||||
|
||||
<div class="field">
|
||||
<div class="input course-grading-droppable">
|
||||
<input type="text" class="short"
|
||||
id="course-grading-assignment-droppable"
|
||||
value = "<%= model.get('drop_count') %>">
|
||||
<span class="tip tip-inline">total exercises that won't be graded</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<a href="#" class="delete-button standard remove-item remove-grading-data"><span class="delete-icon"></span>Delete</a>
|
||||
<div class="field text" id="field-course-grading-assignment-shortname">
|
||||
<label for="course-grading-assignment-shortname">Abbreviation:</label>
|
||||
<input type="text" class="short" id="course-grading-assignment-shortname" value="<%= model.get('short_label') %>" />
|
||||
<span class="tip tip-inline">e.g. HW, Midterm</span>
|
||||
</div>
|
||||
|
||||
<div class="field text" id="field-course-grading-assignment-gradeweight">
|
||||
<label for="course-grading-assignment-gradeweight">Weight of Total Grade</label>
|
||||
<input type="text" class="short" id="course-grading-assignment-gradeweight" value = "<%= model.get('weight') %>" />
|
||||
<span class="tip tip-inline">e.g. 25%</span>
|
||||
</div>
|
||||
|
||||
<div class="field text" id="field-course-grading-assignment-totalassignments">
|
||||
<label for="course-grading-assignment-totalassignments">Total
|
||||
Number</label>
|
||||
<input type="text" class="short" id="course-grading-assignment-totalassignments" value = "<%= model.get('min_count') %>" />
|
||||
<span class="tip tip-inline">total exercises assigned</span>
|
||||
</div>
|
||||
|
||||
<div class="field text" id="field-course-grading-assignment-droppable">
|
||||
<label for="course-grading-assignment-droppable">Number of
|
||||
Droppable</label>
|
||||
<input type="text" class="short" id="course-grading-assignment-droppable" value = "<%= model.get('drop_count') %>" />
|
||||
<span class="tip tip-inline">total exercises that won't be graded</span>
|
||||
</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>
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
class CMS.Views.TabsEdit extends Backbone.View
|
||||
events:
|
||||
'click .new-tab': 'addNewTab'
|
||||
|
||||
initialize: =>
|
||||
@$('.component').each((idx, element) =>
|
||||
@@ -13,6 +11,7 @@ class CMS.Views.TabsEdit extends Backbone.View
|
||||
)
|
||||
)
|
||||
|
||||
@options.mast.find('.new-tab').on('click', @addNewTab)
|
||||
@$('.components').sortable(
|
||||
handle: '.drag-handle'
|
||||
update: @tabMoved
|
||||
|
||||
BIN
cms/static/img/hiw-feature1.png
Normal file
|
After Width: | Height: | Size: 66 KiB |
BIN
cms/static/img/hiw-feature2.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
cms/static/img/hiw-feature3.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 581 B |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 737 B |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 412 B |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 797 B |
|
Before Width: | Height: | Size: 994 B After Width: | Height: | Size: 234 B |
BIN
cms/static/img/logo-edx-studio-white.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
cms/static/img/logo-edx-studio.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
cms/static/img/pl-1x1-000.png
Normal file
|
After Width: | Height: | Size: 924 B |
BIN
cms/static/img/pl-1x1-fff.png
Normal file
|
After Width: | Height: | Size: 925 B |
BIN
cms/static/img/preview-lms-staticpages.png
Normal file
|
After Width: | Height: | Size: 9.3 KiB |
BIN
cms/static/img/thumb-hiw-feature1.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
cms/static/img/thumb-hiw-feature2.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
cms/static/img/thumb-hiw-feature3.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
@@ -5,7 +5,7 @@ var $newComponentItem;
|
||||
var $changedInput;
|
||||
var $spinner;
|
||||
|
||||
$(document).ready(function() {
|
||||
$(document).ready(function () {
|
||||
$body = $('body');
|
||||
$modal = $('.history-modal');
|
||||
$modalCover = $('<div class="modal-cover">');
|
||||
@@ -13,7 +13,7 @@ $(document).ready(function() {
|
||||
// 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 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;
|
||||
@@ -31,25 +31,73 @@ $(document).ready(function() {
|
||||
|
||||
$modal.bind('click', hideModal);
|
||||
$modalCover.bind('click', hideModal);
|
||||
$('.assets .upload-button').bind('click', showUploadModal);
|
||||
$('.uploads .upload-button').bind('click', showUploadModal);
|
||||
$('.upload-modal .close-button').bind('click', hideModal);
|
||||
|
||||
$body.on('click', '.embeddable-xml-input', function(){ $(this).select(); });
|
||||
$body.on('click', '.embeddable-xml-input', function () {
|
||||
$(this).select();
|
||||
});
|
||||
|
||||
$('.unit .item-actions .delete-button').bind('click', deleteUnit);
|
||||
$('.new-unit-item').bind('click', createNewUnit);
|
||||
|
||||
$('body').addClass('js');
|
||||
|
||||
// lean/simple modal
|
||||
$('a[rel*=modal]').leanModal({overlay : 0.80, closeButton: '.action-modal-close' });
|
||||
$('a.action-modal-close').click(function(e){
|
||||
(e).preventDefault();
|
||||
});
|
||||
|
||||
// nav - dropdown related
|
||||
$body.click(function (e) {
|
||||
$('.nav-dropdown .nav-item .wrapper-nav-sub').removeClass('is-shown');
|
||||
$('.nav-dropdown .nav-item .title').removeClass('is-selected');
|
||||
});
|
||||
|
||||
$('.nav-dropdown .nav-item .title').click(function (e) {
|
||||
|
||||
$subnav = $(this).parent().find('.wrapper-nav-sub');
|
||||
$title = $(this).parent().find('.title');
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
if ($subnav.hasClass('is-shown')) {
|
||||
$subnav.removeClass('is-shown');
|
||||
$title.removeClass('is-selected');
|
||||
}
|
||||
|
||||
else {
|
||||
$('.nav-dropdown .nav-item .title').removeClass('is-selected');
|
||||
$('.nav-dropdown .nav-item .wrapper-nav-sub').removeClass('is-shown');
|
||||
$title.addClass('is-selected');
|
||||
$subnav.addClass('is-shown');
|
||||
}
|
||||
});
|
||||
|
||||
// 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();
|
||||
});
|
||||
|
||||
// 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' });
|
||||
$('.action-modal-close').click(function (e) {
|
||||
(e).preventDefault();
|
||||
});
|
||||
|
||||
// toggling overview section details
|
||||
$(function(){
|
||||
if($('.courseware-section').length > 0) {
|
||||
$('.toggle-button-sections').addClass('is-shown');
|
||||
}
|
||||
$(function () {
|
||||
if ($('.courseware-section').length > 0) {
|
||||
$('.toggle-button-sections').addClass('is-shown');
|
||||
}
|
||||
});
|
||||
$('.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) {
|
||||
$('.subsection-display-name-input, .unit-subtitle, .policy-list-name, .policy-list-value').each(function (i) {
|
||||
this.val = $(this).val();
|
||||
});
|
||||
$("#start_date, #start_time, #due_date, #due_time").bind('change', autosaveInput);
|
||||
@@ -61,7 +109,7 @@ $(document).ready(function() {
|
||||
// add new/delete section
|
||||
$('.new-courseware-section-button').bind('click', addNewSection);
|
||||
$('.delete-section-button').bind('click', deleteSection);
|
||||
|
||||
|
||||
// add new/delete subsection
|
||||
$('.new-subsection-item').bind('click', addNewSubsection);
|
||||
$('.delete-subsection-button').bind('click', deleteSubsection);
|
||||
@@ -75,69 +123,11 @@ $(document).ready(function() {
|
||||
|
||||
// import form setup
|
||||
$('.import .file-input').bind('change', showImportSubmit);
|
||||
$('.import .choose-file-button, .import .choose-file-button-inline').bind('click', function(e) {
|
||||
$('.import .choose-file-button, .import .choose-file-button-inline').bind('click', function (e) {
|
||||
e.preventDefault();
|
||||
$('.import .file-input').click();
|
||||
});
|
||||
|
||||
// making the unit list draggable. Note: sortable didn't work b/c it considered
|
||||
// drop points which the user hovered over as destinations and proactively changed
|
||||
// the dom; so, if the user subsequently dropped at an illegal spot, the reversion
|
||||
// point was the last dom change.
|
||||
$('.unit').draggable({
|
||||
axis: 'y',
|
||||
handle: '.drag-handle',
|
||||
zIndex: 999,
|
||||
start: initiateHesitate,
|
||||
drag: checkHoverState,
|
||||
stop: removeHesitate,
|
||||
revert: "invalid"
|
||||
});
|
||||
|
||||
// Subsection reordering
|
||||
$('.id-holder').draggable({
|
||||
axis: 'y',
|
||||
handle: '.section-item .drag-handle',
|
||||
zIndex: 999,
|
||||
start: initiateHesitate,
|
||||
drag: checkHoverState,
|
||||
stop: removeHesitate,
|
||||
revert: "invalid"
|
||||
});
|
||||
|
||||
// Section reordering
|
||||
$('.courseware-section').draggable({
|
||||
axis: 'y',
|
||||
handle: 'header .drag-handle',
|
||||
stack: '.courseware-section',
|
||||
revert: "invalid"
|
||||
});
|
||||
|
||||
|
||||
$('.sortable-unit-list').droppable({
|
||||
accept : '.unit',
|
||||
greedy: true,
|
||||
tolerance: "pointer",
|
||||
hoverClass: "dropover",
|
||||
drop: onUnitReordered
|
||||
});
|
||||
$('.subsection-list > ol').droppable({
|
||||
// why don't we have a more useful class for subsections than id-holder?
|
||||
accept : '.id-holder', // '.unit, .id-holder',
|
||||
tolerance: "pointer",
|
||||
hoverClass: "dropover",
|
||||
drop: onSubsectionReordered,
|
||||
greedy: true
|
||||
});
|
||||
|
||||
// Section reordering
|
||||
$('.courseware-overview').droppable({
|
||||
accept : '.courseware-section',
|
||||
tolerance: "pointer",
|
||||
drop: onSectionReordered,
|
||||
greedy: true
|
||||
});
|
||||
|
||||
$('.new-course-button').bind('click', addNewCourse);
|
||||
|
||||
// section name editing
|
||||
@@ -156,12 +146,12 @@ $(document).ready(function() {
|
||||
$body.on('click', '.section-published-date .schedule-button', editSectionPublishDate);
|
||||
$body.on('click', '.edit-subsection-publish-settings .save-button', saveSetSectionScheduleDate);
|
||||
$body.on('click', '.edit-subsection-publish-settings .cancel-button', hideModal);
|
||||
$body.on('change', '.edit-subsection-publish-settings .start-date', function() {
|
||||
if($('.edit-subsection-publish-settings').find('.start-time').val() == '') {
|
||||
$('.edit-subsection-publish-settings').find('.start-time').val('12:00am');
|
||||
$body.on('change', '.edit-subsection-publish-settings .start-date', function () {
|
||||
if ($('.edit-subsection-publish-settings').find('.start-time').val() == '') {
|
||||
$('.edit-subsection-publish-settings').find('.start-time').val('12:00am');
|
||||
}
|
||||
});
|
||||
$('.edit-subsection-publish-settings').on('change', '.start-date, .start-time', function() {
|
||||
$('.edit-subsection-publish-settings').on('change', '.start-date, .start-time', function () {
|
||||
$('.edit-subsection-publish-settings').find('.save-button').show();
|
||||
});
|
||||
});
|
||||
@@ -172,26 +162,26 @@ $(document).ready(function() {
|
||||
// }
|
||||
|
||||
function toggleSections(e) {
|
||||
e.preventDefault();
|
||||
e.preventDefault();
|
||||
|
||||
$section = $('.courseware-section');
|
||||
sectionCount = $section.length;
|
||||
$button = $(this);
|
||||
$labelCollapsed = $('<i class="ss-icon ss-symbolicons-block">up</i> <span class="label">Collapse All Sections</span>');
|
||||
$labelExpanded = $('<i class="ss-icon ss-symbolicons-block">down</i> <span class="label">Expand All Sections</span>');
|
||||
$section = $('.courseware-section');
|
||||
sectionCount = $section.length;
|
||||
$button = $(this);
|
||||
$labelCollapsed = $('<i class="ss-icon ss-symbolicons-block">up</i> <span class="label">Collapse All Sections</span>');
|
||||
$labelExpanded = $('<i class="ss-icon ss-symbolicons-block">down</i> <span class="label">Expand All Sections</span>');
|
||||
|
||||
var buttonLabel = $button.hasClass('is-activated') ? $labelCollapsed : $labelExpanded;
|
||||
$button.toggleClass('is-activated').html(buttonLabel);
|
||||
var buttonLabel = $button.hasClass('is-activated') ? $labelCollapsed : $labelExpanded;
|
||||
$button.toggleClass('is-activated').html(buttonLabel);
|
||||
|
||||
if($button.hasClass('is-activated')) {
|
||||
$section.addClass('collapsed');
|
||||
// first child in order to avoid the icons on the subsection lists which are not in the first child
|
||||
$section.find('header .expand-collapse-icon').removeClass('collapse').addClass('expand');
|
||||
} else {
|
||||
$section.removeClass('collapsed');
|
||||
// first child in order to avoid the icons on the subsection lists which are not in the first child
|
||||
$section.find('header .expand-collapse-icon').removeClass('expand').addClass('collapse');
|
||||
}
|
||||
if ($button.hasClass('is-activated')) {
|
||||
$section.addClass('collapsed');
|
||||
// first child in order to avoid the icons on the subsection lists which are not in the first child
|
||||
$section.find('header .expand-collapse-icon').removeClass('collapse').addClass('expand');
|
||||
} else {
|
||||
$section.removeClass('collapsed');
|
||||
// first child in order to avoid the icons on the subsection lists which are not in the first child
|
||||
$section.find('header .expand-collapse-icon').removeClass('expand').addClass('collapse');
|
||||
}
|
||||
}
|
||||
|
||||
function editSectionPublishDate(e) {
|
||||
@@ -201,16 +191,16 @@ function editSectionPublishDate(e) {
|
||||
$modal.attr('data-id', $(this).attr('data-id'));
|
||||
$modal.find('.start-date').val($(this).attr('data-date'));
|
||||
$modal.find('.start-time').val($(this).attr('data-time'));
|
||||
if($modal.find('.start-date').val() == '' && $modal.find('.start-time').val() == '') {
|
||||
if ($modal.find('.start-date').val() == '' && $modal.find('.start-time').val() == '') {
|
||||
$modal.find('.save-button').hide();
|
||||
}
|
||||
}
|
||||
$modal.find('.section-name').html('"' + $(this).closest('.courseware-section').find('.section-name-span').text() + '"');
|
||||
$modalCover.show();
|
||||
}
|
||||
|
||||
function showImportSubmit(e) {
|
||||
var filepath = $(this).val();
|
||||
if(filepath.substr(filepath.length - 6, 6) == 'tar.gz') {
|
||||
if (filepath.substr(filepath.length - 6, 6) == 'tar.gz') {
|
||||
$('.error-block').hide();
|
||||
$('.file-name').html($(this).val().replace('C:\\fakepath\\', ''));
|
||||
$('.file-name-block').show();
|
||||
@@ -231,7 +221,7 @@ function syncReleaseDate(e) {
|
||||
|
||||
function addPolicyMetadata(e) {
|
||||
e.preventDefault();
|
||||
var template =$('#add-new-policy-element-template > li');
|
||||
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');
|
||||
@@ -253,7 +243,7 @@ function cancelPolicyMetadata(e) {
|
||||
e.preventDefault();
|
||||
|
||||
var $policyElement = $(this).parents('.policy-list-element');
|
||||
if(!$policyElement.hasClass('editing')) {
|
||||
if (!$policyElement.hasClass('editing')) {
|
||||
$policyElement.remove();
|
||||
} else {
|
||||
$policyElement.removeClass('new-policy-list-element');
|
||||
@@ -266,154 +256,24 @@ function cancelPolicyMetadata(e) {
|
||||
function removePolicyMetadata(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if(!confirm('Are you sure you wish to delete this item. It cannot be reversed!'))
|
||||
return;
|
||||
|
||||
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();
|
||||
_parent_el.remove();
|
||||
} else {
|
||||
_parent_el.appendTo("#policy-to-delete");
|
||||
}
|
||||
saveSubsection()
|
||||
}
|
||||
|
||||
CMS.HesitateEvent.toggleXpandHesitation = null;
|
||||
function initiateHesitate(event, ui) {
|
||||
CMS.HesitateEvent.toggleXpandHesitation = new CMS.HesitateEvent(expandSection, 'dragLeave', true);
|
||||
$('.collapsed').on('dragEnter', CMS.HesitateEvent.toggleXpandHesitation, CMS.HesitateEvent.toggleXpandHesitation.trigger);
|
||||
$('.collapsed').each(function() {
|
||||
this.proportions = {width : this.offsetWidth, height : this.offsetHeight };
|
||||
// reset b/c these were holding values from aborts
|
||||
this.isover = false;
|
||||
});
|
||||
}
|
||||
function checkHoverState(event, ui) {
|
||||
// copied from jquery.ui.droppable.js $.ui.ddmanager.drag & other ui.intersect
|
||||
var draggable = $(this).data("ui-draggable"),
|
||||
x1 = (draggable.positionAbs || draggable.position.absolute).left + (draggable.helperProportions.width / 2),
|
||||
y1 = (draggable.positionAbs || draggable.position.absolute).top + (draggable.helperProportions.height / 2);
|
||||
$('.collapsed').each(function() {
|
||||
// don't expand the thing being carried
|
||||
if (ui.helper.is(this)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$.extend(this, {offset : $(this).offset()});
|
||||
|
||||
var droppable = this,
|
||||
l = droppable.offset.left,
|
||||
r = l + droppable.proportions.width,
|
||||
t = droppable.offset.top,
|
||||
b = t + droppable.proportions.height;
|
||||
|
||||
if (l === r) {
|
||||
// probably wrong values b/c invisible at the time of caching
|
||||
droppable.proportions = { width : droppable.offsetWidth, height : droppable.offsetHeight };
|
||||
r = l + droppable.proportions.width;
|
||||
b = t + droppable.proportions.height;
|
||||
}
|
||||
// equivalent to the intersects test
|
||||
var intersects = (l < x1 && // Right Half
|
||||
x1 < r && // Left Half
|
||||
t < y1 && // Bottom Half
|
||||
y1 < b ), // Top Half
|
||||
|
||||
c = !intersects && this.isover ? "isout" : (intersects && !this.isover ? "isover" : null);
|
||||
|
||||
if(!c) {
|
||||
return;
|
||||
}
|
||||
|
||||
this[c] = true;
|
||||
this[c === "isout" ? "isover" : "isout"] = false;
|
||||
$(this).trigger(c === "isover" ? "dragEnter" : "dragLeave");
|
||||
});
|
||||
}
|
||||
function removeHesitate(event, ui) {
|
||||
$('.collapsed').off('dragEnter', CMS.HesitateEvent.toggleXpandHesitation.trigger);
|
||||
CMS.HesitateEvent.toggleXpandHesitation = null;
|
||||
}
|
||||
|
||||
function expandSection(event) {
|
||||
$(event.delegateTarget).removeClass('collapsed', 400);
|
||||
// don't descend to icon's on children (which aren't under first child) only to this element's icon
|
||||
$(event.delegateTarget).children().first().find('.expand-collapse-icon').removeClass('expand', 400).addClass('collapse');
|
||||
}
|
||||
|
||||
function onUnitReordered(event, ui) {
|
||||
// a unit's been dropped on this subsection,
|
||||
// figure out where it came from and where it slots in.
|
||||
_handleReorder(event, ui, 'subsection-id', 'li:.leaf');
|
||||
}
|
||||
|
||||
function onSubsectionReordered(event, ui) {
|
||||
// a subsection has been dropped on this section,
|
||||
// figure out where it came from and where it slots in.
|
||||
_handleReorder(event, ui, 'section-id', 'li:.branch');
|
||||
}
|
||||
|
||||
function onSectionReordered(event, ui) {
|
||||
// a section moved w/in the overall (cannot change course via this, so no parentage change possible, just order)
|
||||
_handleReorder(event, ui, 'course-id', '.courseware-section');
|
||||
}
|
||||
|
||||
function _handleReorder(event, ui, parentIdField, childrenSelector) {
|
||||
// figure out where it came from and where it slots in.
|
||||
var subsection_id = $(event.target).data(parentIdField);
|
||||
var _els = $(event.target).children(childrenSelector);
|
||||
var children = _els.map(function(idx, el) { return $(el).data('id'); }).get();
|
||||
// if new to this parent, figure out which parent to remove it from and do so
|
||||
if (!_.contains(children, ui.draggable.data('id'))) {
|
||||
var old_parent = ui.draggable.parent();
|
||||
var old_children = old_parent.children(childrenSelector).map(function(idx, el) { return $(el).data('id'); }).get();
|
||||
old_children = _.without(old_children, ui.draggable.data('id'));
|
||||
$.ajax({
|
||||
url: "/save_item",
|
||||
type: "POST",
|
||||
dataType: "json",
|
||||
contentType: "application/json",
|
||||
data:JSON.stringify({ 'id' : old_parent.data(parentIdField), 'children' : old_children})
|
||||
});
|
||||
}
|
||||
else {
|
||||
// staying in same parent
|
||||
// remove so that the replacement in the right place doesn't double it
|
||||
children = _.without(children, ui.draggable.data('id'));
|
||||
}
|
||||
// add to this parent (figure out where)
|
||||
for (var i = 0; i < _els.length; i++) {
|
||||
if (!ui.draggable.is(_els[i]) && ui.offset.top < $(_els[i]).offset().top) {
|
||||
// insert at i in children and _els
|
||||
ui.draggable.insertBefore($(_els[i]));
|
||||
// TODO figure out correct way to have it remove the style: top:n; setting (and similar line below)
|
||||
ui.draggable.attr("style", "position:relative;");
|
||||
children.splice(i, 0, ui.draggable.data('id'));
|
||||
break;
|
||||
}
|
||||
}
|
||||
// see if it goes at end (the above loop didn't insert it)
|
||||
if (!_.contains(children, ui.draggable.data('id'))) {
|
||||
$(event.target).append(ui.draggable);
|
||||
ui.draggable.attr("style", "position:relative;"); // STYLE hack too
|
||||
children.push(ui.draggable.data('id'));
|
||||
}
|
||||
$.ajax({
|
||||
url: "/save_item",
|
||||
type: "POST",
|
||||
dataType: "json",
|
||||
contentType: "application/json",
|
||||
data:JSON.stringify({ 'id' : subsection_id, 'children' : children})
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function getEdxTimeFromDateTimeVals(date_val, time_val, format) {
|
||||
var edxTimeStr = null;
|
||||
|
||||
if (date_val != '') {
|
||||
if (time_val == '')
|
||||
if (time_val == '')
|
||||
time_val = '00:00';
|
||||
|
||||
// Note, we are using date.js utility which has better parsing abilities than the built in JS date parsing
|
||||
@@ -428,30 +288,30 @@ function getEdxTimeFromDateTimeVals(date_val, time_val, format) {
|
||||
}
|
||||
|
||||
function getEdxTimeFromDateTimeInputs(date_id, time_id, format) {
|
||||
var input_date = $('#'+date_id).val();
|
||||
var input_time = $('#'+time_id).val();
|
||||
var input_date = $('#' + date_id).val();
|
||||
var input_time = $('#' + time_id).val();
|
||||
|
||||
return getEdxTimeFromDateTimeVals(input_date, input_time, format);
|
||||
}
|
||||
|
||||
function checkForNewValue(e) {
|
||||
if($(this).parents('.new-policy-list-element')[0]) {
|
||||
if ($(this).parents('.new-policy-list-element')[0]) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(this.val) {
|
||||
this.hasChanged = this.val != $(this).val();
|
||||
if (this.val) {
|
||||
this.hasChanged = this.val != $(this).val();
|
||||
} else {
|
||||
this.hasChanged = false;
|
||||
}
|
||||
|
||||
this.val = $(this).val();
|
||||
if(this.hasChanged) {
|
||||
if(this.saveTimer) {
|
||||
if (this.hasChanged) {
|
||||
if (this.saveTimer) {
|
||||
clearTimeout(this.saveTimer);
|
||||
}
|
||||
|
||||
this.saveTimer = setTimeout(function() {
|
||||
this.saveTimer = setTimeout(function () {
|
||||
$changedInput = $(e.target);
|
||||
saveSubsection();
|
||||
this.saveTimer = null;
|
||||
@@ -460,11 +320,11 @@ function checkForNewValue(e) {
|
||||
}
|
||||
|
||||
function autosaveInput(e) {
|
||||
if(this.saveTimer) {
|
||||
if (this.saveTimer) {
|
||||
clearTimeout(this.saveTimer);
|
||||
}
|
||||
|
||||
this.saveTimer = setTimeout(function() {
|
||||
this.saveTimer = setTimeout(function () {
|
||||
$changedInput = $(e.target);
|
||||
saveSubsection();
|
||||
this.saveTimer = null;
|
||||
@@ -472,7 +332,7 @@ function autosaveInput(e) {
|
||||
}
|
||||
|
||||
function saveSubsection() {
|
||||
if($changedInput && !$changedInput.hasClass('no-spinner')) {
|
||||
if ($changedInput && !$changedInput.hasClass('no-spinner')) {
|
||||
$spinner.css({
|
||||
'position': 'absolute',
|
||||
'top': Math.floor($changedInput.position().top + ($changedInput.outerHeight() / 2) + 3),
|
||||
@@ -482,30 +342,30 @@ function saveSubsection() {
|
||||
$changedInput.after($spinner);
|
||||
$spinner.show();
|
||||
}
|
||||
|
||||
|
||||
var id = $('.subsection-body').data('id');
|
||||
|
||||
// pull all 'normalized' metadata editable fields on page
|
||||
var metadata_fields = $('input[data-metadata-name]');
|
||||
|
||||
|
||||
var metadata = {};
|
||||
for(var i=0; i< metadata_fields.length;i++) {
|
||||
var el = metadata_fields[i];
|
||||
metadata[$(el).data("metadata-name")] = el.value;
|
||||
}
|
||||
for (var i = 0; i < metadata_fields.length; i++) {
|
||||
var el = metadata_fields[i];
|
||||
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) {
|
||||
$('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) {
|
||||
$("#policy-to-delete > li.policy-list-element").each(function (i, element) {
|
||||
var name = $(element).children('.policy-list-name').val();
|
||||
if (name != "")
|
||||
metadata[name] = null;
|
||||
metadata[name] = null;
|
||||
});
|
||||
|
||||
// Piece back together the date/time UI elements into one date/time string
|
||||
@@ -515,18 +375,18 @@ function saveSubsection() {
|
||||
metadata['due'] = getEdxTimeFromDateTimeInputs('due_date', 'due_time', 'MMMM dd HH:mm');
|
||||
|
||||
$.ajax({
|
||||
url: "/save_item",
|
||||
type: "POST",
|
||||
dataType: "json",
|
||||
contentType: "application/json",
|
||||
data:JSON.stringify({ 'id' : id, 'metadata' : metadata}),
|
||||
success: function() {
|
||||
url: "/save_item",
|
||||
type: "POST",
|
||||
dataType: "json",
|
||||
contentType: "application/json",
|
||||
data: JSON.stringify({ 'id': id, 'metadata': metadata}),
|
||||
success: function () {
|
||||
$spinner.delay(500).fadeOut(150);
|
||||
},
|
||||
error: function() {
|
||||
},
|
||||
error: function () {
|
||||
showToastMessage('There has been an error while saving your changes.');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -537,14 +397,14 @@ function createNewUnit(e) {
|
||||
template = $(this).data('template');
|
||||
|
||||
$.post('/clone_item',
|
||||
{'parent_location' : parent,
|
||||
'template' : template,
|
||||
'display_name': 'New Unit'
|
||||
},
|
||||
function(data) {
|
||||
// redirect to the edit page
|
||||
window.location = "/edit/" + data['id'];
|
||||
});
|
||||
{'parent_location': parent,
|
||||
'template': template,
|
||||
'display_name': 'New Unit'
|
||||
},
|
||||
function (data) {
|
||||
// redirect to the edit page
|
||||
window.location = "/edit/" + data['id'];
|
||||
});
|
||||
}
|
||||
|
||||
function deleteUnit(e) {
|
||||
@@ -563,16 +423,16 @@ function deleteSection(e) {
|
||||
}
|
||||
|
||||
function _deleteItem($el) {
|
||||
if(!confirm('Are you sure you wish to delete this item. It cannot be reversed!'))
|
||||
return;
|
||||
|
||||
if (!confirm('Are you sure you wish to delete this item. It cannot be reversed!'))
|
||||
return;
|
||||
|
||||
var id = $el.data('id');
|
||||
|
||||
$.post('/delete_item',
|
||||
{'id': id, 'delete_children' : true, 'delete_all_versions' : true},
|
||||
function(data) {
|
||||
$el.remove();
|
||||
});
|
||||
|
||||
$.post('/delete_item',
|
||||
{'id': id, 'delete_children': true, 'delete_all_versions': true},
|
||||
function (data) {
|
||||
$el.remove();
|
||||
});
|
||||
}
|
||||
|
||||
function showUploadModal(e) {
|
||||
@@ -599,7 +459,7 @@ function startUpload(e) {
|
||||
$('.upload-modal .progress-bar').removeClass('loaded').show();
|
||||
}
|
||||
|
||||
function resetUploadBar(){
|
||||
function resetUploadBar() {
|
||||
var percentVal = '0%';
|
||||
$('.upload-modal .progress-fill').width(percentVal);
|
||||
$('.upload-modal .progress-fill').html(percentVal);
|
||||
@@ -612,7 +472,7 @@ function showUploadFeedback(event, position, total, percentComplete) {
|
||||
}
|
||||
|
||||
function displayFinishedUpload(xhr) {
|
||||
if(xhr.status = 200){
|
||||
if (xhr.status = 200) {
|
||||
markAsLoaded();
|
||||
}
|
||||
|
||||
@@ -636,10 +496,10 @@ function displayFinishedUpload(xhr) {
|
||||
function markAsLoaded() {
|
||||
$('.upload-modal .copy-button').css('display', 'inline-block');
|
||||
$('.upload-modal .progress-bar').addClass('loaded');
|
||||
}
|
||||
}
|
||||
|
||||
function hideModal(e) {
|
||||
if(e) {
|
||||
if (e) {
|
||||
e.preventDefault();
|
||||
}
|
||||
// Unit editors do not want the modal cover to hide when users click outside
|
||||
@@ -653,7 +513,7 @@ function hideModal(e) {
|
||||
}
|
||||
|
||||
function onKeyUp(e) {
|
||||
if(e.which == 87) {
|
||||
if (e.which == 87) {
|
||||
$body.toggleClass('show-wip hide-wip');
|
||||
}
|
||||
}
|
||||
@@ -703,14 +563,14 @@ function showToastMessage(message, $button, lifespan) {
|
||||
var $content = $('<div class="notification-content"></div>');
|
||||
$content.html(message);
|
||||
$toast.append($content);
|
||||
if($button) {
|
||||
if ($button) {
|
||||
$button.addClass('action-button');
|
||||
$button.bind('click', hideToastMessage);
|
||||
$content.append($button);
|
||||
}
|
||||
$closeBtn.bind('click', hideToastMessage);
|
||||
|
||||
if($('.toast-notification')[0]) {
|
||||
if ($('.toast-notification')[0]) {
|
||||
var targetY = $('.toast-notification').offset().top + $('.toast-notification').outerHeight();
|
||||
$toast.css('top', (targetY + 10) + 'px');
|
||||
}
|
||||
@@ -718,8 +578,8 @@ function showToastMessage(message, $button, lifespan) {
|
||||
$body.prepend($toast);
|
||||
$toast.fadeIn(200);
|
||||
|
||||
if(lifespan) {
|
||||
$toast.timer = setTimeout(function() {
|
||||
if (lifespan) {
|
||||
$toast.timer = setTimeout(function () {
|
||||
$toast.fadeOut(300);
|
||||
}, lifespan * 1000);
|
||||
}
|
||||
@@ -745,7 +605,7 @@ function addNewSection(e, isTemplate) {
|
||||
}
|
||||
|
||||
function checkForCancel(e) {
|
||||
if(e.which == 27) {
|
||||
if (e.which == 27) {
|
||||
$body.unbind('keyup', checkForCancel);
|
||||
e.data.$cancelButton.click();
|
||||
}
|
||||
@@ -761,11 +621,11 @@ function saveNewSection(e) {
|
||||
var display_name = $(this).find('.new-section-name').val();
|
||||
|
||||
$.post('/clone_item', {
|
||||
'parent_location' : parent,
|
||||
'template' : template,
|
||||
'parent_location': parent,
|
||||
'template': template,
|
||||
'display_name': display_name,
|
||||
},
|
||||
function(data) {
|
||||
function (data) {
|
||||
if (data.id != undefined)
|
||||
location.reload();
|
||||
}
|
||||
@@ -784,7 +644,7 @@ function addNewCourse(e) {
|
||||
$(e.target).hide();
|
||||
var $newCourse = $($('#new-course-template').html());
|
||||
var $cancelButton = $newCourse.find('.new-course-cancel');
|
||||
$('.new-course-button').after($newCourse);
|
||||
$('.inner-wrapper').prepend($newCourse);
|
||||
$newCourse.find('.new-course-name').focus().select();
|
||||
$newCourse.find('form').bind('submit', saveNewCourse);
|
||||
$cancelButton.bind('click', cancelNewCourse);
|
||||
@@ -800,18 +660,18 @@ function saveNewCourse(e) {
|
||||
var number = $newCourse.find('.new-course-number').val();
|
||||
var display_name = $newCourse.find('.new-course-name').val();
|
||||
|
||||
if (org == '' || number == '' || display_name == ''){
|
||||
if (org == '' || number == '' || display_name == '') {
|
||||
alert('You must specify all fields in order to create a new course.');
|
||||
return;
|
||||
}
|
||||
|
||||
$.post('/create_new_course', {
|
||||
'template' : template,
|
||||
'org' : org,
|
||||
'number' : number,
|
||||
'display_name': display_name
|
||||
'template': template,
|
||||
'org': org,
|
||||
'number': number,
|
||||
'display_name': display_name
|
||||
},
|
||||
function(data) {
|
||||
function (data) {
|
||||
if (data.id != undefined) {
|
||||
window.location = '/' + data.id.replace(/.*:\/\//, '');
|
||||
} else if (data.ErrMsg != undefined) {
|
||||
@@ -855,13 +715,13 @@ function saveNewSubsection(e) {
|
||||
var display_name = $(this).find('.new-subsection-name-input').val();
|
||||
|
||||
$.post('/clone_item', {
|
||||
'parent_location' : parent,
|
||||
'template' : template,
|
||||
'display_name': display_name
|
||||
'parent_location': parent,
|
||||
'template': template,
|
||||
'display_name': display_name
|
||||
},
|
||||
function(data) {
|
||||
function (data) {
|
||||
if (data.id != undefined) {
|
||||
location.reload();
|
||||
location.reload();
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -908,21 +768,20 @@ function saveEditSectionName(e) {
|
||||
}
|
||||
|
||||
var $_this = $(this);
|
||||
// call into server to commit the new order
|
||||
// call into server to commit the new order
|
||||
$.ajax({
|
||||
url: "/save_item",
|
||||
type: "POST",
|
||||
dataType: "json",
|
||||
contentType: "application/json",
|
||||
data:JSON.stringify({ 'id' : id, 'metadata' : {'display_name' : display_name}})
|
||||
}).success(function()
|
||||
{
|
||||
$spinner.delay(250).fadeOut(250);
|
||||
$_this.closest('h3').find('.section-name-span').html(display_name).show();
|
||||
$_this.hide();
|
||||
$_this.closest('.section-name').bind('click', editSectionName);
|
||||
e.stopPropagation();
|
||||
});
|
||||
data: JSON.stringify({ 'id': id, 'metadata': {'display_name': display_name}})
|
||||
}).success(function () {
|
||||
$spinner.delay(250).fadeOut(250);
|
||||
$_this.closest('h3').find('.section-name-span').html(display_name).show();
|
||||
$_this.hide();
|
||||
$_this.closest('.section-name').bind('click', editSectionName);
|
||||
e.stopPropagation();
|
||||
});
|
||||
}
|
||||
|
||||
function setSectionScheduleDate(e) {
|
||||
@@ -953,21 +812,20 @@ function saveSetSectionScheduleDate(e) {
|
||||
type: "POST",
|
||||
dataType: "json",
|
||||
contentType: "application/json",
|
||||
data:JSON.stringify({ 'id' : id, 'metadata' : {'start' : start}})
|
||||
}).success(function()
|
||||
{
|
||||
var $thisSection = $('.courseware-section[data-id="' + id + '"]');
|
||||
$thisSection.find('.section-published-date').html('<span class="published-status"><strong>Will Release:</strong> ' + input_date + ' at ' + input_time + '</span><a href="#" class="edit-button" data-date="' + input_date + '" data-time="' + input_time + '" data-id="' + id + '">Edit</a>');
|
||||
$thisSection.find('.section-published-date').animate({
|
||||
'background-color': 'rgb(182,37,104)'
|
||||
}, 300).animate({
|
||||
'background-color': '#edf1f5'
|
||||
}, 300).animate({
|
||||
'background-color': 'rgb(182,37,104)'
|
||||
}, 300).animate({
|
||||
'background-color': '#edf1f5'
|
||||
}, 300);
|
||||
|
||||
hideModal();
|
||||
});
|
||||
}
|
||||
data: JSON.stringify({ 'id': id, 'metadata': {'start': start}})
|
||||
}).success(function () {
|
||||
var $thisSection = $('.courseware-section[data-id="' + id + '"]');
|
||||
$thisSection.find('.section-published-date').html('<span class="published-status"><strong>Will Release:</strong> ' + input_date + ' at ' + input_time + '</span><a href="#" class="edit-button" data-date="' + input_date + '" data-time="' + input_time + '" data-id="' + id + '">Edit</a>');
|
||||
$thisSection.find('.section-published-date').animate({
|
||||
'background-color': 'rgb(182,37,104)'
|
||||
}, 300).animate({
|
||||
'background-color': '#edf1f5'
|
||||
}, 300).animate({
|
||||
'background-color': 'rgb(182,37,104)'
|
||||
}, 300).animate({
|
||||
'background-color': '#edf1f5'
|
||||
}, 300);
|
||||
|
||||
hideModal();
|
||||
});
|
||||
}
|
||||
62
cms/static/js/models/settings/advanced.js
Normal file
@@ -0,0 +1,62 @@
|
||||
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;
|
||||
},
|
||||
|
||||
save : function (attrs, options) {
|
||||
// wraps the save call w/ the deletion of the removed keys after we know the saved ones worked
|
||||
options = options ? _.clone(options) : {};
|
||||
// add saveSuccess to the success
|
||||
var success = options.success;
|
||||
options.success = function(model, resp, options) {
|
||||
model.afterSave(model);
|
||||
if (success) success(model, resp, options);
|
||||
};
|
||||
Backbone.Model.prototype.save.call(this, attrs, options);
|
||||
},
|
||||
|
||||
afterSave : function(self) {
|
||||
// remove deleted attrs
|
||||
if (!_.isEmpty(self.deleteKeys)) {
|
||||
// remove the to be deleted keys from the returned model
|
||||
_.each(self.deleteKeys, function(key) { self.unset(key); });
|
||||
// not able to do via backbone since we're not destroying the model
|
||||
$.ajax({
|
||||
url : self.url,
|
||||
// json to and fro
|
||||
contentType : "application/json",
|
||||
dataType : "json",
|
||||
// delete
|
||||
type : 'DELETE',
|
||||
// data
|
||||
data : JSON.stringify({ deleteKeys : self.deleteKeys})
|
||||
})
|
||||
.fail(function(hdr, status, error) { CMS.ServerError(self, "Deleting keys:" + status); })
|
||||
.done(function(data, status, error) {
|
||||
// clear deleteKeys on success
|
||||
self.deleteKeys = [];
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1,85 +1,83 @@
|
||||
if (!CMS.Models['Settings']) CMS.Models.Settings = new Object();
|
||||
|
||||
CMS.Models.Settings.CourseDetails = Backbone.Model.extend({
|
||||
defaults: {
|
||||
location : null, // the course's Location model, required
|
||||
start_date: null, // maps to 'start'
|
||||
end_date: null, // maps to 'end'
|
||||
enrollment_start: null,
|
||||
enrollment_end: null,
|
||||
syllabus: null,
|
||||
overview: "",
|
||||
intro_video: null,
|
||||
effort: null // an int or null
|
||||
},
|
||||
|
||||
// When init'g from html script, ensure you pass {parse: true} as an option (2nd arg to reset)
|
||||
parse: function(attributes) {
|
||||
if (attributes['course_location']) {
|
||||
attributes.location = new CMS.Models.Location(attributes.course_location, {parse:true});
|
||||
}
|
||||
if (attributes['start_date']) {
|
||||
attributes.start_date = new Date(attributes.start_date);
|
||||
}
|
||||
if (attributes['end_date']) {
|
||||
attributes.end_date = new Date(attributes.end_date);
|
||||
}
|
||||
if (attributes['enrollment_start']) {
|
||||
attributes.enrollment_start = new Date(attributes.enrollment_start);
|
||||
}
|
||||
if (attributes['enrollment_end']) {
|
||||
attributes.enrollment_end = new Date(attributes.enrollment_end);
|
||||
}
|
||||
return attributes;
|
||||
},
|
||||
|
||||
validate: function(newattrs) {
|
||||
// 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 && newattrs.end_date && newattrs.start_date >= newattrs.end_date) {
|
||||
errors.end_date = "The course end date cannot be before the course start date.";
|
||||
}
|
||||
if (newattrs.start_date && newattrs.enrollment_start && newattrs.start_date < newattrs.enrollment_start) {
|
||||
errors.enrollment_start = "The course start date cannot be before the enrollment start date.";
|
||||
}
|
||||
if (newattrs.enrollment_start && newattrs.enrollment_end && newattrs.enrollment_start >= newattrs.enrollment_end) {
|
||||
errors.enrollment_end = "The enrollment start date cannot be after the enrollment end date.";
|
||||
}
|
||||
if (newattrs.end_date && newattrs.enrollment_end && newattrs.end_date < newattrs.enrollment_end) {
|
||||
errors.enrollment_end = "The enrollment end date cannot be after the course end date.";
|
||||
}
|
||||
if (newattrs.intro_video && newattrs.intro_video !== this.get('intro_video')) {
|
||||
if (this._videokey_illegal_chars.exec(newattrs.intro_video)) {
|
||||
errors.intro_video = "Key should only contain letters, numbers, _, or -";
|
||||
}
|
||||
// TODO check if key points to a real video using google's youtube api
|
||||
}
|
||||
if (!_.isEmpty(errors)) return errors;
|
||||
// NOTE don't return empty errors as that will be interpreted as an error state
|
||||
},
|
||||
|
||||
url: function() {
|
||||
var location = this.get('location');
|
||||
return '/' + location.get('org') + "/" + location.get('course') + '/settings/' + location.get('name') + '/section/details';
|
||||
},
|
||||
|
||||
_videokey_illegal_chars : /[^a-zA-Z0-9_-]/g,
|
||||
save_videosource: function(newsource) {
|
||||
// newsource either is <video youtube="speed:key, *"/> or just the "speed:key, *" string
|
||||
// returns the videosource for the preview which iss the key whose speed is closest to 1
|
||||
if (_.isEmpty(newsource) && !_.isEmpty(this.get('intro_video'))) this.save({'intro_video': null},
|
||||
{ error : CMS.ServerError});
|
||||
// TODO remove all whitespace w/in string
|
||||
else {
|
||||
if (this.get('intro_video') !== newsource) this.save('intro_video', newsource,
|
||||
{ error : CMS.ServerError});
|
||||
}
|
||||
|
||||
return this.videosourceSample();
|
||||
},
|
||||
videosourceSample : function() {
|
||||
if (this.has('intro_video')) return "http://www.youtube.com/embed/" + this.get('intro_video');
|
||||
else return "";
|
||||
}
|
||||
defaults: {
|
||||
location : null, // the course's Location model, required
|
||||
start_date: null, // maps to 'start'
|
||||
end_date: null, // maps to 'end'
|
||||
enrollment_start: null,
|
||||
enrollment_end: null,
|
||||
syllabus: null,
|
||||
overview: "",
|
||||
intro_video: null,
|
||||
effort: null // an int or null
|
||||
},
|
||||
|
||||
// When init'g from html script, ensure you pass {parse: true} as an option (2nd arg to reset)
|
||||
parse: function(attributes) {
|
||||
if (attributes['course_location']) {
|
||||
attributes.location = new CMS.Models.Location(attributes.course_location, {parse:true});
|
||||
}
|
||||
if (attributes['start_date']) {
|
||||
attributes.start_date = new Date(attributes.start_date);
|
||||
}
|
||||
if (attributes['end_date']) {
|
||||
attributes.end_date = new Date(attributes.end_date);
|
||||
}
|
||||
if (attributes['enrollment_start']) {
|
||||
attributes.enrollment_start = new Date(attributes.enrollment_start);
|
||||
}
|
||||
if (attributes['enrollment_end']) {
|
||||
attributes.enrollment_end = new Date(attributes.enrollment_end);
|
||||
}
|
||||
return attributes;
|
||||
},
|
||||
|
||||
validate: function(newattrs) {
|
||||
// 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 && newattrs.end_date && newattrs.start_date >= newattrs.end_date) {
|
||||
errors.end_date = "The course end date cannot be before the course start date.";
|
||||
}
|
||||
if (newattrs.start_date && newattrs.enrollment_start && newattrs.start_date < newattrs.enrollment_start) {
|
||||
errors.enrollment_start = "The course start date cannot be before the enrollment start date.";
|
||||
}
|
||||
if (newattrs.enrollment_start && newattrs.enrollment_end && newattrs.enrollment_start >= newattrs.enrollment_end) {
|
||||
errors.enrollment_end = "The enrollment start date cannot be after the enrollment end date.";
|
||||
}
|
||||
if (newattrs.end_date && newattrs.enrollment_end && newattrs.end_date < newattrs.enrollment_end) {
|
||||
errors.enrollment_end = "The enrollment end date cannot be after the course end date.";
|
||||
}
|
||||
if (newattrs.intro_video && newattrs.intro_video !== this.get('intro_video')) {
|
||||
if (this._videokey_illegal_chars.exec(newattrs.intro_video)) {
|
||||
errors.intro_video = "Key should only contain letters, numbers, _, or -";
|
||||
}
|
||||
// TODO check if key points to a real video using google's youtube api
|
||||
}
|
||||
if (!_.isEmpty(errors)) return errors;
|
||||
// NOTE don't return empty errors as that will be interpreted as an error state
|
||||
},
|
||||
|
||||
url: function() {
|
||||
var location = this.get('location');
|
||||
return '/' + location.get('org') + "/" + location.get('course') + '/settings-details/' + location.get('name') + '/section/details';
|
||||
},
|
||||
|
||||
_videokey_illegal_chars : /[^a-zA-Z0-9_-]/g,
|
||||
save_videosource: function(newsource) {
|
||||
// newsource either is <video youtube="speed:key, *"/> or just the "speed:key, *" string
|
||||
// returns the videosource for the preview which iss the key whose speed is closest to 1
|
||||
if (_.isEmpty(newsource) && !_.isEmpty(this.get('intro_video'))) this.save({'intro_video': null});
|
||||
// TODO remove all whitespace w/in string
|
||||
else {
|
||||
if (this.get('intro_video') !== newsource) this.save('intro_video', newsource);
|
||||
}
|
||||
|
||||
return this.videosourceSample();
|
||||
},
|
||||
videosourceSample : function() {
|
||||
if (this.has('intro_video')) return "http://www.youtube.com/embed/" + this.get('intro_video');
|
||||
else return "";
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,55 +1,56 @@
|
||||
if (!CMS.Models['Settings']) CMS.Models.Settings = new Object();
|
||||
|
||||
CMS.Models.Settings.CourseGradingPolicy = Backbone.Model.extend({
|
||||
defaults : {
|
||||
course_location : null,
|
||||
graders : null, // CourseGraderCollection
|
||||
grade_cutoffs : null, // CourseGradeCutoff model
|
||||
defaults : {
|
||||
course_location : null,
|
||||
graders : null, // CourseGraderCollection
|
||||
grade_cutoffs : null, // CourseGradeCutoff model
|
||||
grace_period : null // either null or { hours: n, minutes: m, ...}
|
||||
},
|
||||
parse: function(attributes) {
|
||||
if (attributes['course_location']) {
|
||||
attributes.course_location = new CMS.Models.Location(attributes.course_location, {parse:true});
|
||||
}
|
||||
if (attributes['graders']) {
|
||||
var graderCollection;
|
||||
if (this.has('graders')) {
|
||||
graderCollection = this.get('graders');
|
||||
graderCollection.reset(attributes.graders);
|
||||
}
|
||||
else {
|
||||
graderCollection = new CMS.Models.Settings.CourseGraderCollection(attributes.graders);
|
||||
graderCollection.course_location = attributes['course_location'] || this.get('course_location');
|
||||
}
|
||||
attributes.graders = graderCollection;
|
||||
}
|
||||
return attributes;
|
||||
},
|
||||
url : function() {
|
||||
var location = this.get('course_location');
|
||||
return '/' + location.get('org') + "/" + location.get('course') + '/settings/' + location.get('name') + '/section/grading';
|
||||
},
|
||||
gracePeriodToDate : function() {
|
||||
var newDate = new Date();
|
||||
if (this.has('grace_period') && this.get('grace_period')['hours'])
|
||||
newDate.setHours(this.get('grace_period')['hours']);
|
||||
else newDate.setHours(0);
|
||||
if (this.has('grace_period') && this.get('grace_period')['minutes'])
|
||||
newDate.setMinutes(this.get('grace_period')['minutes']);
|
||||
else newDate.setMinutes(0);
|
||||
if (this.has('grace_period') && this.get('grace_period')['seconds'])
|
||||
newDate.setSeconds(this.get('grace_period')['seconds']);
|
||||
else newDate.setSeconds(0);
|
||||
|
||||
return newDate;
|
||||
},
|
||||
dateToGracePeriod : function(date) {
|
||||
return {hours : date.getHours(), minutes : date.getMinutes(), seconds : date.getSeconds() };
|
||||
}
|
||||
},
|
||||
parse: function(attributes) {
|
||||
if (attributes['course_location']) {
|
||||
attributes.course_location = new CMS.Models.Location(attributes.course_location, {parse:true});
|
||||
}
|
||||
if (attributes['graders']) {
|
||||
var graderCollection;
|
||||
// interesting race condition: if {parse:true} when newing, then parse called before .attributes created
|
||||
if (this.attributes && this.has('graders')) {
|
||||
graderCollection = this.get('graders');
|
||||
graderCollection.reset(attributes.graders);
|
||||
}
|
||||
else {
|
||||
graderCollection = new CMS.Models.Settings.CourseGraderCollection(attributes.graders);
|
||||
graderCollection.course_location = attributes['course_location'] || this.get('course_location');
|
||||
}
|
||||
attributes.graders = graderCollection;
|
||||
}
|
||||
return attributes;
|
||||
},
|
||||
url : function() {
|
||||
var location = this.get('course_location');
|
||||
return '/' + location.get('org') + "/" + location.get('course') + '/settings-details/' + location.get('name') + '/section/grading';
|
||||
},
|
||||
gracePeriodToDate : function() {
|
||||
var newDate = new Date();
|
||||
if (this.has('grace_period') && this.get('grace_period')['hours'])
|
||||
newDate.setHours(this.get('grace_period')['hours']);
|
||||
else newDate.setHours(0);
|
||||
if (this.has('grace_period') && this.get('grace_period')['minutes'])
|
||||
newDate.setMinutes(this.get('grace_period')['minutes']);
|
||||
else newDate.setMinutes(0);
|
||||
if (this.has('grace_period') && this.get('grace_period')['seconds'])
|
||||
newDate.setSeconds(this.get('grace_period')['seconds']);
|
||||
else newDate.setSeconds(0);
|
||||
|
||||
return newDate;
|
||||
},
|
||||
dateToGracePeriod : function(date) {
|
||||
return {hours : date.getHours(), minutes : date.getMinutes(), seconds : date.getSeconds() };
|
||||
}
|
||||
});
|
||||
|
||||
CMS.Models.Settings.CourseGrader = Backbone.Model.extend({
|
||||
defaults: {
|
||||
defaults: {
|
||||
"type" : "", // must be unique w/in collection (ie. w/in course)
|
||||
"min_count" : 1,
|
||||
"drop_count" : 0,
|
||||
@@ -57,71 +58,71 @@ CMS.Models.Settings.CourseGrader = Backbone.Model.extend({
|
||||
"weight" : 0 // int 0..100
|
||||
},
|
||||
parse : function(attrs) {
|
||||
if (attrs['weight']) {
|
||||
if (!_.isNumber(attrs.weight)) attrs.weight = parseInt(attrs.weight);
|
||||
}
|
||||
if (attrs['min_count']) {
|
||||
if (!_.isNumber(attrs.min_count)) attrs.min_count = parseInt(attrs.min_count);
|
||||
}
|
||||
if (attrs['drop_count']) {
|
||||
if (!_.isNumber(attrs.drop_count)) attrs.drop_count = parseInt(attrs.drop_count);
|
||||
}
|
||||
return attrs;
|
||||
if (attrs['weight']) {
|
||||
if (!_.isNumber(attrs.weight)) attrs.weight = parseInt(attrs.weight);
|
||||
}
|
||||
if (attrs['min_count']) {
|
||||
if (!_.isNumber(attrs.min_count)) attrs.min_count = parseInt(attrs.min_count);
|
||||
}
|
||||
if (attrs['drop_count']) {
|
||||
if (!_.isNumber(attrs.drop_count)) attrs.drop_count = parseInt(attrs.drop_count);
|
||||
}
|
||||
return attrs;
|
||||
},
|
||||
validate : function(attrs) {
|
||||
var errors = {};
|
||||
if (attrs['type']) {
|
||||
if (_.isEmpty(attrs['type'])) {
|
||||
errors.type = "The assignment type must have a name.";
|
||||
}
|
||||
else {
|
||||
// FIXME somehow this.collection is unbound sometimes. I can't track down when
|
||||
var existing = this.collection && this.collection.some(function(other) { return (other != this) && (other.get('type') == attrs['type']);}, this);
|
||||
if (existing) {
|
||||
errors.type = "There's already another assignment type with this name.";
|
||||
}
|
||||
}
|
||||
}
|
||||
if (attrs['weight']) {
|
||||
if (!isFinite(attrs.weight) || /\D+/.test(attrs.weight)) {
|
||||
errors.weight = "Please enter an integer between 0 and 100.";
|
||||
}
|
||||
else {
|
||||
attrs.weight = parseInt(attrs.weight); // see if this ensures value saved is int
|
||||
if (this.collection && attrs.weight > 0) {
|
||||
// FIXME b/c saves don't update the models if validation fails, we should
|
||||
// either revert the field value to the one in the model and make them make room
|
||||
// or figure out a wholistic way to balance the vals across the whole
|
||||
// if ((this.collection.sumWeights() + attrs.weight - this.get('weight')) > 100)
|
||||
// errors.weight = "The weights cannot add to more than 100.";
|
||||
}
|
||||
}}
|
||||
if (attrs['min_count']) {
|
||||
if (!isFinite(attrs.min_count) || /\D+/.test(attrs.min_count)) {
|
||||
errors.min_count = "Please enter an integer.";
|
||||
}
|
||||
else attrs.min_count = parseInt(attrs.min_count);
|
||||
}
|
||||
if (attrs['drop_count']) {
|
||||
if (!isFinite(attrs.drop_count) || /\D+/.test(attrs.drop_count)) {
|
||||
errors.drop_count = "Please enter an integer.";
|
||||
}
|
||||
else attrs.drop_count = parseInt(attrs.drop_count);
|
||||
}
|
||||
if (attrs['min_count'] && attrs['drop_count'] && attrs.drop_count > attrs.min_count) {
|
||||
errors.drop_count = "Cannot drop more " + attrs.type + " than will assigned.";
|
||||
}
|
||||
if (!_.isEmpty(errors)) return errors;
|
||||
var errors = {};
|
||||
if (attrs['type']) {
|
||||
if (_.isEmpty(attrs['type'])) {
|
||||
errors.type = "The assignment type must have a name.";
|
||||
}
|
||||
else {
|
||||
// FIXME somehow this.collection is unbound sometimes. I can't track down when
|
||||
var existing = this.collection && this.collection.some(function(other) { return (other != this) && (other.get('type') == attrs['type']);}, this);
|
||||
if (existing) {
|
||||
errors.type = "There's already another assignment type with this name.";
|
||||
}
|
||||
}
|
||||
}
|
||||
if (attrs['weight']) {
|
||||
if (!isFinite(attrs.weight) || /\D+/.test(attrs.weight)) {
|
||||
errors.weight = "Please enter an integer between 0 and 100.";
|
||||
}
|
||||
else {
|
||||
attrs.weight = parseInt(attrs.weight); // see if this ensures value saved is int
|
||||
if (this.collection && attrs.weight > 0) {
|
||||
// FIXME b/c saves don't update the models if validation fails, we should
|
||||
// either revert the field value to the one in the model and make them make room
|
||||
// or figure out a wholistic way to balance the vals across the whole
|
||||
// if ((this.collection.sumWeights() + attrs.weight - this.get('weight')) > 100)
|
||||
// errors.weight = "The weights cannot add to more than 100.";
|
||||
}
|
||||
}}
|
||||
if (attrs['min_count']) {
|
||||
if (!isFinite(attrs.min_count) || /\D+/.test(attrs.min_count)) {
|
||||
errors.min_count = "Please enter an integer.";
|
||||
}
|
||||
else attrs.min_count = parseInt(attrs.min_count);
|
||||
}
|
||||
if (attrs['drop_count']) {
|
||||
if (!isFinite(attrs.drop_count) || /\D+/.test(attrs.drop_count)) {
|
||||
errors.drop_count = "Please enter an integer.";
|
||||
}
|
||||
else attrs.drop_count = parseInt(attrs.drop_count);
|
||||
}
|
||||
if (attrs['min_count'] && attrs['drop_count'] && attrs.drop_count > attrs.min_count) {
|
||||
errors.drop_count = "Cannot drop more " + attrs.type + " than will assigned.";
|
||||
}
|
||||
if (!_.isEmpty(errors)) return errors;
|
||||
}
|
||||
});
|
||||
|
||||
CMS.Models.Settings.CourseGraderCollection = Backbone.Collection.extend({
|
||||
model : CMS.Models.Settings.CourseGrader,
|
||||
course_location : null, // must be set to a Location object
|
||||
url : function() {
|
||||
return '/' + this.course_location.get('org') + "/" + this.course_location.get('course') + '/grades/' + this.course_location.get('name') + '/';
|
||||
},
|
||||
sumWeights : function() {
|
||||
return this.reduce(function(subtotal, grader) { return subtotal + grader.get('weight'); }, 0);
|
||||
}
|
||||
model : CMS.Models.Settings.CourseGrader,
|
||||
course_location : null, // must be set to a Location object
|
||||
url : function() {
|
||||
return '/' + this.course_location.get('org') + "/" + this.course_location.get('course') + '/settings-grading/' + this.course_location.get('name') + '/';
|
||||
},
|
||||
sumWeights : function() {
|
||||
return this.reduce(function(subtotal, grader) { return subtotal + grader.get('weight'); }, 0);
|
||||
}
|
||||
});
|
||||
@@ -1,43 +1,42 @@
|
||||
if (!CMS.Models['Settings']) CMS.Models.Settings = new Object();
|
||||
CMS.Models.Settings.CourseSettings = Backbone.Model.extend({
|
||||
// a container for the models representing the n possible tabbed states
|
||||
defaults: {
|
||||
courseLocation: null,
|
||||
// NOTE: keep these sync'd w/ the data-section names in settings-page-menu
|
||||
details: null,
|
||||
faculty: null,
|
||||
grading: null,
|
||||
problems: null,
|
||||
discussions: null
|
||||
},
|
||||
// a container for the models representing the n possible tabbed states
|
||||
defaults: {
|
||||
courseLocation: null,
|
||||
details: null,
|
||||
faculty: null,
|
||||
grading: null,
|
||||
problems: null,
|
||||
discussions: null
|
||||
},
|
||||
|
||||
retrieve: function(submodel, callback) {
|
||||
if (this.get(submodel)) callback();
|
||||
else {
|
||||
var cachethis = this;
|
||||
switch (submodel) {
|
||||
case 'details':
|
||||
var details = new CMS.Models.Settings.CourseDetails({location: this.get('courseLocation')});
|
||||
details.fetch( {
|
||||
success : function(model) {
|
||||
cachethis.set('details', model);
|
||||
callback(model);
|
||||
}
|
||||
});
|
||||
break;
|
||||
case 'grading':
|
||||
var grading = new CMS.Models.Settings.CourseGradingPolicy({course_location: this.get('courseLocation')});
|
||||
grading.fetch( {
|
||||
success : function(model) {
|
||||
cachethis.set('grading', model);
|
||||
callback(model);
|
||||
}
|
||||
});
|
||||
break;
|
||||
retrieve: function(submodel, callback) {
|
||||
if (this.get(submodel)) callback();
|
||||
else {
|
||||
var cachethis = this;
|
||||
switch (submodel) {
|
||||
case 'details':
|
||||
var details = new CMS.Models.Settings.CourseDetails({location: this.get('courseLocation')});
|
||||
details.fetch( {
|
||||
success : function(model) {
|
||||
cachethis.set('details', model);
|
||||
callback(model);
|
||||
}
|
||||
});
|
||||
break;
|
||||
case 'grading':
|
||||
var grading = new CMS.Models.Settings.CourseGradingPolicy({course_location: this.get('courseLocation')});
|
||||
grading.fetch( {
|
||||
success : function(model) {
|
||||
cachethis.set('grading', model);
|
||||
callback(model);
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -5,7 +5,7 @@
|
||||
if (typeof window.templateLoader == 'function') return;
|
||||
|
||||
var templateLoader = {
|
||||
templateVersion: "0.0.12",
|
||||
templateVersion: "0.0.15",
|
||||
templates: {},
|
||||
loadRemoteTemplate: function(templateName, filename, callback) {
|
||||
if (!this.templates[templateName]) {
|
||||
|
||||
@@ -10,7 +10,7 @@ CMS.Views.CourseInfoEdit = Backbone.View.extend({
|
||||
render: function() {
|
||||
// instantiate the ClassInfoUpdateView and delegate the proper dom to it
|
||||
new CMS.Views.ClassInfoUpdateView({
|
||||
el: this.$('#course-update-view'),
|
||||
el: $('body.updates'),
|
||||
collection: this.model.get('updates')
|
||||
});
|
||||
|
||||
@@ -27,10 +27,10 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
|
||||
// collection is CourseUpdateCollection
|
||||
events: {
|
||||
"click .new-update-button" : "onNew",
|
||||
"click .save-button" : "onSave",
|
||||
"click .cancel-button" : "onCancel",
|
||||
"click .edit-button" : "onEdit",
|
||||
"click .delete-button" : "onDelete"
|
||||
"click #course-update-view .save-button" : "onSave",
|
||||
"click #course-update-view .cancel-button" : "onCancel",
|
||||
"click .post-actions > .edit-button" : "onEdit",
|
||||
"click .post-actions > .delete-button" : "onDelete"
|
||||
},
|
||||
|
||||
initialize: function() {
|
||||
|
||||
238
cms/static/js/views/overview.js
Normal file
@@ -0,0 +1,238 @@
|
||||
$(document).ready(function() {
|
||||
// making the unit list draggable. Note: sortable didn't work b/c it considered
|
||||
// drop points which the user hovered over as destinations and proactively changed
|
||||
// the dom; so, if the user subsequently dropped at an illegal spot, the reversion
|
||||
// point was the last dom change.
|
||||
$('.unit').draggable({
|
||||
axis: 'y',
|
||||
handle: '.drag-handle',
|
||||
zIndex: 999,
|
||||
start: initiateHesitate,
|
||||
// left 2nd arg in as inert selector b/c i was uncertain whether we'd try to get the shove up/down
|
||||
// to work in the future
|
||||
drag: generateCheckHoverState('.collapsed', ''),
|
||||
stop: removeHesitate,
|
||||
revert: "invalid"
|
||||
});
|
||||
|
||||
// Subsection reordering
|
||||
$('.id-holder').draggable({
|
||||
axis: 'y',
|
||||
handle: '.section-item .drag-handle',
|
||||
zIndex: 999,
|
||||
start: initiateHesitate,
|
||||
drag: generateCheckHoverState('.courseware-section.collapsed', ''),
|
||||
stop: removeHesitate,
|
||||
revert: "invalid"
|
||||
});
|
||||
|
||||
// Section reordering
|
||||
$('.courseware-section').draggable({
|
||||
axis: 'y',
|
||||
handle: 'header .drag-handle',
|
||||
stack: '.courseware-section',
|
||||
revert: "invalid"
|
||||
});
|
||||
|
||||
|
||||
$('.sortable-unit-list').droppable({
|
||||
accept : '.unit',
|
||||
greedy: true,
|
||||
tolerance: "pointer",
|
||||
hoverClass: "dropover",
|
||||
drop: onUnitReordered
|
||||
});
|
||||
$('.subsection-list > ol').droppable({
|
||||
// why don't we have a more useful class for subsections than id-holder?
|
||||
accept : '.id-holder', // '.unit, .id-holder',
|
||||
tolerance: "pointer",
|
||||
hoverClass: "dropover",
|
||||
drop: onSubsectionReordered,
|
||||
greedy: true
|
||||
});
|
||||
|
||||
// Section reordering
|
||||
$('.courseware-overview').droppable({
|
||||
accept : '.courseware-section',
|
||||
tolerance: "pointer",
|
||||
drop: onSectionReordered,
|
||||
greedy: true
|
||||
});
|
||||
|
||||
// stop clicks on drag bars from doing their thing w/o stopping drag
|
||||
$('.drag-handle').click(function(e) {e.preventDefault(); });
|
||||
|
||||
});
|
||||
|
||||
CMS.HesitateEvent.toggleXpandHesitation = null;
|
||||
function initiateHesitate(event, ui) {
|
||||
CMS.HesitateEvent.toggleXpandHesitation = new CMS.HesitateEvent(expandSection, 'dragLeave', true);
|
||||
$('.collapsed').on('dragEnter', CMS.HesitateEvent.toggleXpandHesitation, CMS.HesitateEvent.toggleXpandHesitation.trigger);
|
||||
$('.collapsed, .unit, .id-holder').each(function() {
|
||||
this.proportions = {width : this.offsetWidth, height : this.offsetHeight };
|
||||
// reset b/c these were holding values from aborts
|
||||
this.isover = false;
|
||||
});
|
||||
}
|
||||
|
||||
function computeIntersection(droppable, uiHelper, y) {
|
||||
/*
|
||||
* Test whether y falls within the bounds of the droppable on the Y axis
|
||||
*/
|
||||
// NOTE: this only judges y axis intersection b/c that's all we're doing right now
|
||||
// don't expand the thing being carried
|
||||
if (uiHelper.is(droppable)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$.extend(droppable, {offset : $(droppable).offset()});
|
||||
|
||||
var t = droppable.offset.top,
|
||||
b = t + droppable.proportions.height;
|
||||
|
||||
if (t === b) {
|
||||
// probably wrong values b/c invisible at the time of caching
|
||||
droppable.proportions = { width : droppable.offsetWidth, height : droppable.offsetHeight };
|
||||
b = t + droppable.proportions.height;
|
||||
}
|
||||
// equivalent to the intersects test
|
||||
return (t < y && // Bottom Half
|
||||
y < b ); // Top Half
|
||||
}
|
||||
|
||||
// NOTE: selectorsToShove is not currently being used but I left this code as it did work but not well
|
||||
function generateCheckHoverState(selectorsToOpen, selectorsToShove) {
|
||||
return function(event, ui) {
|
||||
// copied from jquery.ui.droppable.js $.ui.ddmanager.drag & other ui.intersect
|
||||
var draggable = $(this).data("ui-draggable"),
|
||||
centerY = (draggable.positionAbs || draggable.position.absolute).top + (draggable.helperProportions.height / 2);
|
||||
$(selectorsToOpen).each(function() {
|
||||
var intersects = computeIntersection(this, ui.helper, centerY),
|
||||
c = !intersects && this.isover ? "isout" : (intersects && !this.isover ? "isover" : null);
|
||||
|
||||
if(!c) {
|
||||
return;
|
||||
}
|
||||
|
||||
this[c] = true;
|
||||
this[c === "isout" ? "isover" : "isout"] = false;
|
||||
$(this).trigger(c === "isover" ? "dragEnter" : "dragLeave");
|
||||
});
|
||||
|
||||
$(selectorsToShove).each(function() {
|
||||
var intersectsBottom = computeIntersection(this, ui.helper, (draggable.positionAbs || draggable.position.absolute).top);
|
||||
|
||||
if ($(this).hasClass('ui-dragging-pushup')) {
|
||||
if (!intersectsBottom) {
|
||||
console.log('not up', $(this).data('id'));
|
||||
$(this).removeClass('ui-dragging-pushup');
|
||||
}
|
||||
}
|
||||
else if (intersectsBottom) {
|
||||
console.log('up', $(this).data('id'));
|
||||
$(this).addClass('ui-dragging-pushup');
|
||||
}
|
||||
|
||||
var intersectsTop = computeIntersection(this, ui.helper,
|
||||
(draggable.positionAbs || draggable.position.absolute).top + draggable.helperProportions.height);
|
||||
|
||||
if ($(this).hasClass('ui-dragging-pushdown')) {
|
||||
if (!intersectsTop) {
|
||||
console.log('not down', $(this).data('id'));
|
||||
$(this).removeClass('ui-dragging-pushdown');
|
||||
}
|
||||
}
|
||||
else if (intersectsTop) {
|
||||
console.log('down', $(this).data('id'));
|
||||
$(this).addClass('ui-dragging-pushdown');
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function removeHesitate(event, ui) {
|
||||
$('.collapsed').off('dragEnter', CMS.HesitateEvent.toggleXpandHesitation.trigger);
|
||||
$('.ui-dragging-pushdown').removeClass('ui-dragging-pushdown');
|
||||
$('.ui-dragging-pushup').removeClass('ui-dragging-pushup');
|
||||
CMS.HesitateEvent.toggleXpandHesitation = null;
|
||||
}
|
||||
|
||||
function expandSection(event) {
|
||||
$(event.delegateTarget).removeClass('collapsed', 400);
|
||||
// don't descend to icon's on children (which aren't under first child) only to this element's icon
|
||||
$(event.delegateTarget).children().first().find('.expand-collapse-icon').removeClass('expand', 400).addClass('collapse');
|
||||
}
|
||||
|
||||
function onUnitReordered(event, ui) {
|
||||
// a unit's been dropped on this subsection,
|
||||
// figure out where it came from and where it slots in.
|
||||
_handleReorder(event, ui, 'subsection-id', 'li:.leaf');
|
||||
}
|
||||
|
||||
function onSubsectionReordered(event, ui) {
|
||||
// a subsection has been dropped on this section,
|
||||
// figure out where it came from and where it slots in.
|
||||
_handleReorder(event, ui, 'section-id', 'li:.branch');
|
||||
}
|
||||
|
||||
function onSectionReordered(event, ui) {
|
||||
// a section moved w/in the overall (cannot change course via this, so no parentage change possible, just order)
|
||||
_handleReorder(event, ui, 'course-id', '.courseware-section');
|
||||
}
|
||||
|
||||
function _handleReorder(event, ui, parentIdField, childrenSelector) {
|
||||
// figure out where it came from and where it slots in.
|
||||
var subsection_id = $(event.target).data(parentIdField);
|
||||
var _els = $(event.target).children(childrenSelector);
|
||||
var children = _els.map(function(idx, el) { return $(el).data('id'); }).get();
|
||||
// if new to this parent, figure out which parent to remove it from and do so
|
||||
if (!_.contains(children, ui.draggable.data('id'))) {
|
||||
var old_parent = ui.draggable.parent();
|
||||
var old_children = old_parent.children(childrenSelector).map(function(idx, el) { return $(el).data('id'); }).get();
|
||||
old_children = _.without(old_children, ui.draggable.data('id'));
|
||||
$.ajax({
|
||||
url: "/save_item",
|
||||
type: "POST",
|
||||
dataType: "json",
|
||||
contentType: "application/json",
|
||||
data:JSON.stringify({ 'id' : old_parent.data(parentIdField), 'children' : old_children})
|
||||
});
|
||||
}
|
||||
else {
|
||||
// staying in same parent
|
||||
// remove so that the replacement in the right place doesn't double it
|
||||
children = _.without(children, ui.draggable.data('id'));
|
||||
}
|
||||
// add to this parent (figure out where)
|
||||
for (var i = 0, bump = 0; i < _els.length; i++) {
|
||||
if (ui.draggable.is(_els[i])) {
|
||||
bump = -1; // bump indicates that the draggable was passed in the dom but not children's list b/c
|
||||
// it's not in that list
|
||||
}
|
||||
else if (ui.offset.top < $(_els[i]).offset().top) {
|
||||
// insert at i in children and _els
|
||||
ui.draggable.insertBefore($(_els[i]));
|
||||
// TODO figure out correct way to have it remove the style: top:n; setting (and similar line below)
|
||||
ui.draggable.attr("style", "position:relative;");
|
||||
children.splice(i + bump, 0, ui.draggable.data('id'));
|
||||
break;
|
||||
}
|
||||
}
|
||||
// see if it goes at end (the above loop didn't insert it)
|
||||
if (!_.contains(children, ui.draggable.data('id'))) {
|
||||
$(event.target).append(ui.draggable);
|
||||
ui.draggable.attr("style", "position:relative;"); // STYLE hack too
|
||||
children.push(ui.draggable.data('id'));
|
||||
}
|
||||
$.ajax({
|
||||
url: "/save_item",
|
||||
type: "POST",
|
||||
dataType: "json",
|
||||
contentType: "application/json",
|
||||
data:JSON.stringify({ 'id' : subsection_id, 'children' : children})
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
305
cms/static/js/views/settings/advanced_view.js
Normal file
@@ -0,0 +1,305 @@
|
||||
if (!CMS.Views['Settings']) CMS.Views.Settings = {};
|
||||
|
||||
CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({
|
||||
error_saving : "error_saving",
|
||||
successful_changes: "successful_changes",
|
||||
|
||||
// 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)
|
||||
},
|
||||
initialize : function() {
|
||||
var self = this;
|
||||
// instantiates an editor template for each update in the collection
|
||||
window.templateLoader.loadRemoteTemplate("advanced_entry",
|
||||
"/static/client_templates/advanced_entry.html",
|
||||
function (raw_template) {
|
||||
self.template = _.template(raw_template);
|
||||
self.render();
|
||||
}
|
||||
);
|
||||
// because these are outside of this.$el, they can't be in the event hash
|
||||
$('.save-button').on('click', this, this.saveView);
|
||||
$('.cancel-button').on('click', this, this.revertView);
|
||||
this.model.on('error', this.handleValidationError, this);
|
||||
},
|
||||
render: function() {
|
||||
// catch potential outside call before template loaded
|
||||
if (!this.template) return this;
|
||||
|
||||
var listEle$ = this.$el.find('.course-advanced-policy-list');
|
||||
listEle$.empty();
|
||||
|
||||
// b/c we've deleted all old fields, clear the map and repopulate
|
||||
this.fieldToSelectorMap = {};
|
||||
this.selectorToField = {};
|
||||
|
||||
// iterate through model and produce key : value editors for each property in model.get
|
||||
var self = this;
|
||||
_.each(_.sortBy(_.keys(this.model.attributes), _.identity),
|
||||
function(key) {
|
||||
listEle$.append(self.renderTemplate(key, self.model.get(key)));
|
||||
});
|
||||
|
||||
var policyValues = listEle$.find('.json');
|
||||
_.each(policyValues, this.attachJSONEditor, this);
|
||||
this.showMessage();
|
||||
return this;
|
||||
},
|
||||
attachJSONEditor : function (textarea) {
|
||||
// Since we are allowing duplicate keys at the moment, it is possible that we will try to attach
|
||||
// JSON Editor to a value that already has one. Therefore only attach if no CodeMirror peer exists.
|
||||
if ( $(textarea).siblings().hasClass('CodeMirror')) {
|
||||
return;
|
||||
}
|
||||
|
||||
var self = this;
|
||||
var oldValue = $(textarea).val();
|
||||
CodeMirror.fromTextArea(textarea, {
|
||||
mode: "application/json", lineNumbers: false, lineWrapping: false,
|
||||
onChange: function(instance, changeobj) {
|
||||
// this event's being called even when there's no change :-(
|
||||
if (instance.getValue() !== oldValue) self.showSaveCancelButtons();
|
||||
},
|
||||
onFocus : function(mirror) {
|
||||
$(textarea).parent().children('label').addClass("is-focused");
|
||||
},
|
||||
onBlur: function (mirror) {
|
||||
$(textarea).parent().children('label').removeClass("is-focused");
|
||||
var key = $(mirror.getWrapperElement()).closest('.field-group').children('.key').attr('id');
|
||||
var stringValue = $.trim(mirror.getValue());
|
||||
// update CodeMirror to show the trimmed value.
|
||||
mirror.setValue(stringValue);
|
||||
var JSONValue = undefined;
|
||||
try {
|
||||
JSONValue = JSON.parse(stringValue);
|
||||
} catch (e) {
|
||||
// If it didn't parse, try converting non-arrays/non-objects to a String.
|
||||
// But don't convert single-quote strings, which are most likely errors.
|
||||
var firstNonWhite = stringValue.substring(0, 1);
|
||||
if (firstNonWhite !== "{" && firstNonWhite !== "[" && firstNonWhite !== "'") {
|
||||
try {
|
||||
stringValue = '"'+stringValue +'"';
|
||||
JSONValue = JSON.parse(stringValue);
|
||||
mirror.setValue(stringValue);
|
||||
} catch(quotedE) {
|
||||
// TODO: validation error
|
||||
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();
|
||||
self.model.set(key, JSONValue, {validate: true});
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
showMessage: function (type) {
|
||||
this.$el.find(".message-status").removeClass("is-shown");
|
||||
if (type) {
|
||||
if (type === this.error_saving) {
|
||||
this.$el.find(".message-status.error").addClass("is-shown");
|
||||
}
|
||||
else if (type === this.successful_changes) {
|
||||
this.$el.find(".message-status.confirm").addClass("is-shown");
|
||||
this.hideSaveCancelButtons();
|
||||
}
|
||||
}
|
||||
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
|
||||
// check for dupes
|
||||
var self = event.data;
|
||||
self.model.save({},
|
||||
{
|
||||
success : function() {
|
||||
self.render();
|
||||
self.showMessage(self.successful_changes);
|
||||
},
|
||||
error : CMS.ServerError
|
||||
});
|
||||
},
|
||||
revertView : function(event) {
|
||||
var self = event.data;
|
||||
self.model.deleteKeys = [];
|
||||
self.model.clear({silent : true});
|
||||
self.model.fetch({
|
||||
success : function() { self.render(); },
|
||||
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("error", 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("error", 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);
|
||||
return newEle;
|
||||
},
|
||||
|
||||
focusInput : function(event) {
|
||||
$(event.target).prev().addClass("is-focused");
|
||||
},
|
||||
blurInput : function(event) {
|
||||
$(event.target).prev().removeClass("is-focused");
|
||||
}
|
||||
});
|
||||
@@ -1,222 +1,91 @@
|
||||
if (!CMS.Views['Settings']) CMS.Views.Settings = {};
|
||||
|
||||
// TODO move to common place
|
||||
CMS.Views.ValidatingView = Backbone.View.extend({
|
||||
// Intended as an abstract class which catches validation errors on the model and
|
||||
// decorates the fields. Needs wiring per class, but this initialization shows how
|
||||
// either have your init call this one or copy the contents
|
||||
initialize : function() {
|
||||
this.model.on('error', this.handleValidationError, this);
|
||||
this.selectorToField = _.invert(this.fieldToSelectorMap);
|
||||
},
|
||||
|
||||
errorTemplate : _.template('<span class="message-error"><%= message %></span>'),
|
||||
|
||||
events : {
|
||||
"blur input" : "clearValidationErrors",
|
||||
"blur textarea" : "clearValidationErrors"
|
||||
},
|
||||
fieldToSelectorMap : {
|
||||
// Your subclass must populate this w/ all of the model keys and dom selectors
|
||||
// which may be the subjects of validation errors
|
||||
},
|
||||
_cacheValidationErrors : [],
|
||||
handleValidationError : function(model, error) {
|
||||
// error is object w/ fields and error strings
|
||||
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');
|
||||
$(ele).parent().append(this.errorTemplate({message : error[field]}));
|
||||
}
|
||||
},
|
||||
|
||||
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');
|
||||
$(ele).nextAll('.message-error').remove();
|
||||
}
|
||||
},
|
||||
|
||||
saveIfChanged : function(event) {
|
||||
// returns true if the value changed and was thus sent to server
|
||||
var field = this.selectorToField[event.currentTarget.id];
|
||||
var currentVal = this.model.get(field);
|
||||
var newVal = $(event.currentTarget).val();
|
||||
if (currentVal != newVal) {
|
||||
this.clearValidationErrors();
|
||||
this.model.save(field, newVal, { error : CMS.ServerError});
|
||||
return true;
|
||||
}
|
||||
else return false;
|
||||
}
|
||||
});
|
||||
|
||||
CMS.Views.Settings.Main = Backbone.View.extend({
|
||||
// Model class is CMS.Models.Settings.CourseSettings
|
||||
// allow navigation between the tabs
|
||||
events: {
|
||||
'click .settings-page-menu a': "showSettingsTab",
|
||||
'mouseover #timezone' : "updateTime"
|
||||
},
|
||||
|
||||
currentTab: null,
|
||||
subviews: {}, // indexed by tab name
|
||||
|
||||
initialize: function() {
|
||||
// load templates
|
||||
this.currentTab = this.$el.find('.settings-page-menu .is-shown').attr('data-section');
|
||||
// create the initial subview
|
||||
this.subviews[this.currentTab] = this.createSubview();
|
||||
|
||||
// fill in fields
|
||||
this.$el.find("#course-name").val(this.model.get('courseLocation').get('name'));
|
||||
this.$el.find("#course-organization").val(this.model.get('courseLocation').get('org'));
|
||||
this.$el.find("#course-number").val(this.model.get('courseLocation').get('course'));
|
||||
this.$el.find('.set-date').datepicker({ 'dateFormat': 'm/d/yy' });
|
||||
this.$el.find(":input, textarea").focus(function() {
|
||||
$("label[for='" + this.id + "']").addClass("is-focused");
|
||||
}).blur(function() {
|
||||
$("label").removeClass("is-focused");
|
||||
});
|
||||
this.render();
|
||||
},
|
||||
|
||||
render: function() {
|
||||
|
||||
// create any necessary subviews and put them onto the page
|
||||
if (!this.model.has(this.currentTab)) {
|
||||
// TODO disable screen until fetch completes?
|
||||
var cachethis = this;
|
||||
this.model.retrieve(this.currentTab, function() {
|
||||
cachethis.subviews[cachethis.currentTab] = cachethis.createSubview();
|
||||
cachethis.subviews[cachethis.currentTab].render();
|
||||
});
|
||||
}
|
||||
else this.subviews[this.currentTab].render();
|
||||
|
||||
var dateIntrospect = new Date();
|
||||
this.$el.find('#timezone').html("(" + dateIntrospect.getTimezone() + ")");
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
createSubview: function() {
|
||||
switch (this.currentTab) {
|
||||
case 'details':
|
||||
return new CMS.Views.Settings.Details({
|
||||
el: this.$el.find('.settings-' + this.currentTab),
|
||||
model: this.model.get(this.currentTab)
|
||||
});
|
||||
case 'faculty':
|
||||
break;
|
||||
case 'grading':
|
||||
return new CMS.Views.Settings.Grading({
|
||||
el: this.$el.find('.settings-' + this.currentTab),
|
||||
model: this.model.get(this.currentTab)
|
||||
});
|
||||
case 'problems':
|
||||
break;
|
||||
case 'discussions':
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
updateTime : function(e) {
|
||||
var now = new Date();
|
||||
var hours = now.getHours();
|
||||
var minutes = now.getMinutes();
|
||||
$(e.currentTarget).attr('title', (hours % 12 === 0 ? 12 : hours % 12) + ":" + (minutes < 10 ? "0" : "") +
|
||||
now.getMinutes() + (hours < 12 ? "am" : "pm") + " (current local time)");
|
||||
},
|
||||
|
||||
showSettingsTab: function(e) {
|
||||
this.currentTab = $(e.target).attr('data-section');
|
||||
$('.settings-page-section > section').hide();
|
||||
$('.settings-' + this.currentTab).show();
|
||||
$('.settings-page-menu .is-shown').removeClass('is-shown');
|
||||
$(e.target).addClass('is-shown');
|
||||
// fetch model for the tab if not loaded already
|
||||
this.render();
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
CMS.Views.Settings.Details = CMS.Views.ValidatingView.extend({
|
||||
// Model class is CMS.Models.Settings.CourseDetails
|
||||
events : {
|
||||
"blur input" : "updateModel",
|
||||
"blur textarea" : "updateModel",
|
||||
'click .remove-course-syllabus' : "removeSyllabus",
|
||||
'click .new-course-syllabus' : 'assetSyllabus',
|
||||
'click .remove-course-introduction-video' : "removeVideo",
|
||||
'focus #course-overview' : "codeMirrorize"
|
||||
},
|
||||
initialize : function() {
|
||||
// TODO move the html frag to a loaded asset
|
||||
this.fileAnchorTemplate = _.template('<a href="<%= fullpath %>"> <i class="ss-icon ss-standard">📄</i><%= filename %></a>');
|
||||
this.model.on('error', this.handleValidationError, this);
|
||||
this.selectorToField = _.invert(this.fieldToSelectorMap);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
this.setupDatePicker('start_date');
|
||||
this.setupDatePicker('end_date');
|
||||
this.setupDatePicker('enrollment_start');
|
||||
this.setupDatePicker('enrollment_end');
|
||||
|
||||
if (this.model.has('syllabus')) {
|
||||
this.$el.find(this.fieldToSelectorMap['syllabus']).html(
|
||||
this.fileAnchorTemplate({
|
||||
fullpath : this.model.get('syllabus'),
|
||||
filename: 'syllabus'}));
|
||||
this.$el.find('.remove-course-syllabus').show();
|
||||
}
|
||||
else {
|
||||
this.$el.find('#' + this.fieldToSelectorMap['syllabus']).html("");
|
||||
this.$el.find('.remove-course-syllabus').hide();
|
||||
}
|
||||
|
||||
this.$el.find('#' + this.fieldToSelectorMap['overview']).val(this.model.get('overview'));
|
||||
this.codeMirrorize(null, $('#course-overview')[0]);
|
||||
|
||||
this.$el.find('.current-course-introduction-video iframe').attr('src', this.model.videosourceSample());
|
||||
if (this.model.has('intro_video')) {
|
||||
this.$el.find('.remove-course-introduction-video').show();
|
||||
this.$el.find('#' + this.fieldToSelectorMap['intro_video']).val(this.model.get('intro_video'));
|
||||
}
|
||||
else this.$el.find('.remove-course-introduction-video').hide();
|
||||
|
||||
this.$el.find('#' + this.fieldToSelectorMap['effort']).val(this.model.get('effort'));
|
||||
|
||||
return this;
|
||||
},
|
||||
fieldToSelectorMap : {
|
||||
'start_date' : "course-start",
|
||||
'end_date' : 'course-end',
|
||||
'enrollment_start' : 'enrollment-start',
|
||||
'enrollment_end' : 'enrollment-end',
|
||||
'syllabus' : '.current-course-syllabus .doc-filename',
|
||||
'overview' : 'course-overview',
|
||||
'intro_video' : 'course-introduction-video',
|
||||
'effort' : "course-effort"
|
||||
},
|
||||
// Model class is CMS.Models.Settings.CourseDetails
|
||||
events : {
|
||||
"change input" : "updateModel",
|
||||
"change textarea" : "updateModel",
|
||||
'click .remove-course-syllabus' : "removeSyllabus",
|
||||
'click .new-course-syllabus' : 'assetSyllabus',
|
||||
'click .remove-course-introduction-video' : "removeVideo",
|
||||
'focus #course-overview' : "codeMirrorize",
|
||||
'mouseover #timezone' : "updateTime",
|
||||
// would love to move to a general superclass, but event hashes don't inherit in backbone :-(
|
||||
'focus :input' : "inputFocus",
|
||||
'blur :input' : "inputUnfocus"
|
||||
|
||||
},
|
||||
initialize : function() {
|
||||
this.fileAnchorTemplate = _.template('<a href="<%= fullpath %>"> <i class="ss-icon ss-standard">📄</i><%= filename %></a>');
|
||||
// fill in fields
|
||||
this.$el.find("#course-name").val(this.model.get('location').get('name'));
|
||||
this.$el.find("#course-organization").val(this.model.get('location').get('org'));
|
||||
this.$el.find("#course-number").val(this.model.get('location').get('course'));
|
||||
this.$el.find('.set-date').datepicker({ 'dateFormat': 'm/d/yy' });
|
||||
|
||||
var dateIntrospect = new Date();
|
||||
this.$el.find('#timezone').html("(" + dateIntrospect.getTimezone() + ")");
|
||||
|
||||
this.model.on('error', this.handleValidationError, this);
|
||||
this.selectorToField = _.invert(this.fieldToSelectorMap);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
this.setupDatePicker('start_date');
|
||||
this.setupDatePicker('end_date');
|
||||
this.setupDatePicker('enrollment_start');
|
||||
this.setupDatePicker('enrollment_end');
|
||||
|
||||
if (this.model.has('syllabus')) {
|
||||
this.$el.find(this.fieldToSelectorMap['syllabus']).html(
|
||||
this.fileAnchorTemplate({
|
||||
fullpath : this.model.get('syllabus'),
|
||||
filename: 'syllabus'}));
|
||||
this.$el.find('.remove-course-syllabus').show();
|
||||
}
|
||||
else {
|
||||
this.$el.find('#' + this.fieldToSelectorMap['syllabus']).html("");
|
||||
this.$el.find('.remove-course-syllabus').hide();
|
||||
}
|
||||
|
||||
this.$el.find('#' + this.fieldToSelectorMap['overview']).val(this.model.get('overview'));
|
||||
this.codeMirrorize(null, $('#course-overview')[0]);
|
||||
|
||||
this.$el.find('.current-course-introduction-video iframe').attr('src', this.model.videosourceSample());
|
||||
if (this.model.has('intro_video')) {
|
||||
this.$el.find('.remove-course-introduction-video').show();
|
||||
this.$el.find('#' + this.fieldToSelectorMap['intro_video']).val(this.model.get('intro_video'));
|
||||
}
|
||||
else this.$el.find('.remove-course-introduction-video').hide();
|
||||
|
||||
this.$el.find('#' + this.fieldToSelectorMap['effort']).val(this.model.get('effort'));
|
||||
|
||||
return this;
|
||||
},
|
||||
fieldToSelectorMap : {
|
||||
'start_date' : "course-start",
|
||||
'end_date' : 'course-end',
|
||||
'enrollment_start' : 'enrollment-start',
|
||||
'enrollment_end' : 'enrollment-end',
|
||||
'syllabus' : '.current-course-syllabus .doc-filename',
|
||||
'overview' : 'course-overview',
|
||||
'intro_video' : 'course-introduction-video',
|
||||
'effort' : "course-effort"
|
||||
},
|
||||
|
||||
updateTime : function(e) {
|
||||
var now = new Date();
|
||||
var hours = now.getHours();
|
||||
var minutes = now.getMinutes();
|
||||
$(e.currentTarget).attr('title', (hours % 12 === 0 ? 12 : hours % 12) + ":" + (minutes < 10 ? "0" : "") +
|
||||
now.getMinutes() + (hours < 12 ? "am" : "pm") + " (current local time)");
|
||||
},
|
||||
|
||||
setupDatePicker: function (fieldName) {
|
||||
var cacheModel = this.model;
|
||||
var div = this.$el.find('#' + this.fieldToSelectorMap[fieldName]);
|
||||
var datefield = $(div).find(".date");
|
||||
var timefield = $(div).find(".time");
|
||||
var datefield = $(div).find("input:.date");
|
||||
var timefield = $(div).find("input:.time");
|
||||
var cachethis = this;
|
||||
var savefield = function () {
|
||||
cachethis.clearValidationErrors();
|
||||
@@ -227,8 +96,8 @@ CMS.Views.Settings.Details = CMS.Views.ValidatingView.extend({
|
||||
time = 0;
|
||||
}
|
||||
var newVal = new Date(date.getTime() + time * 1000);
|
||||
if (cacheModel.get(fieldName).getTime() !== newVal.getTime()) {
|
||||
cacheModel.save(fieldName, newVal, { error: CMS.ServerError});
|
||||
if (!cacheModel.has(fieldName) || cacheModel.get(fieldName).getTime() !== newVal.getTime()) {
|
||||
cacheModel.save(fieldName, newVal);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -245,58 +114,57 @@ CMS.Views.Settings.Details = CMS.Views.ValidatingView.extend({
|
||||
datefield.datepicker('setDate', this.model.get(fieldName));
|
||||
if (this.model.has(fieldName)) timefield.timepicker('setTime', this.model.get(fieldName));
|
||||
},
|
||||
|
||||
updateModel: function(event) {
|
||||
switch (event.currentTarget.id) {
|
||||
case 'course-start-date': // handled via onSelect method
|
||||
case 'course-end-date':
|
||||
case 'course-enrollment-start-date':
|
||||
case 'course-enrollment-end-date':
|
||||
break;
|
||||
|
||||
case 'course-overview':
|
||||
// handled via code mirror
|
||||
break;
|
||||
updateModel: function(event) {
|
||||
switch (event.currentTarget.id) {
|
||||
case 'course-start-date': // handled via onSelect method
|
||||
case 'course-end-date':
|
||||
case 'course-enrollment-start-date':
|
||||
case 'course-enrollment-end-date':
|
||||
break;
|
||||
|
||||
case 'course-effort':
|
||||
this.saveIfChanged(event);
|
||||
break;
|
||||
case 'course-introduction-video':
|
||||
this.clearValidationErrors();
|
||||
var previewsource = this.model.save_videosource($(event.currentTarget).val());
|
||||
this.$el.find(".current-course-introduction-video iframe").attr("src", previewsource);
|
||||
if (this.model.has('intro_video')) {
|
||||
this.$el.find('.remove-course-introduction-video').show();
|
||||
}
|
||||
else {
|
||||
this.$el.find('.remove-course-introduction-video').hide();
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
removeSyllabus: function() {
|
||||
if (this.model.has('syllabus')) this.model.save({'syllabus': null},
|
||||
{ error : CMS.ServerError});
|
||||
},
|
||||
|
||||
assetSyllabus : function() {
|
||||
// TODO implement
|
||||
},
|
||||
|
||||
removeVideo: function() {
|
||||
if (this.model.has('intro_video')) {
|
||||
this.model.save_videosource(null);
|
||||
this.$el.find(".current-course-introduction-video iframe").attr("src", "");
|
||||
this.$el.find('#' + this.fieldToSelectorMap['intro_video']).val("");
|
||||
this.$el.find('.remove-course-introduction-video').hide();
|
||||
}
|
||||
},
|
||||
codeMirrors : {},
|
||||
case 'course-overview':
|
||||
// handled via code mirror
|
||||
break;
|
||||
|
||||
case 'course-effort':
|
||||
this.saveIfChanged(event);
|
||||
break;
|
||||
case 'course-introduction-video':
|
||||
this.clearValidationErrors();
|
||||
var previewsource = this.model.save_videosource($(event.currentTarget).val());
|
||||
this.$el.find(".current-course-introduction-video iframe").attr("src", previewsource);
|
||||
if (this.model.has('intro_video')) {
|
||||
this.$el.find('.remove-course-introduction-video').show();
|
||||
}
|
||||
else {
|
||||
this.$el.find('.remove-course-introduction-video').hide();
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
removeSyllabus: function() {
|
||||
if (this.model.has('syllabus')) this.model.save({'syllabus': null});
|
||||
},
|
||||
|
||||
assetSyllabus : function() {
|
||||
// TODO implement
|
||||
},
|
||||
|
||||
removeVideo: function() {
|
||||
if (this.model.has('intro_video')) {
|
||||
this.model.save_videosource(null);
|
||||
this.$el.find(".current-course-introduction-video iframe").attr("src", "");
|
||||
this.$el.find('#' + this.fieldToSelectorMap['intro_video']).val("");
|
||||
this.$el.find('.remove-course-introduction-video').hide();
|
||||
}
|
||||
},
|
||||
codeMirrors : {},
|
||||
codeMirrorize: function (e, forcedTarget) {
|
||||
var thisTarget;
|
||||
if (forcedTarget) {
|
||||
@@ -315,374 +183,11 @@ CMS.Views.Settings.Details = CMS.Views.ValidatingView.extend({
|
||||
mirror.save();
|
||||
cachethis.clearValidationErrors();
|
||||
var newVal = mirror.getValue();
|
||||
if (cachethis.model.get(field) != newVal) cachethis.model.save(field, newVal,
|
||||
{ error: CMS.ServerError});
|
||||
if (cachethis.model.get(field) != newVal) cachethis.model.save(field, newVal);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
CMS.Views.Settings.Grading = CMS.Views.ValidatingView.extend({
|
||||
// Model class is CMS.Models.Settings.CourseGradingPolicy
|
||||
events : {
|
||||
"blur input" : "updateModel",
|
||||
"blur textarea" : "updateModel",
|
||||
"blur span[contenteditable=true]" : "updateDesignation",
|
||||
"click .settings-extra header" : "showSettingsExtras",
|
||||
"click .new-grade-button" : "addNewGrade",
|
||||
"click .remove-button" : "removeGrade",
|
||||
"click .add-grading-data" : "addAssignmentType"
|
||||
},
|
||||
initialize : function() {
|
||||
// load template for grading view
|
||||
var self = this;
|
||||
this.gradeCutoffTemplate = _.template('<li class="grade-specific-bar" style="width:<%= width %>%"><span class="letter-grade" contenteditable>' +
|
||||
'<%= descriptor %>' +
|
||||
'</span><span class="range"></span>' +
|
||||
'<% if (removable) {%><a href="#" class="remove-button">remove</a><% ;} %>' +
|
||||
'</li>');
|
||||
|
||||
// Instrument grading scale
|
||||
// convert cutoffs to inversely ordered list
|
||||
var modelCutoffs = this.model.get('grade_cutoffs');
|
||||
for (var cutoff in modelCutoffs) {
|
||||
this.descendingCutoffs.push({designation: cutoff, cutoff: Math.round(modelCutoffs[cutoff] * 100)});
|
||||
}
|
||||
this.descendingCutoffs = _.sortBy(this.descendingCutoffs,
|
||||
function (gradeEle) { return -gradeEle['cutoff']; });
|
||||
|
||||
// Instrument grace period
|
||||
this.$el.find('#course-grading-graceperiod').timepicker();
|
||||
|
||||
// instantiates an editor template for each update in the collection
|
||||
// Because this calls render, put it after everything which render may depend upon to prevent race condition.
|
||||
window.templateLoader.loadRemoteTemplate("course_grade_policy",
|
||||
"/static/client_templates/course_grade_policy.html",
|
||||
function (raw_template) {
|
||||
self.template = _.template(raw_template);
|
||||
self.render();
|
||||
}
|
||||
);
|
||||
this.model.on('error', this.handleValidationError, this);
|
||||
this.model.get('graders').on('remove', this.render, this);
|
||||
this.model.get('graders').on('reset', this.render, this);
|
||||
this.model.get('graders').on('add', this.render, this);
|
||||
this.selectorToField = _.invert(this.fieldToSelectorMap);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
// prevent bootstrap race condition by event dispatch
|
||||
if (!this.template) return;
|
||||
|
||||
// Create and render the grading type subs
|
||||
var self = this;
|
||||
var gradelist = this.$el.find('.course-grading-assignment-list');
|
||||
// Undo the double invocation error. At some point, fix the double invocation
|
||||
$(gradelist).empty();
|
||||
var gradeCollection = this.model.get('graders');
|
||||
gradeCollection.each(function(gradeModel) {
|
||||
$(gradelist).append(self.template({model : gradeModel }));
|
||||
var newEle = gradelist.children().last();
|
||||
var newView = new CMS.Views.Settings.GraderView({el: newEle,
|
||||
model : gradeModel, collection : gradeCollection });
|
||||
});
|
||||
|
||||
// render the grade cutoffs
|
||||
this.renderCutoffBar();
|
||||
|
||||
var graceEle = this.$el.find('#course-grading-graceperiod');
|
||||
graceEle.timepicker({'timeFormat' : 'H:i'}); // init doesn't take setTime
|
||||
if (this.model.has('grace_period')) graceEle.timepicker('setTime', this.model.gracePeriodToDate());
|
||||
// remove any existing listeners to keep them from piling on b/c render gets called frequently
|
||||
graceEle.off('change', this.setGracePeriod);
|
||||
graceEle.on('change', this, this.setGracePeriod);
|
||||
|
||||
return this;
|
||||
},
|
||||
addAssignmentType : function(e) {
|
||||
e.preventDefault();
|
||||
this.model.get('graders').push({});
|
||||
},
|
||||
fieldToSelectorMap : {
|
||||
'grace_period' : 'course-grading-graceperiod'
|
||||
},
|
||||
setGracePeriod : function(event) {
|
||||
event.data.clearValidationErrors();
|
||||
var newVal = event.data.model.dateToGracePeriod($(event.currentTarget).timepicker('getTime'));
|
||||
if (event.data.model.get('grace_period') != newVal) event.data.model.save('grace_period', newVal,
|
||||
{ error : CMS.ServerError});
|
||||
},
|
||||
updateModel : function(event) {
|
||||
if (!this.selectorToField[event.currentTarget.id]) return;
|
||||
|
||||
switch (this.selectorToField[event.currentTarget.id]) {
|
||||
case 'grace_period': // handled above
|
||||
break;
|
||||
|
||||
default:
|
||||
this.saveIfChanged(event);
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
// Grade sliders attributes and methods
|
||||
// Grade bars are li's ordered A -> F with A taking whole width, B overlaying it with its paint, ...
|
||||
// The actual cutoff for each grade is the width % of the next lower grade; so, the hack here
|
||||
// is to lay down a whole width bar claiming it's A and then lay down bars for each actual grade
|
||||
// starting w/ A but posting the label in the preceding li and setting the label of the last to "Fail" or "F"
|
||||
|
||||
// A does not have a drag bar (cannot change its upper limit)
|
||||
// Need to insert new bars in right place.
|
||||
GRADES : ['A', 'B', 'C', 'D'], // defaults for new grade designators
|
||||
descendingCutoffs : [], // array of { designation : , cutoff : }
|
||||
gradeBarWidth : null, // cache of value since it won't change (more certain)
|
||||
|
||||
renderCutoffBar: function() {
|
||||
var gradeBar =this.$el.find('.grade-bar');
|
||||
this.gradeBarWidth = gradeBar.width();
|
||||
var gradelist = gradeBar.children('.grades');
|
||||
// HACK fixing a duplicate call issue by undoing previous call effect. Need to figure out why called 2x
|
||||
gradelist.empty();
|
||||
var nextWidth = 100; // first width is 100%
|
||||
// Can probably be simplified to one variable now.
|
||||
var removable = false;
|
||||
var draggable = false; // first and last are not removable, first is not draggable
|
||||
_.each(this.descendingCutoffs,
|
||||
function(cutoff, index) {
|
||||
var newBar = this.gradeCutoffTemplate({
|
||||
descriptor : cutoff['designation'] ,
|
||||
width : nextWidth,
|
||||
removable : removable });
|
||||
gradelist.append(newBar);
|
||||
if (draggable) {
|
||||
newBar = gradelist.children().last(); // get the dom object not the unparsed string
|
||||
newBar.resizable({
|
||||
handles: "e",
|
||||
containment : "parent",
|
||||
start : this.startMoveClosure(),
|
||||
resize : this.moveBarClosure(),
|
||||
stop : this.stopDragClosure()
|
||||
});
|
||||
}
|
||||
// prepare for next
|
||||
nextWidth = cutoff['cutoff'];
|
||||
removable = true; // first is not removable, all others are
|
||||
draggable = true;
|
||||
},
|
||||
this);
|
||||
// add fail which is not in data
|
||||
var failBar = this.gradeCutoffTemplate({ descriptor : this.failLabel(),
|
||||
width : nextWidth, removable : false});
|
||||
$(failBar).find("span[contenteditable=true]").attr("contenteditable", false);
|
||||
gradelist.append(failBar);
|
||||
gradelist.children().last().resizable({
|
||||
handles: "e",
|
||||
containment : "parent",
|
||||
start : this.startMoveClosure(),
|
||||
resize : this.moveBarClosure(),
|
||||
stop : this.stopDragClosure()
|
||||
});
|
||||
|
||||
this.renderGradeRanges();
|
||||
},
|
||||
|
||||
showSettingsExtras : function(event) {
|
||||
$(event.currentTarget).toggleClass('active');
|
||||
$(event.currentTarget).siblings.toggleClass('is-shown');
|
||||
},
|
||||
|
||||
|
||||
startMoveClosure : function() {
|
||||
// set min/max widths
|
||||
var cachethis = this;
|
||||
var widthPerPoint = cachethis.gradeBarWidth / 100;
|
||||
return function(event, ui) {
|
||||
var barIndex = ui.element.index();
|
||||
// min and max represent limits not labels (note, can's make smaller than 3 points wide)
|
||||
var min = (barIndex < cachethis.descendingCutoffs.length ? cachethis.descendingCutoffs[barIndex]['cutoff'] + 3 : 3);
|
||||
// minus 2 b/c minus 1 is the element we're effecting. It's max is just shy of the next one above it
|
||||
var max = (barIndex >= 2 ? cachethis.descendingCutoffs[barIndex - 2]['cutoff'] - 3 : 97);
|
||||
ui.element.resizable("option",{minWidth : min * widthPerPoint, maxWidth : max * widthPerPoint});
|
||||
};
|
||||
},
|
||||
|
||||
moveBarClosure : function() {
|
||||
// 0th ele doesn't have a bar; so, will never invoke this
|
||||
var cachethis = this;
|
||||
return function(event, ui) {
|
||||
var barIndex = ui.element.index();
|
||||
// min and max represent limits not labels (note, can's make smaller than 3 points wide)
|
||||
var min = (barIndex < cachethis.descendingCutoffs.length ? cachethis.descendingCutoffs[barIndex]['cutoff'] + 3 : 3);
|
||||
// minus 2 b/c minus 1 is the element we're effecting. It's max is just shy of the next one above it
|
||||
var max = (barIndex >= 2 ? cachethis.descendingCutoffs[barIndex - 2]['cutoff'] - 3 : 100);
|
||||
var percentage = Math.min(Math.max(ui.size.width / cachethis.gradeBarWidth * 100, min), max);
|
||||
cachethis.descendingCutoffs[barIndex - 1]['cutoff'] = Math.round(percentage);
|
||||
cachethis.renderGradeRanges();
|
||||
};
|
||||
},
|
||||
|
||||
renderGradeRanges: function() {
|
||||
// the labels showing the range e.g., 71-80
|
||||
var cutoffs = this.descendingCutoffs;
|
||||
this.$el.find('.range').each(function(i) {
|
||||
var min = (i < cutoffs.length ? cutoffs[i]['cutoff'] : 0);
|
||||
var max = (i > 0 ? cutoffs[i - 1]['cutoff'] : 100);
|
||||
$(this).text(min + '-' + max);
|
||||
});
|
||||
},
|
||||
|
||||
stopDragClosure: function() {
|
||||
var cachethis = this;
|
||||
return function(event, ui) {
|
||||
// for some reason the resize is setting height to 0
|
||||
cachethis.saveCutoffs();
|
||||
};
|
||||
},
|
||||
|
||||
saveCutoffs: function() {
|
||||
this.model.save('grade_cutoffs',
|
||||
_.reduce(this.descendingCutoffs,
|
||||
function(object, cutoff) {
|
||||
object[cutoff['designation']] = cutoff['cutoff'] / 100.0;
|
||||
return object;
|
||||
},
|
||||
{}),
|
||||
{ error : CMS.ServerError});
|
||||
},
|
||||
|
||||
addNewGrade: function(e) {
|
||||
e.preventDefault();
|
||||
var gradeLength = this.descendingCutoffs.length; // cutoffs doesn't include fail/f so this is only the passing grades
|
||||
if(gradeLength > 3) {
|
||||
// TODO shouldn't we disable the button
|
||||
return;
|
||||
}
|
||||
var failBarWidth = this.descendingCutoffs[gradeLength - 1]['cutoff'];
|
||||
// going to split the grade above the insertion point in half leaving fail in same place
|
||||
var nextGradeTop = (gradeLength > 1 ? this.descendingCutoffs[gradeLength - 2]['cutoff'] : 100);
|
||||
var targetWidth = failBarWidth + ((nextGradeTop - failBarWidth) / 2);
|
||||
this.descendingCutoffs.push({designation: this.GRADES[gradeLength], cutoff: failBarWidth});
|
||||
this.descendingCutoffs[gradeLength - 1]['cutoff'] = Math.round(targetWidth);
|
||||
|
||||
var $newGradeBar = this.gradeCutoffTemplate({ descriptor : this.GRADES[gradeLength],
|
||||
width : targetWidth, removable : true });
|
||||
var gradeDom = this.$el.find('.grades');
|
||||
gradeDom.children().last().before($newGradeBar);
|
||||
var newEle = gradeDom.children()[gradeLength];
|
||||
$(newEle).resizable({
|
||||
handles: "e",
|
||||
containment : "parent",
|
||||
start : this.startMoveClosure(),
|
||||
resize : this.moveBarClosure(),
|
||||
stop : this.stopDragClosure()
|
||||
});
|
||||
|
||||
// Munge existing grade labels?
|
||||
// If going from Pass/Fail to 3 levels, change to Pass to A
|
||||
if (gradeLength === 1 && this.descendingCutoffs[0]['designation'] === 'Pass') {
|
||||
this.descendingCutoffs[0]['designation'] = this.GRADES[0];
|
||||
this.setTopGradeLabel();
|
||||
}
|
||||
this.setFailLabel();
|
||||
|
||||
this.renderGradeRanges();
|
||||
this.saveCutoffs();
|
||||
},
|
||||
|
||||
removeGrade: function(e) {
|
||||
e.preventDefault();
|
||||
var domElement = $(e.currentTarget).closest('li');
|
||||
var index = domElement.index();
|
||||
// copy the boundary up to the next higher grade then remove
|
||||
this.descendingCutoffs[index - 1]['cutoff'] = this.descendingCutoffs[index]['cutoff'];
|
||||
this.descendingCutoffs.splice(index, 1);
|
||||
domElement.remove();
|
||||
|
||||
if (this.descendingCutoffs.length === 1 && this.descendingCutoffs[0]['designation'] === this.GRADES[0]) {
|
||||
this.descendingCutoffs[0]['designation'] = 'Pass';
|
||||
this.setTopGradeLabel();
|
||||
}
|
||||
this.setFailLabel();
|
||||
this.renderGradeRanges();
|
||||
this.saveCutoffs();
|
||||
},
|
||||
|
||||
updateDesignation: function(e) {
|
||||
var index = $(e.currentTarget).closest('li').index();
|
||||
this.descendingCutoffs[index]['designation'] = $(e.currentTarget).html();
|
||||
this.saveCutoffs();
|
||||
},
|
||||
|
||||
failLabel: function() {
|
||||
if (this.descendingCutoffs.length === 1) return 'Fail';
|
||||
else return 'F';
|
||||
},
|
||||
setFailLabel: function() {
|
||||
this.$el.find('.grades .letter-grade').last().html(this.failLabel());
|
||||
},
|
||||
setTopGradeLabel: function() {
|
||||
this.$el.find('.grades .letter-grade').first().html(this.descendingCutoffs[0]['designation']);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
CMS.Views.Settings.GraderView = CMS.Views.ValidatingView.extend({
|
||||
// Model class is CMS.Models.Settings.CourseGrader
|
||||
events : {
|
||||
"blur input" : "updateModel",
|
||||
"blur textarea" : "updateModel",
|
||||
"click .remove-grading-data" : "deleteModel"
|
||||
},
|
||||
initialize : function() {
|
||||
this.model.on('error', this.handleValidationError, this);
|
||||
this.selectorToField = _.invert(this.fieldToSelectorMap);
|
||||
this.render();
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return this;
|
||||
},
|
||||
fieldToSelectorMap : {
|
||||
'type' : 'course-grading-assignment-name',
|
||||
'short_label' : 'course-grading-assignment-shortname',
|
||||
'min_count' : 'course-grading-assignment-totalassignments',
|
||||
'drop_count' : 'course-grading-assignment-droppable',
|
||||
'weight' : 'course-grading-assignment-gradeweight'
|
||||
},
|
||||
updateModel : function(event) {
|
||||
// HACK to fix model sometimes losing its pointer to the collection [I think I fixed this but leaving
|
||||
// this in out of paranoia. If this error ever happens, the user will get a warning that they cannot
|
||||
// give 2 assignments the same name.]
|
||||
if (!this.model.collection) {
|
||||
this.model.collection = this.collection;
|
||||
}
|
||||
|
||||
switch (event.currentTarget.id) {
|
||||
case 'course-grading-assignment-totalassignments':
|
||||
this.$el.find('#course-grading-assignment-droppable').attr('max', $(event.currentTarget).val());
|
||||
this.saveIfChanged(event);
|
||||
break;
|
||||
case 'course-grading-assignment-name':
|
||||
var oldName = this.model.get('type');
|
||||
if (this.saveIfChanged(event) && !_.isEmpty(oldName)) {
|
||||
// overload the error display logic
|
||||
this._cacheValidationErrors.push(event.currentTarget);
|
||||
$(event.currentTarget).parent().append(
|
||||
this.errorTemplate({message : 'For grading to work, you must change all "' + oldName +
|
||||
'" subsections to "' + this.model.get('type') + '".'}));
|
||||
}
|
||||
break;
|
||||
default:
|
||||
this.saveIfChanged(event);
|
||||
break;
|
||||
}
|
||||
},
|
||||
deleteModel : function(e) {
|
||||
this.model.destroy(
|
||||
{ error : CMS.ServerError});
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
});
|
||||
368
cms/static/js/views/settings/settings_grading_view.js
Normal file
@@ -0,0 +1,368 @@
|
||||
if (!CMS.Views['Settings']) CMS.Views.Settings = {}; // ensure the pseudo pkg exists
|
||||
|
||||
CMS.Views.Settings.Grading = CMS.Views.ValidatingView.extend({
|
||||
// Model class is CMS.Models.Settings.CourseGradingPolicy
|
||||
events : {
|
||||
"change input" : "updateModel",
|
||||
"change textarea" : "updateModel",
|
||||
"change span[contenteditable=true]" : "updateDesignation",
|
||||
"click .settings-extra header" : "showSettingsExtras",
|
||||
"click .new-grade-button" : "addNewGrade",
|
||||
"click .remove-button" : "removeGrade",
|
||||
"click .add-grading-data" : "addAssignmentType",
|
||||
// would love to move to a general superclass, but event hashes don't inherit in backbone :-(
|
||||
'focus :input' : "inputFocus",
|
||||
'blur :input' : "inputUnfocus"
|
||||
},
|
||||
initialize : function() {
|
||||
// load template for grading view
|
||||
var self = this;
|
||||
this.gradeCutoffTemplate = _.template('<li class="grade-specific-bar" style="width:<%= width %>%"><span class="letter-grade" contenteditable>' +
|
||||
'<%= descriptor %>' +
|
||||
'</span><span class="range"></span>' +
|
||||
'<% if (removable) {%><a href="#" class="remove-button">remove</a><% ;} %>' +
|
||||
'</li>');
|
||||
|
||||
// Instrument grading scale
|
||||
// convert cutoffs to inversely ordered list
|
||||
var modelCutoffs = this.model.get('grade_cutoffs');
|
||||
for (var cutoff in modelCutoffs) {
|
||||
this.descendingCutoffs.push({designation: cutoff, cutoff: Math.round(modelCutoffs[cutoff] * 100)});
|
||||
}
|
||||
this.descendingCutoffs = _.sortBy(this.descendingCutoffs,
|
||||
function (gradeEle) { return -gradeEle['cutoff']; });
|
||||
|
||||
// Instrument grace period
|
||||
this.$el.find('#course-grading-graceperiod').timepicker();
|
||||
|
||||
// instantiates an editor template for each update in the collection
|
||||
// Because this calls render, put it after everything which render may depend upon to prevent race condition.
|
||||
window.templateLoader.loadRemoteTemplate("course_grade_policy",
|
||||
"/static/client_templates/course_grade_policy.html",
|
||||
function (raw_template) {
|
||||
self.template = _.template(raw_template);
|
||||
self.render();
|
||||
}
|
||||
);
|
||||
this.model.on('error', this.handleValidationError, this);
|
||||
this.model.get('graders').on('remove', this.render, this);
|
||||
this.model.get('graders').on('reset', this.render, this);
|
||||
this.model.get('graders').on('add', this.render, this);
|
||||
this.selectorToField = _.invert(this.fieldToSelectorMap);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
// prevent bootstrap race condition by event dispatch
|
||||
if (!this.template) return;
|
||||
|
||||
// Create and render the grading type subs
|
||||
var self = this;
|
||||
var gradelist = this.$el.find('.course-grading-assignment-list');
|
||||
// Undo the double invocation error. At some point, fix the double invocation
|
||||
$(gradelist).empty();
|
||||
var gradeCollection = this.model.get('graders');
|
||||
gradeCollection.each(function(gradeModel) {
|
||||
$(gradelist).append(self.template({model : gradeModel }));
|
||||
var newEle = gradelist.children().last();
|
||||
var newView = new CMS.Views.Settings.GraderView({el: newEle,
|
||||
model : gradeModel, collection : gradeCollection });
|
||||
});
|
||||
|
||||
// render the grade cutoffs
|
||||
this.renderCutoffBar();
|
||||
|
||||
var graceEle = this.$el.find('#course-grading-graceperiod');
|
||||
graceEle.timepicker({'timeFormat' : 'H:i'}); // init doesn't take setTime
|
||||
if (this.model.has('grace_period')) graceEle.timepicker('setTime', this.model.gracePeriodToDate());
|
||||
// remove any existing listeners to keep them from piling on b/c render gets called frequently
|
||||
graceEle.off('change', this.setGracePeriod);
|
||||
graceEle.on('change', this, this.setGracePeriod);
|
||||
|
||||
return this;
|
||||
},
|
||||
addAssignmentType : function(e) {
|
||||
e.preventDefault();
|
||||
this.model.get('graders').push({});
|
||||
},
|
||||
fieldToSelectorMap : {
|
||||
'grace_period' : 'course-grading-graceperiod'
|
||||
},
|
||||
setGracePeriod : function(event) {
|
||||
event.data.clearValidationErrors();
|
||||
var newVal = event.data.model.dateToGracePeriod($(event.currentTarget).timepicker('getTime'));
|
||||
if (event.data.model.get('grace_period') != newVal) event.data.model.save('grace_period', newVal);
|
||||
},
|
||||
updateModel : function(event) {
|
||||
if (!this.selectorToField[event.currentTarget.id]) return;
|
||||
|
||||
switch (this.selectorToField[event.currentTarget.id]) {
|
||||
case 'grace_period': // handled above
|
||||
break;
|
||||
|
||||
default:
|
||||
this.saveIfChanged(event);
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
// Grade sliders attributes and methods
|
||||
// Grade bars are li's ordered A -> F with A taking whole width, B overlaying it with its paint, ...
|
||||
// The actual cutoff for each grade is the width % of the next lower grade; so, the hack here
|
||||
// is to lay down a whole width bar claiming it's A and then lay down bars for each actual grade
|
||||
// starting w/ A but posting the label in the preceding li and setting the label of the last to "Fail" or "F"
|
||||
|
||||
// A does not have a drag bar (cannot change its upper limit)
|
||||
// Need to insert new bars in right place.
|
||||
GRADES : ['A', 'B', 'C', 'D'], // defaults for new grade designators
|
||||
descendingCutoffs : [], // array of { designation : , cutoff : }
|
||||
gradeBarWidth : null, // cache of value since it won't change (more certain)
|
||||
|
||||
renderCutoffBar: function() {
|
||||
var gradeBar =this.$el.find('.grade-bar');
|
||||
this.gradeBarWidth = gradeBar.width();
|
||||
var gradelist = gradeBar.children('.grades');
|
||||
// HACK fixing a duplicate call issue by undoing previous call effect. Need to figure out why called 2x
|
||||
gradelist.empty();
|
||||
var nextWidth = 100; // first width is 100%
|
||||
// Can probably be simplified to one variable now.
|
||||
var removable = false;
|
||||
var draggable = false; // first and last are not removable, first is not draggable
|
||||
_.each(this.descendingCutoffs,
|
||||
function(cutoff, index) {
|
||||
var newBar = this.gradeCutoffTemplate({
|
||||
descriptor : cutoff['designation'] ,
|
||||
width : nextWidth,
|
||||
removable : removable });
|
||||
gradelist.append(newBar);
|
||||
if (draggable) {
|
||||
newBar = gradelist.children().last(); // get the dom object not the unparsed string
|
||||
newBar.resizable({
|
||||
handles: "e",
|
||||
containment : "parent",
|
||||
start : this.startMoveClosure(),
|
||||
resize : this.moveBarClosure(),
|
||||
stop : this.stopDragClosure()
|
||||
});
|
||||
}
|
||||
// prepare for next
|
||||
nextWidth = cutoff['cutoff'];
|
||||
removable = true; // first is not removable, all others are
|
||||
draggable = true;
|
||||
},
|
||||
this);
|
||||
// add fail which is not in data
|
||||
var failBar = this.gradeCutoffTemplate({ descriptor : this.failLabel(),
|
||||
width : nextWidth, removable : false});
|
||||
$(failBar).find("span[contenteditable=true]").attr("contenteditable", false);
|
||||
gradelist.append(failBar);
|
||||
gradelist.children().last().resizable({
|
||||
handles: "e",
|
||||
containment : "parent",
|
||||
start : this.startMoveClosure(),
|
||||
resize : this.moveBarClosure(),
|
||||
stop : this.stopDragClosure()
|
||||
});
|
||||
|
||||
this.renderGradeRanges();
|
||||
},
|
||||
|
||||
showSettingsExtras : function(event) {
|
||||
$(event.currentTarget).toggleClass('active');
|
||||
$(event.currentTarget).siblings.toggleClass('is-shown');
|
||||
},
|
||||
|
||||
|
||||
startMoveClosure : function() {
|
||||
// set min/max widths
|
||||
var cachethis = this;
|
||||
var widthPerPoint = cachethis.gradeBarWidth / 100;
|
||||
return function(event, ui) {
|
||||
var barIndex = ui.element.index();
|
||||
// min and max represent limits not labels (note, can's make smaller than 3 points wide)
|
||||
var min = (barIndex < cachethis.descendingCutoffs.length ? cachethis.descendingCutoffs[barIndex]['cutoff'] + 3 : 3);
|
||||
// minus 2 b/c minus 1 is the element we're effecting. It's max is just shy of the next one above it
|
||||
var max = (barIndex >= 2 ? cachethis.descendingCutoffs[barIndex - 2]['cutoff'] - 3 : 97);
|
||||
ui.element.resizable("option",{minWidth : min * widthPerPoint, maxWidth : max * widthPerPoint});
|
||||
};
|
||||
},
|
||||
|
||||
moveBarClosure : function() {
|
||||
// 0th ele doesn't have a bar; so, will never invoke this
|
||||
var cachethis = this;
|
||||
return function(event, ui) {
|
||||
var barIndex = ui.element.index();
|
||||
// min and max represent limits not labels (note, can's make smaller than 3 points wide)
|
||||
var min = (barIndex < cachethis.descendingCutoffs.length ? cachethis.descendingCutoffs[barIndex]['cutoff'] + 3 : 3);
|
||||
// minus 2 b/c minus 1 is the element we're effecting. It's max is just shy of the next one above it
|
||||
var max = (barIndex >= 2 ? cachethis.descendingCutoffs[barIndex - 2]['cutoff'] - 3 : 100);
|
||||
var percentage = Math.min(Math.max(ui.size.width / cachethis.gradeBarWidth * 100, min), max);
|
||||
cachethis.descendingCutoffs[barIndex - 1]['cutoff'] = Math.round(percentage);
|
||||
cachethis.renderGradeRanges();
|
||||
};
|
||||
},
|
||||
|
||||
renderGradeRanges: function() {
|
||||
// the labels showing the range e.g., 71-80
|
||||
var cutoffs = this.descendingCutoffs;
|
||||
this.$el.find('.range').each(function(i) {
|
||||
var min = (i < cutoffs.length ? cutoffs[i]['cutoff'] : 0);
|
||||
var max = (i > 0 ? cutoffs[i - 1]['cutoff'] : 100);
|
||||
$(this).text(min + '-' + max);
|
||||
});
|
||||
},
|
||||
|
||||
stopDragClosure: function() {
|
||||
var cachethis = this;
|
||||
return function(event, ui) {
|
||||
// for some reason the resize is setting height to 0
|
||||
cachethis.saveCutoffs();
|
||||
};
|
||||
},
|
||||
|
||||
saveCutoffs: function() {
|
||||
this.model.save('grade_cutoffs',
|
||||
_.reduce(this.descendingCutoffs,
|
||||
function(object, cutoff) {
|
||||
object[cutoff['designation']] = cutoff['cutoff'] / 100.0;
|
||||
return object;
|
||||
},
|
||||
{}));
|
||||
},
|
||||
|
||||
addNewGrade: function(e) {
|
||||
e.preventDefault();
|
||||
var gradeLength = this.descendingCutoffs.length; // cutoffs doesn't include fail/f so this is only the passing grades
|
||||
if(gradeLength > 3) {
|
||||
// TODO shouldn't we disable the button
|
||||
return;
|
||||
}
|
||||
var failBarWidth = this.descendingCutoffs[gradeLength - 1]['cutoff'];
|
||||
// going to split the grade above the insertion point in half leaving fail in same place
|
||||
var nextGradeTop = (gradeLength > 1 ? this.descendingCutoffs[gradeLength - 2]['cutoff'] : 100);
|
||||
var targetWidth = failBarWidth + ((nextGradeTop - failBarWidth) / 2);
|
||||
this.descendingCutoffs.push({designation: this.GRADES[gradeLength], cutoff: failBarWidth});
|
||||
this.descendingCutoffs[gradeLength - 1]['cutoff'] = Math.round(targetWidth);
|
||||
|
||||
var $newGradeBar = this.gradeCutoffTemplate({ descriptor : this.GRADES[gradeLength],
|
||||
width : targetWidth, removable : true });
|
||||
var gradeDom = this.$el.find('.grades');
|
||||
gradeDom.children().last().before($newGradeBar);
|
||||
var newEle = gradeDom.children()[gradeLength];
|
||||
$(newEle).resizable({
|
||||
handles: "e",
|
||||
containment : "parent",
|
||||
start : this.startMoveClosure(),
|
||||
resize : this.moveBarClosure(),
|
||||
stop : this.stopDragClosure()
|
||||
});
|
||||
|
||||
// Munge existing grade labels?
|
||||
// If going from Pass/Fail to 3 levels, change to Pass to A
|
||||
if (gradeLength === 1 && this.descendingCutoffs[0]['designation'] === 'Pass') {
|
||||
this.descendingCutoffs[0]['designation'] = this.GRADES[0];
|
||||
this.setTopGradeLabel();
|
||||
}
|
||||
this.setFailLabel();
|
||||
|
||||
this.renderGradeRanges();
|
||||
this.saveCutoffs();
|
||||
},
|
||||
|
||||
removeGrade: function(e) {
|
||||
e.preventDefault();
|
||||
var domElement = $(e.currentTarget).closest('li');
|
||||
var index = domElement.index();
|
||||
// copy the boundary up to the next higher grade then remove
|
||||
this.descendingCutoffs[index - 1]['cutoff'] = this.descendingCutoffs[index]['cutoff'];
|
||||
this.descendingCutoffs.splice(index, 1);
|
||||
domElement.remove();
|
||||
|
||||
if (this.descendingCutoffs.length === 1 && this.descendingCutoffs[0]['designation'] === this.GRADES[0]) {
|
||||
this.descendingCutoffs[0]['designation'] = 'Pass';
|
||||
this.setTopGradeLabel();
|
||||
}
|
||||
this.setFailLabel();
|
||||
this.renderGradeRanges();
|
||||
this.saveCutoffs();
|
||||
},
|
||||
|
||||
updateDesignation: function(e) {
|
||||
var index = $(e.currentTarget).closest('li').index();
|
||||
this.descendingCutoffs[index]['designation'] = $(e.currentTarget).html();
|
||||
this.saveCutoffs();
|
||||
},
|
||||
|
||||
failLabel: function() {
|
||||
if (this.descendingCutoffs.length === 1) return 'Fail';
|
||||
else return 'F';
|
||||
},
|
||||
setFailLabel: function() {
|
||||
this.$el.find('.grades .letter-grade').last().html(this.failLabel());
|
||||
},
|
||||
setTopGradeLabel: function() {
|
||||
this.$el.find('.grades .letter-grade').first().html(this.descendingCutoffs[0]['designation']);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
CMS.Views.Settings.GraderView = CMS.Views.ValidatingView.extend({
|
||||
// Model class is CMS.Models.Settings.CourseGrader
|
||||
events : {
|
||||
"change input" : "updateModel",
|
||||
"change textarea" : "updateModel",
|
||||
"click .remove-grading-data" : "deleteModel",
|
||||
// would love to move to a general superclass, but event hashes don't inherit in backbone :-(
|
||||
'focus :input' : "inputFocus",
|
||||
'blur :input' : "inputUnfocus"
|
||||
},
|
||||
initialize : function() {
|
||||
this.model.on('error', this.handleValidationError, this);
|
||||
this.selectorToField = _.invert(this.fieldToSelectorMap);
|
||||
this.render();
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return this;
|
||||
},
|
||||
fieldToSelectorMap : {
|
||||
'type' : 'course-grading-assignment-name',
|
||||
'short_label' : 'course-grading-assignment-shortname',
|
||||
'min_count' : 'course-grading-assignment-totalassignments',
|
||||
'drop_count' : 'course-grading-assignment-droppable',
|
||||
'weight' : 'course-grading-assignment-gradeweight'
|
||||
},
|
||||
updateModel : function(event) {
|
||||
// HACK to fix model sometimes losing its pointer to the collection [I think I fixed this but leaving
|
||||
// this in out of paranoia. If this error ever happens, the user will get a warning that they cannot
|
||||
// give 2 assignments the same name.]
|
||||
if (!this.model.collection) {
|
||||
this.model.collection = this.collection;
|
||||
}
|
||||
|
||||
switch (event.currentTarget.id) {
|
||||
case 'course-grading-assignment-totalassignments':
|
||||
this.$el.find('#course-grading-assignment-droppable').attr('max', $(event.currentTarget).val());
|
||||
this.saveIfChanged(event);
|
||||
break;
|
||||
case 'course-grading-assignment-name':
|
||||
var oldName = this.model.get('type');
|
||||
if (this.saveIfChanged(event) && !_.isEmpty(oldName)) {
|
||||
// overload the error display logic
|
||||
this._cacheValidationErrors.push(event.currentTarget);
|
||||
$(event.currentTarget).parent().append(
|
||||
this.errorTemplate({message : 'For grading to work, you must change all "' + oldName +
|
||||
'" subsections to "' + this.model.get('type') + '".'}));
|
||||
}
|
||||
break;
|
||||
default:
|
||||
this.saveIfChanged(event);
|
||||
break;
|
||||
}
|
||||
},
|
||||
deleteModel : function(e) {
|
||||
this.model.destroy(
|
||||
{ error : CMS.ServerError});
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
});
|
||||
77
cms/static/js/views/validating_view.js
Normal file
@@ -0,0 +1,77 @@
|
||||
CMS.Views.ValidatingView = Backbone.View.extend({
|
||||
// Intended as an abstract class which catches validation errors on the model and
|
||||
// decorates the fields. Needs wiring per class, but this initialization shows how
|
||||
// either have your init call this one or copy the contents
|
||||
initialize : function() {
|
||||
this.model.on('error', this.handleValidationError, this);
|
||||
this.selectorToField = _.invert(this.fieldToSelectorMap);
|
||||
},
|
||||
|
||||
errorTemplate : _.template('<span class="message-error"><%= message %></span>'),
|
||||
|
||||
events : {
|
||||
"change input" : "clearValidationErrors",
|
||||
"change textarea" : "clearValidationErrors"
|
||||
},
|
||||
fieldToSelectorMap : {
|
||||
// Your subclass must populate this w/ all of the model keys and dom selectors
|
||||
// which may be the subjects of validation errors
|
||||
},
|
||||
_cacheValidationErrors : [],
|
||||
handleValidationError : function(model, error) {
|
||||
// error triggered either by validation or server error
|
||||
// error is object w/ fields and error strings
|
||||
for (var field in error) {
|
||||
var ele = this.$el.find('#' + this.fieldToSelectorMap[field]);
|
||||
if (ele.length === 0) {
|
||||
// check if it might a server error: note a typo in the field name
|
||||
// or failure to put in a map may cause this to muffle validation errors
|
||||
if (_.has(error, 'error') && _.has(error, 'responseText')) {
|
||||
CMS.ServerError(model, error);
|
||||
return;
|
||||
}
|
||||
else continue;
|
||||
}
|
||||
this._cacheValidationErrors.push(ele);
|
||||
if ($(ele).is('div')) {
|
||||
// put error on the contained inputs
|
||||
$(ele).find('input, textarea').addClass('error');
|
||||
}
|
||||
else $(ele).addClass('error');
|
||||
$(ele).parent().append(this.errorTemplate({message : error[field]}));
|
||||
}
|
||||
},
|
||||
|
||||
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');
|
||||
$(ele).nextAll('.message-error').remove();
|
||||
}
|
||||
},
|
||||
|
||||
saveIfChanged : function(event) {
|
||||
// returns true if the value changed and was thus sent to server
|
||||
var field = this.selectorToField[event.currentTarget.id];
|
||||
var currentVal = this.model.get(field);
|
||||
var newVal = $(event.currentTarget).val();
|
||||
this.clearValidationErrors(); // curr = new if user reverts manually
|
||||
if (currentVal != newVal) {
|
||||
this.model.save(field, newVal);
|
||||
return true;
|
||||
}
|
||||
else return false;
|
||||
},
|
||||
// these should perhaps go into a superclass but lack of event hash inheritance demotivates me
|
||||
inputFocus : function(event) {
|
||||
$("label[for='" + event.currentTarget.id + "']").addClass("is-focused");
|
||||
},
|
||||
inputUnfocus : function(event) {
|
||||
$("label[for='" + event.currentTarget.id + "']").removeClass("is-focused");
|
||||
}
|
||||
});
|
||||
294
cms/static/sass/_account.scss
Normal file
@@ -0,0 +1,294 @@
|
||||
// Studio - Sign In/Up
|
||||
// ====================
|
||||
body.signup, body.signin {
|
||||
|
||||
.wrapper-content {
|
||||
margin: 0;
|
||||
padding: 0 $baseline;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.content {
|
||||
@include clearfix();
|
||||
@include font-size(16);
|
||||
max-width: $fg-max-width;
|
||||
min-width: $fg-min-width;
|
||||
width: flex-grid(12);
|
||||
margin: 0 auto;
|
||||
color: $gray-d2;
|
||||
|
||||
header {
|
||||
position: relative;
|
||||
margin-bottom: $baseline;
|
||||
border-bottom: 1px solid $gray-l4;
|
||||
padding-bottom: ($baseline/2);
|
||||
|
||||
h1 {
|
||||
@include font-size(32);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.action {
|
||||
@include font-size(13);
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 40%;
|
||||
}
|
||||
}
|
||||
|
||||
.introduction {
|
||||
@include font-size(14);
|
||||
margin: 0 0 $baseline 0;
|
||||
}
|
||||
}
|
||||
|
||||
.content-primary, .content-supplementary {
|
||||
@include box-sizing(border-box);
|
||||
float: left;
|
||||
}
|
||||
|
||||
.content-primary {
|
||||
width: flex-grid(8, 12);
|
||||
margin-right: flex-gutter();
|
||||
|
||||
form {
|
||||
@include box-sizing(border-box);
|
||||
@include box-shadow(0 1px 2px $shadow-l1);
|
||||
@include border-radius(2px);
|
||||
width: 100%;
|
||||
border: 1px solid $gray-l2;
|
||||
padding: $baseline ($baseline*1.5);
|
||||
background: $white;
|
||||
|
||||
.form-actions {
|
||||
margin-top: $baseline;
|
||||
|
||||
.action-primary {
|
||||
@include blue-button;
|
||||
@include transition(all .15s);
|
||||
@include font-size(15);
|
||||
display:block;
|
||||
width: 100%;
|
||||
padding: ($baseline*0.75) ($baseline/2);
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
}
|
||||
|
||||
.list-input {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
|
||||
.field {
|
||||
margin: 0 0 ($baseline*0.75) 0;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&.required {
|
||||
|
||||
label {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
label:after {
|
||||
margin-left: ($baseline/4);
|
||||
content: "*";
|
||||
}
|
||||
}
|
||||
|
||||
label, input, textarea {
|
||||
display: block;
|
||||
}
|
||||
|
||||
label {
|
||||
@include font-size(14);
|
||||
@include transition(color, 0.15s, ease-in-out);
|
||||
margin: 0 0 ($baseline/4) 0;
|
||||
|
||||
&.is-focused {
|
||||
color: $blue;
|
||||
}
|
||||
}
|
||||
|
||||
input, textarea {
|
||||
@include font-size(16);
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding: ($baseline/2);
|
||||
|
||||
&.long {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&.short {
|
||||
width: 25%;
|
||||
}
|
||||
|
||||
::-webkit-input-placeholder {
|
||||
color: $gray-l4;
|
||||
}
|
||||
|
||||
:-moz-placeholder {
|
||||
color: $gray-l3;
|
||||
}
|
||||
|
||||
::-moz-placeholder {
|
||||
color: $gray-l3;
|
||||
}
|
||||
|
||||
:-ms-input-placeholder {
|
||||
color: $gray-l3;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
|
||||
+ .tip {
|
||||
color: $gray;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
textarea.long {
|
||||
height: ($baseline*5);
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
display: inline-block;
|
||||
margin-right: ($baseline/4);
|
||||
width: auto;
|
||||
height: auto;
|
||||
|
||||
& + label {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
.tip {
|
||||
@include transition(color, 0.15s, ease-in-out);
|
||||
@include font-size(13);
|
||||
display: block;
|
||||
margin-top: ($baseline/4);
|
||||
color: $gray-l3;
|
||||
}
|
||||
}
|
||||
|
||||
.field-group {
|
||||
@include clearfix();
|
||||
margin: 0 0 ($baseline/2) 0;
|
||||
|
||||
.field {
|
||||
display: block;
|
||||
width: 47%;
|
||||
border-bottom: none;
|
||||
margin: 0 $baseline 0 0;
|
||||
padding-bottom: 0;
|
||||
|
||||
&:nth-child(odd) {
|
||||
float: left;
|
||||
}
|
||||
|
||||
&:nth-child(even) {
|
||||
float: right;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
input, textarea {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content-supplementary {
|
||||
width: flex-grid(4, 12);
|
||||
|
||||
.bit {
|
||||
@include font-size(13);
|
||||
margin: 0 0 $baseline 0;
|
||||
border-bottom: 1px solid $gray-l4;
|
||||
padding: 0 0 $baseline 0;
|
||||
color: $gray-l1;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
border: none;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
h3 {
|
||||
@include font-size(14);
|
||||
margin: 0 0 ($baseline/4) 0;
|
||||
color: $gray-d2;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.signup {
|
||||
|
||||
}
|
||||
|
||||
.signin {
|
||||
|
||||
#field-password {
|
||||
position: relative;
|
||||
|
||||
.action-forgotpassword {
|
||||
@include font-size(13);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ====================
|
||||
|
||||
// messages
|
||||
.message {
|
||||
@include font-size(14);
|
||||
display: block;
|
||||
}
|
||||
|
||||
.message-status {
|
||||
display: none;
|
||||
@include border-top-radius(2px);
|
||||
@include box-sizing(border-box);
|
||||
border-bottom: 2px solid $yellow-d2;
|
||||
margin: 0 0 $baseline 0;
|
||||
padding: ($baseline/2) $baseline;
|
||||
font-weight: 500;
|
||||
background: $yellow-d1;
|
||||
color: $white;
|
||||
|
||||
.ss-icon {
|
||||
position: relative;
|
||||
top: 3px;
|
||||
@include font-size(16);
|
||||
display: inline-block;
|
||||
margin-right: ($baseline/2);
|
||||
}
|
||||
|
||||
.text {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
&.error {
|
||||
border-color: shade($red, 50%);
|
||||
background: tint($red, 20%);
|
||||
}
|
||||
|
||||
&.is-shown {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,100 @@
|
||||
// notifications
|
||||
.wrapper-notification {
|
||||
@include clearfix();
|
||||
@include box-sizing(border-box);
|
||||
@include transition (bottom 2.0s ease-in-out 5s);
|
||||
@include box-shadow(0 -1px 2px rgba(0,0,0,0.1));
|
||||
position: fixed;
|
||||
bottom: -100px;
|
||||
z-index: 1000;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
opacity: 0;
|
||||
border-top: 1px solid $darkGrey;
|
||||
padding: 20px 40px;
|
||||
|
||||
&.is-shown {
|
||||
bottom: 0;
|
||||
opacity: 1.0;
|
||||
}
|
||||
|
||||
&.wrapper-notification-warning {
|
||||
border-color: shade($yellow, 25%);
|
||||
background: tint($yellow, 25%);
|
||||
}
|
||||
|
||||
&.wrapper-notification-error {
|
||||
border-color: shade($red, 50%);
|
||||
background: tint($red, 20%);
|
||||
color: $white;
|
||||
}
|
||||
|
||||
&.wrapper-notification-confirm {
|
||||
border-color: shade($green, 30%);
|
||||
background: tint($green, 40%);
|
||||
color: shade($green, 30%);
|
||||
}
|
||||
}
|
||||
|
||||
.notification {
|
||||
@include box-sizing(border-box);
|
||||
margin: 0 auto;
|
||||
width: flex-grid(12);
|
||||
max-width: $fg-max-width;
|
||||
min-width: $fg-min-width;
|
||||
|
||||
.copy {
|
||||
float: left;
|
||||
width: flex-grid(9, 12);
|
||||
margin-right: flex-gutter();
|
||||
margin-top: 5px;
|
||||
font-size: 14px;
|
||||
|
||||
.icon {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
margin-right: 5px;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
p {
|
||||
width: flex-grid(8, 9);
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
float: right;
|
||||
width: flex-grid(3, 12);
|
||||
margin-top: ($baseline/2);
|
||||
text-align: right;
|
||||
|
||||
li {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin-right: 10px;
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.save-button {
|
||||
@include blue-button;
|
||||
}
|
||||
|
||||
.cancel-button {
|
||||
@include white-button;
|
||||
}
|
||||
}
|
||||
|
||||
strong {
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
// adopted alerts
|
||||
.alert {
|
||||
padding: 15px 20px;
|
||||
margin-bottom: 30px;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
.assets {
|
||||
.uploads {
|
||||
input.asset-search-input {
|
||||
float: left;
|
||||
width: 260px;
|
||||
|
||||
@@ -1,180 +1,507 @@
|
||||
// -------------------------------------
|
||||
//
|
||||
// Universal
|
||||
//
|
||||
// -------------------------------------
|
||||
// studio base styling
|
||||
// ====================
|
||||
|
||||
body {
|
||||
min-width: 980px;
|
||||
background: rgb(240, 241, 245);
|
||||
font-size: 16px;
|
||||
line-height: 1.6;
|
||||
color: $baseFontColor;
|
||||
// basic reset
|
||||
html {
|
||||
font-size: 62.5%;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
body,
|
||||
input {
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
body {
|
||||
@include font-size(16);
|
||||
min-width: 980px;
|
||||
background: $gray-l5;
|
||||
line-height: 1.6;
|
||||
color: $baseFontColor;
|
||||
}
|
||||
|
||||
body, input {
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: $blue;
|
||||
@include transition(color .15s);
|
||||
text-decoration: none;
|
||||
color: $blue;
|
||||
@include transition(color .15s);
|
||||
|
||||
&:hover {
|
||||
color: #cb9c40;
|
||||
}
|
||||
&:hover {
|
||||
color: #cb9c40;
|
||||
}
|
||||
}
|
||||
|
||||
h1 {
|
||||
float: left;
|
||||
font-size: 28px;
|
||||
font-weight: 300;
|
||||
margin: 24px 6px;
|
||||
@include font-size(28);
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.waiting {
|
||||
opacity: 0.1;
|
||||
opacity: 0.1;
|
||||
}
|
||||
|
||||
.page-actions {
|
||||
margin-bottom: 30px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.main-wrapper {
|
||||
.wrapper {
|
||||
@include clearfix();
|
||||
@include box-sizing(border-box);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
// ====================
|
||||
|
||||
// layout - basic page header
|
||||
.wrapper-mast {
|
||||
margin: 0;
|
||||
padding: 0 $baseline;
|
||||
position: relative;
|
||||
|
||||
.mast, .metadata {
|
||||
@include clearfix();
|
||||
@include font-size(16);
|
||||
position: relative;
|
||||
margin: 0 40px;
|
||||
max-width: $fg-max-width;
|
||||
min-width: $fg-min-width;
|
||||
width: flex-grid(12);
|
||||
margin: 0 auto $baseline auto;
|
||||
color: $gray-d2;
|
||||
}
|
||||
|
||||
.mast {
|
||||
border-bottom: 1px solid $gray-l4;
|
||||
padding-bottom: ($baseline/2);
|
||||
|
||||
.title-sub {
|
||||
@include font-size(14);
|
||||
position: relative;
|
||||
top: ($baseline/4);
|
||||
display: block;
|
||||
margin: 0;
|
||||
color: $gray-l2;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.title, .title-1 {
|
||||
@include font-size(32);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-weight: 600;
|
||||
color: $gray-d3;
|
||||
}
|
||||
|
||||
.nav-hierarchy {
|
||||
@include font-size(14);
|
||||
display: block;
|
||||
margin: 0;
|
||||
color: $gray-l2;
|
||||
font-weight: 400;
|
||||
|
||||
.nav-item {
|
||||
display: inline;
|
||||
vertical-align: middle;
|
||||
margin-right: ($baseline/4);
|
||||
|
||||
&:after {
|
||||
content: ">>";
|
||||
margin-left: ($baseline/4);
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
|
||||
&:after {
|
||||
content: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// layout with actions
|
||||
.title {
|
||||
width: flex-grid(12);
|
||||
}
|
||||
|
||||
// layout with actions
|
||||
&.has-actions {
|
||||
@include clearfix();
|
||||
|
||||
.title {
|
||||
float: left;
|
||||
width: flex-grid(6,12);
|
||||
margin-right: flex-gutter();
|
||||
}
|
||||
|
||||
.nav-actions {
|
||||
position: relative;
|
||||
bottom: -($baseline*0.75);
|
||||
float: right;
|
||||
width: flex-grid(6,12);
|
||||
text-align: right;
|
||||
|
||||
.nav-item {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
margin-right: ($baseline/2);
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// buttons
|
||||
.button {
|
||||
padding: ($baseline/4) ($baseline/2) ($baseline/3) ($baseline/2) !important;
|
||||
font-weight: 400 !important;
|
||||
}
|
||||
|
||||
.new-button {
|
||||
font-weight: 700 !important;
|
||||
}
|
||||
|
||||
.view-button {
|
||||
|
||||
font-weight: 700 !important;
|
||||
}
|
||||
|
||||
.upload-button .icon-create {
|
||||
@include font-size(18);
|
||||
margin-top: ($baseline/4);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// layout with actions
|
||||
&.has-subtitle {
|
||||
|
||||
.nav-actions {
|
||||
bottom: -($baseline*1.5);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// page metadata/action bar
|
||||
.metadata {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// layout - basic page content
|
||||
.wrapper-content {
|
||||
margin: 0;
|
||||
padding: 0 $baseline;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.content {
|
||||
@include clearfix();
|
||||
@include font-size(16);
|
||||
max-width: $fg-max-width;
|
||||
min-width: $fg-min-width;
|
||||
width: flex-grid(12);
|
||||
margin: 0 auto;
|
||||
color: $gray-d2;
|
||||
|
||||
header {
|
||||
position: relative;
|
||||
margin-bottom: $baseline;
|
||||
border-bottom: 1px solid $gray-l4;
|
||||
padding-bottom: ($baseline/2);
|
||||
|
||||
.title-sub {
|
||||
@include font-size(14);
|
||||
display: block;
|
||||
margin: 0;
|
||||
color: $gray-l2;
|
||||
}
|
||||
|
||||
.title, .title-1 {
|
||||
@include font-size(32);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-weight: 600;
|
||||
color: $gray-d3;
|
||||
}
|
||||
}
|
||||
|
||||
.introduction {
|
||||
@include box-sizing(border-box);
|
||||
@include font-size(14);
|
||||
width: flex-grid(12);
|
||||
margin: 0 0 $baseline 0;
|
||||
|
||||
.copy strong {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&.has-links {
|
||||
@include clearfix();
|
||||
|
||||
.copy {
|
||||
float: left;
|
||||
width: flex-grid(8,12);
|
||||
margin-right: flex-gutter();
|
||||
}
|
||||
|
||||
.nav-introduction-supplementary {
|
||||
@include font-size(13);
|
||||
float: right;
|
||||
width: flex-grid(4,12);
|
||||
display: block;
|
||||
text-align: right;
|
||||
|
||||
.icon {
|
||||
@include font-size(14);
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin-right: ($baseline/4);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content-primary, .content-supplementary {
|
||||
@include box-sizing(border-box);
|
||||
}
|
||||
|
||||
// layout - primary content
|
||||
.content-primary {
|
||||
|
||||
.title-1, .title-2, .title-3, .title-4, .title-5, .title-5 {
|
||||
color: $gray-d3;
|
||||
}
|
||||
|
||||
.title-1 {
|
||||
|
||||
}
|
||||
|
||||
.title-2 {
|
||||
@include font-size(24);
|
||||
margin: 0 0 ($baseline/2) 0;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.title-3 {
|
||||
@include font-size(16);
|
||||
margin: 0 0 ($baseline/4) 0;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.title-4 {
|
||||
|
||||
}
|
||||
|
||||
.title-5 {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// layout - supplemental content
|
||||
.content-supplementary {
|
||||
|
||||
.bit {
|
||||
@include font-size(13);
|
||||
margin: 0 0 $baseline 0;
|
||||
border-bottom: 1px solid $gray-l4;
|
||||
padding: 0 0 $baseline 0;
|
||||
color: $gray-l1;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
border: none;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
h3 {
|
||||
@include font-size(14);
|
||||
margin: 0 0 ($baseline/4) 0;
|
||||
color: $gray-d2;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0 0 $baseline 0;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-related {
|
||||
|
||||
.nav-item {
|
||||
margin-bottom: ($baseline/4);
|
||||
border-bottom: 1px dotted $gray-l4;
|
||||
padding-bottom: ($baseline/4);
|
||||
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
border: none;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ====================
|
||||
|
||||
// layout - grandfathered
|
||||
.main-wrapper {
|
||||
position: relative;
|
||||
margin: 0 40px;
|
||||
}
|
||||
|
||||
.inner-wrapper {
|
||||
position: relative;
|
||||
max-width: 1280px;
|
||||
margin: auto;
|
||||
position: relative;
|
||||
max-width: 1280px;
|
||||
margin: auto;
|
||||
|
||||
> article {
|
||||
clear: both;
|
||||
}
|
||||
> article {
|
||||
clear: both;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
float: right;
|
||||
width: 28%;
|
||||
float: right;
|
||||
width: 28%;
|
||||
}
|
||||
|
||||
.left {
|
||||
float: left;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.right {
|
||||
float: right;
|
||||
float: right;
|
||||
}
|
||||
|
||||
footer {
|
||||
clear: both;
|
||||
height: 100px;
|
||||
}
|
||||
// ====================
|
||||
|
||||
// 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;
|
||||
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;
|
||||
|
||||
&::-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;
|
||||
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;
|
||||
}
|
||||
&::-webkit-input-placeholder {
|
||||
color: #979faf;
|
||||
}
|
||||
}
|
||||
|
||||
label {
|
||||
font-size: 12px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
code {
|
||||
padding: 0 4px;
|
||||
border-radius: 3px;
|
||||
background: #eee;
|
||||
font-family: Monaco, monospace;
|
||||
padding: 0 4px;
|
||||
border-radius: 3px;
|
||||
background: #eee;
|
||||
font-family: Monaco, monospace;
|
||||
}
|
||||
|
||||
.CodeMirror {
|
||||
font-size: 13px;
|
||||
border: 1px solid $darkGrey;
|
||||
background: #fff;
|
||||
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;
|
||||
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,
|
||||
.new-policy-item {
|
||||
@include grey-button;
|
||||
margin: 5px 8px;
|
||||
padding: 3px 10px 4px 10px;
|
||||
font-size: 10px;
|
||||
@include grey-button;
|
||||
margin: 5px 8px;
|
||||
padding: 3px 10px 4px 10px;
|
||||
font-size: 10px;
|
||||
|
||||
.new-folder-icon,
|
||||
.new-policy-icon,
|
||||
.new-unit-icon {
|
||||
position: relative;
|
||||
top: 2px;
|
||||
}
|
||||
.new-folder-icon,
|
||||
.new-policy-icon,
|
||||
.new-unit-icon {
|
||||
position: relative;
|
||||
top: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.item-actions {
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
right: 5px;
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
right: 5px;
|
||||
|
||||
.edit-button,
|
||||
.delete-button,
|
||||
.visibility-toggle {
|
||||
float: left;
|
||||
margin-right: 13px;
|
||||
color: #a4aab7;
|
||||
}
|
||||
.edit-button,
|
||||
.delete-button,
|
||||
.visibility-toggle {
|
||||
float: left;
|
||||
margin-right: 13px;
|
||||
color: #a4aab7;
|
||||
}
|
||||
}
|
||||
|
||||
// ====================
|
||||
|
||||
// misc
|
||||
hr.divide {
|
||||
@include text-sr();
|
||||
}
|
||||
|
||||
.item-details {
|
||||
@@ -189,81 +516,56 @@ code {
|
||||
}
|
||||
|
||||
.window {
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid $mediumGrey;
|
||||
border-radius: 3px;
|
||||
background: #fff;
|
||||
@include box-shadow(0 1px 1px rgba(0, 0, 0, 0.1));
|
||||
// @include border-radius(3px);
|
||||
// @include box-shadow(0 1px 1px $shadow-l1);
|
||||
// margin-bottom: $baseline;
|
||||
// border: 1px solid $gray-l2;
|
||||
// background: $white;
|
||||
|
||||
.window-contents {
|
||||
padding: 20px;
|
||||
.window-contents {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.header {
|
||||
padding: 6px 14px;
|
||||
border-bottom: 1px solid $mediumGrey;
|
||||
border-radius: 2px 2px 0 0;
|
||||
@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);
|
||||
font-size: 14px;
|
||||
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
font-weight: 700;
|
||||
|
||||
&.inline-label {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.header {
|
||||
padding: 6px 14px;
|
||||
border-bottom: 1px solid $mediumGrey;
|
||||
border-radius: 2px 2px 0 0;
|
||||
@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);
|
||||
font-size: 14px;
|
||||
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.3);
|
||||
.description {
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
font-weight: 400;
|
||||
font-style: italic;
|
||||
line-height: 1.3;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
font-weight: 700;
|
||||
|
||||
&.inline-label {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.description {
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
font-weight: 400;
|
||||
font-style: italic;
|
||||
line-height: 1.3;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.row {
|
||||
margin-bottom: 10px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid #cbd1db;
|
||||
}
|
||||
.row {
|
||||
margin-bottom: 10px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid #cbd1db;
|
||||
}
|
||||
}
|
||||
|
||||
body.hide-wip {
|
||||
.wip, .wip-box {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
// ====================
|
||||
|
||||
body.show-wip {
|
||||
.wip {
|
||||
outline: 1px solid #f00 !important;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.wip-box {
|
||||
@extend .wip;
|
||||
&:after {
|
||||
content: "WIP";
|
||||
font-size: 8px;
|
||||
padding: 2px;
|
||||
background: #f00;
|
||||
color: #fff;
|
||||
@include position(absolute, 0px 0px 0 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.waiting {
|
||||
|
||||
}
|
||||
// system notifications
|
||||
.toast-notification {
|
||||
display: none;
|
||||
position: fixed;
|
||||
@@ -323,59 +625,86 @@ body.show-wip {
|
||||
}
|
||||
|
||||
.waiting {
|
||||
position: relative;
|
||||
position: relative;
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 999998;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: inherit;
|
||||
background: rgba(255, 255, 255, .9);
|
||||
}
|
||||
&:before {
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 999998;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: inherit;
|
||||
background: rgba(255, 255, 255, .9);
|
||||
}
|
||||
|
||||
&:after {
|
||||
content: '';
|
||||
@extend .spinner-icon;
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
margin-left: -10px;
|
||||
margin-top: -10px;
|
||||
z-index: 999999;
|
||||
}
|
||||
&:after {
|
||||
content: '';
|
||||
@extend .spinner-icon;
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
margin-left: -10px;
|
||||
margin-top: -10px;
|
||||
z-index: 999999;
|
||||
}
|
||||
}
|
||||
|
||||
.waiting-inline {
|
||||
&:after {
|
||||
content: '';
|
||||
@extend .spinner-icon;
|
||||
}
|
||||
&:after {
|
||||
content: '';
|
||||
@extend .spinner-icon;
|
||||
}
|
||||
}
|
||||
|
||||
.new-button {
|
||||
@include green-button;
|
||||
font-size: 13px;
|
||||
padding: 8px 20px 10px;
|
||||
text-align: center;
|
||||
@include green-button;
|
||||
@include font-size(13);
|
||||
padding: 8px 20px 10px;
|
||||
text-align: center;
|
||||
|
||||
&.big {
|
||||
display: block;
|
||||
}
|
||||
&.big {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.icon-create {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin-right: ($baseline/4);
|
||||
margin-top: ($baseline/10);
|
||||
line-height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.view-button {
|
||||
@include blue-button;
|
||||
@include font-size(13);
|
||||
text-align: center;
|
||||
|
||||
&.big {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.icon-view {
|
||||
@include font-size(15);
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin-right: ($baseline/2);
|
||||
margin-top: ($baseline/5);
|
||||
line-height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.edit-button.standard,
|
||||
.delete-button.standard {
|
||||
float: left;
|
||||
@include font-size(12);
|
||||
@include white-button;
|
||||
float: left;
|
||||
padding: 3px 10px 4px;
|
||||
margin-left: 7px;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
|
||||
.edit-icon,
|
||||
@@ -386,9 +715,9 @@ body.show-wip {
|
||||
|
||||
.delete-button.standard {
|
||||
|
||||
&:hover {
|
||||
background-color: tint($orange, 75%);
|
||||
}
|
||||
&:hover {
|
||||
background-color: tint($orange, 75%);
|
||||
}
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
@@ -417,4 +746,114 @@ body.show-wip {
|
||||
font-size: 20px;
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
}
|
||||
}
|
||||
|
||||
// ====================
|
||||
|
||||
// basic utility
|
||||
.sr {
|
||||
@include text-sr();
|
||||
}
|
||||
|
||||
.fake-link {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.non-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.wrap {
|
||||
text-wrap: wrap;
|
||||
white-space: pre-wrap;
|
||||
white-space: -moz-pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
// ====================
|
||||
|
||||
// js dependant
|
||||
body.js {
|
||||
|
||||
// lean/simple modal window
|
||||
.content-modal {
|
||||
@include border-bottom-radius(2px);
|
||||
@include box-sizing(border-box);
|
||||
@include box-shadow(0 2px 4px $shadow-d1);
|
||||
position: relative;
|
||||
display: none;
|
||||
width: 700px;
|
||||
overflow: hidden;
|
||||
border: 1px solid $gray-d1;
|
||||
padding: ($baseline);
|
||||
background: $white;
|
||||
|
||||
.action-modal-close {
|
||||
@include transition(top .25s ease-in-out);
|
||||
@include border-bottom-radius(3px);
|
||||
position: absolute;
|
||||
top: -3px;
|
||||
right: $baseline;
|
||||
padding: ($baseline/4) ($baseline/2) 0 ($baseline/2);
|
||||
background: $gray-l3;
|
||||
text-align: center;
|
||||
|
||||
.label {
|
||||
@include text-sr();
|
||||
}
|
||||
|
||||
.ss-icon {
|
||||
@include font-size(18);
|
||||
color: $white;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: $blue;
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
@include box-sizing(border-box);
|
||||
width: 100%;
|
||||
overflow-y: scroll;
|
||||
padding: ($baseline/10);
|
||||
border: 1px solid $gray-l4;
|
||||
}
|
||||
|
||||
.title {
|
||||
@include font-size(18);
|
||||
margin: 0 0 ($baseline/2) 0;
|
||||
font-weight: 600;
|
||||
color: $gray-d3;
|
||||
}
|
||||
|
||||
.description {
|
||||
@include font-size(13);
|
||||
margin-top: ($baseline/2);
|
||||
color: $gray-l1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ====================
|
||||
|
||||
// works in progress
|
||||
body.hide-wip {
|
||||
|
||||
.wip-box {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
// ====================
|
||||
|
||||
// needed fudges for now
|
||||
body.dashboard {
|
||||
|
||||
.my-classes {
|
||||
margin-top: $baseline;
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
&:hover, &.active {
|
||||
@include box-shadow(0 1px 0 rgba(255, 255, 255, .3) inset, 0 1px 1px rgba(0, 0, 0, .15));
|
||||
}
|
||||
}
|
||||
@@ -41,7 +41,7 @@
|
||||
background-color: $blue;
|
||||
color: #fff;
|
||||
|
||||
&:hover {
|
||||
&:hover, &.active {
|
||||
background-color: #62aaf5;
|
||||
color: #fff;
|
||||
}
|
||||
@@ -285,4 +285,11 @@
|
||||
padding: 0;
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
}
|
||||
|
||||
@mixin active {
|
||||
@include linear-gradient(top, rgba(255, 255, 255, .4), rgba(255, 255, 255, 0));
|
||||
background-color: rgba(255, 255, 255, .3);
|
||||
@include box-shadow(0 -1px 0 rgba(0, 0, 0, .2) inset, 0 1px 0 #fff inset);
|
||||
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
@@ -37,6 +37,11 @@
|
||||
padding: 34px 0 42px;
|
||||
border-top: 1px solid #cbd1db;
|
||||
|
||||
&:first-child {
|
||||
padding-top: 0;
|
||||
border: none;
|
||||
}
|
||||
|
||||
&.editing {
|
||||
position: relative;
|
||||
z-index: 1001;
|
||||
|
||||
@@ -1,90 +1,90 @@
|
||||
|
||||
input.courseware-unit-search-input {
|
||||
float: left;
|
||||
width: 260px;
|
||||
background-color: #fff;
|
||||
float: left;
|
||||
width: 260px;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.branch {
|
||||
|
||||
.section-item {
|
||||
@include clearfix();
|
||||
.section-item {
|
||||
@include clearfix();
|
||||
|
||||
.details {
|
||||
display: block;
|
||||
float: left;
|
||||
margin-bottom: 0;
|
||||
width: 650px;
|
||||
}
|
||||
.details {
|
||||
display: block;
|
||||
float: left;
|
||||
margin-bottom: 0;
|
||||
width: 650px;
|
||||
}
|
||||
|
||||
.gradable-status {
|
||||
float: right;
|
||||
position: relative;
|
||||
top: -4px;
|
||||
right: 50px;
|
||||
width: 145px;
|
||||
.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;
|
||||
}
|
||||
.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;
|
||||
.menu-toggle {
|
||||
z-index: 10;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 5px;
|
||||
padding: 5px;
|
||||
color: $mediumGrey;
|
||||
|
||||
&:hover, &.is-active {
|
||||
color: $blue;
|
||||
}
|
||||
}
|
||||
&: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);
|
||||
.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;
|
||||
li {
|
||||
width: 115px;
|
||||
margin-bottom: 3px;
|
||||
padding-bottom: 3px;
|
||||
border-bottom: 1px solid $lightGrey;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
border: none;
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
border: none;
|
||||
|
||||
a {
|
||||
color: $darkGrey;
|
||||
}
|
||||
}
|
||||
}
|
||||
a {
|
||||
color: $darkGrey;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
color: $blue;
|
||||
@@ -127,262 +127,262 @@ input.courseware-unit-search-input {
|
||||
|
||||
|
||||
.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));
|
||||
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;
|
||||
}
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
&.collapsed {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
&.collapsed {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
label {
|
||||
float: left;
|
||||
line-height: 29px;
|
||||
}
|
||||
label {
|
||||
float: left;
|
||||
line-height: 29px;
|
||||
}
|
||||
|
||||
.datepair {
|
||||
float: left;
|
||||
margin-left: 10px;
|
||||
}
|
||||
.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;
|
||||
.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;
|
||||
.published-status {
|
||||
font-size: 12px;
|
||||
margin-right: 15px;
|
||||
|
||||
strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.schedule-button {
|
||||
@include blue-button;
|
||||
}
|
||||
.schedule-button {
|
||||
@include blue-button;
|
||||
}
|
||||
|
||||
.edit-button {
|
||||
@include blue-button;
|
||||
}
|
||||
.edit-button {
|
||||
@include blue-button;
|
||||
}
|
||||
|
||||
.schedule-button,
|
||||
.edit-button {
|
||||
font-size: 11px;
|
||||
padding: 3px 15px 5px;
|
||||
}
|
||||
}
|
||||
.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,
|
||||
.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 .date {
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.datepair .time {
|
||||
width: 65px;
|
||||
}
|
||||
.datepair .time {
|
||||
width: 65px;
|
||||
}
|
||||
|
||||
&.collapsed .subsection-list,
|
||||
.collapsed .subsection-list,
|
||||
.collapsed > ol {
|
||||
display: none !important;
|
||||
}
|
||||
&.collapsed .subsection-list,
|
||||
.collapsed .subsection-list,
|
||||
.collapsed > ol {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
header {
|
||||
min-height: 75px;
|
||||
@include clearfix();
|
||||
header {
|
||||
min-height: 75px;
|
||||
@include clearfix();
|
||||
|
||||
.item-details, .section-published-date {
|
||||
.item-details, .section-published-date {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.item-details {
|
||||
display: inline-block;
|
||||
padding: 20px 0 10px 0;
|
||||
@include clearfix();
|
||||
.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 {
|
||||
float: left;
|
||||
margin-right: 10px;
|
||||
width: 350px;
|
||||
font-size: 19px;
|
||||
font-weight: bold;
|
||||
color: $blue;
|
||||
}
|
||||
|
||||
.section-name-span {
|
||||
cursor: pointer;
|
||||
@include transition(color .15s);
|
||||
.section-name-span {
|
||||
cursor: pointer;
|
||||
@include transition(color .15s);
|
||||
|
||||
&:hover {
|
||||
color: $orange;
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
color: $orange;
|
||||
}
|
||||
}
|
||||
|
||||
.section-name-edit {
|
||||
position: relative;
|
||||
width: 400px;
|
||||
background: $white;
|
||||
.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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
.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;
|
||||
.section-published-date {
|
||||
float: right;
|
||||
width: 265px;
|
||||
margin-right: 220px;
|
||||
@include border-radius(3px);
|
||||
background: $lightGrey;
|
||||
|
||||
.published-status {
|
||||
font-size: 12px;
|
||||
margin-right: 15px;
|
||||
.published-status {
|
||||
font-size: 12px;
|
||||
margin-right: 15px;
|
||||
|
||||
strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.schedule-button {
|
||||
@include blue-button;
|
||||
}
|
||||
.schedule-button {
|
||||
@include blue-button;
|
||||
}
|
||||
|
||||
.edit-button {
|
||||
@include blue-button;
|
||||
}
|
||||
.edit-button {
|
||||
@include blue-button;
|
||||
}
|
||||
|
||||
.schedule-button,
|
||||
.edit-button {
|
||||
font-size: 11px;
|
||||
padding: 3px 15px 5px;
|
||||
|
||||
}
|
||||
}
|
||||
.schedule-button,
|
||||
.edit-button {
|
||||
font-size: 11px;
|
||||
padding: 3px 15px 5px;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.gradable-status {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 70px;
|
||||
width: 145px;
|
||||
.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;
|
||||
}
|
||||
.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;
|
||||
.menu-toggle {
|
||||
z-index: 10;
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 5px;
|
||||
padding: 5px;
|
||||
color: $lightGrey;
|
||||
|
||||
&:hover, &.is-active {
|
||||
color: $blue;
|
||||
}
|
||||
}
|
||||
&: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);
|
||||
.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;
|
||||
li {
|
||||
width: 115px;
|
||||
margin-bottom: 3px;
|
||||
padding-bottom: 3px;
|
||||
border-bottom: 1px solid $lightGrey;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
border: none;
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
border: none;
|
||||
|
||||
a {
|
||||
color: $darkGrey;
|
||||
}
|
||||
}
|
||||
}
|
||||
a {
|
||||
color: $darkGrey;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
a {
|
||||
|
||||
&.is-selected {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
&.is-selected {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// dropdown state
|
||||
&.is-active {
|
||||
// dropdown state
|
||||
&.is-active {
|
||||
|
||||
.menu {
|
||||
z-index: 1000;
|
||||
display: block;
|
||||
opacity: 1.0;
|
||||
}
|
||||
.menu {
|
||||
z-index: 1000;
|
||||
display: block;
|
||||
opacity: 1.0;
|
||||
}
|
||||
|
||||
|
||||
.menu-toggle {
|
||||
@@ -408,256 +408,282 @@ input.courseware-unit-search-input {
|
||||
}
|
||||
}
|
||||
|
||||
.item-actions {
|
||||
margin-top: 21px;
|
||||
margin-right: 12px;
|
||||
.item-actions {
|
||||
margin-top: 21px;
|
||||
margin-right: 12px;
|
||||
|
||||
.edit-button,
|
||||
.delete-button {
|
||||
margin-top: -3px;
|
||||
}
|
||||
}
|
||||
.edit-button,
|
||||
.delete-button {
|
||||
margin-top: -3px;
|
||||
}
|
||||
}
|
||||
|
||||
.expand-collapse-icon {
|
||||
float: left;
|
||||
margin: 29px 6px 16px 16px;
|
||||
@include transition(none);
|
||||
.expand-collapse-icon {
|
||||
float: left;
|
||||
margin: 29px 6px 16px 16px;
|
||||
@include transition(none);
|
||||
|
||||
&.expand {
|
||||
background-position: 0 0;
|
||||
}
|
||||
&.expand {
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
&.collapsed {
|
||||
|
||||
}
|
||||
}
|
||||
&.collapsed {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.drag-handle {
|
||||
margin-left: 11px;
|
||||
}
|
||||
}
|
||||
.drag-handle {
|
||||
margin-left: 11px;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 19px;
|
||||
font-weight: 700;
|
||||
color: $blue;
|
||||
}
|
||||
h3 {
|
||||
font-size: 19px;
|
||||
font-weight: 700;
|
||||
color: $blue;
|
||||
}
|
||||
|
||||
.section-name-span {
|
||||
cursor: pointer;
|
||||
@include transition(color .15s);
|
||||
.section-name-span {
|
||||
cursor: pointer;
|
||||
@include transition(color .15s);
|
||||
|
||||
&:hover {
|
||||
color: $orange;
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
color: $orange;
|
||||
}
|
||||
}
|
||||
|
||||
.section-name-form {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.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;
|
||||
}
|
||||
.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;
|
||||
}
|
||||
}
|
||||
.cancel-button {
|
||||
@include white-button;
|
||||
padding: 7px 20px 7px;
|
||||
}
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 12px;
|
||||
color: #878e9d;
|
||||
h4 {
|
||||
font-size: 12px;
|
||||
color: #878e9d;
|
||||
|
||||
strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
.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;
|
||||
.subsection-list {
|
||||
margin: 0 12px;
|
||||
|
||||
> ol {
|
||||
@include tree-view;
|
||||
border-top-width: 0;
|
||||
}
|
||||
}
|
||||
> ol {
|
||||
@include tree-view;
|
||||
border-top-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.new-section {
|
||||
header {
|
||||
height: auto;
|
||||
@include clearfix();
|
||||
}
|
||||
&.new-section {
|
||||
|
||||
.expand-collapse-icon {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
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;
|
||||
display: none;
|
||||
position: relative;
|
||||
float: right;
|
||||
margin-top: 10px;
|
||||
|
||||
font-size: 13px;
|
||||
color: $darkGrey;
|
||||
font-size: 13px;
|
||||
color: $darkGrey;
|
||||
|
||||
&.is-shown {
|
||||
display: block;
|
||||
}
|
||||
&.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;
|
||||
}
|
||||
.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;
|
||||
}
|
||||
.label {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
.new-section-name,
|
||||
.new-subsection-name-input {
|
||||
width: 515px;
|
||||
width: 515px;
|
||||
}
|
||||
|
||||
.new-section-name-save,
|
||||
.new-subsection-name-save {
|
||||
@include blue-button;
|
||||
padding: 4px 20px 7px;
|
||||
margin: 0 5px;
|
||||
color: #fff !important;
|
||||
@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;
|
||||
@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));
|
||||
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;
|
||||
padding: 20px 40px;
|
||||
|
||||
label {
|
||||
display: block;
|
||||
}
|
||||
label {
|
||||
display: block;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
font-size: 20px;
|
||||
}
|
||||
input {
|
||||
width: 100%;
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.preview {
|
||||
background: url(../img/preview.jpg) center top no-repeat;
|
||||
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;
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 100px;
|
||||
left: 50%;
|
||||
z-index: 99999;
|
||||
width: 600px;
|
||||
margin-left: -300px;
|
||||
background: #fff;
|
||||
text-align: center;
|
||||
|
||||
.settings {
|
||||
padding: 40px;
|
||||
}
|
||||
.settings {
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 34px;
|
||||
font-weight: 300;
|
||||
}
|
||||
h3 {
|
||||
font-size: 34px;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.picker {
|
||||
margin: 30px 0 65px;
|
||||
}
|
||||
.picker {
|
||||
margin: 30px 0 65px;
|
||||
}
|
||||
|
||||
.description {
|
||||
margin-top: 30px;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
}
|
||||
.description {
|
||||
margin-top: 30px;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
strong {
|
||||
font-weight: 700;
|
||||
}
|
||||
strong {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.start-date,
|
||||
.start-time {
|
||||
font-size: 19px;
|
||||
}
|
||||
.start-date,
|
||||
.start-time {
|
||||
font-size: 19px;
|
||||
}
|
||||
|
||||
.save-button {
|
||||
@include blue-button;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.save-button {
|
||||
@include blue-button;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.cancel-button {
|
||||
@include white-button;
|
||||
}
|
||||
.cancel-button {
|
||||
@include white-button;
|
||||
}
|
||||
|
||||
.save-button,
|
||||
.cancel-button {
|
||||
font-size: 16px;
|
||||
}
|
||||
.save-button,
|
||||
.cancel-button {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.collapse-all-button {
|
||||
float: right;
|
||||
margin-top: 10px;
|
||||
font-size: 13px;
|
||||
color: $darkGrey;
|
||||
float: right;
|
||||
margin-top: 10px;
|
||||
font-size: 13px;
|
||||
color: $darkGrey;
|
||||
}
|
||||
|
||||
// sort/drag and drop
|
||||
.ui-droppable {
|
||||
min-height: 20px;
|
||||
@include transition (padding 0.5s ease-in-out 0s);
|
||||
min-height: 20px;
|
||||
padding: 0;
|
||||
|
||||
&.dropover {
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
&.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;
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -6,19 +6,27 @@
|
||||
@include box-shadow(0 1px 2px rgba(0, 0, 0, .1));
|
||||
|
||||
li {
|
||||
position: relative;
|
||||
border-bottom: 1px solid $mediumGrey;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
padding: 20px 25px;
|
||||
line-height: 1.3;
|
||||
|
||||
&:hover {
|
||||
background: $paleYellow;
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +42,22 @@
|
||||
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 {
|
||||
|
||||
78
cms/static/sass/_extends.scss
Normal file
@@ -0,0 +1,78 @@
|
||||
.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;
|
||||
}
|
||||
48
cms/static/sass/_footer.scss
Normal file
@@ -0,0 +1,48 @@
|
||||
//studio global footer
|
||||
.wrapper-footer {
|
||||
margin: ($baseline*1.5) 0 $baseline 0;
|
||||
padding: $baseline;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
||||
footer.primary {
|
||||
@include clearfix();
|
||||
@include font-size(13);
|
||||
max-width: $fg-max-width;
|
||||
min-width: $fg-min-width;
|
||||
width: flex-grid(12);
|
||||
margin: 0 auto;
|
||||
padding-top: $baseline;
|
||||
border-top: 1px solid $gray-l4;
|
||||
color: $gray-l2;
|
||||
|
||||
.colophon {
|
||||
width: flex-grid(4, 12);
|
||||
float: left;
|
||||
margin-right: flex-gutter(2);
|
||||
}
|
||||
|
||||
.nav-peripheral {
|
||||
width: flex-grid(6, 12);
|
||||
float: right;
|
||||
text-align: right;
|
||||
|
||||
.nav-item {
|
||||
display: inline-block;
|
||||
margin-right: ($baseline/2);
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
color: $gray-l1;
|
||||
|
||||
&:hover, &:active {
|
||||
color: $blue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,109 +1,562 @@
|
||||
body.no-header {
|
||||
.primary-header {
|
||||
display: none;
|
||||
}
|
||||
// studio global header and navigation
|
||||
// ====================
|
||||
|
||||
.wrapper-header {
|
||||
margin: 0 0 ($baseline*1.5) 0;
|
||||
padding: $baseline;
|
||||
border-bottom: 1px solid $gray;
|
||||
@include box-shadow(0 1px 5px 0 rgba(0,0,0, 0.1));
|
||||
background: $white;
|
||||
height: 76px;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
z-index: 10;
|
||||
|
||||
a {
|
||||
color: $baseFontColor;
|
||||
display: block;
|
||||
|
||||
&:hover, &:active {
|
||||
color: $blue;
|
||||
}
|
||||
}
|
||||
|
||||
header.primary {
|
||||
@include clearfix();
|
||||
max-width: $fg-max-width;
|
||||
min-width: $fg-min-width;
|
||||
width: flex-grid(12);
|
||||
margin: 0 auto;
|
||||
color: $gray-l1;
|
||||
}
|
||||
|
||||
nav .nav-item {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin active {
|
||||
@include linear-gradient(top, rgba(255, 255, 255, .4), rgba(255, 255, 255, 0));
|
||||
background-color: rgba(255, 255, 255, .3);
|
||||
@include box-shadow(0 -1px 0 rgba(0, 0, 0, .2) inset, 0 1px 0 #fff inset);
|
||||
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
|
||||
// ====================
|
||||
|
||||
// basic layout
|
||||
.wrapper-left, .wrapper-right {
|
||||
@include box-sizing(border-box);
|
||||
}
|
||||
|
||||
.primary-header {
|
||||
width: 100%;
|
||||
margin-bottom: 30px;
|
||||
.wrapper-left {
|
||||
width: flex-grid(10, 12);
|
||||
float: left;
|
||||
margin-right: flex-gutter();
|
||||
}
|
||||
|
||||
&.active-tab-courseware #courseware-tab {
|
||||
@include active;
|
||||
}
|
||||
.wrapper-right {
|
||||
width: flex-grid(2, 12);
|
||||
float: right;
|
||||
}
|
||||
|
||||
&.active-tab-assets #assets-tab {
|
||||
@include active;
|
||||
}
|
||||
|
||||
&.active-tab-pages #pages-tab {
|
||||
@include active;
|
||||
}
|
||||
// ====================
|
||||
|
||||
&.active-tab-users #users-tab {
|
||||
@include active;
|
||||
}
|
||||
// specific elements - branding
|
||||
.branding, .info-course, .nav-course, .nav-account, .nav-unauth, .nav-pitch {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
&.active-tab-settings #settings-tab {
|
||||
@include active;
|
||||
}
|
||||
.branding {
|
||||
position: relative;
|
||||
margin: 0 ($baseline/2) 0 0;
|
||||
padding-right: ($baseline*0.75);
|
||||
|
||||
&.active-tab-import #import-tab {
|
||||
@include active;
|
||||
}
|
||||
a {
|
||||
@include text-hide();
|
||||
display: block;
|
||||
width: 164px;
|
||||
height: 32px;
|
||||
background: transparent url('../img/logo-edx-studio.png') 0 0 no-repeat;
|
||||
}
|
||||
}
|
||||
|
||||
&.active-tab-export #export-tab {
|
||||
@include active;
|
||||
}
|
||||
// ====================
|
||||
|
||||
.drop-icon {
|
||||
margin-left: 5px;
|
||||
font-size: 11px;
|
||||
}
|
||||
// specific elements - course name/info
|
||||
.info-course {
|
||||
@include font-size(14);
|
||||
position: relative;
|
||||
margin: -3px ($baseline/2) 0 0;
|
||||
padding-right: ($baseline*0.75);
|
||||
|
||||
.settings-icon {
|
||||
font-size: 18px;
|
||||
line-height: 18px;
|
||||
}
|
||||
&:before {
|
||||
@extend .faded-vertical-divider;
|
||||
content: "";
|
||||
display: block;
|
||||
height: 50px;
|
||||
position: absolute;
|
||||
right: 1px;
|
||||
top: -8px;
|
||||
width: 1px;
|
||||
}
|
||||
|
||||
.class-nav-bar {
|
||||
clear: both;
|
||||
@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);
|
||||
}
|
||||
&:after {
|
||||
@extend .faded-vertical-divider-light;
|
||||
content: "";
|
||||
display: block;
|
||||
height: 50px;
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
top: -12px;
|
||||
width: 1px;
|
||||
}
|
||||
|
||||
.class-nav {
|
||||
@include clearfix;
|
||||
.course-org {
|
||||
margin-right: ($baseline/4);
|
||||
}
|
||||
|
||||
a {
|
||||
float: left;
|
||||
display: inline-block;
|
||||
padding: 15px 25px 17px;
|
||||
font-size: 15px;
|
||||
color: #3c3c3c;
|
||||
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.3);
|
||||
.course-number, .course-org {
|
||||
@include font-size(12);
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@include linear-gradient(top, rgba(255, 255, 255, .2), rgba(255, 255, 255, 0));
|
||||
}
|
||||
}
|
||||
.course-title {
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-width: 220px;
|
||||
overflow: hidden;
|
||||
margin-top: -4px;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
@include font-size(16);
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
li {
|
||||
float: left;
|
||||
}
|
||||
}
|
||||
// ====================
|
||||
|
||||
.class {
|
||||
@include clearfix;
|
||||
height: 100%;
|
||||
font-size: 12px;
|
||||
color: rgb(163, 171, 184);
|
||||
@include linear-gradient(top, rgba(255, 255, 255, 0), rgba(255, 255, 255, .1));
|
||||
background-color: rgb(47, 53, 63);
|
||||
// specific elements - course nav
|
||||
.nav-course {
|
||||
width: 335px;
|
||||
margin-top: -($baseline/4);
|
||||
@include font-size(14);
|
||||
|
||||
a {
|
||||
display: inline-block;
|
||||
height: 20px;
|
||||
padding: 5px 10px 6px;
|
||||
color: rgb(163, 171, 184);
|
||||
}
|
||||
> ol > .nav-item {
|
||||
vertical-align: bottom;
|
||||
margin: 0 ($baseline/2) 0 0;
|
||||
|
||||
.home {
|
||||
position: relative;
|
||||
top: 1px;
|
||||
}
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.log-out {
|
||||
position: relative;
|
||||
top: 3px;
|
||||
}
|
||||
}
|
||||
.title {
|
||||
display: block;
|
||||
padding: 5px;
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
color: $gray-d3;
|
||||
|
||||
.label-prefix {
|
||||
display: block;
|
||||
@include font-size(11);
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
|
||||
// specific nav items
|
||||
&.nav-course-courseware {
|
||||
}
|
||||
|
||||
&.nav-course-settings {
|
||||
}
|
||||
|
||||
&.nav-course-tools {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ====================
|
||||
|
||||
// specific elements - account-based nav
|
||||
.nav-account {
|
||||
width: 100%;
|
||||
margin-top: ($baseline*0.75);
|
||||
@include font-size(14);
|
||||
text-align: right;
|
||||
|
||||
.nav-account-username {
|
||||
width: 100%;
|
||||
|
||||
.icon-user {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin-right: 3px;
|
||||
@include font-size(12);
|
||||
}
|
||||
|
||||
.account-username {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
width: 80%;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.icon-expand {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ====================
|
||||
|
||||
// UI - dropdown
|
||||
.nav-dropdown {
|
||||
|
||||
.nav-item {
|
||||
position: relative;
|
||||
|
||||
.icon-expand {
|
||||
@include font-size(14);
|
||||
@include transition (color 0.5s ease-in-out, opacity 0.5s ease-in-out);
|
||||
display: inline-block;
|
||||
margin-left: 2px;
|
||||
opacity: 0.5;
|
||||
color: $gray-l2;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
|
||||
.icon-expand {
|
||||
color: $blue;
|
||||
opacity: 1.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.wrapper-nav-sub {
|
||||
position: absolute;
|
||||
left: -7px;
|
||||
top: 47px;
|
||||
width: 140px;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.nav-sub {
|
||||
@include border-radius(2px);
|
||||
@include box-sizing(border-box);
|
||||
@include box-shadow(0 1px 5px 0 rgba(0,0,0, 0.1));
|
||||
position: relative;
|
||||
width: 100%;
|
||||
border: 1px solid $gray-l2;
|
||||
padding: ($baseline/4) ($baseline/2);
|
||||
background: $white;
|
||||
|
||||
&:after, &:before {
|
||||
bottom: 100%;
|
||||
border: solid transparent;
|
||||
content: " ";
|
||||
height: 0;
|
||||
width: 0;
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&:after {
|
||||
border-color: rgba(255, 255, 255, 0);
|
||||
border-bottom-color: #fff;
|
||||
border-width: 5px;
|
||||
right: 3px;
|
||||
margin-left: -5px;
|
||||
}
|
||||
|
||||
&:before {
|
||||
border-color: rgba(178, 178, 178, 0);
|
||||
border-bottom-color: $gray-l2;
|
||||
border-width: 6px;
|
||||
right: 3px;
|
||||
margin-left: -6px;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
display: block;
|
||||
margin: 0 0 ($baseline/4) 0;
|
||||
border-bottom: 1px solid $gray-l5;
|
||||
padding: 0 0($baseline/4) 0;
|
||||
@include font-size(13);
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
border-bottom: none;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
a {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// UI - dropdown - specific navs
|
||||
&.nav-account {
|
||||
|
||||
.wrapper-nav-sub {
|
||||
top: 27px;
|
||||
left: auto;
|
||||
right: -13px;
|
||||
width: 110px;
|
||||
}
|
||||
|
||||
.nav-sub {
|
||||
text-align: left;
|
||||
|
||||
.icon-expand {
|
||||
top: -2px;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-sub:after {
|
||||
left: auto;
|
||||
right: 11px;
|
||||
}
|
||||
|
||||
.nav-sub:before {
|
||||
left: auto;
|
||||
right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
&.nav-course {
|
||||
|
||||
.nav-course-courseware {
|
||||
|
||||
.nav-sub:after {
|
||||
left: 88px;
|
||||
}
|
||||
|
||||
.nav-sub:before {
|
||||
left: 88px;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-course-settings {
|
||||
|
||||
.nav-sub:after {
|
||||
left: 88px;
|
||||
}
|
||||
|
||||
.nav-sub:before {
|
||||
left: 88px;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-course-tools {
|
||||
|
||||
.wrapper-nav-sub {
|
||||
top: ($baseline*1.5);
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.nav-sub:after {
|
||||
left: 68px;
|
||||
}
|
||||
|
||||
.nav-sub:before {
|
||||
left: 68px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ====================
|
||||
|
||||
// STATE: is-signed in
|
||||
.is-signedin {
|
||||
|
||||
&.course .branding {
|
||||
|
||||
&:before {
|
||||
@extend .faded-vertical-divider;
|
||||
content: "";
|
||||
display: block;
|
||||
height: 50px;
|
||||
position: absolute;
|
||||
right: 1px;
|
||||
top: -8px;
|
||||
width: 1px;
|
||||
}
|
||||
|
||||
&:after {
|
||||
@extend .faded-vertical-divider-light;
|
||||
content: "";
|
||||
display: block;
|
||||
height: 50px;
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
top: -12px;
|
||||
width: 1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ====================
|
||||
|
||||
// STATE: not signed in
|
||||
.not-signedin {
|
||||
|
||||
.wrapper-left {
|
||||
width: flex-grid(4, 12);
|
||||
}
|
||||
|
||||
.wrapper-right {
|
||||
width: flex-grid(8, 12);
|
||||
}
|
||||
|
||||
// STATE: not signed in - unauthenticated nav
|
||||
.nav-not-signedin {
|
||||
float: right;
|
||||
margin-top: ($baseline/4);
|
||||
|
||||
.nav-item {
|
||||
@include font-size(16);
|
||||
vertical-align: middle;
|
||||
margin: 0 $baseline 0 0;
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.action {
|
||||
margin-top: -($baseline/4);
|
||||
display: inline-block;
|
||||
padding: ($baseline/4) ($baseline/2);
|
||||
}
|
||||
}
|
||||
|
||||
// STATE: not signed in - specific items
|
||||
.nav-not-signedin-help {
|
||||
|
||||
}
|
||||
|
||||
.nav-not-signedin-signup {
|
||||
margin-right: ($baseline/2);
|
||||
|
||||
.action-signup {
|
||||
@include blue-button;
|
||||
@include transition(all .15s);
|
||||
@include font-size(14);
|
||||
padding: ($baseline/4) ($baseline/2);
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-not-signedin-signin {
|
||||
|
||||
.action-signin {
|
||||
@include white-button;
|
||||
@include transition(all .15s);
|
||||
@include font-size(14);
|
||||
padding: ($baseline/4) ($baseline/2);
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ====================
|
||||
|
||||
// STATE: active/current nav states
|
||||
|
||||
.nav-item.is-current,
|
||||
body.howitworks .nav-not-signedin-hiw,
|
||||
|
||||
// dashboard
|
||||
body.dashboard .nav-account-dashboard,
|
||||
|
||||
// course content
|
||||
body.course.outline .nav-course-courseware .title,
|
||||
body.course.updates .nav-course-courseware .title,
|
||||
body.course.pages .nav-course-courseware .title,
|
||||
body.course.uploads .nav-course-courseware .title,
|
||||
|
||||
body.course.outline .nav-course-courseware-outline,
|
||||
body.course.updates .nav-course-courseware-updates,
|
||||
body.course.pages .nav-course-courseware-pages,
|
||||
body.course.uploads .nav-course-courseware-uploads,
|
||||
|
||||
// course settings
|
||||
body.course.schedule .nav-course-settings .title,
|
||||
body.course.grading .nav-course-settings .title,
|
||||
body.course.team .nav-course-settings .title,
|
||||
body.course.advanced .nav-course-settings .title,
|
||||
|
||||
body.course.schedule .nav-course-settings-schedule,
|
||||
body.course.grading .nav-course-settings-grading,
|
||||
body.course.team .nav-course-settings-team,
|
||||
body.course.advanced .nav-course-settings-advanced,
|
||||
|
||||
// course tools
|
||||
body.course.import .nav-course-tools .title,
|
||||
body.course.export .nav-course-tools .title,
|
||||
|
||||
body.course.import .nav-course-tools-import,
|
||||
body.course.export .nav-course-tools-export,
|
||||
|
||||
{
|
||||
|
||||
color: $blue;
|
||||
|
||||
a {
|
||||
color: $blue;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
body.signup .nav-not-signedin-signin {
|
||||
|
||||
a {
|
||||
background-color: #d9e3ee;
|
||||
color: #6d788b;
|
||||
}
|
||||
}
|
||||
|
||||
body.signin .nav-not-signedin-signup {
|
||||
|
||||
a {
|
||||
background-color: #62aaf5;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
// ====================
|
||||
|
||||
// STATE: js enabled
|
||||
.js {
|
||||
|
||||
.nav-dropdown {
|
||||
|
||||
.nav-item .title {
|
||||
outline: 0;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover, &:active, &.is-selected {
|
||||
color: $blue;
|
||||
|
||||
.icon-expand {
|
||||
color: $blue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.wrapper-nav-sub {
|
||||
@include transition (opacity 1.0s ease-in-out 0s);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
|
||||
&.is-shown {
|
||||
opacity: 1.0;
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,80 +1,353 @@
|
||||
body.index {
|
||||
> header {
|
||||
display: none;
|
||||
}
|
||||
// how it works/not signed in index
|
||||
.index {
|
||||
|
||||
> h1 {
|
||||
font-weight: 300;
|
||||
color: lighten($dark-blue, 40%);
|
||||
text-shadow: 0 1px 0 #fff;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
max-width: 600px;
|
||||
text-align: center;
|
||||
margin: 80px auto 30px;
|
||||
}
|
||||
&.not-signedin {
|
||||
|
||||
section.main-container {
|
||||
border-right: 3px;
|
||||
background: #FFF;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
display: block;
|
||||
@include box-sizing(border-box);
|
||||
border: 1px solid lighten( $dark-blue , 30% );
|
||||
@include border-radius(3px);
|
||||
overflow: hidden;
|
||||
@include bounce-in-animation(.8s);
|
||||
.wrapper-header {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
header {
|
||||
border-bottom: 1px solid lighten($dark-blue, 50%);
|
||||
@include linear-gradient(#fff, lighten($dark-blue, 62%));
|
||||
@include clearfix();
|
||||
@include box-shadow( 0 2px 0 $light-blue, inset 0 -1px 0 #fff);
|
||||
text-shadow: 0 1px 0 #fff;
|
||||
.wrapper-footer {
|
||||
margin: 0;
|
||||
border-top: 2px solid $gray-l3;
|
||||
|
||||
h1 {
|
||||
font-size: 14px;
|
||||
padding: 8px 20px;
|
||||
float: left;
|
||||
color: $dark-blue;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
a {
|
||||
float: right;
|
||||
padding: 8px 20px;
|
||||
border-left: 1px solid lighten($dark-blue, 50%);
|
||||
@include box-shadow( inset -1px 0 0 #fff);
|
||||
font-weight: bold;
|
||||
font-size: 22px;
|
||||
line-height: 1;
|
||||
color: $dark-blue;
|
||||
footer.primary {
|
||||
border: none;
|
||||
margin-top: 0;
|
||||
padding-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
ol {
|
||||
list-style: none;
|
||||
.wrapper-content-header, .wrapper-content-features, .wrapper-content-cta {
|
||||
@include box-sizing(border-box);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
padding: 0 $baseline;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
li {
|
||||
border-bottom: 1px solid lighten($dark-blue, 50%);
|
||||
.content {
|
||||
@include clearfix();
|
||||
@include font-size(16);
|
||||
max-width: $fg-max-width;
|
||||
min-width: $fg-min-width;
|
||||
width: flex-grid(12);
|
||||
margin: 0 auto;
|
||||
color: $gray-d2;
|
||||
|
||||
header {
|
||||
border: none;
|
||||
padding-bottom: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
a {
|
||||
display: block;
|
||||
padding: 10px 20px;
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
color: $gray-d3;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: $dark-blue;
|
||||
background: lighten($yellow, 10%);
|
||||
text-shadow: 0 1px 0 #fff;
|
||||
h2 {
|
||||
|
||||
}
|
||||
|
||||
h3 {
|
||||
|
||||
}
|
||||
|
||||
h4 {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// welcome content
|
||||
.wrapper-content-header {
|
||||
@include linear-gradient($blue-l1,$blue,$blue-d1);
|
||||
padding-bottom: ($baseline*4);
|
||||
padding-top: ($baseline*4);
|
||||
}
|
||||
|
||||
.content-header {
|
||||
position: relative;
|
||||
text-align: center;
|
||||
color: $white;
|
||||
|
||||
h1 {
|
||||
@include font-size(52);
|
||||
float: none;
|
||||
margin: 0 0 ($baseline/2) 0;
|
||||
border-bottom: 1px solid $blue-l1;
|
||||
padding: 0;
|
||||
font-weight: 500;
|
||||
color: $white;
|
||||
}
|
||||
|
||||
.logo {
|
||||
@include text-hide();
|
||||
position: relative;
|
||||
top: 3px;
|
||||
display: inline-block;
|
||||
vertical-align: baseline;
|
||||
width: 282px;
|
||||
height: 57px;
|
||||
background: transparent url('../img/logo-edx-studio-white.png') 0 0 no-repeat;
|
||||
}
|
||||
|
||||
.tagline {
|
||||
@include font-size(24);
|
||||
margin: 0;
|
||||
color: $blue-l3;
|
||||
}
|
||||
}
|
||||
|
||||
.arrow_box {
|
||||
position: relative;
|
||||
background: #fff;
|
||||
border: 4px solid #000;
|
||||
}
|
||||
.arrow_box:after, .arrow_box:before {
|
||||
top: 100%;
|
||||
border: solid transparent;
|
||||
content: " ";
|
||||
height: 0;
|
||||
width: 0;
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.arrow_box:after {
|
||||
border-color: rgba(255, 255, 255, 0);
|
||||
border-top-color: #fff;
|
||||
border-width: 30px;
|
||||
left: 50%;
|
||||
margin-left: -30px;
|
||||
}
|
||||
.arrow_box:before {
|
||||
border-color: rgba(0, 0, 0, 0);
|
||||
border-top-color: #000;
|
||||
border-width: 36px;
|
||||
left: 50%;
|
||||
margin-left: -36px;
|
||||
}
|
||||
|
||||
// feature content
|
||||
.wrapper-content-features {
|
||||
@include box-shadow(0 -1px ($baseline/4) $shadow);
|
||||
padding-bottom: ($baseline*2);
|
||||
padding-top: ($baseline*3);
|
||||
background: $white;
|
||||
}
|
||||
|
||||
.content-features {
|
||||
|
||||
.list-features {
|
||||
|
||||
}
|
||||
|
||||
// indiv features
|
||||
.feature {
|
||||
@include clearfix();
|
||||
margin: 0 0 ($baseline*2) 0;
|
||||
border-bottom: 1px solid $gray-l4;
|
||||
padding: 0 0 ($baseline*2) 0;
|
||||
|
||||
.img {
|
||||
@include box-sizing(border-box);
|
||||
float: left;
|
||||
width: flex-grid(3, 12);
|
||||
margin-right: flex-gutter();
|
||||
|
||||
a {
|
||||
@include box-sizing(border-box);
|
||||
@include box-shadow(0 1px ($baseline/10) $shadow-l1);
|
||||
position: relative;
|
||||
top: 0;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
border: 1px solid $gray-l3;
|
||||
padding: ($baseline/4);
|
||||
background: $white;
|
||||
|
||||
.action-zoom {
|
||||
@include transition(bottom .50s ease-in-out);
|
||||
position: absolute;
|
||||
bottom: -30px;
|
||||
right: ($baseline/2);
|
||||
opacity: 0;
|
||||
|
||||
.ss-icon {
|
||||
@include font-size(18);
|
||||
@include border-top-radius(3px);
|
||||
display: inline-block;
|
||||
padding: ($baseline/4) ($baseline/2);
|
||||
background: $blue;
|
||||
color: $white;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: $blue;
|
||||
|
||||
.action-zoom {
|
||||
opacity: 1.0;
|
||||
bottom: -2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.copy {
|
||||
float: left;
|
||||
width: flex-grid(9, 12);
|
||||
margin-top: -($baseline/4);
|
||||
|
||||
h3 {
|
||||
margin: 0 0 ($baseline/2) 0;
|
||||
@include font-size(24);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
> p {
|
||||
@include font-size(18);
|
||||
color: $gray-d1;
|
||||
}
|
||||
|
||||
strong {
|
||||
color: $gray-d2;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.list-proofpoints {
|
||||
@include clearfix();
|
||||
@include font-size(14);
|
||||
width: flex-grid(9, 9);
|
||||
margin: ($baseline*1.5) 0 0 0;
|
||||
|
||||
.proofpoint {
|
||||
@include box-sizing(border-box);
|
||||
@include border-radius(($baseline/4));
|
||||
@include transition(color .50s ease-in-out);
|
||||
position: relative;
|
||||
top: 0;
|
||||
float: left;
|
||||
width: flex-grid(3, 9);
|
||||
min-height: ($baseline*8);
|
||||
margin-right: flex-gutter();
|
||||
padding: ($baseline*0.75) $baseline;
|
||||
color: $gray-l1;
|
||||
|
||||
.title {
|
||||
@include font-size(16);
|
||||
margin: 0 0 ($baseline/4) 0;
|
||||
font-weight: 500;
|
||||
color: $gray-d3;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@include box-shadow(0 1px ($baseline/10) $shadow-l1);
|
||||
background: $blue-l5;
|
||||
top: -($baseline/5);
|
||||
|
||||
.title {
|
||||
color: $blue;
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
margin-bottom: 0;
|
||||
border: none;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
&:nth-child(even) {
|
||||
|
||||
.img {
|
||||
float: right;
|
||||
margin-right: 0;
|
||||
margin-left: flex-gutter();
|
||||
}
|
||||
|
||||
.copy {
|
||||
float: right;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.list-proofpoints {
|
||||
|
||||
.proofpoint {
|
||||
float: right;
|
||||
width: flex-grid(3, 9);
|
||||
margin-left: flex-gutter();
|
||||
margin-right: 0;
|
||||
|
||||
&:last-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// call to action content
|
||||
.wrapper-content-cta {
|
||||
padding-bottom: ($baseline*2);
|
||||
padding-top: ($baseline*2);
|
||||
background: $white;
|
||||
}
|
||||
|
||||
.content-cta {
|
||||
border-top: 1px solid $gray-l4;
|
||||
|
||||
header {
|
||||
border: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.list-actions {
|
||||
position: relative;
|
||||
margin-top: -($baseline*1.5);
|
||||
|
||||
li {
|
||||
width: flex-grid(6, 12);
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.action {
|
||||
display: block;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.action-primary {
|
||||
@include blue-button;
|
||||
@include transition(all .15s);
|
||||
@include font-size(18);
|
||||
padding: ($baseline*0.75) ($baseline/2);
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.action-secondary {
|
||||
@include font-size(14);
|
||||
margin-top: ($baseline/2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -54,4 +54,16 @@
|
||||
@include white-button;
|
||||
margin-top: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
// lean modal alternative
|
||||
#lean_overlay {
|
||||
position: fixed;
|
||||
z-index: 10000;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
display: none;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background: $black;
|
||||
}
|
||||
@@ -54,4 +54,118 @@ del {
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
.ui-corner-all, .ui-corner-bottom, .ui-corner-left, ui-corner-top, .ui-corner-br, .ui-corner-right {
|
||||
border-bottom-right-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
border-top-left-radius: 0;
|
||||
}
|
||||
|
||||
|
||||
.ui-widget-content {
|
||||
border: 0;
|
||||
background: none;
|
||||
}
|
||||
.ui-widget {
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.ui-widget-header {
|
||||
border:none;
|
||||
background: none;
|
||||
}
|
||||
|
||||
.ui-tabs .ui-tabs-nav {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.ui-tabs .ui-tabs-nav li {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: none;
|
||||
top: 0;
|
||||
margin: 0;
|
||||
float: none;
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
|
||||
.ui-tabs-nav {
|
||||
|
||||
li {
|
||||
top: 0;
|
||||
margin: 0;
|
||||
}
|
||||
a {
|
||||
float: none;
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
|
||||
.ui-tabs .ui-tabs-panel {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* reapplying the tab styles from unit.scss after
|
||||
removing jquery ui ui-lightness styling
|
||||
*/
|
||||
|
||||
.problem-type-tabs {
|
||||
border:none;
|
||||
list-style-type: none;
|
||||
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 {
|
||||
opacity: .8;
|
||||
|
||||
&:ui-state-active {
|
||||
background-color: rgba(255, 255, 255, .3);
|
||||
opacity: 1;
|
||||
font-weight: 400;
|
||||
}
|
||||
a:focus {
|
||||
outline: none;
|
||||
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;
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
@@ -1,800 +1,740 @@
|
||||
.settings {
|
||||
.settings-overview {
|
||||
// Studio - Course Settings
|
||||
// ====================
|
||||
body.course.settings {
|
||||
|
||||
.content-primary, .content-supplementary {
|
||||
@include box-sizing(border-box);
|
||||
float: left;
|
||||
}
|
||||
|
||||
.content-primary {
|
||||
@extend .window;
|
||||
@include clearfix;
|
||||
display: table;
|
||||
width: 100%;
|
||||
width: flex-grid(9, 12);
|
||||
margin-right: flex-gutter();
|
||||
padding: $baseline ($baseline*1.5);
|
||||
}
|
||||
|
||||
// layout
|
||||
.sidebar {
|
||||
display: table-cell;
|
||||
float: none;
|
||||
width: 20%;
|
||||
padding: 30px 0 30px 20px;
|
||||
@include border-radius(3px 0 0 3px);
|
||||
background: $lightGrey;
|
||||
// messages - should be synced up with global messages in the future
|
||||
.message {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.message-status {
|
||||
display: none;
|
||||
@include border-top-radius(2px);
|
||||
@include box-sizing(border-box);
|
||||
border-bottom: 2px solid $yellow;
|
||||
margin: 0 0 20px 0;
|
||||
padding: 10px 20px;
|
||||
font-weight: 500;
|
||||
background: $paleYellow;
|
||||
|
||||
.text {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.main-column {
|
||||
display: table-cell;
|
||||
float: none;
|
||||
width: 80%;
|
||||
padding: 30px 40px 30px 60px;
|
||||
&.error {
|
||||
border-color: shade($red, 50%);
|
||||
background: tint($red, 20%);
|
||||
color: $white;
|
||||
}
|
||||
|
||||
.settings-page-menu {
|
||||
a {
|
||||
display: block;
|
||||
padding-left: 20px;
|
||||
line-height: 52px;
|
||||
&.confirm {
|
||||
border-color: shade($green, 50%);
|
||||
background: tint($green, 20%);
|
||||
color: $white;
|
||||
}
|
||||
|
||||
&.is-shown {
|
||||
background: #fff;
|
||||
@include border-radius(5px 0 0 5px);
|
||||
}
|
||||
&.is-shown {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
// in form - elements
|
||||
.group-settings {
|
||||
margin: 0 0 ($baseline*2) 0;
|
||||
|
||||
header {
|
||||
@include clearfix();
|
||||
|
||||
.title-2 {
|
||||
width: flex-grid(4, 9);
|
||||
margin: 0 flex-gutter() 0 0;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.tip {
|
||||
@include font-size(13);
|
||||
width: flex-grid(5, 9);
|
||||
float: right;
|
||||
margin-top: ($baseline/2);
|
||||
text-align: right;
|
||||
color: $gray-l2;
|
||||
}
|
||||
}
|
||||
|
||||
.settings-page-section {
|
||||
> .alert {
|
||||
display: none;
|
||||
|
||||
&.is-shown {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
> section {
|
||||
display: none;
|
||||
margin-bottom: 40px;
|
||||
|
||||
&.is-shown {
|
||||
display: block;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
> .title {
|
||||
margin-bottom: 30px;
|
||||
font-size: 28px;
|
||||
font-weight: 300;
|
||||
color: $blue;
|
||||
}
|
||||
|
||||
> section {
|
||||
margin-bottom: 100px;
|
||||
@include clearfix;
|
||||
|
||||
header {
|
||||
@include clearfix;
|
||||
border-bottom: 1px solid $mediumGrey;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 10px;
|
||||
|
||||
h3 {
|
||||
color: $darkGrey;
|
||||
float: left;
|
||||
|
||||
margin: 0 40px 0 0;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.detail {
|
||||
float: right;
|
||||
margin-top: 3px;
|
||||
color: $mediumGrey;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
padding-bottom: 0;
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// form basics
|
||||
label, .label {
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: none;
|
||||
font-size: 15px;
|
||||
font-weight: 400;
|
||||
|
||||
&.check-label {
|
||||
display: inline;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
&.ranges {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
input, textarea {
|
||||
@include transition(all 1s ease-in-out);
|
||||
@include box-sizing(border-box);
|
||||
font-size: 15px;
|
||||
|
||||
&.long {
|
||||
width: 100%;
|
||||
min-width: 400px;
|
||||
}
|
||||
|
||||
&.tall {
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
&.short {
|
||||
min-width: 100px;
|
||||
width: 25%;
|
||||
}
|
||||
|
||||
&.date {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
&.time {
|
||||
width: 85px !important;
|
||||
min-width: 85px !important;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
border: none;
|
||||
@include box-shadow(none);
|
||||
padding: 0;
|
||||
color: $darkGrey !important;
|
||||
font-weight: bold;
|
||||
background: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
textarea.tinymce {
|
||||
border: 1px solid $darkGrey;
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
input[type="checkbox"], input[type="radio"] {
|
||||
// basic layout/elements
|
||||
.title-2 {
|
||||
|
||||
}
|
||||
|
||||
input:disabled + .copy > label, input:disabled + .label {
|
||||
color: $mediumGrey;
|
||||
.title-3 {
|
||||
|
||||
}
|
||||
|
||||
|
||||
.input-default input, .input-default textarea {
|
||||
color: $mediumGrey;
|
||||
background: $lightGrey;
|
||||
}
|
||||
|
||||
::-webkit-input-placeholder {
|
||||
color: $mediumGrey;
|
||||
font-size: 13px;
|
||||
}
|
||||
:-moz-placeholder {
|
||||
color: $mediumGrey;
|
||||
font-size: 13px;
|
||||
// in form -UI hints/tips/messages
|
||||
.instructions {
|
||||
@include font-size(14);
|
||||
margin: 0 0 $baseline 0;
|
||||
}
|
||||
|
||||
.tip {
|
||||
color: $mediumGrey;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
|
||||
// form layouts
|
||||
.row {
|
||||
margin-bottom: 30px;
|
||||
padding-bottom: 30px;
|
||||
border-bottom: 1px solid $lightGrey;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
// structural labels, not semantic labels per se
|
||||
> label, .label {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
// tips
|
||||
.tip-inline {
|
||||
display: inline-block;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.tip-stacked {
|
||||
display: block;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
// structural field, not semantic fields per se
|
||||
.field {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
|
||||
> input, > textarea, .input {
|
||||
display: inline-block;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.group {
|
||||
input, textarea {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.label, label {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
// multi-field
|
||||
&.multi {
|
||||
display: block;
|
||||
background: tint($lightGrey, 50%);
|
||||
padding: 20px;
|
||||
@include border-radius(4px);
|
||||
@include box-sizing(border-box);
|
||||
|
||||
.group {
|
||||
margin-bottom: 10px;
|
||||
max-width: 175px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
input, .input, textarea {
|
||||
|
||||
}
|
||||
|
||||
.tip-stacked {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// multi stacked
|
||||
&.multi-stacked {
|
||||
|
||||
.group {
|
||||
input, .input, textarea {
|
||||
min-width: 370px;
|
||||
width: 370px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// multi-field inline
|
||||
&.multi-inline {
|
||||
@include clearfix;
|
||||
|
||||
.group {
|
||||
float: left;
|
||||
margin-right: 20px;
|
||||
|
||||
&:nth-child(2) {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.input, input, textarea {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// input-list
|
||||
.input-list {
|
||||
|
||||
.input {
|
||||
margin-bottom: 15px;
|
||||
padding-bottom: 15px;
|
||||
border-bottom: 1px dotted $lightGrey;
|
||||
@include clearfix();
|
||||
|
||||
&:last-child {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.row {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//radio buttons and checkboxes
|
||||
.input-radio {
|
||||
@include clearfix();
|
||||
|
||||
input {
|
||||
display: block;
|
||||
float: left;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.copy {
|
||||
position: relative;
|
||||
top: -5px;
|
||||
float: left;
|
||||
width: 350px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.tip {
|
||||
display: block;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.message-error {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.input-checkbox {
|
||||
|
||||
}
|
||||
|
||||
// enumerated inputs
|
||||
&.enum {
|
||||
}
|
||||
}
|
||||
|
||||
// layout - aligned label/field pairs
|
||||
&.row-col2 {
|
||||
|
||||
> label, .label {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.field {
|
||||
width: 400px ! important;
|
||||
}
|
||||
|
||||
&.multi-inline {
|
||||
@include clearfix();
|
||||
|
||||
.group {
|
||||
width: 170px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.field-additional {
|
||||
margin-left: 204px;
|
||||
}
|
||||
}
|
||||
|
||||
// editing controls - adding
|
||||
.new-item, .replace-item {
|
||||
clear: both;
|
||||
@include transition(color, 0.15s, ease-in-out);
|
||||
@include font-size(13);
|
||||
display: block;
|
||||
margin-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
@include grey-button;
|
||||
@include box-sizing(border-box);
|
||||
}
|
||||
|
||||
|
||||
// editing controls - removing
|
||||
.delete-button {
|
||||
float: right;
|
||||
}
|
||||
|
||||
// editing controls - preview
|
||||
.input-existing {
|
||||
display: block !important;
|
||||
|
||||
.current {
|
||||
width: 100%;
|
||||
margin: 10px 0;
|
||||
padding: 10px;
|
||||
@include box-sizing(border-box);
|
||||
@include border-radius(5px);
|
||||
font-size: 14px;
|
||||
background: tint($lightGrey, 50%);
|
||||
@include clearfix();
|
||||
|
||||
.doc-filename {
|
||||
display: inline-block;
|
||||
width: 220px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.remove-doc-data {
|
||||
display: inline-block;
|
||||
margin-top: 0;
|
||||
width: 150px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// specific sections
|
||||
.settings-details {
|
||||
|
||||
}
|
||||
|
||||
.settings-faculty {
|
||||
|
||||
.settings-faculty-members {
|
||||
|
||||
> header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.field .multi {
|
||||
display: block;
|
||||
margin-bottom: 40px;
|
||||
padding: 20px;
|
||||
background: tint($lightGrey, 50%);
|
||||
@include border-radius(4px);
|
||||
@include box-sizing(border-box);
|
||||
}
|
||||
|
||||
.course-faculty-list-item {
|
||||
|
||||
.row {
|
||||
|
||||
&:nth-child(4) {
|
||||
padding-bottom: 0;
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.remove-faculty-photo {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
#course-faculty-bio-input {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.new-course-faculty-item {
|
||||
}
|
||||
|
||||
.current-faculty-photo {
|
||||
padding: 0;
|
||||
|
||||
img {
|
||||
display: block;
|
||||
@include box-shadow(0 1px 3px rgba(0,0,0,0.1));
|
||||
padding: 10px;
|
||||
border: 2px solid $mediumGrey;
|
||||
background: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.settings-grading {
|
||||
|
||||
.setting-grading-assignment-types {
|
||||
|
||||
.row .field.enum {
|
||||
width: 684px;
|
||||
}
|
||||
}
|
||||
|
||||
.course-grading-assignment-list-item {
|
||||
|
||||
}
|
||||
|
||||
.input-list {
|
||||
.row {
|
||||
|
||||
.input {
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.settings-handouts {
|
||||
|
||||
}
|
||||
|
||||
.settings-problems {
|
||||
|
||||
> section {
|
||||
|
||||
&.is-shown {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.settings-discussions {
|
||||
|
||||
.course-discussions-categories-list-item {
|
||||
|
||||
label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.group {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.remove-item {
|
||||
display: inline-block !important;
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// states
|
||||
label.is-focused {
|
||||
color: $blue;
|
||||
@include transition(color 1s ease-in-out);
|
||||
}
|
||||
|
||||
// extras/abbreviations
|
||||
// .settings-extras {
|
||||
|
||||
// > header {
|
||||
// cursor: pointer;
|
||||
|
||||
// &.active {
|
||||
|
||||
// }
|
||||
// }
|
||||
|
||||
// > div {
|
||||
// display: none;
|
||||
// @include transition(display 0.25s ease-in-out);
|
||||
|
||||
// &.is-shown {
|
||||
// display: block;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
input.error, textarea.error {
|
||||
border-color: $red;
|
||||
margin-top: ($baseline/4);
|
||||
color: $gray-l3;
|
||||
}
|
||||
|
||||
.message-error {
|
||||
@include font-size(13);
|
||||
display: block;
|
||||
margin-top: 5px;
|
||||
margin-top: ($baseline/4);
|
||||
margin-bottom: ($baseline/2);
|
||||
color: $red;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
// misc
|
||||
.divide {
|
||||
display: none;
|
||||
// buttons
|
||||
.remove-item {
|
||||
@include white-button;
|
||||
@include font-size(13);
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
i.ss-icon {
|
||||
position: relative;
|
||||
top: 1px;
|
||||
margin-right: 5px;
|
||||
.new-button {
|
||||
@include font-size(13);
|
||||
}
|
||||
|
||||
.well {
|
||||
padding: 20px;
|
||||
background: $lightGrey;
|
||||
border: 1px solid $mediumGrey;
|
||||
@include border-radius(4px);
|
||||
@include box-shadow(0 1px 1px rgba(0,0,0,0.05) inset)
|
||||
}
|
||||
}
|
||||
// form basics
|
||||
.list-input {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
|
||||
.field {
|
||||
margin: 0 0 $baseline 0;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin-bottom: 30px;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
color: $blue;
|
||||
}
|
||||
&.required {
|
||||
|
||||
.grade-controls {
|
||||
@include clearfix;
|
||||
width: 642px;
|
||||
}
|
||||
|
||||
.new-grade-button {
|
||||
position: relative;
|
||||
float: left;
|
||||
display: block;
|
||||
width: 29px;
|
||||
height: 29px;
|
||||
margin: 10px 20px 0 0;
|
||||
border-radius: 20px;
|
||||
border: 1px solid $darkGrey;
|
||||
@include linear-gradient(top, rgba(255, 255, 255, 0.8), rgba(255, 255, 255, 0));
|
||||
background-color: #d1dae3;
|
||||
@include box-shadow(0 1px 0 rgba(255, 255, 255, .3) inset);
|
||||
color: #6d788b;
|
||||
|
||||
.plus-icon {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
margin-left: -6px;
|
||||
margin-top: -6px;
|
||||
}
|
||||
}
|
||||
|
||||
.grade-slider {
|
||||
float: left;
|
||||
width: 580px;
|
||||
margin-bottom: 10px;
|
||||
|
||||
.grade-bar {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
background: $lightGrey;
|
||||
|
||||
.increments {
|
||||
position: relative;
|
||||
|
||||
li {
|
||||
position: absolute;
|
||||
top: 52px;
|
||||
width: 30px;
|
||||
margin-left: -15px;
|
||||
font-size: 9px;
|
||||
text-align: center;
|
||||
|
||||
&.increment-0 {
|
||||
left: 0;
|
||||
label {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&.increment-10 {
|
||||
left: 10%;
|
||||
label:after {
|
||||
margin-left: ($baseline/4);
|
||||
content: "*";
|
||||
}
|
||||
}
|
||||
|
||||
label, input, textarea {
|
||||
display: block;
|
||||
}
|
||||
|
||||
label {
|
||||
@include font-size(14);
|
||||
@include transition(color, 0.15s, ease-in-out);
|
||||
margin: 0 0 ($baseline/4) 0;
|
||||
font-weight: 400;
|
||||
|
||||
&.is-focused {
|
||||
color: $blue;
|
||||
}
|
||||
}
|
||||
|
||||
input, textarea {
|
||||
@include placeholder($gray-l4);
|
||||
@include font-size(16);
|
||||
@include size(100%,100%);
|
||||
padding: ($baseline/2);
|
||||
|
||||
&.long {
|
||||
}
|
||||
|
||||
&.increment-20 {
|
||||
left: 20%;
|
||||
&.short {
|
||||
}
|
||||
|
||||
&.increment-30 {
|
||||
left: 30%;
|
||||
&.error {
|
||||
border-color: $red;
|
||||
}
|
||||
|
||||
&.increment-40 {
|
||||
left: 40%;
|
||||
}
|
||||
&:focus {
|
||||
|
||||
&.increment-50 {
|
||||
left: 50%;
|
||||
+ .tip {
|
||||
color: $gray;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.increment-60 {
|
||||
left: 60%;
|
||||
}
|
||||
textarea.long {
|
||||
height: ($baseline*5);
|
||||
}
|
||||
|
||||
&.increment-70 {
|
||||
left: 70%;
|
||||
}
|
||||
input[type="checkbox"] {
|
||||
display: inline-block;
|
||||
margin-right: ($baseline/4);
|
||||
width: auto;
|
||||
height: auto;
|
||||
|
||||
&.increment-80 {
|
||||
left: 80%;
|
||||
}
|
||||
|
||||
&.increment-90 {
|
||||
left: 90%;
|
||||
}
|
||||
|
||||
&.increment-100 {
|
||||
left: 100%;
|
||||
& + label {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.grade-specific-bar {
|
||||
height: 50px !important;
|
||||
|
||||
.field-group {
|
||||
@include clearfix();
|
||||
margin: 0 0 ($baseline/2) 0;
|
||||
}
|
||||
|
||||
.grades {
|
||||
position: relative;
|
||||
// enumerated/grouped lists
|
||||
&.enum {
|
||||
|
||||
li {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
height: 50px;
|
||||
text-align: right;
|
||||
@include border-radius(2px);
|
||||
.field-group {
|
||||
@include box-sizing(border-box);
|
||||
@include border-radius(3px);
|
||||
background: $gray-l5;
|
||||
padding: $baseline;
|
||||
|
||||
&:hover,
|
||||
&.is-dragging {
|
||||
.remove-button {
|
||||
display: block;
|
||||
&:last-child {
|
||||
padding-bottom: $baseline;
|
||||
}
|
||||
|
||||
.actions {
|
||||
@include clearfix();
|
||||
margin-top: ($baseline/2);
|
||||
border-top: 1px solid $gray-l4;
|
||||
padding-top: ($baseline/2);
|
||||
|
||||
.remove-item {
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-dragging {
|
||||
// existing inputs
|
||||
.input-existing {
|
||||
margin: 0 0 $baseline 0;
|
||||
|
||||
.actions {
|
||||
margin: ($baseline/4) 0 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
// not editable fields
|
||||
.field.is-not-editable {
|
||||
|
||||
.remove-button {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: -17px;
|
||||
right: 1px;
|
||||
height: 17px;
|
||||
font-size: 10px;
|
||||
}
|
||||
label, .label {
|
||||
color: $gray-l3;
|
||||
}
|
||||
|
||||
&:nth-child(1) {
|
||||
background: #4fe696;
|
||||
}
|
||||
input {
|
||||
opacity: 0.25;
|
||||
}
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
background: #ffdf7e;
|
||||
}
|
||||
// field with error
|
||||
.field.error {
|
||||
|
||||
&:nth-child(3) {
|
||||
background: #ffb657;
|
||||
}
|
||||
input, textarea {
|
||||
border-color: $red;
|
||||
}
|
||||
}
|
||||
|
||||
// specific fields - basic
|
||||
&.basic {
|
||||
|
||||
&:nth-child(4) {
|
||||
background: #ef54a1;
|
||||
}
|
||||
.list-input {
|
||||
@include clearfix();
|
||||
|
||||
&:nth-child(5),
|
||||
&.bar-fail {
|
||||
background: #fb336c;
|
||||
}
|
||||
.field {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.letter-grade {
|
||||
display: block;
|
||||
margin: 10px 15px 0 0;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
line-height: 14px;
|
||||
}
|
||||
#field-course-organization {
|
||||
float: left;
|
||||
width: flex-grid(2, 9);
|
||||
margin-right: flex-gutter();
|
||||
}
|
||||
|
||||
.range {
|
||||
display: block;
|
||||
margin-right: 15px;
|
||||
font-size: 10px;
|
||||
line-height: 12px;
|
||||
}
|
||||
#field-course-number {
|
||||
float: left;
|
||||
width: flex-grid(2, 9);
|
||||
margin-right: flex-gutter();
|
||||
}
|
||||
|
||||
.drag-bar {
|
||||
#field-course-name {
|
||||
float: left;
|
||||
width: flex-grid(5, 9);
|
||||
}
|
||||
}
|
||||
|
||||
// specific fields - schedule
|
||||
&.schedule {
|
||||
|
||||
.list-input {
|
||||
margin-bottom: ($baseline*1.5);
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.field-group {
|
||||
@include clearfix();
|
||||
border-bottom: 1px solid $gray-l5;
|
||||
padding-bottom: ($baseline/2);
|
||||
|
||||
&:last-child {
|
||||
border: none;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.field {
|
||||
float: left;
|
||||
width: flex-grid(3, 9);
|
||||
margin-bottom: ($baseline/4);
|
||||
margin-right: flex-gutter();
|
||||
}
|
||||
|
||||
.field.time {
|
||||
position: relative;
|
||||
|
||||
.tip {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: -1px;
|
||||
height: 50px;
|
||||
width: 2px;
|
||||
background-color: #fff;
|
||||
@include box-shadow(-1px 0 3px rgba(0,0,0,0.1));
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// specific fields - overview
|
||||
#field-course-overview {
|
||||
|
||||
cursor: ew-resize;
|
||||
@include transition(none);
|
||||
#course-overview {
|
||||
height: ($baseline*20);
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
width: 6px;
|
||||
right: -2px;
|
||||
// specific fields - video
|
||||
#field-course-introduction-video {
|
||||
|
||||
.input-existing {
|
||||
@include box-sizing(border-box);
|
||||
@include border-radius(3px);
|
||||
background: $gray-l5;
|
||||
padding: ($baseline/2);
|
||||
|
||||
.actions {
|
||||
@include clearfix();
|
||||
margin-top: ($baseline/2);
|
||||
border-top: 1px solid $gray-l4;
|
||||
padding-top: ($baseline/2);
|
||||
|
||||
.remove-item {
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
margin-top: ($baseline/2);
|
||||
border-top: 1px solid $gray-l5;
|
||||
padding-top: ($baseline/2);
|
||||
}
|
||||
}
|
||||
|
||||
// specific fields - requirements
|
||||
&.requirements {
|
||||
|
||||
#field-course-effort {
|
||||
width: flex-grid(3, 9);
|
||||
}
|
||||
}
|
||||
|
||||
// specific fields - grading range (artifact styling)
|
||||
&.grade-range {
|
||||
margin-bottom: ($baseline*3);
|
||||
|
||||
.grade-controls {
|
||||
@include clearfix;
|
||||
width: flex-grid(9,9);
|
||||
}
|
||||
|
||||
.new-grade-button {
|
||||
@include box-sizing(border-box);
|
||||
@include linear-gradient(top, rgba(255, 255, 255, 0.8), rgba(255, 255, 255, 0));
|
||||
@include box-shadow(0 1px 0 rgba(255, 255, 255, .3) inset);
|
||||
width: flex-grid(1,9);
|
||||
height: ($baseline*2);
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin-right: flex-gutter();
|
||||
border-radius: 20px;
|
||||
border: 1px solid $darkGrey;
|
||||
background-color: #d1dae3;
|
||||
color: #6d788b;
|
||||
|
||||
.plus-icon {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
margin-left: -6px;
|
||||
margin-top: -6px;
|
||||
}
|
||||
}
|
||||
|
||||
.grade-slider {
|
||||
@include box-sizing(border-box);
|
||||
width: flex-grid(8,9);
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
|
||||
.grade-bar {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: ($baseline*2.5);
|
||||
background: $lightGrey;
|
||||
|
||||
.increments {
|
||||
position: relative;
|
||||
|
||||
li {
|
||||
position: absolute;
|
||||
top: 52px;
|
||||
width: 30px;
|
||||
margin-left: -15px;
|
||||
font-size: 9px;
|
||||
text-align: center;
|
||||
|
||||
&.increment-0 {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
&.increment-10 {
|
||||
left: 10%;
|
||||
}
|
||||
|
||||
&.increment-20 {
|
||||
left: 20%;
|
||||
}
|
||||
|
||||
&.increment-30 {
|
||||
left: 30%;
|
||||
}
|
||||
|
||||
&.increment-40 {
|
||||
left: 40%;
|
||||
}
|
||||
|
||||
&.increment-50 {
|
||||
left: 50%;
|
||||
}
|
||||
|
||||
&.increment-60 {
|
||||
left: 60%;
|
||||
}
|
||||
|
||||
&.increment-70 {
|
||||
left: 70%;
|
||||
}
|
||||
|
||||
&.increment-80 {
|
||||
left: 80%;
|
||||
}
|
||||
|
||||
&.increment-90 {
|
||||
left: 90%;
|
||||
}
|
||||
|
||||
&.increment-100 {
|
||||
left: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.grade-specific-bar {
|
||||
height: 50px !important;
|
||||
}
|
||||
|
||||
.grades {
|
||||
position: relative;
|
||||
|
||||
li {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
height: 50px;
|
||||
text-align: right;
|
||||
@include border-radius(2px);
|
||||
|
||||
&:hover,
|
||||
&.is-dragging {
|
||||
.remove-button {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-dragging {
|
||||
|
||||
}
|
||||
|
||||
.remove-button {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: -17px;
|
||||
right: 1px;
|
||||
height: 17px;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
&:nth-child(1) {
|
||||
background: #4fe696;
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
background: #ffdf7e;
|
||||
}
|
||||
|
||||
&:nth-child(3) {
|
||||
background: #ffb657;
|
||||
}
|
||||
|
||||
&:nth-child(4) {
|
||||
background: #ef54a1;
|
||||
}
|
||||
|
||||
&:nth-child(5),
|
||||
&.bar-fail {
|
||||
background: #fb336c;
|
||||
}
|
||||
|
||||
.letter-grade {
|
||||
display: block;
|
||||
margin: 10px 15px 0 0;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
line-height: 14px;
|
||||
}
|
||||
|
||||
.range {
|
||||
display: block;
|
||||
margin-right: 15px;
|
||||
font-size: 10px;
|
||||
line-height: 12px;
|
||||
}
|
||||
|
||||
.drag-bar {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: -1px;
|
||||
height: 50px;
|
||||
width: 2px;
|
||||
background-color: #fff;
|
||||
@include box-shadow(-1px 0 3px rgba(0,0,0,0.1));
|
||||
|
||||
cursor: ew-resize;
|
||||
@include transition(none);
|
||||
|
||||
&:hover {
|
||||
width: 6px;
|
||||
right: -2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// specific fields - grading rules
|
||||
&.grade-rules {
|
||||
|
||||
#field-course-grading-graceperiod {
|
||||
width: flex-grid(3, 9);
|
||||
}
|
||||
}
|
||||
|
||||
&.assignment-types {
|
||||
|
||||
.list-input {
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.field-group {
|
||||
@include clearfix();
|
||||
width: flex-grid(9, 9);
|
||||
margin-bottom: ($baseline*1.5);
|
||||
border-bottom: 1px solid $gray-l5;
|
||||
padding-bottom: ($baseline*1.5);
|
||||
|
||||
&:last-child {
|
||||
border: none;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.field {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
width: flex-grid(3, 6);
|
||||
margin-bottom: ($baseline/2);
|
||||
margin-right: flex-gutter();
|
||||
}
|
||||
|
||||
#field-course-grading-assignment-shortname,
|
||||
#field-course-grading-assignment-totalassignments,
|
||||
#field-course-grading-assignment-gradeweight,
|
||||
#field-course-grading-assignment-droppable {
|
||||
width: flex-grid(2, 6);
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
float: left;
|
||||
width: flex-grid(9, 9);
|
||||
|
||||
.delete-button {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// specific fields - advanced settings
|
||||
&.advanced-policies {
|
||||
|
||||
.field-group {
|
||||
margin-bottom: ($baseline*1.5);
|
||||
|
||||
&:last-child {
|
||||
border: none;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.course-advanced-policy-list-item {
|
||||
@include clearfix();
|
||||
position: relative;
|
||||
|
||||
.field {
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tip {
|
||||
@include transition (opacity 0.5s ease-in-out 0s);
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
bottom: ($baseline*1.25);
|
||||
}
|
||||
|
||||
input:focus {
|
||||
|
||||
& + .tip {
|
||||
opacity: 1.0;
|
||||
}
|
||||
}
|
||||
|
||||
input.error {
|
||||
|
||||
& + .tip {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.key, .value {
|
||||
float: left;
|
||||
margin: 0 0 ($baseline/2) 0;
|
||||
}
|
||||
|
||||
.key {
|
||||
width: flex-grid(3, 9);
|
||||
margin-right: flex-gutter();
|
||||
}
|
||||
|
||||
.value {
|
||||
width: flex-grid(6, 9);
|
||||
}
|
||||
|
||||
.actions {
|
||||
float: left;
|
||||
width: flex-grid(9, 9);
|
||||
|
||||
.delete-button {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.message-error {
|
||||
position: absolute;
|
||||
bottom: ($baseline*0.75);
|
||||
}
|
||||
|
||||
// specific to code mirror instance in JSON policy editing, need to sync up with other similar code mirror UIs
|
||||
.CodeMirror {
|
||||
@include font-size(16);
|
||||
@include box-sizing(border-box);
|
||||
@include box-shadow(0 1px 2px rgba(0, 0, 0, .1) inset);
|
||||
@include linear-gradient($lightGrey, tint($lightGrey, 90%));
|
||||
padding: 5px 8px;
|
||||
border: 1px solid $mediumGrey;
|
||||
border-radius: 2px;
|
||||
background-color: $lightGrey;
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
color: $baseFontColor;
|
||||
outline: 0;
|
||||
|
||||
&.CodeMirror-focused {
|
||||
@include linear-gradient($paleYellow, tint($paleYellow, 90%));
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.CodeMirror-scroll {
|
||||
overflow: hidden;
|
||||
height: auto;
|
||||
min-height: ($baseline*1.5);
|
||||
max-height: ($baseline*10);
|
||||
}
|
||||
|
||||
// editor color changes just for JSON
|
||||
.CodeMirror-lines {
|
||||
|
||||
.cm-string {
|
||||
color: #cb9c40;
|
||||
}
|
||||
|
||||
pre {
|
||||
line-height: 2.0rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content-supplementary {
|
||||
width: flex-grid(3, 12);
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,9 @@
|
||||
border-radius: 0;
|
||||
|
||||
&.new-component-item {
|
||||
margin-top: 20px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
@include box-shadow(none);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
.subsection .main-wrapper {
|
||||
margin: 40px;
|
||||
}
|
||||
|
||||
.subsection .inner-wrapper {
|
||||
@include clearfix();
|
||||
}
|
||||
|
||||
.subsection-body {
|
||||
padding: 32px 40px;
|
||||
@include clearfix;
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
.unit .main-wrapper,
|
||||
.subsection .main-wrapper {
|
||||
.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;
|
||||
@@ -58,6 +64,7 @@
|
||||
margin: 20px 40px;
|
||||
|
||||
|
||||
|
||||
.title {
|
||||
margin: 0 0 15px 0;
|
||||
color: $mediumGrey;
|
||||
@@ -67,22 +74,25 @@
|
||||
}
|
||||
|
||||
&.new-component-item {
|
||||
padding: 20px;
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
background: $lightGrey;
|
||||
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: #6d788b;
|
||||
color: #edf1f5;
|
||||
}
|
||||
|
||||
h5 {
|
||||
margin-bottom: 8px;
|
||||
margin: 20px 0px;
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
font-weight: 600;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.rendered-component {
|
||||
@@ -92,18 +102,21 @@
|
||||
}
|
||||
|
||||
.new-component-type {
|
||||
|
||||
a,
|
||||
li {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
a {
|
||||
border: 1px solid $mediumGrey;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
margin-right: 10px;
|
||||
margin-bottom: 10px;
|
||||
color: #fff;
|
||||
margin-right: 15px;
|
||||
margin-bottom: 20px;
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
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);
|
||||
@@ -115,25 +128,40 @@
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
@include box-sizing(border-box);
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.new-component-templates {
|
||||
display: none;
|
||||
padding: 20px;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,7 +174,6 @@
|
||||
border: 1px solid $darkGreen;
|
||||
background: tint($green,20%);
|
||||
color: #fff;
|
||||
@include transition(background-color .15s);
|
||||
|
||||
&:hover {
|
||||
background: $brightGreen;
|
||||
@@ -154,19 +181,81 @@
|
||||
}
|
||||
}
|
||||
|
||||
.new-component-template {
|
||||
margin-bottom: 20px;
|
||||
.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:last-child {
|
||||
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-radius: 0 0 3px 3px;
|
||||
border-bottom: 1px solid $darkGreen;
|
||||
border-top: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
li:nth-child(2) {
|
||||
a {
|
||||
border-radius: 3px 3px 0 0;
|
||||
border-radius: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,18 +264,20 @@
|
||||
display: block;
|
||||
padding: 7px 20px;
|
||||
border-bottom: none;
|
||||
font-weight: 300;
|
||||
font-weight: 500;
|
||||
|
||||
.name {
|
||||
float: left;
|
||||
|
||||
.ss-icon {
|
||||
@include transition(opacity .15s);
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
top: 1px;
|
||||
font-size: 13px;
|
||||
margin-right: 5px;
|
||||
opacity: 0.5;
|
||||
width: 17;
|
||||
height: 21px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,6 +295,7 @@
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: #fff;
|
||||
|
||||
.ss-icon {
|
||||
opacity: 1.0;
|
||||
@@ -217,14 +309,18 @@
|
||||
|
||||
// specific editor types
|
||||
.empty {
|
||||
@include box-shadow(0 1px 3px rgba(0,0,0,0.2));
|
||||
margin-bottom: 10px;
|
||||
|
||||
a {
|
||||
border-bottom: 1px solid $darkGreen;
|
||||
border-radius: 3px;
|
||||
font-weight: 500;
|
||||
background: $green;
|
||||
line-height: 1.4;
|
||||
font-weight: 400;
|
||||
background: #fff;
|
||||
color: #3c3c3c;
|
||||
|
||||
|
||||
&:hover {
|
||||
background: tint($green,30%);
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -233,7 +329,7 @@
|
||||
text-align: center;
|
||||
|
||||
h5 {
|
||||
color: $green;
|
||||
color: $darkGreen;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -305,6 +401,7 @@
|
||||
.wrapper-component-editor {
|
||||
z-index: 9999;
|
||||
position: relative;
|
||||
background: $lightBluishGrey2;
|
||||
}
|
||||
|
||||
.component-editor {
|
||||
@@ -506,6 +603,7 @@
|
||||
|
||||
.edit-state-draft {
|
||||
.visibility,
|
||||
|
||||
.edit-draft-message,
|
||||
.view-button {
|
||||
display: none;
|
||||
|
||||
@@ -1,25 +1,85 @@
|
||||
$gw-column: 80px;
|
||||
$gw-gutter: 20px;
|
||||
$baseline: 20px;
|
||||
|
||||
// grid
|
||||
$gw-column: ($baseline*3);
|
||||
$gw-gutter: $baseline;
|
||||
$fg-column: $gw-column;
|
||||
$fg-gutter: $gw-gutter;
|
||||
$fg-max-columns: 12;
|
||||
$fg-max-width: 1400px;
|
||||
$fg-min-width: 810px;
|
||||
$fg-max-width: 1280px;
|
||||
$fg-min-width: 900px;
|
||||
|
||||
// type
|
||||
$sans-serif: 'Open Sans', $verdana;
|
||||
$body-line-height: golden-ratio(.875em, 1);
|
||||
|
||||
$white: rgb(255,255,255);
|
||||
$black: rgb(0,0,0);
|
||||
$pink: rgb(182,37,104);
|
||||
$error-red: rgb(253, 87, 87);
|
||||
|
||||
$baseFontColor: #3c3c3c;
|
||||
$offBlack: #3c3c3c;
|
||||
// colors - new for re-org
|
||||
$black: rgb(0,0,0);
|
||||
$white: rgb(255,255,255);
|
||||
$blue: #5597dd;
|
||||
|
||||
$gray: rgb(127,127,127);
|
||||
$gray-l1: tint($gray,20%);
|
||||
$gray-l2: tint($gray,40%);
|
||||
$gray-l3: tint($gray,60%);
|
||||
$gray-l4: tint($gray,80%);
|
||||
$gray-l5: tint($gray,90%);
|
||||
$gray-d1: shade($gray,20%);
|
||||
$gray-d2: shade($gray,40%);
|
||||
$gray-d3: shade($gray,60%);
|
||||
$gray-d4: shade($gray,80%);
|
||||
|
||||
$blue: rgb(85, 151, 221);
|
||||
$blue-l1: tint($blue,20%);
|
||||
$blue-l2: tint($blue,40%);
|
||||
$blue-l3: tint($blue,60%);
|
||||
$blue-l4: tint($blue,80%);
|
||||
$blue-l5: tint($blue,90%);
|
||||
$blue-d1: shade($blue,20%);
|
||||
$blue-d2: shade($blue,40%);
|
||||
$blue-d3: shade($blue,60%);
|
||||
$blue-d4: shade($blue,80%);
|
||||
|
||||
$pink: rgb(183, 37, 103);
|
||||
$pink-l1: tint($pink,20%);
|
||||
$pink-l2: tint($pink,40%);
|
||||
$pink-l3: tint($pink,60%);
|
||||
$pink-l4: tint($pink,80%);
|
||||
$pink-l5: tint($pink,90%);
|
||||
$pink-d1: shade($pink,20%);
|
||||
$pink-d2: shade($pink,40%);
|
||||
$pink-d3: shade($pink,60%);
|
||||
$pink-d4: shade($pink,80%);
|
||||
|
||||
$green: rgb(37, 184, 90);
|
||||
$green-l1: tint($green,20%);
|
||||
$green-l2: tint($green,40%);
|
||||
$green-l3: tint($green,60%);
|
||||
$green-l4: tint($green,80%);
|
||||
$green-l5: tint($green,90%);
|
||||
$green-d1: shade($green,20%);
|
||||
$green-d2: shade($green,40%);
|
||||
$green-d3: shade($green,60%);
|
||||
$green-d4: shade($green,80%);
|
||||
|
||||
$yellow: rgb(231, 214, 143);
|
||||
$yellow-l1: tint($yellow,20%);
|
||||
$yellow-l2: tint($yellow,40%);
|
||||
$yellow-l3: tint($yellow,60%);
|
||||
$yellow-l4: tint($yellow,80%);
|
||||
$yellow-l5: tint($yellow,90%);
|
||||
$yellow-d1: shade($yellow,20%);
|
||||
$yellow-d2: shade($yellow,40%);
|
||||
$yellow-d3: shade($yellow,60%);
|
||||
$yellow-d4: shade($yellow,80%);
|
||||
|
||||
$shadow: rgba(0,0,0,0.2);
|
||||
$shadow-l1: rgba(0,0,0,0.1);
|
||||
$shadow-d1: rgba(0,0,0,0.4);
|
||||
|
||||
// colors - inherited
|
||||
$baseFontColor: #3c3c3c;
|
||||
$offBlack: #3c3c3c;
|
||||
$orange: #edbd3c;
|
||||
$red: #b20610;
|
||||
$green: #108614;
|
||||
@@ -34,4 +94,4 @@ $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);
|
||||
@@ -1,4 +1,5 @@
|
||||
@import 'bourbon/bourbon';
|
||||
@import 'bourbon/addons/button';
|
||||
@import 'vendor/normalize';
|
||||
@import 'keyframes';
|
||||
|
||||
@@ -8,8 +9,10 @@
|
||||
@import "fonts";
|
||||
@import "variables";
|
||||
@import "cms_mixins";
|
||||
@import "extends";
|
||||
@import "base";
|
||||
@import "header";
|
||||
@import "footer";
|
||||
@import "dashboard";
|
||||
@import "courseware";
|
||||
@import "subsection";
|
||||
@@ -26,6 +29,8 @@
|
||||
@import "modal";
|
||||
@import "alerts";
|
||||
@import "login";
|
||||
@import "account";
|
||||
@import "index";
|
||||
@import 'jquery-ui-calendar';
|
||||
|
||||
@import 'content-types';
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
<section class="activation">
|
||||
<h1>Account already active!</h1>
|
||||
<p> This account has already been activated. <a href="/login">Log in here</a>.</p>
|
||||
<p> This account has already been activated. <a href="/signin">Log in here</a>.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<section class="tos">
|
||||
<div>
|
||||
<h1>Activation Complete!</h1>
|
||||
<p>Thanks for activating your account. <a href="/login">Log in here</a>.</p>
|
||||
<p>Thanks for activating your account. <a href="/signin">Log in here</a>.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<%inherit file="base.html" />
|
||||
<%! from django.core.urlresolvers import reverse %>
|
||||
<%block name="bodyclass">assets</%block>
|
||||
<%block name="title">Courseware Assets</%block>
|
||||
<%block name="bodyclass">is-signedin course uploads</%block>
|
||||
<%block name="title">Uploads & Files</%block>
|
||||
|
||||
<%namespace name='static' file='static_content.html'/>
|
||||
|
||||
@@ -33,12 +33,27 @@
|
||||
</tr>
|
||||
</script>
|
||||
|
||||
<div class="wrapper-mast wrapper">
|
||||
<header class="mast has-actions has-subtitle">
|
||||
<div class="title">
|
||||
<span class="title-sub">Course Content</span>
|
||||
<h1 class="title-1">Files & Uploads</h1>
|
||||
</div>
|
||||
|
||||
<nav class="nav-actions">
|
||||
<h3 class="sr">Page Actions</h3>
|
||||
<ul>
|
||||
<li class="nav-item">
|
||||
<a href="#" class="button upload-button new-button"><i class="ss-icon ss-symbolicons-standard icon icon-create"></i> Upload New File</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
</div>
|
||||
|
||||
<div class="main-wrapper">
|
||||
<div class="inner-wrapper">
|
||||
<div class="page-actions">
|
||||
<a href="#" class="upload-button new-button">
|
||||
<span class="upload-icon"></span>Upload New Asset
|
||||
</a>
|
||||
<input type="text" class="asset-search-input search wip-box" placeholder="search assets" style="display:none"/>
|
||||
</div>
|
||||
<article class="asset-library">
|
||||
|
||||
@@ -5,23 +5,29 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
||||
<title>
|
||||
<%block name="title"></%block> |
|
||||
% if context_course:
|
||||
<% ctx_loc = context_course.location %>
|
||||
${context_course.display_name} |
|
||||
% endif
|
||||
edX Studio
|
||||
</title>
|
||||
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<meta name="path_prefix" content="${MITX_ROOT_URL}">
|
||||
|
||||
<%static:css group='base-style'/>
|
||||
<link rel="stylesheet" type="text/css" href="${static.url('js/vendor/markitup/skins/simple/style.css')}" />
|
||||
<link rel="stylesheet" type="text/css" href="${static.url('js/vendor/markitup/sets/wiki/style.css')}" />
|
||||
<link rel="stylesheet" type="text/css" href="${static.url('css/vendor/symbolset.ss-standard.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')}" />
|
||||
|
||||
<title><%block name="title"></%block></title>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
|
||||
<meta name="path_prefix" content="${MITX_ROOT_URL}">
|
||||
|
||||
<%block name="header_extras"></%block>
|
||||
</head>
|
||||
|
||||
<body class="<%block name='bodyclass'></%block> hide-wip">
|
||||
<%include file="widgets/header.html" args="active_tab=active_tab"/>
|
||||
<%include file="widgets/header.html" />
|
||||
<%include file="courseware_vendor_js.html"/>
|
||||
|
||||
<script type="text/javascript" src="${static.url('js/vendor/json2.js')}"></script>
|
||||
@@ -47,9 +53,9 @@
|
||||
</script>
|
||||
|
||||
<%block name="content"></%block>
|
||||
<%include file="widgets/footer.html" />
|
||||
|
||||
<%block name="jsextra"></%block>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<%inherit file="base.html" />
|
||||
<%block name="title">Course Manager</%block>
|
||||
|
||||
<%include file="widgets/header.html"/>
|
||||
|
||||
<%block name="content">
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
<%namespace name='static' file='static_content.html'/>
|
||||
|
||||
<!-- TODO decode course # from context_course into title -->
|
||||
<%block name="title">Course Info</%block>
|
||||
<%block name="bodyclass">course-info</%block>
|
||||
<%block name="title">Updates</%block>
|
||||
<%block name="bodyclass">is-signedin course course-info updates</%block>
|
||||
|
||||
|
||||
<%block name="jsextra">
|
||||
<script type="text/javascript" src="${static.url('js/template_loader.js')}"></script>
|
||||
@@ -41,16 +42,38 @@
|
||||
</%block>
|
||||
|
||||
<%block name="content">
|
||||
<div class="wrapper-mast wrapper">
|
||||
<header class="mast has-actions has-subtitle">
|
||||
<div class="title">
|
||||
<span class="title-sub">Course Content</span>
|
||||
<h1 class="title-1">Course Updates</h1>
|
||||
</div>
|
||||
|
||||
<nav class="nav-actions">
|
||||
<h3 class="sr">Page Actions</h3>
|
||||
<ul>
|
||||
<li class="nav-item">
|
||||
<a href="#" class=" button new-button new-update-button"><i class="ss-icon ss-symbolicons-standard icon icon-create">+</i> New Update</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
</div>
|
||||
|
||||
<div class="wrapper-content wrapper">
|
||||
<section class="content">
|
||||
<div class="introduction">
|
||||
<p clas="copy">Course updates are announcements or notifications you want to share with your class. Other course authors have used them for important exam/date reminders, change in schedules, and to call out any important steps students need to be aware of.</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="main-wrapper">
|
||||
<div class="inner-wrapper">
|
||||
<h1>Course Info</h1>
|
||||
<div class="course-info-wrapper">
|
||||
<div class="main-column window">
|
||||
<article class="course-updates" id="course-update-view">
|
||||
<h2>Course Updates & News</h2>
|
||||
<a href="#" class="new-update-button">New Update</a>
|
||||
<ol class="update-list" id="course-update-list"></ol>
|
||||
<!-- probably replace w/ a vertical where each element of the vertical is a separate update w/ a date and html field -->
|
||||
</article>
|
||||
</div>
|
||||
<div class="sidebar window course-handouts" id="course-handouts-view"></div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<%inherit file="base.html" />
|
||||
<%! from django.core.urlresolvers import reverse %>
|
||||
<%block name="title">Edit Static Page</%block>
|
||||
<%block name="bodyclass">edit-static-page</%block>
|
||||
<%block name="title">Editing Static Page</%block>
|
||||
<%block name="bodyclass">is-signedin course pages edit-static-page</%block>
|
||||
|
||||
<%block name="content">
|
||||
<div class="main-wrapper">
|
||||
|
||||