Merge branch 'master' of github.com:MITx/mitx
This commit is contained in:
@@ -41,7 +41,8 @@ disable=
|
||||
# R0902: Too many instance attributes
|
||||
# R0903: Too few public methods (1/2)
|
||||
# R0904: Too many public methods
|
||||
W0141,W0142,R0201,R0901,R0902,R0903,R0904
|
||||
# R0913: Too many arguments
|
||||
W0141,W0142,R0201,R0901,R0902,R0903,R0904,R0913
|
||||
|
||||
|
||||
[REPORTS]
|
||||
@@ -137,7 +138,7 @@ bad-names=foo,bar,baz,toto,tutu,tata
|
||||
|
||||
# Regular expression which should only match functions or classes name which do
|
||||
# not require a docstring
|
||||
no-docstring-rgx=__.*__
|
||||
no-docstring-rgx=(__.*__|test_.*)
|
||||
|
||||
|
||||
[MISCELLANEOUS]
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from lxml import html, etree
|
||||
from lxml import html
|
||||
import re
|
||||
from django.http import HttpResponseBadRequest
|
||||
import logging
|
||||
import django.utils
|
||||
|
||||
## TODO store as array of { date, content } and override course_info_module.definition_from_xml
|
||||
## This should be in a class which inherits from XmlDescriptor
|
||||
# # TODO store as array of { date, content } and override course_info_module.definition_from_xml
|
||||
# # This should be in a class which inherits from XmlDescriptor
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_course_updates(location):
|
||||
@@ -26,9 +28,11 @@ def get_course_updates(location):
|
||||
|
||||
# purely to handle free formed updates not done via editor. Actually kills them, but at least doesn't break.
|
||||
try:
|
||||
course_html_parsed = etree.fromstring(course_updates.data)
|
||||
except etree.XMLSyntaxError:
|
||||
course_html_parsed = etree.fromstring("<ol></ol>")
|
||||
course_html_parsed = html.fromstring(course_updates.data)
|
||||
except:
|
||||
log.error("Cannot parse: " + course_updates.data)
|
||||
escaped = django.utils.html.escape(course_updates.data)
|
||||
course_html_parsed = html.fromstring("<ol><li>" + escaped + "</li></ol>")
|
||||
|
||||
# Confirm that root is <ol>, iterate over <li>, pull out <h2> subs and then rest of val
|
||||
course_upd_collection = []
|
||||
@@ -64,9 +68,11 @@ def update_course_updates(location, update, passed_id=None):
|
||||
|
||||
# purely to handle free formed updates not done via editor. Actually kills them, but at least doesn't break.
|
||||
try:
|
||||
course_html_parsed = etree.fromstring(course_updates.data)
|
||||
except etree.XMLSyntaxError:
|
||||
course_html_parsed = etree.fromstring("<ol></ol>")
|
||||
course_html_parsed = html.fromstring(course_updates.data)
|
||||
except:
|
||||
log.error("Cannot parse: " + course_updates.data)
|
||||
escaped = django.utils.html.escape(course_updates.data)
|
||||
course_html_parsed = html.fromstring("<ol><li>" + escaped + "</li></ol>")
|
||||
|
||||
# No try/catch b/c failure generates an error back to client
|
||||
new_html_parsed = html.fromstring('<li><h2>' + update['date'] + '</h2>' + update['content'] + '</li>')
|
||||
@@ -85,12 +91,19 @@ def update_course_updates(location, update, passed_id=None):
|
||||
passed_id = course_updates.location.url() + "/" + str(idx)
|
||||
|
||||
# update db record
|
||||
course_updates.data = etree.tostring(course_html_parsed)
|
||||
course_updates.data = html.tostring(course_html_parsed)
|
||||
modulestore('direct').update_item(location, course_updates.data)
|
||||
|
||||
return {"id" : passed_id,
|
||||
"date" : update['date'],
|
||||
"content" :update['content']}
|
||||
if (len(new_html_parsed) == 1):
|
||||
content = new_html_parsed[0].tail
|
||||
else:
|
||||
content = "\n".join([html.tostring(ele)
|
||||
for ele in new_html_parsed[1:]])
|
||||
|
||||
return {"id": passed_id,
|
||||
"date": update['date'],
|
||||
"content": content}
|
||||
|
||||
|
||||
def delete_course_update(location, update, passed_id):
|
||||
"""
|
||||
@@ -108,9 +121,11 @@ def delete_course_update(location, update, passed_id):
|
||||
# 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 = etree.fromstring(course_updates.data)
|
||||
except etree.XMLSyntaxError:
|
||||
course_html_parsed = etree.fromstring("<ol></ol>")
|
||||
course_html_parsed = html.fromstring(course_updates.data)
|
||||
except:
|
||||
log.error("Cannot parse: " + course_updates.data)
|
||||
escaped = django.utils.html.escape(course_updates.data)
|
||||
course_html_parsed = html.fromstring("<ol><li>" + escaped + "</li></ol>")
|
||||
|
||||
if course_html_parsed.tag == 'ol':
|
||||
# ??? Should this use the id in the json or in the url or does it matter?
|
||||
@@ -121,7 +136,7 @@ def delete_course_update(location, update, passed_id):
|
||||
course_html_parsed.remove(element_to_delete)
|
||||
|
||||
# update db record
|
||||
course_updates.data = etree.tostring(course_html_parsed)
|
||||
course_updates.data = html.tostring(course_html_parsed)
|
||||
store = modulestore('direct')
|
||||
store.update_item(location, course_updates.data)
|
||||
|
||||
@@ -132,7 +147,6 @@ def get_idx(passed_id):
|
||||
"""
|
||||
From the url w/ idx appended, get the idx.
|
||||
"""
|
||||
# TODO compile this regex into a class static and reuse for each call
|
||||
idx_matcher = re.search(r'.*/(\d+)$', passed_id)
|
||||
idx_matcher = re.search(r'.*?/?(\d+)$', passed_id)
|
||||
if idx_matcher:
|
||||
return int(idx_matcher.group(1))
|
||||
|
||||
24
cms/djangoapps/contentstore/features/checklists.feature
Normal file
24
cms/djangoapps/contentstore/features/checklists.feature
Normal file
@@ -0,0 +1,24 @@
|
||||
Feature: Course checklists
|
||||
|
||||
Scenario: A course author sees checklists defined by edX
|
||||
Given I have opened a new course in Studio
|
||||
When I select Checklists from the Tools menu
|
||||
Then I see the four default edX checklists
|
||||
|
||||
Scenario: A course author can mark tasks as complete
|
||||
Given I have opened Checklists
|
||||
Then I can check and uncheck tasks in a checklist
|
||||
And They are correctly selected after I reload the page
|
||||
|
||||
Scenario: A task can link to a location within Studio
|
||||
Given I have opened Checklists
|
||||
When I select a link to the course outline
|
||||
Then I am brought to the course outline page
|
||||
And I press the browser back button
|
||||
Then I am brought back to the course outline in the correct state
|
||||
|
||||
Scenario: A task can link to a location outside Studio
|
||||
Given I have opened Checklists
|
||||
When I select a link to help page
|
||||
Then I am brought to the help page in a new window
|
||||
|
||||
119
cms/djangoapps/contentstore/features/checklists.py
Normal file
119
cms/djangoapps/contentstore/features/checklists.py
Normal file
@@ -0,0 +1,119 @@
|
||||
from lettuce import world, step
|
||||
from common import *
|
||||
from terrain.steps import reload_the_page
|
||||
|
||||
############### ACTIONS ####################
|
||||
@step('I select Checklists from the Tools menu$')
|
||||
def i_select_checklists(step):
|
||||
expand_icon_css = 'li.nav-course-tools i.icon-expand'
|
||||
if world.browser.is_element_present_by_css(expand_icon_css):
|
||||
css_click(expand_icon_css)
|
||||
link_css = 'li.nav-course-tools-checklists a'
|
||||
css_click(link_css)
|
||||
|
||||
|
||||
@step('I have opened Checklists$')
|
||||
def i_have_opened_checklists(step):
|
||||
step.given('I have opened a new course in Studio')
|
||||
step.given('I select Checklists from the Tools menu')
|
||||
|
||||
|
||||
@step('I see the four default edX checklists$')
|
||||
def i_see_default_checklists(step):
|
||||
checklists = css_find('.checklist-title')
|
||||
assert_equal(4, len(checklists))
|
||||
assert_true(checklists[0].text.endswith('Getting Started With Studio'))
|
||||
assert_true(checklists[1].text.endswith('Draft a Rough Course Outline'))
|
||||
assert_true(checklists[2].text.endswith("Explore edX\'s Support Tools"))
|
||||
assert_true(checklists[3].text.endswith('Draft Your Course About Page'))
|
||||
|
||||
|
||||
@step('I can check and uncheck tasks in a checklist$')
|
||||
def i_can_check_and_uncheck_tasks(step):
|
||||
# Use the 2nd checklist as a reference
|
||||
verifyChecklist2Status(0, 7, 0)
|
||||
toggleTask(1, 0)
|
||||
verifyChecklist2Status(1, 7, 14)
|
||||
toggleTask(1, 3)
|
||||
verifyChecklist2Status(2, 7, 29)
|
||||
toggleTask(1, 6)
|
||||
verifyChecklist2Status(3, 7, 43)
|
||||
toggleTask(1, 3)
|
||||
verifyChecklist2Status(2, 7, 29)
|
||||
|
||||
|
||||
@step('They are correctly selected after I reload the page$')
|
||||
def tasks_correctly_selected_after_reload(step):
|
||||
reload_the_page(step)
|
||||
verifyChecklist2Status(2, 7, 29)
|
||||
# verify that task 7 is still selected by toggling its checkbox state and making sure that it deselects
|
||||
toggleTask(1, 6)
|
||||
verifyChecklist2Status(1, 7, 14)
|
||||
|
||||
|
||||
@step('I select a link to the course outline$')
|
||||
def i_select_a_link_to_the_course_outline(step):
|
||||
clickActionLink(1, 0, 'Edit Course Outline')
|
||||
|
||||
|
||||
@step('I am brought to the course outline page$')
|
||||
def i_am_brought_to_course_outline(step):
|
||||
assert_equal('Course Outline', css_find('.outline .title-1')[0].text)
|
||||
assert_equal(1, len(world.browser.windows))
|
||||
|
||||
|
||||
@step('I am brought back to the course outline in the correct state$')
|
||||
def i_am_brought_back_to_course_outline(step):
|
||||
step.given('I see the four default edX checklists')
|
||||
# In a previous step, we selected (1, 0) in order to click the 'Edit Course Outline' link.
|
||||
# Make sure the task is still showing as selected (there was a caching bug with the collection).
|
||||
verifyChecklist2Status(1, 7, 14)
|
||||
|
||||
|
||||
@step('I select a link to help page$')
|
||||
def i_select_a_link_to_the_help_page(step):
|
||||
clickActionLink(2, 0, 'Visit Studio Help')
|
||||
|
||||
|
||||
@step('I am brought to the help page in a new window$')
|
||||
def i_am_brought_to_help_page_in_new_window(step):
|
||||
step.given('I see the four default edX checklists')
|
||||
windows = world.browser.windows
|
||||
assert_equal(2, len(windows))
|
||||
world.browser.switch_to_window(windows[1])
|
||||
assert_equal('http://help.edge.edx.org/', world.browser.url)
|
||||
|
||||
|
||||
|
||||
|
||||
############### HELPER METHODS ####################
|
||||
def verifyChecklist2Status(completed, total, percentage):
|
||||
def verify_count(driver):
|
||||
try:
|
||||
statusCount = css_find('#course-checklist1 .status-count').first
|
||||
return statusCount.text == str(completed)
|
||||
except StaleElementReferenceException:
|
||||
return False
|
||||
|
||||
wait_for(verify_count)
|
||||
assert_equal(str(total), css_find('#course-checklist1 .status-amount').first.text)
|
||||
# Would like to check the CSS width, but not sure how to do that.
|
||||
assert_equal(str(percentage), css_find('#course-checklist1 .viz-checklist-status-value .int').first.text)
|
||||
|
||||
|
||||
def toggleTask(checklist, task):
|
||||
css_click('#course-checklist' + str(checklist) +'-task' + str(task))
|
||||
|
||||
|
||||
def clickActionLink(checklist, task, actionText):
|
||||
# toggle checklist item to make sure that the link button is showing
|
||||
toggleTask(checklist, task)
|
||||
action_link = css_find('#course-checklist' + str(checklist) + ' a')[task]
|
||||
|
||||
# text will be empty initially, wait for it to populate
|
||||
def verify_action_link_text(driver):
|
||||
return action_link.text == actionText
|
||||
|
||||
wait_for(verify_action_link_text)
|
||||
action_link.click()
|
||||
|
||||
@@ -7,8 +7,6 @@ from selenium.common.exceptions import WebDriverException, StaleElementReference
|
||||
from selenium.webdriver.support import expected_conditions as EC
|
||||
from selenium.webdriver.common.by import By
|
||||
|
||||
from terrain.factories import UserFactory, RegistrationFactory, UserProfileFactory
|
||||
from terrain.factories import CourseFactory, GroupFactory
|
||||
from xmodule.modulestore.django import _MODULESTORES, modulestore
|
||||
from xmodule.templates import update_templates
|
||||
from auth.authz import get_user_by_email
|
||||
@@ -61,7 +59,7 @@ def create_studio_user(
|
||||
email='robot+studio@edx.org',
|
||||
password='test',
|
||||
is_staff=False):
|
||||
studio_user = UserFactory.build(
|
||||
studio_user = world.UserFactory.build(
|
||||
username=uname,
|
||||
email=email,
|
||||
password=password,
|
||||
@@ -69,11 +67,11 @@ def create_studio_user(
|
||||
studio_user.set_password(password)
|
||||
studio_user.save()
|
||||
|
||||
registration = RegistrationFactory(user=studio_user)
|
||||
registration = world.RegistrationFactory(user=studio_user)
|
||||
registration.register(studio_user)
|
||||
registration.activate()
|
||||
|
||||
user_profile = UserProfileFactory(user=studio_user)
|
||||
user_profile = world.UserProfileFactory(user=studio_user)
|
||||
|
||||
|
||||
def flush_xmodule_store():
|
||||
@@ -175,11 +173,11 @@ def log_into_studio(
|
||||
|
||||
|
||||
def create_a_course():
|
||||
c = CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course')
|
||||
c = world.CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course')
|
||||
|
||||
# Add the user to the instructor group of the course
|
||||
# so they will have the permissions to see it in studio
|
||||
g = GroupFactory.create(name='instructor_MITx/999/Robot_Super_Course')
|
||||
g = world.GroupFactory.create(name='instructor_MITx/999/Robot_Super_Course')
|
||||
u = get_user_by_email('robot+studio@edx.org')
|
||||
u.groups.add(g)
|
||||
u.save()
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import factory
|
||||
from student.models import User, UserProfile, Registration
|
||||
from datetime import datetime
|
||||
import uuid
|
||||
|
||||
|
||||
class UserProfileFactory(factory.Factory):
|
||||
FACTORY_FOR = UserProfile
|
||||
|
||||
user = None
|
||||
name = 'Robot Studio'
|
||||
courseware = 'course.xml'
|
||||
|
||||
|
||||
class RegistrationFactory(factory.Factory):
|
||||
FACTORY_FOR = Registration
|
||||
|
||||
user = None
|
||||
activation_key = uuid.uuid4().hex
|
||||
|
||||
|
||||
class UserFactory(factory.Factory):
|
||||
FACTORY_FOR = User
|
||||
|
||||
username = 'robot-studio'
|
||||
email = 'robot+studio@edx.org'
|
||||
password = 'test'
|
||||
first_name = 'Robot'
|
||||
last_name = 'Studio'
|
||||
is_staff = False
|
||||
is_active = True
|
||||
is_superuser = False
|
||||
last_login = datetime.now()
|
||||
date_joined = datetime.now()
|
||||
@@ -1,5 +1,4 @@
|
||||
from lettuce import world, step
|
||||
from terrain.factories import *
|
||||
from common import *
|
||||
from nose.tools import assert_true, assert_false, assert_equal
|
||||
|
||||
@@ -10,15 +9,15 @@ logger = getLogger(__name__)
|
||||
@step(u'I have a course with no sections$')
|
||||
def have_a_course(step):
|
||||
clear_courses()
|
||||
course = CourseFactory.create()
|
||||
course = world.CourseFactory.create()
|
||||
|
||||
|
||||
@step(u'I have a course with 1 section$')
|
||||
def have_a_course_with_1_section(step):
|
||||
clear_courses()
|
||||
course = CourseFactory.create()
|
||||
section = ItemFactory.create(parent_location=course.location)
|
||||
subsection1 = ItemFactory.create(
|
||||
course = world.CourseFactory.create()
|
||||
section = world.ItemFactory.create(parent_location=course.location)
|
||||
subsection1 = world.ItemFactory.create(
|
||||
parent_location=section.location,
|
||||
template='i4x://edx/templates/sequential/Empty',
|
||||
display_name='Subsection One',)
|
||||
@@ -27,20 +26,20 @@ def have_a_course_with_1_section(step):
|
||||
@step(u'I have a course with multiple sections$')
|
||||
def have_a_course_with_two_sections(step):
|
||||
clear_courses()
|
||||
course = CourseFactory.create()
|
||||
section = ItemFactory.create(parent_location=course.location)
|
||||
subsection1 = ItemFactory.create(
|
||||
course = world.CourseFactory.create()
|
||||
section = world.ItemFactory.create(parent_location=course.location)
|
||||
subsection1 = world.ItemFactory.create(
|
||||
parent_location=section.location,
|
||||
template='i4x://edx/templates/sequential/Empty',
|
||||
display_name='Subsection One',)
|
||||
section2 = ItemFactory.create(
|
||||
section2 = world.ItemFactory.create(
|
||||
parent_location=course.location,
|
||||
display_name='Section Two',)
|
||||
subsection2 = ItemFactory.create(
|
||||
subsection2 = world.ItemFactory.create(
|
||||
parent_location=section2.location,
|
||||
template='i4x://edx/templates/sequential/Empty',
|
||||
display_name='Subsection Alpha',)
|
||||
subsection3 = ItemFactory.create(
|
||||
subsection3 = world.ItemFactory.create(
|
||||
parent_location=section2.location,
|
||||
template='i4x://edx/templates/sequential/Empty',
|
||||
display_name='Subsection Beta',)
|
||||
|
||||
96
cms/djangoapps/contentstore/tests/test_checklists.py
Normal file
96
cms/djangoapps/contentstore/tests/test_checklists.py
Normal file
@@ -0,0 +1,96 @@
|
||||
""" Unit tests for checklist methods in views.py. """
|
||||
from contentstore.utils import get_modulestore, get_url_reverse
|
||||
from contentstore.tests.test_course_settings import CourseTestCase
|
||||
from xmodule.modulestore.inheritance import own_metadata
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
from django.core.urlresolvers import reverse
|
||||
import json
|
||||
|
||||
|
||||
class ChecklistTestCase(CourseTestCase):
|
||||
""" Test for checklist get and put methods. """
|
||||
def setUp(self):
|
||||
""" Creates the test course. """
|
||||
super(ChecklistTestCase, self).setUp()
|
||||
self.course = CourseFactory.create(org='mitX', number='333', display_name='Checklists Course')
|
||||
|
||||
def get_persisted_checklists(self):
|
||||
""" Returns the checklists as persisted in the modulestore. """
|
||||
modulestore = get_modulestore(self.course.location)
|
||||
return modulestore.get_item(self.course.location).checklists
|
||||
|
||||
def test_get_checklists(self):
|
||||
""" Tests the get checklists method. """
|
||||
checklists_url = get_url_reverse('Checklists', self.course)
|
||||
response = self.client.get(checklists_url)
|
||||
self.assertContains(response, "Getting Started With Studio")
|
||||
payload = response.content
|
||||
|
||||
# Now delete the checklists from the course and verify they get repopulated (for courses
|
||||
# created before checklists were introduced).
|
||||
self.course.checklists = None
|
||||
modulestore = get_modulestore(self.course.location)
|
||||
modulestore.update_metadata(self.course.location, own_metadata(self.course))
|
||||
self.assertEquals(self.get_persisted_checklists(), None)
|
||||
response = self.client.get(checklists_url)
|
||||
self.assertEquals(payload, response.content)
|
||||
|
||||
def test_update_checklists_no_index(self):
|
||||
""" No checklist index, should return all of them. """
|
||||
update_url = reverse('checklists_updates', kwargs={
|
||||
'org': self.course.location.org,
|
||||
'course': self.course.location.course,
|
||||
'name': self.course.location.name})
|
||||
|
||||
returned_checklists = json.loads(self.client.get(update_url).content)
|
||||
self.assertListEqual(self.get_persisted_checklists(), returned_checklists)
|
||||
|
||||
def test_update_checklists_index_ignored_on_get(self):
|
||||
""" Checklist index ignored on get. """
|
||||
update_url = reverse('checklists_updates', kwargs={'org': self.course.location.org,
|
||||
'course': self.course.location.course,
|
||||
'name': self.course.location.name,
|
||||
'checklist_index': 1})
|
||||
|
||||
returned_checklists = json.loads(self.client.get(update_url).content)
|
||||
self.assertListEqual(self.get_persisted_checklists(), returned_checklists)
|
||||
|
||||
def test_update_checklists_post_no_index(self):
|
||||
""" No checklist index, will error on post. """
|
||||
update_url = reverse('checklists_updates', kwargs={'org': self.course.location.org,
|
||||
'course': self.course.location.course,
|
||||
'name': self.course.location.name})
|
||||
response = self.client.post(update_url)
|
||||
self.assertContains(response, 'Could not save checklist', status_code=400)
|
||||
|
||||
def test_update_checklists_index_out_of_range(self):
|
||||
""" Checklist index out of range, will error on post. """
|
||||
update_url = reverse('checklists_updates', kwargs={'org': self.course.location.org,
|
||||
'course': self.course.location.course,
|
||||
'name': self.course.location.name,
|
||||
'checklist_index': 100})
|
||||
response = self.client.post(update_url)
|
||||
self.assertContains(response, 'Could not save checklist', status_code=400)
|
||||
|
||||
def test_update_checklists_index(self):
|
||||
""" Check that an update of a particular checklist works. """
|
||||
update_url = reverse('checklists_updates', kwargs={'org': self.course.location.org,
|
||||
'course': self.course.location.course,
|
||||
'name': self.course.location.name,
|
||||
'checklist_index': 2})
|
||||
payload = self.course.checklists[2]
|
||||
self.assertFalse(payload.get('is_checked'))
|
||||
payload['is_checked'] = True
|
||||
|
||||
returned_checklist = json.loads(self.client.post(update_url, json.dumps(payload), "application/json").content)
|
||||
self.assertTrue(returned_checklist.get('is_checked'))
|
||||
self.assertEqual(self.get_persisted_checklists()[2], returned_checklist)
|
||||
|
||||
def test_update_checklists_delete_unsupported(self):
|
||||
""" Delete operation is not supported. """
|
||||
update_url = reverse('checklists_updates', kwargs={'org': self.course.location.org,
|
||||
'course': self.course.location.course,
|
||||
'name': self.course.location.name,
|
||||
'checklist_index': 100})
|
||||
response = self.client.delete(update_url)
|
||||
self.assertContains(response, 'Unsupported request', status_code=400)
|
||||
@@ -101,6 +101,20 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
|
||||
self.assertEqual(reverse_tabs, course_tabs)
|
||||
|
||||
def test_import_polls(self):
|
||||
import_from_xml(modulestore(), 'common/test/data/', ['full'])
|
||||
|
||||
module_store = modulestore('direct')
|
||||
found = False
|
||||
|
||||
item = None
|
||||
items = module_store.get_items(['i4x', 'edX', 'full', 'poll_question', None, None])
|
||||
found = len(items) > 0
|
||||
|
||||
self.assertTrue(found)
|
||||
# check that there's actually content in the 'question' field
|
||||
self.assertGreater(len(items[0].question),0)
|
||||
|
||||
def test_delete(self):
|
||||
import_from_xml(modulestore(), 'common/test/data/', ['full'])
|
||||
|
||||
|
||||
@@ -22,25 +22,61 @@ from xmodule.modulestore.tests.factories import CourseFactory
|
||||
from models.settings.course_metadata import CourseMetadata
|
||||
from xmodule.modulestore.xml_importer import import_from_xml
|
||||
from xmodule.modulestore.django import modulestore
|
||||
import time
|
||||
|
||||
|
||||
# YYYY-MM-DDThh:mm:ss.s+/-HH:MM
|
||||
class ConvertersTestCase(TestCase):
|
||||
@staticmethod
|
||||
def struct_to_datetime(struct_time):
|
||||
return datetime.datetime(struct_time.tm_year, struct_time.tm_mon, struct_time.tm_mday, struct_time.tm_hour,
|
||||
struct_time.tm_min, struct_time.tm_sec, tzinfo=UTC())
|
||||
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))
|
||||
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))
|
||||
'''Test conversion from iso compatible date strings to struct_time'''
|
||||
self.compare_dates(converters.jsdate_to_time("2013-01-01"),
|
||||
converters.jsdate_to_time("2012-12-31"),
|
||||
datetime.timedelta(days=1))
|
||||
self.compare_dates(converters.jsdate_to_time("2013-01-01T00"),
|
||||
converters.jsdate_to_time("2012-12-31T23"),
|
||||
datetime.timedelta(hours=1))
|
||||
self.compare_dates(converters.jsdate_to_time("2013-01-01T00:00"),
|
||||
converters.jsdate_to_time("2012-12-31T23:59"),
|
||||
datetime.timedelta(minutes=1))
|
||||
self.compare_dates(converters.jsdate_to_time("2013-01-01T00:00:00"),
|
||||
converters.jsdate_to_time("2012-12-31T23:59:59"),
|
||||
datetime.timedelta(seconds=1))
|
||||
self.compare_dates(converters.jsdate_to_time("2013-01-01T00:00:00Z"),
|
||||
converters.jsdate_to_time("2012-12-31T23:59:59Z"),
|
||||
datetime.timedelta(seconds=1))
|
||||
self.compare_dates(
|
||||
converters.jsdate_to_time("2012-12-31T23:00:01-01:00"),
|
||||
converters.jsdate_to_time("2013-01-01T00:00:00+01:00"),
|
||||
datetime.timedelta(hours=1, seconds=1))
|
||||
|
||||
def test_struct_to_iso(self):
|
||||
'''
|
||||
Test converting time reprs to iso dates
|
||||
'''
|
||||
self.assertEqual(
|
||||
converters.time_to_isodate(
|
||||
time.strptime("2012-12-31T23:59:59Z", "%Y-%m-%dT%H:%M:%SZ")),
|
||||
"2012-12-31T23:59:59Z")
|
||||
self.assertEqual(
|
||||
converters.time_to_isodate(
|
||||
jsdate_to_time("2012-12-31T23:59:59Z")),
|
||||
"2012-12-31T23:59:59Z")
|
||||
self.assertEqual(
|
||||
converters.time_to_isodate(
|
||||
jsdate_to_time("2012-12-31T23:00:01-01:00")),
|
||||
"2013-01-01T00:00:01Z")
|
||||
|
||||
|
||||
class CourseTestCase(ModuleStoreTestCase):
|
||||
@@ -104,7 +140,7 @@ class CourseDetailsTestCase(CourseTestCase):
|
||||
self.assertIsNone(jsondetails['effort'], "effort somehow initialized")
|
||||
|
||||
def test_update_and_fetch(self):
|
||||
## NOTE: I couldn't figure out how to validly test time setting w/ all the conversions
|
||||
# # NOTE: I couldn't figure out how to validly test time setting w/ all the conversions
|
||||
jsondetails = CourseDetails.fetch(self.course_location)
|
||||
jsondetails.syllabus = "<a href='foo'>bar</a>"
|
||||
# encode - decode to convert date fields and other data which changes form
|
||||
@@ -182,7 +218,7 @@ class CourseDetailsViewTest(CourseTestCase):
|
||||
details_encoded = jsdate_to_time(details[field])
|
||||
dt2 = ConvertersTestCase.struct_to_datetime(details_encoded)
|
||||
|
||||
expected_delta = datetime.timedelta(0)
|
||||
expected_delta = datetime.timedelta(0)
|
||||
self.assertEqual(dt1 - dt2, expected_delta, str(dt1) + "!=" + str(dt2) + " at " + context)
|
||||
else:
|
||||
self.fail(field + " missing from encoded but in details at " + context)
|
||||
@@ -269,7 +305,7 @@ class CourseMetadataEditingTest(CourseTestCase):
|
||||
CourseTestCase.setUp(self)
|
||||
# add in the full class too
|
||||
import_from_xml(modulestore(), 'common/test/data/', ['full'])
|
||||
self.fullcourse_location = Location(['i4x','edX','full','course','6.002_Spring_2012', None])
|
||||
self.fullcourse_location = Location(['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None])
|
||||
|
||||
|
||||
def test_fetch_initial_fields(self):
|
||||
|
||||
@@ -1,31 +1,145 @@
|
||||
'''unit tests for course_info views and models.'''
|
||||
from contentstore.tests.test_course_settings import CourseTestCase
|
||||
from django.core.urlresolvers import reverse
|
||||
import json
|
||||
|
||||
|
||||
class CourseUpdateTest(CourseTestCase):
|
||||
'''The do all and end all of unit test cases.'''
|
||||
def test_course_update(self):
|
||||
'''Go through each interface and ensure it works.'''
|
||||
# first get the update to force the creation
|
||||
url = reverse('course_info', kwargs={'org': self.course_location.org, 'course': self.course_location.course,
|
||||
'name': self.course_location.name})
|
||||
url = reverse('course_info',
|
||||
kwargs={'org': self.course_location.org,
|
||||
'course': self.course_location.course,
|
||||
'name': self.course_location.name})
|
||||
self.client.get(url)
|
||||
|
||||
content = '<iframe width="560" height="315" src="http://www.youtube.com/embed/RocY-Jd93XU" frameborder="0"></iframe>'
|
||||
init_content = '<iframe width="560" height="315" src="http://www.youtube.com/embed/RocY-Jd93XU" frameborder="0">'
|
||||
content = init_content + '</iframe>'
|
||||
payload = {'content': content,
|
||||
'date': 'January 8, 2013'}
|
||||
url = reverse('course_info', kwargs={'org': self.course_location.org, 'course': self.course_location.course,
|
||||
'provided_id': ''})
|
||||
url = reverse('course_info_json',
|
||||
kwargs={'org': self.course_location.org,
|
||||
'course': self.course_location.course,
|
||||
'provided_id': ''})
|
||||
|
||||
resp = self.client.post(url, json.dumps(payload), "application/json")
|
||||
|
||||
payload = json.loads(resp.content)
|
||||
|
||||
self.assertHTMLEqual(content, payload['content'], "single iframe")
|
||||
self.assertHTMLEqual(payload['content'], content)
|
||||
|
||||
url = reverse('course_info', kwargs={'org': self.course_location.org, 'course': self.course_location.course,
|
||||
'provided_id': payload['id']})
|
||||
content += '<div>div <p>p</p></div>'
|
||||
first_update_url = reverse('course_info_json',
|
||||
kwargs={'org': self.course_location.org,
|
||||
'course': self.course_location.course,
|
||||
'provided_id': payload['id']})
|
||||
content += '<div>div <p>p<br/></p></div>'
|
||||
payload['content'] = content
|
||||
resp = self.client.post(first_update_url, json.dumps(payload),
|
||||
"application/json")
|
||||
|
||||
self.assertHTMLEqual(content, json.loads(resp.content)['content'],
|
||||
"iframe w/ div")
|
||||
|
||||
# now put in an evil update
|
||||
content = '<ol/>'
|
||||
payload = {'content': content,
|
||||
'date': 'January 11, 2013'}
|
||||
url = reverse('course_info_json',
|
||||
kwargs={'org': self.course_location.org,
|
||||
'course': self.course_location.course,
|
||||
'provided_id': ''})
|
||||
|
||||
resp = self.client.post(url, json.dumps(payload), "application/json")
|
||||
|
||||
self.assertHTMLEqual(content, json.loads(resp.content)['content'], "iframe w/ div")
|
||||
payload = json.loads(resp.content)
|
||||
|
||||
self.assertHTMLEqual(content, payload['content'], "self closing ol")
|
||||
|
||||
url = reverse('course_info_json',
|
||||
kwargs={'org': self.course_location.org,
|
||||
'course': self.course_location.course,
|
||||
'provided_id': ''})
|
||||
resp = self.client.get(url)
|
||||
payload = json.loads(resp.content)
|
||||
self.assertTrue(len(payload) == 2)
|
||||
|
||||
# can't test non-json paylod b/c expect_json throws error
|
||||
# try json w/o required fields
|
||||
self.assertContains(
|
||||
self.client.post(url, json.dumps({'garbage': 1}),
|
||||
"application/json"),
|
||||
'Failed to save', status_code=400)
|
||||
|
||||
# now try to update a non-existent update
|
||||
url = reverse('course_info_json',
|
||||
kwargs={'org': self.course_location.org,
|
||||
'course': self.course_location.course,
|
||||
'provided_id': '9'})
|
||||
content = 'blah blah'
|
||||
payload = {'content': content,
|
||||
'date': 'January 21, 2013'}
|
||||
self.assertContains(
|
||||
self.client.post(url, json.dumps(payload), "application/json"),
|
||||
'Failed to save', status_code=400)
|
||||
|
||||
# update w/ malformed html
|
||||
content = '<garbage tag No closing brace to force <span>error</span>'
|
||||
payload = {'content': content,
|
||||
'date': 'January 11, 2013'}
|
||||
url = reverse('course_info_json', kwargs={'org': self.course_location.org,
|
||||
'course': self.course_location.course,
|
||||
'provided_id': ''})
|
||||
|
||||
self.assertContains(
|
||||
self.client.post(url, json.dumps(payload), "application/json"),
|
||||
'<garbage')
|
||||
|
||||
# set to valid html which would break an xml parser
|
||||
content = "<p><br><br></p>"
|
||||
payload = {'content': content,
|
||||
'date': 'January 11, 2013'}
|
||||
url = reverse('course_info_json', kwargs={'org': self.course_location.org,
|
||||
'course': self.course_location.course,
|
||||
'provided_id': ''})
|
||||
|
||||
resp = self.client.post(url, json.dumps(payload), "application/json")
|
||||
payload = json.loads(resp.content)
|
||||
self.assertHTMLEqual(content, json.loads(resp.content)['content'])
|
||||
|
||||
# now try to delete a non-existent update
|
||||
url = reverse('course_info_json', kwargs={'org': self.course_location.org,
|
||||
'course': self.course_location.course,
|
||||
'provided_id': '19'})
|
||||
payload = {'content': content,
|
||||
'date': 'January 21, 2013'}
|
||||
self.assertContains(self.client.delete(url), "delete", status_code=400)
|
||||
|
||||
# now delete a real update
|
||||
content = 'blah blah'
|
||||
payload = {'content': content,
|
||||
'date': 'January 28, 2013'}
|
||||
url = reverse('course_info_json', kwargs={'org': self.course_location.org,
|
||||
'course': self.course_location.course,
|
||||
'provided_id': ''})
|
||||
resp = self.client.post(url, json.dumps(payload), "application/json")
|
||||
payload = json.loads(resp.content)
|
||||
this_id = payload['id']
|
||||
self.assertHTMLEqual(content, payload['content'], "single iframe")
|
||||
# first count the entries
|
||||
url = reverse('course_info_json',
|
||||
kwargs={'org': self.course_location.org,
|
||||
'course': self.course_location.course,
|
||||
'provided_id': ''})
|
||||
resp = self.client.get(url)
|
||||
payload = json.loads(resp.content)
|
||||
before_delete = len(payload)
|
||||
|
||||
url = reverse('course_info_json',
|
||||
kwargs={'org': self.course_location.org,
|
||||
'course': self.course_location.course,
|
||||
'provided_id': this_id})
|
||||
resp = self.client.delete(url)
|
||||
payload = json.loads(resp.content)
|
||||
self.assertTrue(len(payload) == before_delete - 1)
|
||||
|
||||
@@ -1,19 +1,72 @@
|
||||
from contentstore import utils
|
||||
""" Tests for utils. """
|
||||
from contentstore import utils
|
||||
import mock
|
||||
from django.test import TestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
from .utils import ModuleStoreTestCase
|
||||
|
||||
|
||||
class LMSLinksTestCase(TestCase):
|
||||
""" Tests for LMS links. """
|
||||
def about_page_test(self):
|
||||
""" Get URL for about page. """
|
||||
location = 'i4x', 'mitX', '101', 'course', 'test'
|
||||
utils.get_course_id = mock.Mock(return_value="mitX/101/test")
|
||||
link = utils.get_lms_link_for_about_page(location)
|
||||
self.assertEquals(link, "//localhost:8000/courses/mitX/101/test/about")
|
||||
|
||||
def ls_link_test(self):
|
||||
def lms_link_test(self):
|
||||
""" Tests get_lms_link_for_item. """
|
||||
location = 'i4x', 'mitX', '101', 'vertical', 'contacting_us'
|
||||
utils.get_course_id = mock.Mock(return_value="mitX/101/test")
|
||||
link = utils.get_lms_link_for_item(location, False)
|
||||
self.assertEquals(link, "//localhost:8000/courses/mitX/101/test/jump_to/i4x://mitX/101/vertical/contacting_us")
|
||||
link = utils.get_lms_link_for_item(location, True)
|
||||
self.assertEquals(link, "//preview.localhost:8000/courses/mitX/101/test/jump_to/i4x://mitX/101/vertical/contacting_us")
|
||||
self.assertEquals(
|
||||
link,
|
||||
"//preview.localhost:8000/courses/mitX/101/test/jump_to/i4x://mitX/101/vertical/contacting_us"
|
||||
)
|
||||
|
||||
|
||||
class UrlReverseTestCase(ModuleStoreTestCase):
|
||||
""" Tests for get_url_reverse """
|
||||
def test_CoursePageNames(self):
|
||||
""" Test the defined course pages. """
|
||||
course = CourseFactory.create(org='mitX', number='666', display_name='URL Reverse Course')
|
||||
|
||||
self.assertEquals(
|
||||
'/manage_users/i4x://mitX/666/course/URL_Reverse_Course',
|
||||
utils.get_url_reverse('ManageUsers', course)
|
||||
)
|
||||
|
||||
self.assertEquals(
|
||||
'/mitX/666/settings-details/URL_Reverse_Course',
|
||||
utils.get_url_reverse('SettingsDetails', course)
|
||||
)
|
||||
|
||||
self.assertEquals(
|
||||
'/mitX/666/settings-grading/URL_Reverse_Course',
|
||||
utils.get_url_reverse('SettingsGrading', course)
|
||||
)
|
||||
|
||||
self.assertEquals(
|
||||
'/mitX/666/course/URL_Reverse_Course',
|
||||
utils.get_url_reverse('CourseOutline', course)
|
||||
)
|
||||
|
||||
self.assertEquals(
|
||||
'/mitX/666/checklists/URL_Reverse_Course',
|
||||
utils.get_url_reverse('Checklists', course)
|
||||
)
|
||||
|
||||
def test_unknown_passes_through(self):
|
||||
""" Test that unknown values pass through. """
|
||||
course = CourseFactory.create(org='mitX', number='666', display_name='URL Reverse Course')
|
||||
self.assertEquals(
|
||||
'foobar',
|
||||
utils.get_url_reverse('foobar', course)
|
||||
)
|
||||
self.assertEquals(
|
||||
'https://edge.edx.org/courses/edX/edX101/How_to_Create_an_edX_Course/about',
|
||||
utils.get_url_reverse('https://edge.edx.org/courses/edX/edX101/How_to_Create_an_edX_Course/about', course)
|
||||
)
|
||||
@@ -2,6 +2,7 @@ from django.conf import settings
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
from django.core.urlresolvers import reverse
|
||||
|
||||
DIRECT_ONLY_CATEGORIES = ['course', 'chapter', 'sequential', 'about', 'static_tab', 'course_info']
|
||||
|
||||
@@ -158,3 +159,35 @@ def update_item(location, value):
|
||||
get_modulestore(location).delete_item(location)
|
||||
else:
|
||||
get_modulestore(location).update_item(location, value)
|
||||
|
||||
|
||||
def get_url_reverse(course_page_name, course_module):
|
||||
"""
|
||||
Returns the course URL link to the specified location. This value is suitable to use as an href link.
|
||||
|
||||
course_page_name should correspond to an attribute in CoursePageNames (for example, 'ManageUsers'
|
||||
or 'SettingsDetails'), or else it will simply be returned. This method passes back unknown values of
|
||||
course_page_names so that it can also be used for absolute (known) URLs.
|
||||
|
||||
course_module is used to obtain the location, org, course, and name properties for a course, if
|
||||
course_page_name corresponds to an attribute in CoursePageNames.
|
||||
"""
|
||||
url_name = getattr(CoursePageNames, course_page_name, None)
|
||||
ctx_loc = course_module.location
|
||||
|
||||
if CoursePageNames.ManageUsers == url_name:
|
||||
return reverse(url_name, kwargs={"location": ctx_loc})
|
||||
elif url_name in [CoursePageNames.SettingsDetails, CoursePageNames.SettingsGrading,
|
||||
CoursePageNames.CourseOutline, CoursePageNames.Checklists]:
|
||||
return reverse(url_name, kwargs={'org': ctx_loc.org, 'course': ctx_loc.course, 'name': ctx_loc.name})
|
||||
else:
|
||||
return course_page_name
|
||||
|
||||
|
||||
class CoursePageNames:
|
||||
""" Constants for pages that are recognized by get_url_reverse method. """
|
||||
ManageUsers = "manage_users"
|
||||
SettingsDetails = "settings_details"
|
||||
SettingsGrading = "settings_grading"
|
||||
CourseOutline = "course_index"
|
||||
Checklists = "checklists"
|
||||
|
||||
@@ -51,15 +51,15 @@ from xmodule.contentstore.content import StaticContent
|
||||
from auth.authz import is_user_in_course_group_role, get_users_in_course_group_by_role
|
||||
from auth.authz import get_user_by_email, add_user_to_course_group, remove_user_from_course_group
|
||||
from auth.authz import INSTRUCTOR_ROLE_NAME, STAFF_ROLE_NAME, create_all_course_groups
|
||||
from .utils import get_course_location_for_item, get_lms_link_for_item, compute_unit_state, get_date_display, UnitState, get_course_for_item
|
||||
from .utils import get_course_location_for_item, get_lms_link_for_item, compute_unit_state, \
|
||||
get_date_display, UnitState, get_course_for_item, get_url_reverse
|
||||
|
||||
from xmodule.modulestore.xml_importer import import_from_xml
|
||||
from contentstore.course_info_model import get_course_updates,\
|
||||
from contentstore.course_info_model import get_course_updates, \
|
||||
update_course_updates, delete_course_update
|
||||
from cache_toolbox.core import del_cached_content
|
||||
from xmodule.timeparse import stringify_time
|
||||
from contentstore.module_info_model import get_module_info, set_module_info
|
||||
from models.settings.course_details import CourseDetails,\
|
||||
from models.settings.course_details import CourseDetails, \
|
||||
CourseSettingsEncoder
|
||||
from models.settings.course_grading import CourseGradingModel
|
||||
from contentstore.utils import get_modulestore
|
||||
@@ -73,7 +73,7 @@ log = logging.getLogger(__name__)
|
||||
|
||||
COMPONENT_TYPES = ['customtag', 'discussion', 'html', 'problem', 'video']
|
||||
|
||||
ADVANCED_COMPONENT_TYPES = ['annotatable','combinedopenended', 'peergrading']
|
||||
ADVANCED_COMPONENT_TYPES = ['annotatable', 'combinedopenended', 'peergrading']
|
||||
ADVANCED_COMPONENT_CATEGORY = 'advanced'
|
||||
ADVANCED_COMPONENT_POLICY_KEY = 'advanced_modules'
|
||||
|
||||
@@ -141,10 +141,7 @@ def index(request):
|
||||
return render_to_response('index.html', {
|
||||
'new_course_template': Location('i4x', 'edx', 'templates', 'course', 'Empty'),
|
||||
'courses': [(course.display_name,
|
||||
reverse('course_index', args=[
|
||||
course.location.org,
|
||||
course.location.course,
|
||||
course.location.name]),
|
||||
get_url_reverse('CourseOutline', course),
|
||||
get_lms_link_for_item(course.location, course_id=course.location.course_id))
|
||||
for course in courses],
|
||||
'user': request.user,
|
||||
@@ -181,19 +178,15 @@ def course_index(request, org, course, name):
|
||||
|
||||
org, course, name: Attributes of the Location for the item to edit
|
||||
"""
|
||||
location = ['i4x', org, course, 'course', name]
|
||||
|
||||
# check that logged in user has permissions to this item
|
||||
if not has_access(request.user, location):
|
||||
raise PermissionDenied()
|
||||
location = get_location_and_verify_access(request, org, course, name)
|
||||
|
||||
lms_link = get_lms_link_for_item(location)
|
||||
|
||||
upload_asset_callback_url = reverse('upload_asset', kwargs={
|
||||
'org': org,
|
||||
'course': course,
|
||||
'coursename': name
|
||||
})
|
||||
'org': org,
|
||||
'course': course,
|
||||
'coursename': name
|
||||
})
|
||||
|
||||
course = modulestore().get_item(location)
|
||||
sections = course.get_children()
|
||||
@@ -249,7 +242,7 @@ def edit_subsection(request, location):
|
||||
for field
|
||||
in item.fields
|
||||
if field.name not in ['display_name', 'start', 'due', 'format'] and
|
||||
field.scope == Scope.settings
|
||||
field.scope == Scope.settings
|
||||
)
|
||||
|
||||
can_view_live = False
|
||||
@@ -261,18 +254,18 @@ def edit_subsection(request, location):
|
||||
break
|
||||
|
||||
return render_to_response('edit_subsection.html',
|
||||
{'subsection': item,
|
||||
'context_course': course,
|
||||
'create_new_unit_template': Location('i4x', 'edx', 'templates', 'vertical', 'Empty'),
|
||||
'lms_link': lms_link,
|
||||
'preview_link': preview_link,
|
||||
'course_graders': json.dumps(CourseGradingModel.fetch(course.location).graders),
|
||||
'parent_location': course.location,
|
||||
'parent_item': parent,
|
||||
'policy_metadata': policy_metadata,
|
||||
'subsection_units': subsection_units,
|
||||
'can_view_live': can_view_live
|
||||
})
|
||||
{'subsection': item,
|
||||
'context_course': course,
|
||||
'create_new_unit_template': Location('i4x', 'edx', 'templates', 'vertical', 'Empty'),
|
||||
'lms_link': lms_link,
|
||||
'preview_link': preview_link,
|
||||
'course_graders': json.dumps(CourseGradingModel.fetch(course.location).graders),
|
||||
'parent_location': course.location,
|
||||
'parent_item': parent,
|
||||
'policy_metadata': policy_metadata,
|
||||
'subsection_units': subsection_units,
|
||||
'can_view_live': can_view_live
|
||||
})
|
||||
|
||||
|
||||
@login_required
|
||||
@@ -322,7 +315,7 @@ def edit_unit(request, location):
|
||||
category = ADVANCED_COMPONENT_CATEGORY
|
||||
|
||||
if category in component_types:
|
||||
#This is a hack to create categories for different xmodules
|
||||
# This is a hack to create categories for different xmodules
|
||||
component_templates[category].append((
|
||||
template.display_name_with_default,
|
||||
template.location.url(),
|
||||
@@ -417,7 +410,7 @@ def assignment_type_update(request, org, course, category, name):
|
||||
if request.method == 'GET':
|
||||
return HttpResponse(json.dumps(CourseGradingModel.get_section_grader_type(location)),
|
||||
mimetype="application/json")
|
||||
elif request.method == 'POST': # post or put, doesn't matter.
|
||||
elif request.method == 'POST': # post or put, doesn't matter.
|
||||
return HttpResponse(json.dumps(CourseGradingModel.update_section_grader_type(location, request.POST)),
|
||||
mimetype="application/json")
|
||||
|
||||
@@ -798,9 +791,7 @@ def upload_asset(request, org, course, coursename):
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
# construct a location from the passed in path
|
||||
location = ['i4x', org, course, 'course', coursename]
|
||||
if not has_access(request.user, location):
|
||||
return HttpResponseForbidden()
|
||||
location = get_location_and_verify_access(request, org, course, coursename)
|
||||
|
||||
# Does the course actually exist?!? Get anything from it to prove its existance
|
||||
|
||||
@@ -831,7 +822,7 @@ def upload_asset(request, org, course, coursename):
|
||||
if thumbnail_content is not None:
|
||||
content.thumbnail_location = thumbnail_location
|
||||
|
||||
#then commit the content
|
||||
# then commit the content
|
||||
contentstore().save(content)
|
||||
del_cached_content(content.location)
|
||||
|
||||
@@ -874,7 +865,7 @@ def manage_users(request, location):
|
||||
})
|
||||
|
||||
|
||||
def create_json_response(errmsg = None):
|
||||
def create_json_response(errmsg=None):
|
||||
if errmsg is not None:
|
||||
resp = HttpResponse(json.dumps({'Status': 'Failed', 'ErrMsg': errmsg}))
|
||||
else:
|
||||
@@ -952,11 +943,7 @@ def landing(request, org, course, coursename):
|
||||
@ensure_csrf_cookie
|
||||
def static_pages(request, org, course, coursename):
|
||||
|
||||
location = ['i4x', org, course, 'course', coursename]
|
||||
|
||||
# check that logged in user has permissions to this item
|
||||
if not has_access(request.user, location):
|
||||
raise PermissionDenied()
|
||||
location = get_location_and_verify_access(request, org, course, coursename)
|
||||
|
||||
course = modulestore().get_item(location)
|
||||
|
||||
@@ -1068,11 +1055,7 @@ def course_info(request, org, course, name, provided_id=None):
|
||||
|
||||
org, course, name: Attributes of the Location for the item to edit
|
||||
"""
|
||||
location = ['i4x', org, course, 'course', name]
|
||||
|
||||
# check that logged in user has permissions to this item
|
||||
if not has_access(request.user, location):
|
||||
raise PermissionDenied()
|
||||
location = get_location_and_verify_access(request, org, course, name)
|
||||
|
||||
course_module = modulestore().get_item(location)
|
||||
|
||||
@@ -1111,21 +1094,25 @@ def course_info_updates(request, org, course, provided_id=None):
|
||||
if not has_access(request.user, location):
|
||||
raise PermissionDenied()
|
||||
|
||||
# NB: we're setting Backbone.emulateHTTP to true on the client so everything comes as a post!!!
|
||||
if request.method == 'POST' and 'HTTP_X_HTTP_METHOD_OVERRIDE' in request.META:
|
||||
real_method = request.META['HTTP_X_HTTP_METHOD_OVERRIDE']
|
||||
else:
|
||||
real_method = request.method
|
||||
real_method = get_request_method(request)
|
||||
|
||||
if request.method == 'GET':
|
||||
return HttpResponse(json.dumps(get_course_updates(location)), mimetype="application/json")
|
||||
elif real_method == 'DELETE': # coming as POST need to pull from Request Header X-HTTP-Method-Override DELETE
|
||||
return HttpResponse(json.dumps(delete_course_update(location, request.POST, provided_id)), mimetype="application/json")
|
||||
return HttpResponse(json.dumps(get_course_updates(location)),
|
||||
mimetype="application/json")
|
||||
elif real_method == 'DELETE':
|
||||
try:
|
||||
return HttpResponse(json.dumps(delete_course_update(location,
|
||||
request.POST, provided_id)), mimetype="application/json")
|
||||
except:
|
||||
return HttpResponseBadRequest("Failed to delete",
|
||||
content_type="text/plain")
|
||||
elif request.method == 'POST':
|
||||
try:
|
||||
return HttpResponse(json.dumps(update_course_updates(location, request.POST, provided_id)), mimetype="application/json")
|
||||
return HttpResponse(json.dumps(update_course_updates(location,
|
||||
request.POST, provided_id)), mimetype="application/json")
|
||||
except:
|
||||
return HttpResponseBadRequest("Failed to save: malformed html", content_type="text/plain")
|
||||
return HttpResponseBadRequest("Failed to save",
|
||||
content_type="text/plain")
|
||||
|
||||
|
||||
@expect_json
|
||||
@@ -1138,11 +1125,7 @@ def module_info(request, module_location):
|
||||
if not has_access(request.user, location):
|
||||
raise PermissionDenied()
|
||||
|
||||
# NB: we're setting Backbone.emulateHTTP to true on the client so everything comes as a post!!!
|
||||
if request.method == 'POST' and 'HTTP_X_HTTP_METHOD_OVERRIDE' in request.META:
|
||||
real_method = request.META['HTTP_X_HTTP_METHOD_OVERRIDE']
|
||||
else:
|
||||
real_method = request.method
|
||||
real_method = get_request_method(request)
|
||||
|
||||
rewrite_static_links = request.GET.get('rewrite_url_links', 'True') in ['True', 'true']
|
||||
logging.debug('rewrite_static_links = {0} {1}'.format(request.GET.get('rewrite_url_links', 'False'), rewrite_static_links))
|
||||
@@ -1167,11 +1150,7 @@ def get_course_settings(request, org, course, name):
|
||||
|
||||
org, course, name: Attributes of the Location for the item to edit
|
||||
"""
|
||||
location = ['i4x', org, course, 'course', name]
|
||||
|
||||
# check that logged in user has permissions to this item
|
||||
if not has_access(request.user, location):
|
||||
raise PermissionDenied()
|
||||
location = get_location_and_verify_access(request, org, course, name)
|
||||
|
||||
course_module = modulestore().get_item(location)
|
||||
|
||||
@@ -1194,11 +1173,7 @@ def course_config_graders_page(request, org, course, name):
|
||||
|
||||
org, course, name: Attributes of the Location for the item to edit
|
||||
"""
|
||||
location = ['i4x', org, course, 'course', name]
|
||||
|
||||
# check that logged in user has permissions to this item
|
||||
if not has_access(request.user, location):
|
||||
raise PermissionDenied()
|
||||
location = get_location_and_verify_access(request, org, course, name)
|
||||
|
||||
course_module = modulestore().get_item(location)
|
||||
course_details = CourseGradingModel.fetch(location)
|
||||
@@ -1218,11 +1193,7 @@ def course_config_advanced_page(request, org, course, name):
|
||||
|
||||
org, course, name: Attributes of the Location for the item to edit
|
||||
"""
|
||||
location = ['i4x', org, course, 'course', name]
|
||||
|
||||
# check that logged in user has permissions to this item
|
||||
if not has_access(request.user, location):
|
||||
raise PermissionDenied()
|
||||
location = get_location_and_verify_access(request, org, course, name)
|
||||
|
||||
course_module = modulestore().get_item(location)
|
||||
|
||||
@@ -1244,11 +1215,7 @@ def course_settings_updates(request, org, course, name, section):
|
||||
org, course: Attributes of the Location for the item to edit
|
||||
section: one of details, faculty, grading, problems, discussions
|
||||
"""
|
||||
location = ['i4x', org, course, 'course', name]
|
||||
|
||||
# check that logged in user has permissions to this item
|
||||
if not has_access(request.user, location):
|
||||
raise PermissionDenied()
|
||||
get_location_and_verify_access(request, org, course, name)
|
||||
|
||||
if section == 'details':
|
||||
manager = CourseDetails
|
||||
@@ -1260,7 +1227,7 @@ def course_settings_updates(request, org, course, name, section):
|
||||
# Cannot just do a get w/o knowing the course name :-(
|
||||
return HttpResponse(json.dumps(manager.fetch(Location(['i4x', org, course, 'course', name])), cls=CourseSettingsEncoder),
|
||||
mimetype="application/json")
|
||||
elif request.method == 'POST': # post or put, doesn't matter.
|
||||
elif request.method == 'POST': # post or put, doesn't matter.
|
||||
return HttpResponse(json.dumps(manager.update_from_json(request.POST), cls=CourseSettingsEncoder),
|
||||
mimetype="application/json")
|
||||
|
||||
@@ -1276,31 +1243,24 @@ def course_grader_updates(request, org, course, name, grader_index=None):
|
||||
org, course: Attributes of the Location for the item to edit
|
||||
"""
|
||||
|
||||
location = ['i4x', org, course, 'course', name]
|
||||
location = get_location_and_verify_access(request, org, course, name)
|
||||
|
||||
# check that logged in user has permissions to this item
|
||||
if not has_access(request.user, location):
|
||||
raise PermissionDenied()
|
||||
|
||||
if request.method == 'POST' and 'HTTP_X_HTTP_METHOD_OVERRIDE' in request.META:
|
||||
real_method = request.META['HTTP_X_HTTP_METHOD_OVERRIDE']
|
||||
else:
|
||||
real_method = request.method
|
||||
real_method = get_request_method(request)
|
||||
|
||||
if real_method == 'GET':
|
||||
# Cannot just do a get w/o knowing the course name :-(
|
||||
return HttpResponse(json.dumps(CourseGradingModel.fetch_grader(Location(['i4x', org, course, 'course', name]), grader_index)),
|
||||
return HttpResponse(json.dumps(CourseGradingModel.fetch_grader(Location(location), grader_index)),
|
||||
mimetype="application/json")
|
||||
elif real_method == "DELETE":
|
||||
# ??? Shoudl this return anything? Perhaps success fail?
|
||||
CourseGradingModel.delete_grader(Location(['i4x', org, course, 'course', name]), grader_index)
|
||||
# ??? Should this return anything? Perhaps success fail?
|
||||
CourseGradingModel.delete_grader(Location(location), grader_index)
|
||||
return HttpResponse()
|
||||
elif request.method == 'POST': # post or put, doesn't matter.
|
||||
return HttpResponse(json.dumps(CourseGradingModel.update_grader_from_json(Location(['i4x', org, course, 'course', name]), request.POST)),
|
||||
return HttpResponse(json.dumps(CourseGradingModel.update_grader_from_json(Location(location), request.POST)),
|
||||
mimetype="application/json")
|
||||
|
||||
|
||||
## NB: expect_json failed on ["key", "key2"] and json payload
|
||||
# # NB: expect_json failed on ["key", "key2"] and json payload
|
||||
@login_required
|
||||
@ensure_csrf_cookie
|
||||
def course_advanced_updates(request, org, course, name):
|
||||
@@ -1310,18 +1270,10 @@ def course_advanced_updates(request, org, course, name):
|
||||
|
||||
org, course: Attributes of the Location for the item to edit
|
||||
"""
|
||||
location = ['i4x', org, course, 'course', name]
|
||||
|
||||
# check that logged in user has permissions to this item
|
||||
if not has_access(request.user, location):
|
||||
raise PermissionDenied()
|
||||
|
||||
# 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
|
||||
location = get_location_and_verify_access(request, org, course, name)
|
||||
|
||||
real_method = get_request_method(request)
|
||||
|
||||
if real_method == 'GET':
|
||||
return HttpResponse(json.dumps(CourseMetadata.fetch(location)), mimetype="application/json")
|
||||
elif real_method == 'DELETE':
|
||||
@@ -1331,6 +1283,95 @@ def course_advanced_updates(request, org, course, name):
|
||||
return HttpResponse(json.dumps(CourseMetadata.update_from_json(location, json.loads(request.body))), mimetype="application/json")
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
@login_required
|
||||
def get_checklists(request, org, course, name):
|
||||
"""
|
||||
Send models, views, and html for displaying the course checklists.
|
||||
|
||||
org, course, name: Attributes of the Location for the item to edit
|
||||
"""
|
||||
location = get_location_and_verify_access(request, org, course, name)
|
||||
|
||||
modulestore = get_modulestore(location)
|
||||
course_module = modulestore.get_item(location)
|
||||
new_course_template = Location('i4x', 'edx', 'templates', 'course', 'Empty')
|
||||
template_module = modulestore.get_item(new_course_template)
|
||||
|
||||
# If course was created before checklists were introduced, copy them over from the template.
|
||||
copied = False
|
||||
if not course_module.checklists:
|
||||
course_module.checklists = template_module.checklists
|
||||
copied = True
|
||||
|
||||
checklists, modified = expand_checklist_action_urls(course_module)
|
||||
if copied or modified:
|
||||
modulestore.update_metadata(location, own_metadata(course_module))
|
||||
return render_to_response('checklists.html',
|
||||
{
|
||||
'context_course': course_module,
|
||||
'checklists': checklists
|
||||
})
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
@login_required
|
||||
def update_checklist(request, org, course, name, checklist_index=None):
|
||||
"""
|
||||
restful CRUD operations on course checklists. The payload is a json rep of
|
||||
the modified checklist. For PUT or POST requests, the index of the
|
||||
checklist being modified must be included; the returned payload will
|
||||
be just that one checklist. For GET requests, the returned payload
|
||||
is a json representation of the list of all checklists.
|
||||
|
||||
org, course, name: Attributes of the Location for the item to edit
|
||||
"""
|
||||
location = get_location_and_verify_access(request, org, course, name)
|
||||
modulestore = get_modulestore(location)
|
||||
course_module = modulestore.get_item(location)
|
||||
|
||||
real_method = get_request_method(request)
|
||||
if real_method == 'POST' or real_method == 'PUT':
|
||||
if checklist_index is not None and 0 <= int(checklist_index) < len(course_module.checklists):
|
||||
index = int(checklist_index)
|
||||
course_module.checklists[index] = json.loads(request.body)
|
||||
checklists, modified = expand_checklist_action_urls(course_module)
|
||||
modulestore.update_metadata(location, own_metadata(course_module))
|
||||
return HttpResponse(json.dumps(checklists[index]), mimetype="application/json")
|
||||
else:
|
||||
return HttpResponseBadRequest(
|
||||
"Could not save checklist state because the checklist index was out of range or unspecified.",
|
||||
content_type="text/plain")
|
||||
elif request.method == 'GET':
|
||||
# In the JavaScript view initialize method, we do a fetch to get all the checklists.
|
||||
checklists, modified = expand_checklist_action_urls(course_module)
|
||||
if modified:
|
||||
modulestore.update_metadata(location, own_metadata(course_module))
|
||||
return HttpResponse(json.dumps(checklists), mimetype="application/json")
|
||||
else:
|
||||
return HttpResponseBadRequest("Unsupported request.", content_type="text/plain")
|
||||
|
||||
|
||||
def expand_checklist_action_urls(course_module):
|
||||
"""
|
||||
Gets the checklists out of the course module and expands their action urls
|
||||
if they have not yet been expanded.
|
||||
|
||||
Returns the checklists with modified urls, as well as a boolean
|
||||
indicating whether or not the checklists were modified.
|
||||
"""
|
||||
checklists = course_module.checklists
|
||||
modified = False
|
||||
for checklist in checklists:
|
||||
if not checklist.get('action_urls_expanded', False):
|
||||
for item in checklist.get('items'):
|
||||
item['action_url'] = get_url_reverse(item.get('action_url'), course_module)
|
||||
checklist['action_urls_expanded'] = True
|
||||
modified = True
|
||||
|
||||
return checklists, modified
|
||||
|
||||
|
||||
@login_required
|
||||
@ensure_csrf_cookie
|
||||
def asset_index(request, org, course, name):
|
||||
@@ -1339,18 +1380,13 @@ def asset_index(request, org, course, name):
|
||||
|
||||
org, course, name: Attributes of the Location for the item to edit
|
||||
"""
|
||||
location = ['i4x', org, course, 'course', name]
|
||||
|
||||
# check that logged in user has permissions to this item
|
||||
if not has_access(request.user, location):
|
||||
raise PermissionDenied()
|
||||
|
||||
location = get_location_and_verify_access(request, org, course, name)
|
||||
|
||||
upload_asset_callback_url = reverse('upload_asset', kwargs={
|
||||
'org': org,
|
||||
'course': course,
|
||||
'coursename': name
|
||||
})
|
||||
'org': org,
|
||||
'course': course,
|
||||
'coursename': name
|
||||
})
|
||||
|
||||
course_module = modulestore().get_item(location)
|
||||
|
||||
@@ -1466,11 +1502,7 @@ def initialize_course_tabs(course):
|
||||
@login_required
|
||||
def import_course(request, org, course, name):
|
||||
|
||||
location = ['i4x', org, course, 'course', name]
|
||||
|
||||
# check that logged in user has permissions to this item
|
||||
if not has_access(request.user, location):
|
||||
raise PermissionDenied()
|
||||
location = get_location_and_verify_access(request, org, course, name)
|
||||
|
||||
if request.method == 'POST':
|
||||
filename = request.FILES['course-data'].name
|
||||
@@ -1533,20 +1565,14 @@ def import_course(request, org, course, name):
|
||||
return render_to_response('import.html', {
|
||||
'context_course': course_module,
|
||||
'active_tab': 'import',
|
||||
'successful_import_redirect_url': reverse('course_index', args=[
|
||||
course_module.location.org,
|
||||
course_module.location.course,
|
||||
course_module.location.name])
|
||||
'successful_import_redirect_url': get_url_reverse('CourseOutline', course_module)
|
||||
})
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
@login_required
|
||||
def generate_export_course(request, org, course, name):
|
||||
location = ['i4x', org, course, 'course', name]
|
||||
# check that logged in user has permissions to this item
|
||||
if not has_access(request.user, location):
|
||||
raise PermissionDenied()
|
||||
location = get_location_and_verify_access(request, org, course, name)
|
||||
|
||||
loc = Location(location)
|
||||
export_file = NamedTemporaryFile(prefix=name + '.', suffix=".tar.gz")
|
||||
@@ -1558,7 +1584,7 @@ def generate_export_course(request, org, course, name):
|
||||
logging.debug('root = {0}'.format(root_dir))
|
||||
|
||||
export_to_xml(modulestore('direct'), contentstore(), loc, root_dir, name)
|
||||
#filename = root_dir / name + '.tar.gz'
|
||||
# filename = root_dir / name + '.tar.gz'
|
||||
|
||||
logging.debug('tar file being generated at {0}'.format(export_file.name))
|
||||
tf = tarfile.open(name=export_file.name, mode='w:gz')
|
||||
@@ -1579,11 +1605,9 @@ def generate_export_course(request, org, course, name):
|
||||
@login_required
|
||||
def export_course(request, org, course, name):
|
||||
|
||||
location = ['i4x', org, course, 'course', name]
|
||||
location = get_location_and_verify_access(request, org, course, name)
|
||||
|
||||
course_module = modulestore().get_item(location)
|
||||
# check that logged in user has permissions to this item
|
||||
if not has_access(request.user, location):
|
||||
raise PermissionDenied()
|
||||
|
||||
return render_to_response('export.html', {
|
||||
'context_course': course_module,
|
||||
@@ -1606,3 +1630,31 @@ def render_404(request):
|
||||
|
||||
def render_500(request):
|
||||
return HttpResponseServerError(render_to_string('500.html', {}))
|
||||
|
||||
|
||||
def get_location_and_verify_access(request, org, course, name):
|
||||
"""
|
||||
Create the location tuple verify that the user has permissions
|
||||
to view the location. Returns the location.
|
||||
"""
|
||||
location = ['i4x', org, course, 'course', name]
|
||||
|
||||
# check that logged in user has permissions to this item
|
||||
if not has_access(request.user, location):
|
||||
raise PermissionDenied()
|
||||
|
||||
return location
|
||||
|
||||
|
||||
def get_request_method(request):
|
||||
"""
|
||||
Using HTTP_X_HTTP_METHOD_OVERRIDE, in the request metadata, determine
|
||||
what type of request came from the client, and return it.
|
||||
"""
|
||||
# NB: we're setting Backbone.emulateHTTP to true on the client so everything comes as a post!!!
|
||||
if request.method == 'POST' and 'HTTP_X_HTTP_METHOD_OVERRIDE' in request.META:
|
||||
real_method = request.META['HTTP_X_HTTP_METHOD_OVERRIDE']
|
||||
else:
|
||||
real_method = request.method
|
||||
|
||||
return real_method
|
||||
|
||||
@@ -3,19 +3,24 @@ from contentstore.utils import get_modulestore
|
||||
from xmodule.x_module import XModuleDescriptor
|
||||
from xmodule.modulestore.inheritance import own_metadata
|
||||
from xblock.core import Scope
|
||||
from xmodule.course_module import CourseDescriptor
|
||||
|
||||
|
||||
class CourseMetadata(object):
|
||||
'''
|
||||
For CRUD operations on metadata fields which do not have specific editors on the other pages including any user generated ones.
|
||||
The objects have no predefined attrs but instead are obj encodings of the editable metadata.
|
||||
For CRUD operations on metadata fields which do not have specific editors
|
||||
on the other pages including any user generated ones.
|
||||
The objects have no predefined attrs but instead are obj encodings of the
|
||||
editable metadata.
|
||||
'''
|
||||
FILTERED_LIST = XModuleDescriptor.system_metadata_fields + ['start', 'end', 'enrollment_start', 'enrollment_end', 'tabs', 'graceperiod']
|
||||
FILTERED_LIST = XModuleDescriptor.system_metadata_fields + ['start', 'end',
|
||||
'enrollment_start', 'enrollment_end', 'tabs', 'graceperiod', 'checklists']
|
||||
|
||||
@classmethod
|
||||
def fetch(cls, course_location):
|
||||
"""
|
||||
Fetch the key:value editable course details for the given course from persistence and return a CourseMetadata model.
|
||||
Fetch the key:value editable course details for the given course from
|
||||
persistence and return a CourseMetadata model.
|
||||
"""
|
||||
if not isinstance(course_location, Location):
|
||||
course_location = Location(course_location)
|
||||
@@ -29,7 +34,7 @@ class CourseMetadata(object):
|
||||
continue
|
||||
|
||||
if field.name not in cls.FILTERED_LIST:
|
||||
course[field.name] = field.read_from(descriptor)
|
||||
course[field.name] = field.read_json(descriptor)
|
||||
|
||||
return course
|
||||
|
||||
@@ -51,22 +56,26 @@ class CourseMetadata(object):
|
||||
|
||||
if hasattr(descriptor, k) and getattr(descriptor, k) != v:
|
||||
dirty = True
|
||||
setattr(descriptor, k, v)
|
||||
value = getattr(CourseDescriptor, k).from_json(v)
|
||||
setattr(descriptor, k, value)
|
||||
elif hasattr(descriptor.lms, k) and getattr(descriptor.lms, k) != k:
|
||||
dirty = True
|
||||
setattr(descriptor.lms, k, v)
|
||||
value = getattr(CourseDescriptor.lms, k).from_json(v)
|
||||
setattr(descriptor.lms, k, value)
|
||||
|
||||
if dirty:
|
||||
get_modulestore(course_location).update_metadata(course_location, own_metadata(descriptor))
|
||||
get_modulestore(course_location).update_metadata(course_location,
|
||||
own_metadata(descriptor))
|
||||
|
||||
# Could just generate and return a course obj w/o doing any db reads, but I put the reads in as a means to confirm
|
||||
# it persisted correctly
|
||||
# Could just generate and return a course obj w/o doing any db reads,
|
||||
# but I put the reads in as a means to confirm it persisted correctly
|
||||
return cls.fetch(course_location)
|
||||
|
||||
@classmethod
|
||||
def delete_key(cls, course_location, payload):
|
||||
'''
|
||||
Remove the given metadata key(s) from the course. payload can be a single key or [key..]
|
||||
Remove the given metadata key(s) from the course. payload can be a
|
||||
single key or [key..]
|
||||
'''
|
||||
descriptor = get_modulestore(course_location).get_item(course_location)
|
||||
|
||||
@@ -76,6 +85,7 @@ class CourseMetadata(object):
|
||||
elif hasattr(descriptor.lms, key):
|
||||
delattr(descriptor.lms, key)
|
||||
|
||||
get_modulestore(course_location).update_metadata(course_location, own_metadata(descriptor))
|
||||
get_modulestore(course_location).update_metadata(course_location,
|
||||
own_metadata(descriptor))
|
||||
|
||||
return cls.fetch(course_location)
|
||||
|
||||
61
cms/static/client_templates/checklist.html
Normal file
61
cms/static/client_templates/checklist.html
Normal file
@@ -0,0 +1,61 @@
|
||||
<% var allChecked = itemsChecked == items.length; %>
|
||||
<section
|
||||
<% if (allChecked) { %>
|
||||
class="course-checklist is-completed"
|
||||
<% } else { %>
|
||||
class="course-checklist"
|
||||
<% } %>
|
||||
id="<%= 'course-checklist' + checklistIndex %>">
|
||||
<% var widthPercentage = 'width:' + percentChecked + '%;'; %>
|
||||
<span class="viz viz-checklist-status"><span class="viz value viz-checklist-status-value" style="<%= widthPercentage %>">
|
||||
<span class="int"><%= percentChecked %></span>% of checklist completed</span></span>
|
||||
<header>
|
||||
<h3 class="checklist-title title-2 is-selectable" title="Collapse/Expand this Checklist">
|
||||
<i class="ss-icon ss-symbolicons-standard icon-arrow ui-toggle-expansion">▾</i>
|
||||
<%= checklistShortDescription %></h3>
|
||||
<span class="checklist-status status">
|
||||
Tasks Completed: <span class="status-count"><%= itemsChecked %></span>/<span class="status-amount"><%= items.length %></span>
|
||||
<i class="ss-icon ss-symbolicons-standard icon-confirm">✓</i>
|
||||
</span>
|
||||
</header>
|
||||
|
||||
<ul class="list list-tasks">
|
||||
<% var taskIndex = 0; %>
|
||||
<% _.each(items, function(item) { %>
|
||||
<% var checked = item['is_checked']; %>
|
||||
<li
|
||||
<% if (checked) { %>
|
||||
class="task is-completed"
|
||||
<% } else { %>
|
||||
class="task"
|
||||
<% } %>
|
||||
>
|
||||
<% var taskId = 'course-checklist' + checklistIndex + '-task' + taskIndex; %>
|
||||
<input type="checkbox" class="task-input" data-checklist="<%= checklistIndex %>" data-task="<%= taskIndex %>"
|
||||
name="<%= taskId %>" id="<%= taskId %>"
|
||||
<% if (checked) { %>
|
||||
checked="checked"
|
||||
<% } %>
|
||||
>
|
||||
<label class="task-details" for="<%= taskId %>">
|
||||
<h4 class="task-name title title-3"><%= item['short_description'] %></h4>
|
||||
<p class="task-description"><%= item['long_description'] %></p>
|
||||
</label>
|
||||
|
||||
<% if (item['action_text'] !== '' && item['action_url'] !== '') { %>
|
||||
<ul class="list-actions task-actions">
|
||||
<li>
|
||||
<a href="<%= item['action_url'] %>" class="action action-primary"
|
||||
<% if (item['action_external']) { %>
|
||||
rel="external" title="This link will open in a new browser window/tab"
|
||||
<% } %>
|
||||
><%= item['action_text'] %></a>
|
||||
</li>
|
||||
</ul>
|
||||
<% } %>
|
||||
</li>
|
||||
|
||||
<% taskIndex+=1; }) %>
|
||||
|
||||
</ul>
|
||||
</section>
|
||||
@@ -34,7 +34,10 @@ class CMS.Views.UnitEdit extends Backbone.View
|
||||
|
||||
@$('.components').sortable(
|
||||
handle: '.drag-handle'
|
||||
update: (event, ui) => @model.save(children: @components())
|
||||
update: (event, ui) =>
|
||||
payload = children : @components()
|
||||
options = success : => @model.unset('children')
|
||||
@model.save(payload, options)
|
||||
helper: 'clone'
|
||||
opacity: '0.5'
|
||||
placeholder: 'component-placeholder'
|
||||
@@ -109,7 +112,14 @@ class CMS.Views.UnitEdit extends Backbone.View
|
||||
id: $component.data('id')
|
||||
}, =>
|
||||
$component.remove()
|
||||
@model.save(children: @components())
|
||||
# b/c we don't vigilantly keep children up to date
|
||||
# get rid of it before it hurts someone
|
||||
# sorry for the js, i couldn't figure out the coffee equivalent
|
||||
`_this.model.save({children: _this.components()},
|
||||
{success: function(model) {
|
||||
model.unset('children');
|
||||
}}
|
||||
);`
|
||||
)
|
||||
|
||||
deleteDraft: (event) ->
|
||||
@@ -157,7 +167,7 @@ class CMS.Views.UnitEdit extends Backbone.View
|
||||
|
||||
class CMS.Views.UnitEdit.NameEdit extends Backbone.View
|
||||
events:
|
||||
"keyup .unit-display-name-input": "saveName"
|
||||
'change .unit-display-name-input': 'saveName'
|
||||
|
||||
initialize: =>
|
||||
@model.on('change:metadata', @render)
|
||||
@@ -180,29 +190,10 @@ class CMS.Views.UnitEdit.NameEdit extends Backbone.View
|
||||
# Treat the metadata dictionary as immutable
|
||||
metadata = $.extend({}, @model.get('metadata'))
|
||||
metadata.display_name = @$('.unit-display-name-input').val()
|
||||
@model.save(metadata: metadata)
|
||||
# Update name shown in the right-hand side location summary.
|
||||
$('.unit-location .editing .unit-name').html(metadata.display_name)
|
||||
|
||||
inputField = this.$el.find('input')
|
||||
|
||||
# add a spinner
|
||||
@$spinner.css({
|
||||
'position': 'absolute',
|
||||
'top': Math.floor(inputField.position().top + (inputField.outerHeight() / 2) + 3),
|
||||
'left': inputField.position().left + inputField.outerWidth() - 24,
|
||||
'margin-top': '-10px'
|
||||
});
|
||||
inputField.after(@$spinner);
|
||||
@$spinner.fadeIn(10)
|
||||
|
||||
# save the name after a slight delay
|
||||
if @timer
|
||||
clearTimeout @timer
|
||||
@timer = setTimeout( =>
|
||||
@model.save(metadata: metadata)
|
||||
@timer = null
|
||||
@$spinner.delay(500).fadeOut(150)
|
||||
, 500)
|
||||
|
||||
class CMS.Views.UnitEdit.LocationState extends Backbone.View
|
||||
initialize: =>
|
||||
@model.on('change:state', @render)
|
||||
|
||||
@@ -14,10 +14,6 @@ $(document).ready(function () {
|
||||
// scopes (namely the course-info tab)
|
||||
window.$modalCover = $modalCover;
|
||||
|
||||
// Control whether template caching in local memory occurs (see template_loader.js). Caching screws up development but may
|
||||
// be a good optimization in production (it works fairly well)
|
||||
window.cachetemplates = false;
|
||||
|
||||
$body.append($modalCover);
|
||||
$newComponentItem = $('.new-component-item');
|
||||
$newComponentTypePicker = $('.new-component');
|
||||
@@ -76,10 +72,7 @@ $(document).ready(function () {
|
||||
});
|
||||
|
||||
// general link management - new window/tab
|
||||
$('a[rel="external"]').attr('title', 'This link will open in a new browser window/tab').click(function (e) {
|
||||
window.open($(this).attr('href'));
|
||||
e.preventDefault();
|
||||
});
|
||||
$('a[rel="external"]').attr('title', 'This link will open in a new browser window/tab').bind('click', linkNewWindow);
|
||||
|
||||
// general link management - lean modal window
|
||||
$('a[rel="modal"]').attr('title', 'This link will open in a modal window').leanModal({overlay: 0.50, closeButton: '.action-modal-close' });
|
||||
@@ -87,6 +80,10 @@ $(document).ready(function () {
|
||||
(e).preventDefault();
|
||||
});
|
||||
|
||||
// general link management - smooth scrolling page links
|
||||
$('a[rel*="view"]').bind('click', linkSmoothScroll);
|
||||
|
||||
|
||||
// toggling overview section details
|
||||
$(function () {
|
||||
if ($('.courseware-section').length > 0) {
|
||||
@@ -95,9 +92,9 @@ $(document).ready(function () {
|
||||
});
|
||||
$('.toggle-button-sections').bind('click', toggleSections);
|
||||
|
||||
// autosave when a field is updated on the subsection page
|
||||
$body.on('keyup', '.subsection-display-name-input, .unit-subtitle, .policy-list-value', checkForNewValue);
|
||||
$('.subsection-display-name-input, .unit-subtitle, .policy-list-name, .policy-list-value').each(function (i) {
|
||||
// autosave when leaving input field
|
||||
$body.on('change', '.subsection-display-name-input', saveSubsection);
|
||||
$('.subsection-display-name-input').each(function () {
|
||||
this.val = $(this).val();
|
||||
});
|
||||
$("#start_date, #start_time, #due_date, #due_time").bind('change', autosaveInput);
|
||||
@@ -113,11 +110,6 @@ $(document).ready(function () {
|
||||
// add new/delete subsection
|
||||
$('.new-subsection-item').bind('click', addNewSubsection);
|
||||
$('.delete-subsection-button').bind('click', deleteSubsection);
|
||||
// add/remove policy metadata button click handlers
|
||||
$('.add-policy-data').bind('click', addPolicyMetadata);
|
||||
$('.remove-policy-data').bind('click', removePolicyMetadata);
|
||||
$body.on('click', '.policy-list-element .save-button', savePolicyMetadata);
|
||||
$body.on('click', '.policy-list-element .cancel-button', cancelPolicyMetadata);
|
||||
|
||||
$('.sync-date').bind('click', syncReleaseDate);
|
||||
|
||||
@@ -156,10 +148,27 @@ $(document).ready(function () {
|
||||
});
|
||||
});
|
||||
|
||||
// function collapseAll(e) {
|
||||
// $('.branch').addClass('collapsed');
|
||||
// $('.expand-collapse-icon').removeClass('collapse').addClass('expand');
|
||||
// }
|
||||
function linkSmoothScroll(e) {
|
||||
(e).preventDefault();
|
||||
|
||||
$.smoothScroll({
|
||||
offset: -200,
|
||||
easing: 'swing',
|
||||
speed: 1000,
|
||||
scrollElement: null,
|
||||
scrollTarget: $(this).attr('href')
|
||||
});
|
||||
}
|
||||
|
||||
function linkNewWindow(e) {
|
||||
window.open($(e.target).attr('href'));
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
// On AWS instances, base.js gets wrapped in a separate scope as part of Django static
|
||||
// pipelining (note, this doesn't happen on local runtimes). So if we set it on window,
|
||||
// when we can access it from other scopes (namely the checklists)
|
||||
window.cmsLinkNewWindow = linkNewWindow;
|
||||
|
||||
function toggleSections(e) {
|
||||
e.preventDefault();
|
||||
@@ -219,56 +228,6 @@ function syncReleaseDate(e) {
|
||||
$("#start_time").val("");
|
||||
}
|
||||
|
||||
function addPolicyMetadata(e) {
|
||||
e.preventDefault();
|
||||
var template = $('#add-new-policy-element-template > li');
|
||||
var newNode = template.clone();
|
||||
var _parent_el = $(this).parent('ol:.policy-list');
|
||||
newNode.insertBefore('.add-policy-data');
|
||||
$('.remove-policy-data').bind('click', removePolicyMetadata);
|
||||
newNode.find('.policy-list-name').focus();
|
||||
}
|
||||
|
||||
function savePolicyMetadata(e) {
|
||||
e.preventDefault();
|
||||
|
||||
var $policyElement = $(this).parents('.policy-list-element');
|
||||
saveSubsection()
|
||||
$policyElement.removeClass('new-policy-list-element');
|
||||
$policyElement.find('.policy-list-name').attr('disabled', 'disabled');
|
||||
$policyElement.removeClass('editing');
|
||||
}
|
||||
|
||||
function cancelPolicyMetadata(e) {
|
||||
e.preventDefault();
|
||||
|
||||
var $policyElement = $(this).parents('.policy-list-element');
|
||||
if (!$policyElement.hasClass('editing')) {
|
||||
$policyElement.remove();
|
||||
} else {
|
||||
$policyElement.removeClass('new-policy-list-element');
|
||||
$policyElement.find('.policy-list-name').val($policyElement.data('currentValues')[0]);
|
||||
$policyElement.find('.policy-list-value').val($policyElement.data('currentValues')[1]);
|
||||
}
|
||||
$policyElement.removeClass('editing');
|
||||
}
|
||||
|
||||
function removePolicyMetadata(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (!confirm('Are you sure you wish to delete this item. It cannot be reversed!'))
|
||||
return;
|
||||
|
||||
policy_name = $(this).data('policy-name');
|
||||
var _parent_el = $(this).parent('li:.policy-list-element');
|
||||
if ($(_parent_el).hasClass("new-policy-list-element")) {
|
||||
_parent_el.remove();
|
||||
} else {
|
||||
_parent_el.appendTo("#policy-to-delete");
|
||||
}
|
||||
saveSubsection()
|
||||
}
|
||||
|
||||
function getEdxTimeFromDateTimeVals(date_val, time_val, format) {
|
||||
var edxTimeStr = null;
|
||||
|
||||
@@ -294,31 +253,6 @@ function getEdxTimeFromDateTimeInputs(date_id, time_id, format) {
|
||||
return getEdxTimeFromDateTimeVals(input_date, input_time, format);
|
||||
}
|
||||
|
||||
function checkForNewValue(e) {
|
||||
if ($(this).parents('.new-policy-list-element')[0]) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.val) {
|
||||
this.hasChanged = this.val != $(this).val();
|
||||
} else {
|
||||
this.hasChanged = false;
|
||||
}
|
||||
|
||||
this.val = $(this).val();
|
||||
if (this.hasChanged) {
|
||||
if (this.saveTimer) {
|
||||
clearTimeout(this.saveTimer);
|
||||
}
|
||||
|
||||
this.saveTimer = setTimeout(function () {
|
||||
$changedInput = $(e.target);
|
||||
saveSubsection();
|
||||
this.saveTimer = null;
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
function autosaveInput(e) {
|
||||
if (this.saveTimer) {
|
||||
clearTimeout(this.saveTimer);
|
||||
@@ -332,6 +266,7 @@ function autosaveInput(e) {
|
||||
}
|
||||
|
||||
function saveSubsection() {
|
||||
// Spinner is no longer used by subsection name, but is still used by date and time pickers on the right.
|
||||
if ($changedInput && !$changedInput.hasClass('no-spinner')) {
|
||||
$spinner.css({
|
||||
'position': 'absolute',
|
||||
@@ -354,20 +289,6 @@ function saveSubsection() {
|
||||
metadata[$(el).data("metadata-name")] = el.value;
|
||||
}
|
||||
|
||||
// now add 'free-formed' metadata which are presented to the user as dual input fields (name/value)
|
||||
$('ol.policy-list > li.policy-list-element').each(function (i, element) {
|
||||
var name = $(element).children('.policy-list-name').val();
|
||||
metadata[name] = $(element).children('.policy-list-value').val();
|
||||
});
|
||||
|
||||
// now add any 'removed' policy metadata which is stored in a separate hidden div
|
||||
// 'null' presented to the server means 'remove'
|
||||
$("#policy-to-delete > li.policy-list-element").each(function (i, element) {
|
||||
var name = $(element).children('.policy-list-name').val();
|
||||
if (name != "")
|
||||
metadata[name] = null;
|
||||
});
|
||||
|
||||
// Piece back together the date/time UI elements into one date/time string
|
||||
// NOTE: our various "date/time" metadata elements don't always utilize the same formatting string
|
||||
// so make sure we're passing back the correct format
|
||||
|
||||
24
cms/static/js/models/checklists.js
Normal file
24
cms/static/js/models/checklists.js
Normal file
@@ -0,0 +1,24 @@
|
||||
// Model for checklists_view.js.
|
||||
CMS.Models.Checklist = Backbone.Model.extend({
|
||||
});
|
||||
|
||||
CMS.Models.ChecklistCollection = Backbone.Collection.extend({
|
||||
model : CMS.Models.Checklist,
|
||||
|
||||
parse: function(response) {
|
||||
_.each(response,
|
||||
function( element, idx ) {
|
||||
element.id = idx;
|
||||
});
|
||||
|
||||
return response;
|
||||
},
|
||||
|
||||
// Disable caching so the browser back button will work (checklists have links to other
|
||||
// places within Studio).
|
||||
fetch: function (options) {
|
||||
options.cache = false;
|
||||
return Backbone.Collection.prototype.fetch.call(this, options);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,78 +1,79 @@
|
||||
// <!-- from https://github.com/Gazler/Underscore-Template-Loader/blob/master/index.html -->
|
||||
// TODO Figure out how to initialize w/ static views from server (don't call .load but instead inject in django as strings)
|
||||
// so this only loads the lazily loaded ones.
|
||||
(function() {
|
||||
if (typeof window.templateLoader == 'function') return;
|
||||
|
||||
var templateLoader = {
|
||||
templateVersion: "0.0.16",
|
||||
templates: {},
|
||||
loadRemoteTemplate: function(templateName, filename, callback) {
|
||||
if (!this.templates[templateName]) {
|
||||
var self = this;
|
||||
jQuery.ajax({url : filename,
|
||||
success : function(data) {
|
||||
self.addTemplate(templateName, data);
|
||||
self.saveLocalTemplates();
|
||||
callback(data);
|
||||
},
|
||||
error : function(xhdr, textStatus, errorThrown) {
|
||||
console.log(textStatus); },
|
||||
dataType : "html"
|
||||
})
|
||||
}
|
||||
else {
|
||||
callback(this.templates[templateName]);
|
||||
}
|
||||
},
|
||||
|
||||
addTemplate: function(templateName, data) {
|
||||
// is there a reason this doesn't go ahead and compile the template? _.template(data)
|
||||
// I suppose localstorage use would still req raw string rather than compiled version, but that sd work
|
||||
// if it maintains a separate cache of uncompiled ones
|
||||
this.templates[templateName] = data;
|
||||
},
|
||||
|
||||
localStorageAvailable: function() {
|
||||
try {
|
||||
// window.cachetemplates is global set in base.js to turn caching on/off
|
||||
return window.cachetemplates && 'localStorage' in window && window['localStorage'] !== null;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
saveLocalTemplates: function() {
|
||||
if (this.localStorageAvailable) {
|
||||
localStorage.setItem("templates", JSON.stringify(this.templates));
|
||||
localStorage.setItem("templateVersion", this.templateVersion);
|
||||
}
|
||||
},
|
||||
|
||||
loadLocalTemplates: function() {
|
||||
if (this.localStorageAvailable) {
|
||||
var templateVersion = localStorage.getItem("templateVersion");
|
||||
if (templateVersion && templateVersion == this.templateVersion) {
|
||||
var templates = localStorage.getItem("templates");
|
||||
if (templates) {
|
||||
templates = JSON.parse(templates);
|
||||
for (var x in templates) {
|
||||
if (!this.templates[x]) {
|
||||
this.addTemplate(x, templates[x]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
localStorage.removeItem("templates");
|
||||
localStorage.removeItem("templateVersion");
|
||||
}
|
||||
}
|
||||
}
|
||||
(function () {
|
||||
if (typeof window.templateLoader == 'function') return;
|
||||
|
||||
var templateLoader = {
|
||||
templateVersion: "0.0.15",
|
||||
templates: {},
|
||||
// Control whether template caching in local memory occurs. Caching screws up development but may
|
||||
// be a good optimization in production (it works fairly well).
|
||||
cacheTemplates: false,
|
||||
loadRemoteTemplate: function (templateName, filename, callback) {
|
||||
if (!this.templates[templateName]) {
|
||||
var self = this;
|
||||
jQuery.ajax({url: filename,
|
||||
success: function (data) {
|
||||
self.addTemplate(templateName, data);
|
||||
self.saveLocalTemplates();
|
||||
callback(data);
|
||||
},
|
||||
error: function (xhdr, textStatus, errorThrown) {
|
||||
console.log(textStatus);
|
||||
},
|
||||
dataType: "html"
|
||||
})
|
||||
}
|
||||
else {
|
||||
callback(this.templates[templateName]);
|
||||
}
|
||||
},
|
||||
|
||||
addTemplate: function (templateName, data) {
|
||||
// is there a reason this doesn't go ahead and compile the template? _.template(data)
|
||||
// I suppose localstorage use would still req raw string rather than compiled version, but that sd work
|
||||
// if it maintains a separate cache of uncompiled ones
|
||||
this.templates[templateName] = data;
|
||||
},
|
||||
|
||||
localStorageAvailable: function () {
|
||||
try {
|
||||
return this.cacheTemplates && 'localStorage' in window && window['localStorage'] !== null;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
saveLocalTemplates: function () {
|
||||
if (this.localStorageAvailable()) {
|
||||
localStorage.setItem("templates", JSON.stringify(this.templates));
|
||||
localStorage.setItem("templateVersion", this.templateVersion);
|
||||
}
|
||||
},
|
||||
|
||||
loadLocalTemplates: function () {
|
||||
if (this.localStorageAvailable()) {
|
||||
var templateVersion = localStorage.getItem("templateVersion");
|
||||
if (templateVersion && templateVersion == this.templateVersion) {
|
||||
var templates = localStorage.getItem("templates");
|
||||
if (templates) {
|
||||
templates = JSON.parse(templates);
|
||||
for (var x in templates) {
|
||||
if (!this.templates[x]) {
|
||||
this.addTemplate(x, templates[x]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
localStorage.removeItem("templates");
|
||||
localStorage.removeItem("templateVersion");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
};
|
||||
templateLoader.loadLocalTemplates();
|
||||
window.templateLoader = templateLoader;
|
||||
})();
|
||||
})();
|
||||
|
||||
89
cms/static/js/views/checklists_view.js
Normal file
89
cms/static/js/views/checklists_view.js
Normal file
@@ -0,0 +1,89 @@
|
||||
if (!CMS.Views['Checklists']) CMS.Views.Checklists = {};
|
||||
|
||||
CMS.Views.Checklists = Backbone.View.extend({
|
||||
// takes CMS.Models.Checklists as model
|
||||
|
||||
events : {
|
||||
'click .course-checklist .checklist-title' : "toggleChecklist",
|
||||
'click .course-checklist .task input' : "toggleTask",
|
||||
'click a[rel="external"]' : window.cmsLinkNewWindow
|
||||
},
|
||||
|
||||
initialize : function() {
|
||||
var self = this;
|
||||
|
||||
this.collection.fetch({
|
||||
complete: function () {
|
||||
window.templateLoader.loadRemoteTemplate("checklist",
|
||||
"/static/client_templates/checklist.html",
|
||||
function (raw_template) {
|
||||
self.template = _.template(raw_template);
|
||||
self.render();
|
||||
}
|
||||
);
|
||||
},
|
||||
error: CMS.ServerError
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
// catch potential outside call before template loaded
|
||||
if (!this.template) return this;
|
||||
|
||||
this.$el.empty();
|
||||
|
||||
var self = this;
|
||||
_.each(this.collection.models,
|
||||
function(checklist, index) {
|
||||
self.$el.append(self.renderTemplate(checklist, index));
|
||||
});
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
renderTemplate: function (checklist, index) {
|
||||
var checklistItems = checklist.attributes['items'];
|
||||
var itemsChecked = 0;
|
||||
_.each(checklistItems,
|
||||
function(checklist) {
|
||||
if (checklist['is_checked']) {
|
||||
itemsChecked +=1;
|
||||
}
|
||||
});
|
||||
var percentChecked = Math.round((itemsChecked/checklistItems.length)*100);
|
||||
return this.template({
|
||||
checklistIndex : index,
|
||||
checklistShortDescription : checklist.attributes['short_description'],
|
||||
items: checklistItems,
|
||||
itemsChecked: itemsChecked,
|
||||
percentChecked: percentChecked});
|
||||
},
|
||||
|
||||
toggleChecklist : function(e) {
|
||||
e.preventDefault();
|
||||
$(e.target).closest('.course-checklist').toggleClass('is-collapsed');
|
||||
},
|
||||
|
||||
toggleTask : function (e) {
|
||||
var self = this;
|
||||
|
||||
var completed = 'is-completed';
|
||||
var $checkbox = $(e.target);
|
||||
var $task = $checkbox.closest('.task');
|
||||
$task.toggleClass(completed);
|
||||
|
||||
var checklist_index = $checkbox.data('checklist');
|
||||
var task_index = $checkbox.data('task');
|
||||
var model = this.collection.at(checklist_index);
|
||||
model.attributes.items[task_index].is_checked = $task.hasClass(completed);
|
||||
model.save({},
|
||||
{
|
||||
success : function() {
|
||||
var updatedTemplate = self.renderTemplate(model, checklist_index);
|
||||
self.$el.find('#course-checklist'+checklist_index).first().replaceWith(updatedTemplate);
|
||||
},
|
||||
error : CMS.ServerError
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -142,8 +142,11 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
|
||||
|
||||
onDelete: function(event) {
|
||||
event.preventDefault();
|
||||
// TODO ask for confirmation
|
||||
// remove the dom element and delete the model
|
||||
|
||||
if (!confirm('Are you sure you want to delete this update? This action cannot be undone.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
var targetModel = this.eventModel(event);
|
||||
this.modelDom(event).remove();
|
||||
var cacheThis = this;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// studio base styling
|
||||
// studio - base styling
|
||||
// ====================
|
||||
|
||||
// basic reset
|
||||
// basic setup
|
||||
html {
|
||||
font-size: 62.5%;
|
||||
overflow-y: scroll;
|
||||
@@ -9,7 +9,7 @@ html {
|
||||
|
||||
body {
|
||||
@include font-size(16);
|
||||
min-width: 980px;
|
||||
min-width: $fg-min-width;
|
||||
background: $gray-l5;
|
||||
line-height: 1.6;
|
||||
color: $baseFontColor;
|
||||
@@ -214,7 +214,7 @@ h1 {
|
||||
color: $gray-l2;
|
||||
}
|
||||
|
||||
.title, .title-1 {
|
||||
.title-1 {
|
||||
@include font-size(32);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
@@ -283,8 +283,8 @@ h1 {
|
||||
|
||||
.title-3 {
|
||||
@include font-size(16);
|
||||
margin: 0 0 ($baseline/4) 0;
|
||||
font-weight: 500;
|
||||
margin: 0 0 ($baseline/2) 0;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.title-4 {
|
||||
@@ -327,7 +327,8 @@ h1 {
|
||||
}
|
||||
}
|
||||
|
||||
.nav-related {
|
||||
// navigation
|
||||
.nav-related, .nav-page {
|
||||
|
||||
.nav-item {
|
||||
margin-bottom: ($baseline/4);
|
||||
@@ -350,10 +351,11 @@ h1 {
|
||||
// layout - grandfathered
|
||||
.main-wrapper {
|
||||
position: relative;
|
||||
margin: 0 40px;
|
||||
margin: 40px;
|
||||
}
|
||||
|
||||
.inner-wrapper {
|
||||
@include clearfix();
|
||||
position: relative;
|
||||
max-width: 1280px;
|
||||
margin: auto;
|
||||
@@ -363,6 +365,12 @@ h1 {
|
||||
}
|
||||
}
|
||||
|
||||
.main-column {
|
||||
clear: both;
|
||||
float: left;
|
||||
width: 70%;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
float: right;
|
||||
width: 28%;
|
||||
@@ -378,109 +386,6 @@ h1 {
|
||||
|
||||
// ====================
|
||||
|
||||
// forms
|
||||
input[type="text"],
|
||||
input[type="email"],
|
||||
input[type="password"],
|
||||
textarea.text {
|
||||
padding: 6px 8px 8px;
|
||||
@include box-sizing(border-box);
|
||||
border: 1px solid $mediumGrey;
|
||||
border-radius: 2px;
|
||||
@include linear-gradient($lightGrey, tint($lightGrey, 90%));
|
||||
background-color: $lightGrey;
|
||||
@include box-shadow(0 1px 2px rgba(0, 0, 0, .1) inset);
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
font-size: 11px;
|
||||
color: $baseFontColor;
|
||||
outline: 0;
|
||||
|
||||
&::-webkit-input-placeholder,
|
||||
&:-moz-placeholder,
|
||||
&:-ms-input-placeholder {
|
||||
color: #979faf;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
@include linear-gradient($paleYellow, tint($paleYellow, 90%));
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
&[disabled] {
|
||||
border-color: $gray-l4;
|
||||
color: $gray-l2;
|
||||
}
|
||||
|
||||
&[readonly] {
|
||||
border-color: $gray-l4;
|
||||
color: $gray-l1;
|
||||
|
||||
&:focus {
|
||||
@include linear-gradient($lightGrey, tint($lightGrey, 90%));
|
||||
outline: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// forms - specific
|
||||
input.search {
|
||||
padding: 6px 15px 8px 30px;
|
||||
@include box-sizing(border-box);
|
||||
border: 1px solid $darkGrey;
|
||||
border-radius: 20px;
|
||||
background: url(../img/search-icon.png) no-repeat 8px 7px #edf1f5;
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
color: $baseFontColor;
|
||||
outline: 0;
|
||||
|
||||
&::-webkit-input-placeholder {
|
||||
color: #979faf;
|
||||
}
|
||||
}
|
||||
|
||||
label {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
code {
|
||||
padding: 0 4px;
|
||||
border-radius: 3px;
|
||||
background: #eee;
|
||||
font-family: Monaco, monospace;
|
||||
}
|
||||
|
||||
.CodeMirror {
|
||||
font-size: 13px;
|
||||
border: 1px solid $darkGrey;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.text-editor {
|
||||
width: 100%;
|
||||
min-height: 80px;
|
||||
padding: 10px;
|
||||
@include box-sizing(border-box);
|
||||
border: 1px solid $mediumGrey;
|
||||
@include linear-gradient(top, rgba(255, 255, 255, 0), rgba(255, 255, 255, 0.3));
|
||||
background-color: #edf1f5;
|
||||
@include box-shadow(0 1px 2px rgba(0, 0, 0, 0.1) inset);
|
||||
font-family: Monaco, monospace;
|
||||
}
|
||||
|
||||
// ====================
|
||||
|
||||
// UI - chrome
|
||||
.window {
|
||||
@include clearfix();
|
||||
@include border-radius(3px);
|
||||
@include box-shadow(0 1px 1px $shadow-l1);
|
||||
margin-bottom: $baseline;
|
||||
border: 1px solid $gray-l2;
|
||||
background: $white;
|
||||
}
|
||||
|
||||
// ====================
|
||||
|
||||
// UI - actions
|
||||
.new-unit-item,
|
||||
.new-subsection-item,
|
||||
@@ -787,6 +692,10 @@ hr.divide {
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
hr.divider {
|
||||
@extend .sr;
|
||||
}
|
||||
|
||||
// ====================
|
||||
|
||||
// js dependant
|
||||
@@ -861,14 +770,4 @@ body.hide-wip {
|
||||
.wip-box {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
// ====================
|
||||
|
||||
// needed fudges for now
|
||||
body.dashboard {
|
||||
|
||||
.my-classes {
|
||||
margin-top: $baseline;
|
||||
}
|
||||
}
|
||||
@@ -1,367 +0,0 @@
|
||||
section.cal {
|
||||
@include box-sizing(border-box);
|
||||
@include clearfix;
|
||||
padding: 20px;
|
||||
|
||||
> header {
|
||||
display: none;
|
||||
@include clearfix;
|
||||
margin-bottom: 10px;
|
||||
opacity: .4;
|
||||
@include transition;
|
||||
text-shadow: 0 1px 0 #fff;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
h2 {
|
||||
@include inline-block();
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
font-size: 14px;
|
||||
padding: 6px 6px 6px 0;
|
||||
font-size: 12px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
ul {
|
||||
@include inline-block;
|
||||
float: right;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
&.actions {
|
||||
float: left;
|
||||
}
|
||||
|
||||
li {
|
||||
@include inline-block;
|
||||
margin-right: 6px;
|
||||
border-right: 1px solid #ddd;
|
||||
padding: 0 6px 0 0;
|
||||
|
||||
&:last-child {
|
||||
border-right: 0;
|
||||
margin-right: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
a {
|
||||
@include inline-block();
|
||||
font-size: 12px;
|
||||
@include inline-block;
|
||||
margin: 0 6px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
ul {
|
||||
@include inline-block();
|
||||
margin: 0;
|
||||
|
||||
li {
|
||||
@include inline-block();
|
||||
padding: 0;
|
||||
border-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ol {
|
||||
list-style: none;
|
||||
@include clearfix;
|
||||
border: 1px solid lighten( $dark-blue , 30% );
|
||||
background: #FFF;
|
||||
width: 100%;
|
||||
@include box-sizing(border-box);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
@include box-shadow(0 0 5px lighten($dark-blue, 45%));
|
||||
@include border-radius(3px);
|
||||
overflow: hidden;
|
||||
|
||||
> li {
|
||||
border-right: 1px solid lighten($dark-blue, 40%);
|
||||
border-bottom: 1px solid lighten($dark-blue, 40%);
|
||||
@include box-sizing(border-box);
|
||||
float: left;
|
||||
width: flex-grid(3) + ((flex-gutter() * 3) / 4);
|
||||
background-color: $light-blue;
|
||||
@include box-shadow(inset 0 0 0 1px lighten($light-blue, 8%));
|
||||
|
||||
&:hover {
|
||||
li.create-module {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&:nth-child(4n) {
|
||||
border-right: 0;
|
||||
}
|
||||
|
||||
header {
|
||||
border-bottom: 1px solid lighten($dark-blue, 40%);
|
||||
@include box-shadow(0 2px 2px $light-blue);
|
||||
display: block;
|
||||
margin-bottom: 2px;
|
||||
background: #FFF;
|
||||
|
||||
h1 {
|
||||
font-size: 14px;
|
||||
text-transform: uppercase;
|
||||
border-bottom: 1px solid lighten($dark-blue, 60%);
|
||||
padding: 6px;
|
||||
color: $bright-blue;
|
||||
margin: 0;
|
||||
|
||||
a {
|
||||
color: $bright-blue;
|
||||
display: block;
|
||||
padding: 6px;
|
||||
margin: -6px;
|
||||
|
||||
&:hover {
|
||||
color: darken($bright-blue, 10%);
|
||||
background: lighten($yellow, 10%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
li {
|
||||
background: #fff;
|
||||
color: #888;
|
||||
border-bottom: 0;
|
||||
font-size: 12px;
|
||||
@include box-shadow(none);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
margin: 0 0 1px 0;
|
||||
padding: 0;
|
||||
|
||||
li {
|
||||
border-bottom: 1px solid darken($light-blue, 6%);
|
||||
// @include box-shadow(0 1px 0 lighten($light-blue, 4%));
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
text-shadow: 0 1px 0 #fff;
|
||||
|
||||
&:hover {
|
||||
background-color: lighten($yellow, 14%);
|
||||
|
||||
a.draggable {
|
||||
background-color: lighten($yellow, 14%);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&.editable {
|
||||
padding: 3px 6px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: lighten($dark-blue, 10%);
|
||||
display: block;
|
||||
padding: 6px 35px 6px 6px;
|
||||
|
||||
&:hover {
|
||||
background-color: lighten($yellow, 10%);
|
||||
}
|
||||
|
||||
&.draggable {
|
||||
background-color: $light-blue;
|
||||
opacity: .3;
|
||||
padding: 0;
|
||||
|
||||
&:hover {
|
||||
background-color: lighten($yellow, 10%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.create-module {
|
||||
position: relative;
|
||||
opacity: 0;
|
||||
@include transition(all 3s ease-in-out);
|
||||
background: darken($light-blue, 2%);
|
||||
|
||||
> div {
|
||||
background: $dark-blue;
|
||||
@include box-shadow(0 0 5px darken($light-blue, 60%));
|
||||
@include box-sizing(border-box);
|
||||
display: none;
|
||||
margin-left: 3%;
|
||||
padding: 10px;
|
||||
@include position(absolute, 30px 0 0 0);
|
||||
width: 90%;
|
||||
z-index: 99;
|
||||
|
||||
ul {
|
||||
li {
|
||||
border-bottom: 0;
|
||||
background: none;
|
||||
|
||||
input {
|
||||
@include box-sizing(border-box);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
select {
|
||||
@include box-sizing(border-box);
|
||||
width: 100%;
|
||||
|
||||
option {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
div {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: $light-blue;
|
||||
float: right;
|
||||
|
||||
&:first-child {
|
||||
float: left;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
section.new-section {
|
||||
margin: 10px 0 40px;
|
||||
@include inline-block();
|
||||
position: relative;
|
||||
|
||||
> a {
|
||||
@extend .button;
|
||||
display: block;
|
||||
}
|
||||
|
||||
section {
|
||||
display: none;
|
||||
@include position(absolute, 30px 0 0 0);
|
||||
background: rgba(#000, .8);
|
||||
min-width: 300px;
|
||||
padding: 10px;
|
||||
@include box-sizing(border-box);
|
||||
@include border-radius(3px);
|
||||
z-index: 99;
|
||||
|
||||
&:before {
|
||||
content: " ";
|
||||
display: block;
|
||||
background: rgba(#000, .8);
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
@include position(absolute, -5px 0 0 20%);
|
||||
@include transform(rotate(45deg));
|
||||
}
|
||||
|
||||
form {
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
|
||||
li {
|
||||
border-bottom: 0;
|
||||
background: none;
|
||||
margin-bottom: 6px;
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
@include box-sizing(border-box);
|
||||
border-color: #000;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
select {
|
||||
width: 100%;
|
||||
@include box-sizing(border-box);
|
||||
|
||||
option {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
float: right;
|
||||
|
||||
&:first-child {
|
||||
float: left;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
body.content
|
||||
section.cal {
|
||||
width: flex-grid(3);
|
||||
float: left;
|
||||
overflow: scroll;
|
||||
@include box-sizing(border-box);
|
||||
opacity: .4;
|
||||
@include transition();
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
> header {
|
||||
@include transition;
|
||||
overflow: hidden;
|
||||
|
||||
> a {
|
||||
display: none;
|
||||
}
|
||||
|
||||
ul {
|
||||
float: none;
|
||||
display: block;
|
||||
|
||||
li {
|
||||
|
||||
ul {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ol {
|
||||
li {
|
||||
@include box-sizing(border-box);
|
||||
width: 100%;
|
||||
border-right: 0;
|
||||
|
||||
&.create-module {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
// studio - utilities - mixins and extends
|
||||
// ====================
|
||||
|
||||
@mixin clearfix {
|
||||
&:after {
|
||||
content: '';
|
||||
|
||||
@@ -1,689 +0,0 @@
|
||||
|
||||
input.courseware-unit-search-input {
|
||||
float: left;
|
||||
width: 260px;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.branch {
|
||||
|
||||
.section-item {
|
||||
@include clearfix();
|
||||
|
||||
.details {
|
||||
display: block;
|
||||
float: left;
|
||||
margin-bottom: 0;
|
||||
width: 650px;
|
||||
}
|
||||
|
||||
.gradable-status {
|
||||
float: right;
|
||||
position: relative;
|
||||
top: -4px;
|
||||
right: 50px;
|
||||
width: 145px;
|
||||
|
||||
.status-label {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: -5px;
|
||||
display: none;
|
||||
width: 110px;
|
||||
padding: 5px 40px 5px 10px;
|
||||
@include border-radius(3px);
|
||||
color: $lightGrey;
|
||||
text-align: right;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
.menu-toggle {
|
||||
z-index: 10;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 5px;
|
||||
padding: 5px;
|
||||
color: $mediumGrey;
|
||||
|
||||
&:hover, &.is-active {
|
||||
color: $blue;
|
||||
}
|
||||
}
|
||||
|
||||
.menu {
|
||||
z-index: 1;
|
||||
display: none;
|
||||
opacity: 0.0;
|
||||
position: absolute;
|
||||
top: -1px;
|
||||
left: 5px;
|
||||
margin: 0;
|
||||
padding: 8px 12px;
|
||||
background: $white;
|
||||
border: 1px solid $mediumGrey;
|
||||
font-size: 12px;
|
||||
@include border-radius(4px);
|
||||
@include box-shadow(0 1px 2px rgba(0, 0, 0, .2));
|
||||
@include transition(opacity .15s);
|
||||
|
||||
|
||||
li {
|
||||
width: 115px;
|
||||
margin-bottom: 3px;
|
||||
padding-bottom: 3px;
|
||||
border-bottom: 1px solid $lightGrey;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
border: none;
|
||||
|
||||
a {
|
||||
color: $darkGrey;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
color: $blue;
|
||||
|
||||
&.is-selected {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// dropdown state
|
||||
&.is-active {
|
||||
|
||||
.menu {
|
||||
z-index: 1000;
|
||||
display: block;
|
||||
opacity: 1.0;
|
||||
}
|
||||
|
||||
.menu-toggle {
|
||||
z-index: 10000;
|
||||
}
|
||||
}
|
||||
|
||||
// set state
|
||||
&.is-set {
|
||||
|
||||
.menu-toggle {
|
||||
color: $blue;
|
||||
}
|
||||
|
||||
.status-label {
|
||||
display: block;
|
||||
color: $blue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.courseware-section {
|
||||
position: relative;
|
||||
background: #fff;
|
||||
border-radius: 3px;
|
||||
border: 1px solid $mediumGrey;
|
||||
margin-top: 15px;
|
||||
padding-bottom: 12px;
|
||||
@include box-shadow(0 1px 1px rgba(0, 0, 0, 0.1));
|
||||
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
&.collapsed {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
label {
|
||||
float: left;
|
||||
line-height: 29px;
|
||||
}
|
||||
|
||||
.datepair {
|
||||
float: left;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.section-published-date {
|
||||
position: absolute;
|
||||
top: 19px;
|
||||
right: 90px;
|
||||
padding: 4px 10px;
|
||||
border-radius: 3px;
|
||||
background: $lightGrey;
|
||||
text-align: right;
|
||||
|
||||
.published-status {
|
||||
font-size: 12px;
|
||||
margin-right: 15px;
|
||||
|
||||
strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.schedule-button {
|
||||
@include blue-button;
|
||||
}
|
||||
|
||||
.edit-button {
|
||||
@include blue-button;
|
||||
}
|
||||
|
||||
.schedule-button,
|
||||
.edit-button {
|
||||
font-size: 11px;
|
||||
padding: 3px 15px 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.datepair .date,
|
||||
.datepair .time {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
border: none;
|
||||
background: none;
|
||||
@include box-shadow(none);
|
||||
font-size: 13px;
|
||||
font-weight: bold;
|
||||
color: $blue;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.datepair .date {
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.datepair .time {
|
||||
width: 65px;
|
||||
}
|
||||
|
||||
&.collapsed .subsection-list,
|
||||
.collapsed .subsection-list,
|
||||
.collapsed > ol {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
header {
|
||||
min-height: 75px;
|
||||
@include clearfix();
|
||||
|
||||
.item-details, .section-published-date {
|
||||
|
||||
}
|
||||
|
||||
.item-details {
|
||||
display: inline-block;
|
||||
padding: 20px 0 10px 0;
|
||||
@include clearfix();
|
||||
|
||||
.section-name {
|
||||
float: left;
|
||||
margin-right: 10px;
|
||||
width: 350px;
|
||||
font-size: 19px;
|
||||
font-weight: bold;
|
||||
color: $blue;
|
||||
}
|
||||
|
||||
.section-name-span {
|
||||
cursor: pointer;
|
||||
@include transition(color .15s);
|
||||
|
||||
&:hover {
|
||||
color: $orange;
|
||||
}
|
||||
}
|
||||
|
||||
.section-name-edit {
|
||||
position: relative;
|
||||
width: 400px;
|
||||
background: $white;
|
||||
|
||||
input {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.save-button {
|
||||
@include blue-button;
|
||||
padding: 7px 20px 7px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.cancel-button {
|
||||
@include white-button;
|
||||
padding: 7px 20px 7px;
|
||||
}
|
||||
}
|
||||
|
||||
.section-published-date {
|
||||
float: right;
|
||||
width: 265px;
|
||||
margin-right: 220px;
|
||||
@include border-radius(3px);
|
||||
background: $lightGrey;
|
||||
|
||||
.published-status {
|
||||
font-size: 12px;
|
||||
margin-right: 15px;
|
||||
|
||||
strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.schedule-button {
|
||||
@include blue-button;
|
||||
}
|
||||
|
||||
.edit-button {
|
||||
@include blue-button;
|
||||
}
|
||||
|
||||
.schedule-button,
|
||||
.edit-button {
|
||||
font-size: 11px;
|
||||
padding: 3px 15px 5px;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.gradable-status {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 70px;
|
||||
width: 145px;
|
||||
|
||||
.status-label {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 2px;
|
||||
display: none;
|
||||
width: 100px;
|
||||
padding: 10px 35px 10px 10px;
|
||||
@include border-radius(3px);
|
||||
background: $lightGrey;
|
||||
color: $lightGrey;
|
||||
text-align: right;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
.menu-toggle {
|
||||
z-index: 10;
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 5px;
|
||||
padding: 5px;
|
||||
color: $lightGrey;
|
||||
|
||||
&:hover, &.is-active {
|
||||
color: $blue;
|
||||
}
|
||||
}
|
||||
|
||||
.menu {
|
||||
z-index: 1;
|
||||
display: none;
|
||||
opacity: 0.0;
|
||||
position: absolute;
|
||||
top: -1px;
|
||||
left: 2px;
|
||||
margin: 0;
|
||||
padding: 8px 12px;
|
||||
background: $white;
|
||||
border: 1px solid $mediumGrey;
|
||||
font-size: 12px;
|
||||
@include border-radius(4px);
|
||||
@include box-shadow(0 1px 2px rgba(0, 0, 0, .2));
|
||||
@include transition(opacity .15s);
|
||||
@include transition(display .15s);
|
||||
|
||||
|
||||
li {
|
||||
width: 115px;
|
||||
margin-bottom: 3px;
|
||||
padding-bottom: 3px;
|
||||
border-bottom: 1px solid $lightGrey;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
border: none;
|
||||
|
||||
a {
|
||||
color: $darkGrey;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
|
||||
&.is-selected {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// dropdown state
|
||||
&.is-active {
|
||||
|
||||
.menu {
|
||||
z-index: 1000;
|
||||
display: block;
|
||||
opacity: 1.0;
|
||||
}
|
||||
|
||||
|
||||
.menu-toggle {
|
||||
z-index: 10000;
|
||||
}
|
||||
}
|
||||
|
||||
// set state
|
||||
&.is-set {
|
||||
|
||||
.menu-toggle {
|
||||
color: $blue;
|
||||
}
|
||||
|
||||
.status-label {
|
||||
display: block;
|
||||
color: $blue;
|
||||
}
|
||||
}
|
||||
|
||||
float: left;
|
||||
padding: 21px 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
.item-actions {
|
||||
margin-top: 21px;
|
||||
margin-right: 12px;
|
||||
|
||||
.edit-button,
|
||||
.delete-button {
|
||||
margin-top: -3px;
|
||||
}
|
||||
}
|
||||
|
||||
.expand-collapse-icon {
|
||||
float: left;
|
||||
margin: 29px 6px 16px 16px;
|
||||
@include transition(none);
|
||||
|
||||
&.expand {
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
&.collapsed {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.drag-handle {
|
||||
margin-left: 11px;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 19px;
|
||||
font-weight: 700;
|
||||
color: $blue;
|
||||
}
|
||||
|
||||
.section-name-span {
|
||||
cursor: pointer;
|
||||
@include transition(color .15s);
|
||||
|
||||
&:hover {
|
||||
color: $orange;
|
||||
}
|
||||
}
|
||||
|
||||
.section-name-form {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.section-name-edit {
|
||||
input {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.save-button {
|
||||
@include blue-button;
|
||||
padding: 7px 20px 7px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.cancel-button {
|
||||
@include white-button;
|
||||
padding: 7px 20px 7px;
|
||||
}
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 12px;
|
||||
color: #878e9d;
|
||||
|
||||
strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.list-header {
|
||||
@include linear-gradient(top, transparent, rgba(0, 0, 0, .1));
|
||||
background-color: #ced2db;
|
||||
border-radius: 3px 3px 0 0;
|
||||
}
|
||||
|
||||
.subsection-list {
|
||||
margin: 0 12px;
|
||||
|
||||
> ol {
|
||||
@include tree-view;
|
||||
border-top-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.new-section {
|
||||
|
||||
header {
|
||||
height: auto;
|
||||
@include clearfix();
|
||||
}
|
||||
|
||||
.expand-collapse-icon {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.item-details {
|
||||
padding: 25px 0 0 0;
|
||||
|
||||
.section-name {
|
||||
float: none;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.toggle-button-sections {
|
||||
display: none;
|
||||
position: relative;
|
||||
float: right;
|
||||
margin-top: 10px;
|
||||
|
||||
font-size: 13px;
|
||||
color: $darkGrey;
|
||||
|
||||
&.is-shown {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.ss-icon {
|
||||
@include border-radius(20px);
|
||||
position: relative;
|
||||
top: -1px;
|
||||
display: inline-block;
|
||||
margin-right: 2px;
|
||||
line-height: 5px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.label {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
.new-section-name,
|
||||
.new-subsection-name-input {
|
||||
width: 515px;
|
||||
}
|
||||
|
||||
.new-section-name-save,
|
||||
.new-subsection-name-save {
|
||||
@include blue-button;
|
||||
padding: 4px 20px 7px;
|
||||
margin: 0 5px;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
.new-section-name-cancel,
|
||||
.new-subsection-name-cancel {
|
||||
@include white-button;
|
||||
padding: 4px 20px 7px;
|
||||
color: #8891a1 !important;
|
||||
}
|
||||
|
||||
.dummy-calendar {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 55px;
|
||||
left: 110px;
|
||||
z-index: 9999;
|
||||
border: 1px solid #3C3C3C;
|
||||
@include box-shadow(0 1px 15px rgba(0, 0, 0, .2));
|
||||
}
|
||||
|
||||
.unit-name-input {
|
||||
padding: 20px 40px;
|
||||
|
||||
label {
|
||||
display: block;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.preview {
|
||||
background: url(../img/preview.jpg) center top no-repeat;
|
||||
}
|
||||
|
||||
.edit-subsection-publish-settings {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 100px;
|
||||
left: 50%;
|
||||
z-index: 99999;
|
||||
width: 600px;
|
||||
margin-left: -300px;
|
||||
background: #fff;
|
||||
text-align: center;
|
||||
|
||||
.settings {
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 34px;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.picker {
|
||||
margin: 30px 0 65px;
|
||||
}
|
||||
|
||||
.description {
|
||||
margin-top: 30px;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
strong {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.start-date,
|
||||
.start-time {
|
||||
font-size: 19px;
|
||||
}
|
||||
|
||||
.save-button {
|
||||
@include blue-button;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.cancel-button {
|
||||
@include white-button;
|
||||
}
|
||||
|
||||
.save-button,
|
||||
.cancel-button {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.collapse-all-button {
|
||||
float: right;
|
||||
margin-top: 10px;
|
||||
font-size: 13px;
|
||||
color: $darkGrey;
|
||||
}
|
||||
|
||||
// sort/drag and drop
|
||||
.ui-droppable {
|
||||
@include transition (padding 0.5s ease-in-out 0s);
|
||||
min-height: 20px;
|
||||
padding: 0;
|
||||
|
||||
&.dropover {
|
||||
padding: 15px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.ui-draggable-dragging {
|
||||
@include box-shadow(0 1px 2px rgba(0, 0, 0, .3));
|
||||
border: 1px solid $darkGrey;
|
||||
opacity : 0.2;
|
||||
&:hover {
|
||||
opacity : 1.0;
|
||||
.section-item {
|
||||
background: $yellow !important;
|
||||
}
|
||||
}
|
||||
|
||||
// hiding unit button - temporary fix until this semantically corrected
|
||||
.new-unit-item {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
ol.ui-droppable .branch:first-child .section-item {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
.class-list {
|
||||
margin-top: 20px;
|
||||
border-radius: 3px;
|
||||
border: 1px solid $darkGrey;
|
||||
background: #fff;
|
||||
@include box-shadow(0 1px 2px rgba(0, 0, 0, .1));
|
||||
|
||||
li {
|
||||
position: relative;
|
||||
border-bottom: 1px solid $mediumGrey;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.class-link {
|
||||
z-index: 100;
|
||||
display: block;
|
||||
padding: 20px 25px;
|
||||
line-height: 1.3;
|
||||
|
||||
&:hover {
|
||||
background: $paleYellow;
|
||||
|
||||
+ .view-live-button {
|
||||
opacity: 1.0;
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.class-name {
|
||||
display: block;
|
||||
font-size: 19px;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.detail {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
margin-right: 20px;
|
||||
color: #3c3c3c;
|
||||
}
|
||||
|
||||
// view live button
|
||||
.view-live-button {
|
||||
z-index: 10000;
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
right: $baseline;
|
||||
padding: ($baseline/4) ($baseline/2);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
|
||||
&:hover {
|
||||
opacity: 1.0;
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.new-course {
|
||||
padding: 15px 25px;
|
||||
margin-top: 20px;
|
||||
border-radius: 3px;
|
||||
border: 1px solid $darkGrey;
|
||||
background: #fff;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, .1);
|
||||
@include clearfix;
|
||||
|
||||
.row {
|
||||
margin-bottom: 15px;
|
||||
@include clearfix;
|
||||
}
|
||||
|
||||
.column {
|
||||
float: left;
|
||||
width: 48%;
|
||||
}
|
||||
|
||||
.column:first-child {
|
||||
margin-right: 4%;
|
||||
}
|
||||
|
||||
.course-info {
|
||||
width: 600px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.new-course-org,
|
||||
.new-course-number,
|
||||
.new-course-name {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.new-course-name {
|
||||
font-size: 19px;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.new-course-save {
|
||||
@include blue-button;
|
||||
}
|
||||
|
||||
.new-course-cancel {
|
||||
@include white-button;
|
||||
}
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
.faded-hr-divider {
|
||||
@include background-image(linear-gradient(180deg, rgba(200,200,200, 0) 0%,
|
||||
rgba(200,200,200, 1) 50%,
|
||||
rgba(200,200,200, 0)));
|
||||
height: 1px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.faded-hr-divider-medium {
|
||||
@include background-image(linear-gradient(180deg, rgba(240,240,240, 0) 0%,
|
||||
rgba(240,240,240, 1) 50%,
|
||||
rgba(240,240,240, 0)));
|
||||
height: 1px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.faded-hr-divider-light {
|
||||
@include background-image(linear-gradient(180deg, rgba(255,255,255, 0) 0%,
|
||||
rgba(255,255,255, 0.8) 50%,
|
||||
rgba(255,255,255, 0)));
|
||||
height: 1px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.faded-vertical-divider {
|
||||
@include background-image(linear-gradient(90deg, rgba(200,200,200, 0) 0%,
|
||||
rgba(200,200,200, 1) 50%,
|
||||
rgba(200,200,200, 0)));
|
||||
height: 100%;
|
||||
width: 1px;
|
||||
}
|
||||
|
||||
.faded-vertical-divider-light {
|
||||
@include background-image(linear-gradient(90deg, rgba(255,255,255, 0) 0%,
|
||||
rgba(255,255,255, 0.6) 50%,
|
||||
rgba(255,255,255, 0)));
|
||||
height: 100%;
|
||||
width: 1px;
|
||||
}
|
||||
|
||||
.vertical-divider {
|
||||
@extend .faded-vertical-divider;
|
||||
position: relative;
|
||||
|
||||
&::after {
|
||||
@extend .faded-vertical-divider-light;
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
left: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
.horizontal-divider {
|
||||
border: none;
|
||||
@extend .faded-hr-divider;
|
||||
position: relative;
|
||||
|
||||
&::after {
|
||||
@extend .faded-hr-divider-light;
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
.fade-right-hr-divider {
|
||||
@include background-image(linear-gradient(180deg, rgba(200,200,200, 0) 0%,
|
||||
rgba(200,200,200, 1)));
|
||||
border: none;
|
||||
}
|
||||
|
||||
.fade-left-hr-divider {
|
||||
@include background-image(linear-gradient(180deg, rgba(200,200,200, 1) 0%,
|
||||
rgba(200,200,200, 0)));
|
||||
border: none;
|
||||
}
|
||||
@@ -1,126 +0,0 @@
|
||||
// This is a temporary page, which will be replaced once we have a more extensive course catalog and marketing site for edX labs.
|
||||
|
||||
.class-landing {
|
||||
|
||||
.main-wrapper {
|
||||
width: 700px !important;
|
||||
margin: 100px auto;
|
||||
}
|
||||
|
||||
.class-info {
|
||||
padding: 30px 40px 40px;
|
||||
@extend .window;
|
||||
|
||||
hgroup {
|
||||
padding-bottom: 26px;
|
||||
border-bottom: 1px solid $mediumGrey;
|
||||
}
|
||||
|
||||
h1 {
|
||||
float: none;
|
||||
font-size: 30px;
|
||||
font-weight: 300;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: #5d6779;
|
||||
}
|
||||
|
||||
.class-actions {
|
||||
@include clearfix;
|
||||
padding: 15px 0;
|
||||
margin-bottom: 18px;
|
||||
border-bottom: 1px solid $mediumGrey;
|
||||
}
|
||||
|
||||
.log-in-form {
|
||||
@include clearfix;
|
||||
padding: 15px 0 20px;
|
||||
margin-bottom: 18px;
|
||||
border-bottom: 1px solid $mediumGrey;
|
||||
|
||||
.log-in-submit-button {
|
||||
@include blue-button;
|
||||
padding: 6px 20px 8px;
|
||||
margin: 24px 0 0;
|
||||
}
|
||||
|
||||
.column {
|
||||
float: left;
|
||||
width: 41%;
|
||||
margin-right: 1%;
|
||||
|
||||
&.submit {
|
||||
width: 16%;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
label {
|
||||
float: left;
|
||||
}
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
font-family: $sans-serif;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.forgot-button {
|
||||
float: right;
|
||||
margin-bottom: 6px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.sign-up-button {
|
||||
@include blue-button;
|
||||
display: block;
|
||||
width: 250px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.log-in-button {
|
||||
@include white-button;
|
||||
float: right;
|
||||
}
|
||||
|
||||
.sign-up-button,
|
||||
.log-in-button {
|
||||
padding: 8px 0 12px;
|
||||
font-size: 18px;
|
||||
font-weight: 300;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.class-description {
|
||||
margin-top: 30px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
p + p {
|
||||
margin-top: 22px;
|
||||
}
|
||||
}
|
||||
|
||||
.edx-labs-logo-small {
|
||||
display: block;
|
||||
width: 124px;
|
||||
height: 30px;
|
||||
margin: auto;
|
||||
background: url(../img/edx-labs-logo-small.png) no-repeat;
|
||||
text-indent: -9999px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.edge-logo {
|
||||
display: block;
|
||||
width: 143px;
|
||||
height: 39px;
|
||||
margin: auto;
|
||||
background: url(../images/edge-logo-small.png) no-repeat;
|
||||
text-indent: -9999px;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
body {
|
||||
@include clearfix();
|
||||
height: 100%;
|
||||
font: 14px $body-font-family;
|
||||
background-color: lighten($dark-blue, 62%);
|
||||
background-image: url('/static/img/noise.png');
|
||||
|
||||
> section {
|
||||
display: table;
|
||||
table-layout: fixed;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
> header {
|
||||
background: $dark-blue;
|
||||
@include background-image(url('/static/img/noise.png'), linear-gradient(lighten($dark-blue, 10%), $dark-blue));
|
||||
border-bottom: 1px solid darken($dark-blue, 15%);
|
||||
@include box-shadow(inset 0 -1px 0 lighten($dark-blue, 10%));
|
||||
@include box-sizing(border-box);
|
||||
color: #fff;
|
||||
display: block;
|
||||
float: none;
|
||||
padding: 0 20px;
|
||||
text-shadow: 0 -1px 0 darken($dark-blue, 15%);
|
||||
width: 100%;
|
||||
|
||||
nav {
|
||||
@include clearfix;
|
||||
|
||||
> a {
|
||||
@include hide-text;
|
||||
background: url('/static/img/menu.png') 0 center no-repeat;
|
||||
border-right: 1px solid darken($dark-blue, 10%);
|
||||
@include box-shadow(1px 0 0 lighten($dark-blue, 10%));
|
||||
display: block;
|
||||
float: left;
|
||||
height: 19px;
|
||||
padding: 8px 10px 8px 0;
|
||||
width: 14px;
|
||||
|
||||
&:hover, &:focus {
|
||||
opacity: .7;
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
border-right: 1px solid darken($dark-blue, 10%);
|
||||
@include box-shadow(1px 0 0 lighten($dark-blue, 10%));
|
||||
float: left;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
text-transform: uppercase;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
|
||||
a {
|
||||
color: #fff;
|
||||
padding: 8px 20px;
|
||||
display: block;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(darken($dark-blue, 15%), .5);
|
||||
color: $yellow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
color: rgba(#fff, .8);
|
||||
|
||||
&:hover {
|
||||
color: rgba(#fff, .6);
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
float: left;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
@include clearfix;
|
||||
|
||||
&.user-nav {
|
||||
float: right;
|
||||
border-left: 1px solid darken($dark-blue, 10%);
|
||||
}
|
||||
|
||||
li {
|
||||
border-right: 1px solid darken($dark-blue, 10%);
|
||||
float: left;
|
||||
@include box-shadow(1px 0 0 lighten($dark-blue, 10%));
|
||||
|
||||
a {
|
||||
padding: 8px 20px;
|
||||
display: block;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(darken($dark-blue, 15%), .5);
|
||||
color: $yellow;
|
||||
}
|
||||
|
||||
&.new-module {
|
||||
&:before {
|
||||
@include inline-block;
|
||||
content: "+";
|
||||
font-weight: bold;
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.content {
|
||||
section.main-content {
|
||||
border-left: 2px solid $dark-blue;
|
||||
@include box-sizing(border-box);
|
||||
width: flex-grid(9) + flex-gutter();
|
||||
float: left;
|
||||
@include box-shadow( -2px 0 0 lighten($dark-blue, 55%));
|
||||
@include transition();
|
||||
background: #FFF;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
.component {
|
||||
font-family: 'Open Sans', Verdana, Arial, Helvetica, sans-serif;
|
||||
font-size: 16px;
|
||||
line-height: 1.6;
|
||||
color: #3c3c3c;
|
||||
|
||||
a {
|
||||
color: #1d9dd9;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 16px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
h1 {
|
||||
float: none;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: #646464;
|
||||
font-size: 19px;
|
||||
font-weight: 300;
|
||||
letter-spacing: 1px;
|
||||
margin-bottom: 15px;
|
||||
margin-left: 0;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 19px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
h4 {
|
||||
background: none;
|
||||
padding: 0;
|
||||
border: none;
|
||||
@include box-shadow(none);
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
code {
|
||||
margin: 0 2px;
|
||||
padding: 0px 5px;
|
||||
border-radius: 3px;
|
||||
border: 1px solid #eaeaea;
|
||||
white-space: nowrap;
|
||||
font-family: Monaco, monospace;
|
||||
font-size: 14px;
|
||||
background-color: #f8f8f8;
|
||||
}
|
||||
|
||||
p + h2, ul + h2, ol + h2, p + h3 {
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
p + p, ul + p, ol + p {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #3c3c3c;
|
||||
font: normal 1em/1.6em;
|
||||
margin: 0px;
|
||||
}
|
||||
}
|
||||
@@ -1,139 +0,0 @@
|
||||
.edx-studio-logo-large {
|
||||
display: block;
|
||||
width: 224px;
|
||||
height: 45px;
|
||||
margin: 100px auto 30px;
|
||||
background: url(../img/edx-studio-large.png) no-repeat;
|
||||
}
|
||||
|
||||
.sign-up-box,
|
||||
.log-in-box {
|
||||
width: 500px;
|
||||
margin: auto;
|
||||
border-radius: 3px;
|
||||
|
||||
header {
|
||||
height: 36px;
|
||||
border-radius: 3px 3px 0 0;
|
||||
border: 1px solid #2c2e33;
|
||||
@include linear-gradient(top, #686b76, #54565e);
|
||||
color: #fff;
|
||||
@include box-shadow(0 1px 1px rgba(0, 0, 0, 0.2), 0 -1px 0px rgba(255, 255, 255, 0.05) inset, 0 1px 0 rgba(255, 255, 255, .25) inset);
|
||||
|
||||
h1 {
|
||||
float: none;
|
||||
margin: 5px 0;
|
||||
font-size: 15px;
|
||||
font-weight: 300;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
form {
|
||||
padding: 40px;
|
||||
border: 1px solid $darkGrey;
|
||||
border-top-width: 0;
|
||||
border-radius: 0 0 3px 3px;
|
||||
background: #fff;
|
||||
@include box-shadow(0 1px 2px rgba(0, 0, 0, .1));
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
input[type="text"],
|
||||
input[type="email"],
|
||||
input[type="password"] {
|
||||
width: 100%;
|
||||
font-size: 20px;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.row {
|
||||
@include clearfix;
|
||||
margin-bottom: 24px;
|
||||
|
||||
.split {
|
||||
float: left;
|
||||
width: 48%;
|
||||
|
||||
&:first-child {
|
||||
margin-right: 4%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
@include clearfix;
|
||||
margin-top: 32px;
|
||||
margin-bottom: 5px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.log-in-button,
|
||||
.create-account-button {
|
||||
@include blue-button;
|
||||
padding: 8px 0 10px;
|
||||
font-family: $sans-serif;
|
||||
@include transition(all .15s);
|
||||
}
|
||||
|
||||
.create-account-button {
|
||||
padding: 10px 40px 12px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.enrolled {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.sign-up-button {
|
||||
@include white-button;
|
||||
padding: 7px 0 9px;
|
||||
}
|
||||
|
||||
.log-in-button,
|
||||
.sign-up-button {
|
||||
@include box-sizing(border-box);
|
||||
float: left;
|
||||
width: 45%;
|
||||
}
|
||||
|
||||
.or {
|
||||
float: left;
|
||||
display: inline-block;
|
||||
width: 10%;
|
||||
font-size: 15px;
|
||||
line-height: 36px;
|
||||
color: $darkGrey;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.forgot-button {
|
||||
float: right;
|
||||
font-size: 11px;
|
||||
font-weight: 400;
|
||||
line-height: 21px;
|
||||
}
|
||||
|
||||
.log-in-extra {
|
||||
margin-top: 10px;
|
||||
text-align: right;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
#login_error,
|
||||
#register_error {
|
||||
display: none;
|
||||
margin-bottom: 30px;
|
||||
padding: 5px 10px;
|
||||
border-radius: 3px;
|
||||
background: $error-red;
|
||||
font-size: 14px;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
@@ -1,128 +0,0 @@
|
||||
section.video-new, section.video-edit, section.problem-new, section.problem-edit {
|
||||
position: absolute;
|
||||
top: 72px;
|
||||
right: 0;
|
||||
background: #fff;
|
||||
width: flex-grid(6);
|
||||
@include box-shadow(0 0 6px #666);
|
||||
border: 1px solid #333;
|
||||
border-right: 0;
|
||||
z-index: 4;
|
||||
|
||||
> header {
|
||||
background: #666;
|
||||
@include clearfix;
|
||||
color: #fff;
|
||||
padding: 6px;
|
||||
border-bottom: 1px solid #333;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
|
||||
h2 {
|
||||
float: left;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #fff;
|
||||
|
||||
&.save-update {
|
||||
float: right;
|
||||
}
|
||||
|
||||
&.cancel {
|
||||
float: left;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
> section {
|
||||
padding: 20px;
|
||||
|
||||
> header {
|
||||
h1 {
|
||||
font-size: 24px;
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
||||
section {
|
||||
&.status-settings {
|
||||
ul {
|
||||
list-style: none;
|
||||
@include border-radius(2px);
|
||||
border: 1px solid #999;
|
||||
@include inline-block();
|
||||
|
||||
li {
|
||||
@include inline-block();
|
||||
border-right: 1px solid #999;
|
||||
padding: 6px;
|
||||
|
||||
&:last-child {
|
||||
border-right: 0;
|
||||
}
|
||||
|
||||
&.current {
|
||||
background: #eee;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a.settings {
|
||||
@include inline-block();
|
||||
margin: 0 20px;
|
||||
border: 1px solid #999;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
select {
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
|
||||
&.meta {
|
||||
background: #eee;
|
||||
padding: 10px;
|
||||
margin: 20px 0;
|
||||
@include clearfix();
|
||||
|
||||
div {
|
||||
float: left;
|
||||
margin-right: 20px;
|
||||
|
||||
h2 {
|
||||
font-size: 14px;
|
||||
@include inline-block();
|
||||
}
|
||||
|
||||
p {
|
||||
@include inline-block();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
section.notes {
|
||||
margin-top: 20px;
|
||||
padding: 6px;
|
||||
background: #eee;
|
||||
border: 1px solid #ccc;
|
||||
|
||||
textarea {
|
||||
@include box-sizing(border-box);
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 14px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
input[type="submit"]{
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
section.problem-new, section.problem-edit {
|
||||
> section {
|
||||
textarea {
|
||||
@include box-sizing(border-box);
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
div.preview {
|
||||
background: #eee;
|
||||
@include box-sizing(border-box);
|
||||
height: 40px;
|
||||
padding: 10px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
a.save {
|
||||
@extend .button;
|
||||
@include inline-block();
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
// studio - utilities - reset
|
||||
// ====================
|
||||
|
||||
// * {
|
||||
// @include box-sizing(border-box);
|
||||
// }
|
||||
|
||||
html, body, div, span, applet, object, iframe,
|
||||
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
|
||||
a, abbr, acronym, address, big, cite, code,
|
||||
@@ -18,7 +25,7 @@ time, mark, audio, video {
|
||||
font: inherit;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
/* HTML5 display-role reset for older browsers */
|
||||
|
||||
article, aside, details, figcaption, figure,
|
||||
footer, header, hgroup, menu, nav, section {
|
||||
display: block;
|
||||
@@ -38,12 +45,6 @@ q:before, q:after {
|
||||
content: none;
|
||||
}
|
||||
|
||||
/* remember to define visible focus styles!
|
||||
:focus {
|
||||
outline: ?????;
|
||||
} */
|
||||
|
||||
/* remember to highlight inserts somehow! */
|
||||
ins {
|
||||
text-decoration: none;
|
||||
}
|
||||
@@ -56,10 +57,11 @@ table {
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
/* Reset styles to remove ui-lightness jquery ui theme
|
||||
from the tabs component (used in the add component problem tab menu)
|
||||
*/
|
||||
// ====================
|
||||
|
||||
// grandfathered styles
|
||||
|
||||
// reset styles to remove ui-lightness jquery ui theme from the tabs component (used in the add component problem tab menu)
|
||||
.ui-tabs {
|
||||
padding: 0;
|
||||
white-space: normal;
|
||||
@@ -118,10 +120,7 @@ from the tabs component (used in the add component problem tab menu)
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* reapplying the tab styles from unit.scss after
|
||||
removing jquery ui ui-lightness styling
|
||||
*/
|
||||
|
||||
// reapplying the tab styles from unit.scss after removing jquery ui ui-lightness styling
|
||||
.problem-type-tabs {
|
||||
border:none;
|
||||
list-style-type: none;
|
||||
@@ -146,26 +145,4 @@ removing jquery ui ui-lightness styling
|
||||
border: 0px;
|
||||
}
|
||||
}
|
||||
/*
|
||||
li {
|
||||
float:left;
|
||||
display:inline-block;
|
||||
text-align:center;
|
||||
width: auto;
|
||||
//@include linear-gradient(top, rgba(255, 255, 255, .4), rgba(255, 255, 255, 0));
|
||||
//background-color: tint($lightBluishGrey, 20%);
|
||||
//@include box-shadow(0 1px 0 rgba(255, 255, 255, 0.2) inset, 0 -1px 0 rgba(0, 0, 0, 0.2) inset);
|
||||
opacity:.8;
|
||||
|
||||
&:hover {
|
||||
opacity:1;
|
||||
}
|
||||
|
||||
&.current {
|
||||
border: 0px;
|
||||
//@include active;
|
||||
opacity:1;
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
@@ -1,239 +0,0 @@
|
||||
section#unit-wrapper {
|
||||
section.filters {
|
||||
@include clearfix;
|
||||
display: none;
|
||||
opacity: .4;
|
||||
margin-bottom: 10px;
|
||||
@include transition;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
h2 {
|
||||
@include inline-block();
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
font-size: 14px;
|
||||
padding: 6px 6px 6px 0;
|
||||
font-size: 12px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
ul {
|
||||
@include clearfix();
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
li {
|
||||
@include inline-block;
|
||||
margin-right: 6px;
|
||||
border-right: 1px solid #ddd;
|
||||
padding-right: 6px;
|
||||
|
||||
&.search {
|
||||
float: right;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
a {
|
||||
&.more {
|
||||
font-size: 12px;
|
||||
@include inline-block;
|
||||
margin: 0 6px;
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
div.content {
|
||||
display: table;
|
||||
border: 1px solid lighten($dark-blue, 40%);
|
||||
width: 100%;
|
||||
@include border-radius(3px);
|
||||
@include box-shadow(0 0 4px lighten($dark-blue, 50%));
|
||||
|
||||
section {
|
||||
header {
|
||||
background: #fff;
|
||||
padding: 6px;
|
||||
border-bottom: 1px solid lighten($dark-blue, 60%);
|
||||
@include clearfix;
|
||||
|
||||
h2 {
|
||||
color: $bright-blue;
|
||||
// float: left;
|
||||
font-size: 14px;
|
||||
letter-spacing: 1px;
|
||||
// line-height: 20px;
|
||||
text-transform: uppercase;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.modules {
|
||||
@include box-sizing(border-box);
|
||||
display: table-cell;
|
||||
width: flex-grid(6, 9);
|
||||
border-right: 1px solid lighten($dark-blue, 40%);
|
||||
|
||||
&.empty {
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
|
||||
a {
|
||||
@extend .button;
|
||||
@include inline-block();
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
ol {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
li {
|
||||
border-bottom: 1px solid lighten($dark-blue, 60%);
|
||||
|
||||
a {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
ol {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
li {
|
||||
padding: 6px;
|
||||
position: relative;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: lighten($yellow, 10%);
|
||||
|
||||
a.draggable {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
a.draggable {
|
||||
float: right;
|
||||
opacity: .4;
|
||||
}
|
||||
|
||||
&.group {
|
||||
padding: 0;
|
||||
|
||||
header {
|
||||
padding: 6px;
|
||||
background: none;
|
||||
|
||||
h3 {
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
ol {
|
||||
border-left: 4px solid #999;
|
||||
border-bottom: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
li {
|
||||
&:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.scratch-pad {
|
||||
@include box-sizing(border-box);
|
||||
display: table-cell;
|
||||
width: flex-grid(3, 9) + flex-gutter(9);
|
||||
vertical-align: top;
|
||||
|
||||
ol {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
li {
|
||||
background: $light-blue;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
&.new-module a {
|
||||
background-color: darken($light-blue, 2%);
|
||||
border-bottom: 1px solid darken($light-blue, 8%);
|
||||
|
||||
&:hover {
|
||||
background-color: lighten($yellow, 10%);
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
color: $dark-blue;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
li {
|
||||
padding: 6px;
|
||||
border-collapse: collapse;
|
||||
border-bottom: 1px solid darken($light-blue, 8%);
|
||||
position: relative;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: 1px solid darken($light-blue, 8%);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: lighten($yellow, 10%);
|
||||
|
||||
a.draggable {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
&.empty {
|
||||
padding: 12px;
|
||||
|
||||
a {
|
||||
@extend .button;
|
||||
display: block;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
a.draggable {
|
||||
opacity: .3;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,295 +0,0 @@
|
||||
.subsection .main-wrapper {
|
||||
margin: 40px;
|
||||
}
|
||||
|
||||
.subsection .inner-wrapper {
|
||||
@include clearfix();
|
||||
}
|
||||
|
||||
.subsection-body {
|
||||
padding: 32px 40px;
|
||||
@include clearfix;
|
||||
|
||||
> div {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
input {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.unit-subtitle {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sortable-unit-list {
|
||||
ol {
|
||||
@include tree-view;
|
||||
}
|
||||
}
|
||||
|
||||
.policy-list {
|
||||
input[disabled] {
|
||||
border: none;
|
||||
@include box-shadow(none);
|
||||
}
|
||||
|
||||
.policy-list-name {
|
||||
margin-right: 5px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.policy-list-value {
|
||||
width: 320px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.policy-list-element {
|
||||
.save-button,
|
||||
.cancel-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.edit-icon {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
&.editing,
|
||||
&.new-policy-list-element {
|
||||
.policy-list-name,
|
||||
.policy-list-value {
|
||||
border: 1px solid #b0b6c2;
|
||||
@include linear-gradient(top, rgba(255, 255, 255, 0), rgba(255, 255, 255, .3));
|
||||
background-color: #edf1f5;
|
||||
@include box-shadow(0 1px 2px rgba(0, 0, 0, .1) inset);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.new-policy-list-element {
|
||||
padding: 10px 10px 0;
|
||||
margin: 0 -10px 10px;
|
||||
border-radius: 3px;
|
||||
background: $mediumGrey;
|
||||
|
||||
.save-button {
|
||||
@include blue-button;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.cancel-button {
|
||||
@include white-button;
|
||||
}
|
||||
|
||||
.edit-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.delete-icon {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.new-policy-item {
|
||||
margin: 10px 0;
|
||||
|
||||
.plus-icon-small {
|
||||
position: relative;
|
||||
top: -1px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.subsection-name-input {
|
||||
label {
|
||||
display: block;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.scheduled-date-input,
|
||||
.due-date-input {
|
||||
@include clearfix;
|
||||
|
||||
.date-input,
|
||||
.time-input {
|
||||
display: inline-block;
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.inherits-check {
|
||||
label {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
.notice {
|
||||
margin-top: 6px;
|
||||
font-size: 11px;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.due-date-input {
|
||||
label {
|
||||
display: inline-block !important;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
a {
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.date-setter {
|
||||
@include clearfix;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.remove-date {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.row.visibility {
|
||||
label {
|
||||
display: inline-block !important;
|
||||
margin-right: 10px;
|
||||
line-height: 21px;
|
||||
}
|
||||
|
||||
a {
|
||||
display: inline-block;
|
||||
height: 31px;
|
||||
margin-right: 8px;
|
||||
vertical-align: middle;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
line-height: 31px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.large-toggle {
|
||||
width: 41px;
|
||||
background: url(../img/large-toggles.png) no-repeat;
|
||||
background-position: 0 -50px;
|
||||
|
||||
.hidden {
|
||||
background-position: 0 -5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.gradable {
|
||||
|
||||
label {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.gradable-status {
|
||||
position: relative;
|
||||
top: -4px;
|
||||
display: inline-block;
|
||||
margin-left: 10px;
|
||||
width: 65%;
|
||||
|
||||
.status-label {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
color: $blue;
|
||||
border: none;
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.menu-toggle {
|
||||
z-index: 100;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
background: transparent;
|
||||
|
||||
&:hover, &.is-active {
|
||||
color: $blue;
|
||||
}
|
||||
}
|
||||
|
||||
.menu {
|
||||
z-index: 1;
|
||||
position: absolute;
|
||||
top: -12px;
|
||||
left: -7px;
|
||||
display: none;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 8px 12px;
|
||||
opacity: 0.0;
|
||||
background: $white;
|
||||
border: 1px solid $mediumGrey;
|
||||
font-size: 12px;
|
||||
@include border-radius(4px);
|
||||
@include box-shadow(0 1px 2px rgba(0, 0, 0, .2));
|
||||
@include transition(opacity .15s);
|
||||
|
||||
|
||||
li {
|
||||
margin-bottom: 3px;
|
||||
padding-bottom: 3px;
|
||||
border-bottom: 1px solid $lightGrey;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
|
||||
&.is-selected {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// dropdown state
|
||||
&.is-active {
|
||||
|
||||
.menu {
|
||||
z-index: 10000;
|
||||
display: block;
|
||||
opacity: 1.0;
|
||||
}
|
||||
|
||||
.menu-toggle {
|
||||
z-index: 1000;
|
||||
}
|
||||
}
|
||||
|
||||
// set state
|
||||
&.is-set {
|
||||
|
||||
.menu-toggle {
|
||||
color: $blue;
|
||||
}
|
||||
|
||||
.status-label {
|
||||
display: block;
|
||||
color: $blue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,667 +0,0 @@
|
||||
.unit .main-wrapper {
|
||||
@include clearfix();
|
||||
margin: 40px;
|
||||
}
|
||||
|
||||
//Problem Selector tab menu requirements
|
||||
.js .tabs .tab {
|
||||
display: none;
|
||||
}
|
||||
//end problem selector reqs
|
||||
|
||||
.main-column {
|
||||
clear: both;
|
||||
float: left;
|
||||
width: 70%;
|
||||
}
|
||||
|
||||
.unit-body.published {
|
||||
.components > li {
|
||||
border: none;
|
||||
|
||||
.rendered-component {
|
||||
padding: 0 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.unit-body {
|
||||
.breadcrumbs {
|
||||
border-radius: 3px 3px 0 0;
|
||||
border-bottom: 1px solid #cbd1db;
|
||||
@include linear-gradient(top, rgba(255, 255, 255, .3), rgba(255, 255, 255, 0) 70%);
|
||||
background-color: #edf1f5;
|
||||
@include box-shadow(0 1px 0 rgba(255, 255, 255, .7) inset);
|
||||
@include clearfix;
|
||||
|
||||
li {
|
||||
float: left;
|
||||
}
|
||||
|
||||
a,
|
||||
.current-page {
|
||||
display: block;
|
||||
padding: 15px 35px 15px 30px;
|
||||
font-size: 14px;
|
||||
background: url(../img/breadcrumb-arrow.png) no-repeat right center;
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 30px 40px 30px 0;
|
||||
color: #646464;
|
||||
font-size: 19px;
|
||||
font-weight: 300;
|
||||
letter-spacing: 1px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.components {
|
||||
|
||||
> li {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
margin: 20px 40px;
|
||||
|
||||
|
||||
|
||||
.title {
|
||||
margin: 0 0 15px 0;
|
||||
color: $mediumGrey;
|
||||
|
||||
.value {
|
||||
}
|
||||
}
|
||||
|
||||
&.new-component-item {
|
||||
margin: 20px 0px;
|
||||
border-top: 1px solid $mediumGrey;
|
||||
box-shadow: 0 2px 1px rgba(182, 182, 182, 0.75) inset;
|
||||
background-color: $lightGrey;
|
||||
margin-bottom: 0px;
|
||||
padding-bottom: 20px;
|
||||
|
||||
.new-component-button {
|
||||
display: block;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
color: #edf1f5;
|
||||
}
|
||||
|
||||
h5 {
|
||||
margin: 20px 0px;
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.rendered-component {
|
||||
display: none;
|
||||
background: #fff;
|
||||
border-radius: 3px 3px 0 0;
|
||||
}
|
||||
|
||||
.new-component-type {
|
||||
|
||||
a,
|
||||
li {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
a {
|
||||
border: 1px solid $mediumGrey;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
color: #fff;
|
||||
margin-right: 15px;
|
||||
margin-bottom: 20px;
|
||||
border-radius: 8px;
|
||||
font-size: 15px;
|
||||
line-height: 14px;
|
||||
text-align: center;
|
||||
@include box-shadow(0 1px 1px rgba(0, 0, 0, .2), 0 1px 0 rgba(255, 255, 255, .4) inset);
|
||||
|
||||
.name {
|
||||
position: absolute;
|
||||
bottom: 5px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
@include box-sizing(border-box);
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.new-component-templates {
|
||||
display: none;
|
||||
margin: 20px 40px 20px 40px;
|
||||
border-radius: 3px;
|
||||
border: 1px solid $mediumGrey;
|
||||
background-color: #fff;
|
||||
@include box-shadow(0 1px 1px rgba(0, 0, 0, .2), 0 1px 0 rgba(255, 255, 255, .4) inset);
|
||||
@include clearfix;
|
||||
|
||||
.cancel-button {
|
||||
margin: 20px 0px 10px 10px;
|
||||
@include white-button;
|
||||
}
|
||||
|
||||
.problem-type-tabs {
|
||||
display: none;
|
||||
}
|
||||
|
||||
// specific menu types
|
||||
&.new-component-problem {
|
||||
padding-bottom:10px;
|
||||
|
||||
.ss-icon, .editor-indicator {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.problem-type-tabs {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.new-component-type,
|
||||
.new-component-template {
|
||||
@include clearfix;
|
||||
|
||||
a {
|
||||
position: relative;
|
||||
border: 1px solid $darkGreen;
|
||||
background: tint($green,20%);
|
||||
color: #fff;
|
||||
|
||||
&:hover {
|
||||
background: $brightGreen;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.problem-type-tabs {
|
||||
list-style-type: none;
|
||||
border-radius: 0;
|
||||
width: 100%;
|
||||
@include linear-gradient(top, rgba(255, 255, 255, .4), rgba(255, 255, 255, 0));
|
||||
background-color: $lightBluishGrey;
|
||||
@include box-shadow(0 1px 0 rgba(255, 255, 255, 0.2) inset, 0 -1px 0 rgba(0, 0, 0, 0.2) inset);
|
||||
|
||||
li:first-child {
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
li {
|
||||
float:left;
|
||||
display:inline-block;
|
||||
text-align:center;
|
||||
width: auto;
|
||||
@include linear-gradient(top, rgba(255, 255, 255, .4), rgba(255, 255, 255, 0));
|
||||
background-color: tint($lightBluishGrey, 10%);
|
||||
@include box-shadow(0 1px 0 rgba(255, 255, 255, 0.2) inset, 0 -1px 0 rgba(0, 0, 0, 0.2) inset);
|
||||
opacity:.8;
|
||||
|
||||
&:hover {
|
||||
opacity:1;
|
||||
background-color: tint($lightBluishGrey, 20%);
|
||||
}
|
||||
|
||||
&.ui-state-active {
|
||||
border: 0px;
|
||||
@include active;
|
||||
opacity:1;
|
||||
}
|
||||
}
|
||||
|
||||
a{
|
||||
display: block;
|
||||
padding: 15px 25px;
|
||||
font-size: 15px;
|
||||
line-height: 16px;
|
||||
text-align: center;
|
||||
color: #3c3c3c;
|
||||
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
.new-component-template {
|
||||
|
||||
a {
|
||||
background: #fff;
|
||||
border: 0px;
|
||||
color: #3c3c3c;
|
||||
@include transition (none);
|
||||
|
||||
&:hover {
|
||||
background: tint($green,30%);
|
||||
color: #fff;
|
||||
@include transition(background-color .15s);
|
||||
}
|
||||
}
|
||||
|
||||
li {
|
||||
border:none;
|
||||
border-bottom: 1px dashed $lightGrey;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
li:first-child {
|
||||
a {
|
||||
border-top: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
li:nth-child(2) {
|
||||
a {
|
||||
border-radius: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
@include clearfix();
|
||||
display: block;
|
||||
padding: 7px 20px;
|
||||
border-bottom: none;
|
||||
font-weight: 500;
|
||||
|
||||
.name {
|
||||
float: left;
|
||||
|
||||
.ss-icon {
|
||||
@include transition(opacity .15s);
|
||||
display: inline-block;
|
||||
top: 1px;
|
||||
margin-right: 5px;
|
||||
opacity: 0.5;
|
||||
width: 17;
|
||||
height: 21px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
.editor-indicator {
|
||||
@include transition(opacity .15s);
|
||||
float: right;
|
||||
position: relative;
|
||||
top: 3px;
|
||||
font-size: 12px;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.ss-icon, .editor-indicator {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: #fff;
|
||||
|
||||
.ss-icon {
|
||||
opacity: 1.0;
|
||||
}
|
||||
|
||||
.editor-indicator {
|
||||
opacity: 1.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// specific editor types
|
||||
.empty {
|
||||
|
||||
a {
|
||||
line-height: 1.4;
|
||||
font-weight: 400;
|
||||
background: #fff;
|
||||
color: #3c3c3c;
|
||||
|
||||
|
||||
&:hover {
|
||||
background: tint($green,30%);
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.new-component {
|
||||
text-align: center;
|
||||
|
||||
h5 {
|
||||
color: $darkGreen;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.component {
|
||||
border: 1px solid $lightBluishGrey2;
|
||||
border-radius: 3px;
|
||||
background: #fff;
|
||||
@include transition(none);
|
||||
|
||||
&:hover {
|
||||
border-color: #6696d7;
|
||||
|
||||
.drag-handle {
|
||||
background-color: $blue;
|
||||
border-color: $blue;
|
||||
}
|
||||
}
|
||||
|
||||
&.editing {
|
||||
border: 1px solid $lightBluishGrey2;
|
||||
z-index: auto;
|
||||
|
||||
.drag-handle,
|
||||
.component-actions {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.component-placeholder {
|
||||
border-color: #6696d7;
|
||||
}
|
||||
|
||||
.component-actions {
|
||||
position: absolute;
|
||||
top: 7px;
|
||||
right: 9px;
|
||||
}
|
||||
|
||||
.drag-handle {
|
||||
position: absolute;
|
||||
display: block;
|
||||
top: -1px;
|
||||
right: -16px;
|
||||
z-index: 10;
|
||||
width: 15px;
|
||||
height: 100%;
|
||||
border-radius: 0 3px 3px 0;
|
||||
border: 1px solid $lightBluishGrey2;
|
||||
background: url(../img/white-drag-handles.png) center no-repeat $lightBluishGrey2;
|
||||
cursor: move;
|
||||
@include transition(none);
|
||||
}
|
||||
}
|
||||
|
||||
.xmodule_display {
|
||||
padding: 40px 20px 20px;
|
||||
overflow-x: auto;
|
||||
|
||||
h1 {
|
||||
float: none;
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.wrapper-component-editor {
|
||||
z-index: 9999;
|
||||
position: relative;
|
||||
background: $lightBluishGrey2;
|
||||
}
|
||||
|
||||
.component-editor {
|
||||
@include edit-box;
|
||||
@include box-shadow(none);
|
||||
display: none;
|
||||
padding: 20px;
|
||||
border-radius: 2px 2px 0 0;
|
||||
|
||||
.metadata_edit {
|
||||
margin-bottom: 20px;
|
||||
font-size: 13px;
|
||||
|
||||
li {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: inline-block;
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin-bottom: 10px;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
h5 {
|
||||
margin-bottom: 8px;
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.save-button {
|
||||
margin-top: 10px;
|
||||
margin: 15px 8px 0 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.unit-settings {
|
||||
.window-contents {
|
||||
padding: 10px 20px;
|
||||
}
|
||||
|
||||
.unit-actions {
|
||||
border-bottom: none;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.published-alert {
|
||||
display: none;
|
||||
padding: 10px;
|
||||
border: 1px solid #edbd3c;
|
||||
border-radius: 3px;
|
||||
background: #fbf6e1;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
|
||||
div {
|
||||
margin-top: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
input[type="radio"] {
|
||||
margin-right: 7px;
|
||||
}
|
||||
|
||||
.status {
|
||||
font-size: 12px;
|
||||
|
||||
strong {
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
.preview-button, .view-button {
|
||||
@include white-button;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.publish-button {
|
||||
@include orange-button;
|
||||
}
|
||||
|
||||
.delete-button {
|
||||
@include blue-button;
|
||||
}
|
||||
|
||||
.delete-draft {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.delete-button,
|
||||
.preview-button,
|
||||
.publish-button,
|
||||
.view-button {
|
||||
font-size: 11px;
|
||||
margin-top: 10px;
|
||||
padding: 6px 15px 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.unit-history {
|
||||
&.collapsed {
|
||||
h4 {
|
||||
border-bottom: none;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.window-contents {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
ol {
|
||||
border: 1px solid #ced2db;
|
||||
|
||||
li {
|
||||
display: block;
|
||||
padding: 6px 8px 8px 10px;
|
||||
background: #edf1f5;
|
||||
font-size: 12px;
|
||||
|
||||
&:hover {
|
||||
background: #fffcf1;
|
||||
|
||||
.item-actions {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
&.checked {
|
||||
background: #d1dae3;
|
||||
}
|
||||
|
||||
.item-actions {
|
||||
display: none;
|
||||
}
|
||||
|
||||
input[type="radio"] {
|
||||
margin-right: 7px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.unit-location {
|
||||
.url {
|
||||
width: 100%;
|
||||
margin-bottom: 10px;
|
||||
@include box-shadow(none);
|
||||
}
|
||||
|
||||
.draft-tag,
|
||||
.hidden-tag,
|
||||
.private-tag,
|
||||
.has-new-draft-tag {
|
||||
font-size: 8px;
|
||||
}
|
||||
|
||||
.window-contents > ol {
|
||||
@include tree-view;
|
||||
|
||||
.section-item {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
font-size: 11px;
|
||||
padding: 2px 8px 4px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
@include box-sizing(border-box);
|
||||
}
|
||||
|
||||
ol {
|
||||
.section-item {
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.new-unit-item {
|
||||
margin-left: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
ol ol {
|
||||
.section-item {
|
||||
padding-left: 34px;
|
||||
}
|
||||
|
||||
.new-unit-item {
|
||||
margin: 0 0 10px 41px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.edit-state-draft {
|
||||
.visibility,
|
||||
|
||||
.edit-draft-message,
|
||||
.view-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.published-alert {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.edit-state-public {
|
||||
.delete-draft,
|
||||
.component-actions,
|
||||
.new-component-item,
|
||||
.editing-draft-alert,
|
||||
.publish-draft-message,
|
||||
.preview-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.published-alert {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.drag-handle {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.edit-state-private {
|
||||
.delete-draft,
|
||||
.publish-draft,
|
||||
.editing-draft-alert,
|
||||
.create-draft,
|
||||
.view-button {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
// editing units from courseware
|
||||
body.unit {
|
||||
|
||||
.component {
|
||||
padding-top: 30px;
|
||||
|
||||
.component-actions {
|
||||
@include box-sizing(border-box);
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
padding: 15px;
|
||||
top: 0;
|
||||
left: 0;
|
||||
border-bottom: 1px solid $lightBluishGrey2;
|
||||
background: $lightGrey;
|
||||
}
|
||||
|
||||
&.editing {
|
||||
padding-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
// studio - utilities - variables
|
||||
// ====================
|
||||
|
||||
$baseline: 20px;
|
||||
|
||||
// grid
|
||||
@@ -12,11 +15,18 @@ $fg-min-width: 900px;
|
||||
// type
|
||||
$sans-serif: 'Open Sans', $verdana;
|
||||
$body-line-height: golden-ratio(.875em, 1);
|
||||
$error-red: rgb(253, 87, 87);
|
||||
|
||||
// colors - new for re-org
|
||||
$black: rgb(0,0,0);
|
||||
$black-t0: rgba(0,0,0,0.125);
|
||||
$black-t1: rgba(0,0,0,0.25);
|
||||
$black-t2: rgba(0,0,0,0.50);
|
||||
$black-t3: rgba(0,0,0,0.75);
|
||||
$white: rgb(255,255,255);
|
||||
$white-t0: rgba(255,255,255,0.125);
|
||||
$white-t1: rgba(255,255,255,0.25);
|
||||
$white-t2: rgba(255,255,255,0.50);
|
||||
$white-t3: rgba(255,255,255,0.75);
|
||||
|
||||
$gray: rgb(127,127,127);
|
||||
$gray-l1: tint($gray,20%);
|
||||
@@ -24,6 +34,7 @@ $gray-l2: tint($gray,40%);
|
||||
$gray-l3: tint($gray,60%);
|
||||
$gray-l4: tint($gray,80%);
|
||||
$gray-l5: tint($gray,90%);
|
||||
$gray-l6: tint($gray,95%);
|
||||
$gray-d1: shade($gray,20%);
|
||||
$gray-d2: shade($gray,40%);
|
||||
$gray-d3: shade($gray,60%);
|
||||
@@ -39,6 +50,12 @@ $blue-d1: shade($blue,20%);
|
||||
$blue-d2: shade($blue,40%);
|
||||
$blue-d3: shade($blue,60%);
|
||||
$blue-d4: shade($blue,80%);
|
||||
$blue-s1: saturate($blue,15%);
|
||||
$blue-s2: saturate($blue,30%);
|
||||
$blue-s3: saturate($blue,45%);
|
||||
$blue-u1: desaturate($blue,15%);
|
||||
$blue-u2: desaturate($blue,30%);
|
||||
$blue-u3: desaturate($blue,45%);
|
||||
|
||||
$pink: rgb(183, 37, 103);
|
||||
$pink-l1: tint($pink,20%);
|
||||
@@ -50,6 +67,29 @@ $pink-d1: shade($pink,20%);
|
||||
$pink-d2: shade($pink,40%);
|
||||
$pink-d3: shade($pink,60%);
|
||||
$pink-d4: shade($pink,80%);
|
||||
$pink-s1: saturate($pink,15%);
|
||||
$pink-s2: saturate($pink,30%);
|
||||
$pink-s3: saturate($pink,45%);
|
||||
$pink-u1: desaturate($pink,15%);
|
||||
$pink-u2: desaturate($pink,30%);
|
||||
$pink-u3: desaturate($pink,45%);
|
||||
|
||||
$red: rgb(178, 6, 16);
|
||||
$red-l1: tint($red,20%);
|
||||
$red-l2: tint($red,40%);
|
||||
$red-l3: tint($red,60%);
|
||||
$red-l4: tint($red,80%);
|
||||
$red-l5: tint($red,90%);
|
||||
$red-d1: shade($red,20%);
|
||||
$red-d2: shade($red,40%);
|
||||
$red-d3: shade($red,60%);
|
||||
$red-d4: shade($red,80%);
|
||||
$red-s1: saturate($red,15%);
|
||||
$red-s2: saturate($red,30%);
|
||||
$red-s3: saturate($red,45%);
|
||||
$red-u1: desaturate($red,15%);
|
||||
$red-u2: desaturate($red,30%);
|
||||
$red-u3: desaturate($red,45%);
|
||||
|
||||
$green: rgb(37, 184, 90);
|
||||
$green-l1: tint($green,20%);
|
||||
@@ -61,6 +101,12 @@ $green-d1: shade($green,20%);
|
||||
$green-d2: shade($green,40%);
|
||||
$green-d3: shade($green,60%);
|
||||
$green-d4: shade($green,80%);
|
||||
$green-s1: saturate($green,15%);
|
||||
$green-s2: saturate($green,30%);
|
||||
$green-s3: saturate($green,45%);
|
||||
$green-u1: desaturate($green,15%);
|
||||
$green-u2: desaturate($green,30%);
|
||||
$green-u3: desaturate($green,45%);
|
||||
|
||||
$yellow: rgb(231, 214, 143);
|
||||
$yellow-l1: tint($yellow,20%);
|
||||
@@ -72,6 +118,29 @@ $yellow-d1: shade($yellow,20%);
|
||||
$yellow-d2: shade($yellow,40%);
|
||||
$yellow-d3: shade($yellow,60%);
|
||||
$yellow-d4: shade($yellow,80%);
|
||||
$yellow-s1: saturate($yellow,15%);
|
||||
$yellow-s2: saturate($yellow,30%);
|
||||
$yellow-s3: saturate($yellow,45%);
|
||||
$yellow-u1: desaturate($yellow,15%);
|
||||
$yellow-u2: desaturate($yellow,30%);
|
||||
$yellow-u3: desaturate($yellow,45%);
|
||||
|
||||
$orange: rgb(237, 189, 60);
|
||||
$orange-l1: tint($orange,20%);
|
||||
$orange-l2: tint($orange,40%);
|
||||
$orange-l3: tint($orange,60%);
|
||||
$orange-l4: tint($orange,80%);
|
||||
$orange-l5: tint($orange,90%);
|
||||
$orange-d1: shade($orange,20%);
|
||||
$orange-d2: shade($orange,40%);
|
||||
$orange-d3: shade($orange,60%);
|
||||
$orange-d4: shade($orange,80%);
|
||||
$orange-s1: saturate($orange,15%);
|
||||
$orange-s2: saturate($orange,30%);
|
||||
$orange-s3: saturate($orange,45%);
|
||||
$orange-u1: desaturate($orange,15%);
|
||||
$orange-u2: desaturate($orange,30%);
|
||||
$orange-u3: desaturate($orange,45%);
|
||||
|
||||
$shadow: rgba(0,0,0,0.2);
|
||||
$shadow-l1: rgba(0,0,0,0.1);
|
||||
@@ -80,8 +149,6 @@ $shadow-d1: rgba(0,0,0,0.4);
|
||||
// colors - inherited
|
||||
$baseFontColor: #3c3c3c;
|
||||
$offBlack: #3c3c3c;
|
||||
$orange: #edbd3c;
|
||||
$red: #b20610;
|
||||
$green: #108614;
|
||||
$lightGrey: #edf1f5;
|
||||
$mediumGrey: #b0b6c2;
|
||||
@@ -94,4 +161,5 @@ $brightGreen: rgb(22, 202, 87);
|
||||
$disabledGreen: rgb(124, 206, 153);
|
||||
$darkGreen: rgb(52, 133, 76);
|
||||
$lightBluishGrey: rgb(197, 207, 223);
|
||||
$lightBluishGrey2: rgb(213, 220, 228);
|
||||
$lightBluishGrey2: rgb(213, 220, 228);
|
||||
$error-red: rgb(253, 87, 87);
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
section.video-new, section.video-edit {
|
||||
> section {
|
||||
|
||||
section.upload {
|
||||
padding: 6px;
|
||||
margin-bottom: 10px;
|
||||
border: 1px solid #ddd;
|
||||
|
||||
a.upload-button {
|
||||
@extend .button;
|
||||
@include inline-block();
|
||||
}
|
||||
}
|
||||
|
||||
section.in-use {
|
||||
h2 {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
div {
|
||||
background: #eee;
|
||||
text-align: center;
|
||||
padding: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
a.save-update {
|
||||
@extend .button;
|
||||
@include inline-block();
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,256 +0,0 @@
|
||||
section.week-edit,
|
||||
section.week-new,
|
||||
section.sequence-edit {
|
||||
|
||||
> header {
|
||||
border-bottom: 2px solid #333;
|
||||
@include clearfix();
|
||||
|
||||
div {
|
||||
@include clearfix();
|
||||
padding: 6px 20px;
|
||||
|
||||
h1 {
|
||||
font-size: 18px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
float: left;
|
||||
}
|
||||
|
||||
p {
|
||||
float: right;
|
||||
}
|
||||
|
||||
&.week {
|
||||
background: #eee;
|
||||
font-size: 12px;
|
||||
border-bottom: 1px solid #ccc;
|
||||
|
||||
h2 {
|
||||
font-size: 12px;
|
||||
@include inline-block();
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
@include inline-block();
|
||||
|
||||
li {
|
||||
@include inline-block();
|
||||
margin-right: 10px;
|
||||
|
||||
p {
|
||||
float: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
section.goals {
|
||||
background: #eee;
|
||||
padding: 6px 20px;
|
||||
border-top: 1px solid #ccc;
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
color: #999;
|
||||
|
||||
li {
|
||||
margin-bottom: 6px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> section.content {
|
||||
@include box-sizing(border-box);
|
||||
padding: 20px;
|
||||
|
||||
section.filters {
|
||||
@include clearfix;
|
||||
margin-bottom: 10px;
|
||||
background: #efefef;
|
||||
border: 1px solid #ddd;
|
||||
|
||||
ul {
|
||||
@include clearfix();
|
||||
list-style: none;
|
||||
padding: 6px;
|
||||
|
||||
li {
|
||||
@include inline-block();
|
||||
|
||||
&.advanced {
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> div {
|
||||
display: table;
|
||||
border: 1px solid;
|
||||
width: 100%;
|
||||
|
||||
section {
|
||||
header {
|
||||
background: #eee;
|
||||
padding: 6px;
|
||||
border-bottom: 1px solid #ccc;
|
||||
@include clearfix;
|
||||
|
||||
h2 {
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
font-size: 12px;
|
||||
float: left;
|
||||
}
|
||||
}
|
||||
|
||||
&.modules {
|
||||
@include box-sizing(border-box);
|
||||
display: table-cell;
|
||||
width: flex-grid(6, 9);
|
||||
border-right: 1px solid #333;
|
||||
|
||||
&.empty {
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
|
||||
a {
|
||||
@extend .button;
|
||||
@include inline-block();
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
ol {
|
||||
list-style: none;
|
||||
border-bottom: 1px solid #333;
|
||||
|
||||
li {
|
||||
border-bottom: 1px solid #333;
|
||||
|
||||
&:last-child{
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
ol {
|
||||
list-style: none;
|
||||
|
||||
li {
|
||||
padding: 6px;
|
||||
|
||||
&:hover {
|
||||
a.draggable {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
a.draggable {
|
||||
float: right;
|
||||
opacity: .5;
|
||||
}
|
||||
|
||||
&.group {
|
||||
padding: 0;
|
||||
|
||||
header {
|
||||
padding: 6px;
|
||||
background: none;
|
||||
|
||||
h3 {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
ol {
|
||||
border-left: 4px solid #999;
|
||||
border-bottom: 0;
|
||||
|
||||
li {
|
||||
&:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.scratch-pad {
|
||||
@include box-sizing(border-box);
|
||||
display: table-cell;
|
||||
width: flex-grid(3, 9) + flex-gutter(9);
|
||||
vertical-align: top;
|
||||
|
||||
ol {
|
||||
list-style: none;
|
||||
border-bottom: 1px solid #999;
|
||||
|
||||
li {
|
||||
border-bottom: 1px solid #999;
|
||||
background: #f9f9f9;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
|
||||
li {
|
||||
padding: 6px;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
a.draggable {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&.empty {
|
||||
padding: 12px;
|
||||
|
||||
a {
|
||||
@extend .button;
|
||||
display: block;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
a.draggable {
|
||||
float: right;
|
||||
opacity: .3;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #000;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,39 +1,52 @@
|
||||
// studio - css architecture
|
||||
// ====================
|
||||
|
||||
// bourbon libs and resets
|
||||
@import 'bourbon/bourbon';
|
||||
@import 'bourbon/addons/button';
|
||||
@import 'vendor/normalize';
|
||||
@import 'keyframes';
|
||||
|
||||
@import 'reset';
|
||||
|
||||
// utilities
|
||||
@import 'variables';
|
||||
@import 'mixins';
|
||||
@import 'cms_mixins';
|
||||
|
||||
@import "fonts";
|
||||
@import "variables";
|
||||
@import "cms_mixins";
|
||||
@import "extends";
|
||||
@import "base";
|
||||
@import "header";
|
||||
@import "footer";
|
||||
@import "dashboard";
|
||||
@import "courseware";
|
||||
@import "subsection";
|
||||
@import "unit";
|
||||
@import "assets";
|
||||
@import "static-pages";
|
||||
@import "users";
|
||||
@import "import";
|
||||
@import "export";
|
||||
@import "settings";
|
||||
@import "course-info";
|
||||
@import "landing";
|
||||
@import "graphics";
|
||||
@import "modal";
|
||||
@import "alerts";
|
||||
@import "login";
|
||||
@import "account";
|
||||
@import "index";
|
||||
@import 'jquery-ui-calendar';
|
||||
// assets
|
||||
@import 'assets/fonts';
|
||||
@import 'assets/graphics';
|
||||
@import 'assets/keyframes';
|
||||
|
||||
@import 'content-types';
|
||||
// base
|
||||
@import 'base';
|
||||
|
||||
// elements
|
||||
@import 'elements/header';
|
||||
@import 'elements/footer';
|
||||
@import 'elements/navigation';
|
||||
@import 'elements/forms';
|
||||
@import 'elements/modal';
|
||||
@import 'elements/alerts';
|
||||
@import 'elements/jquery-ui-calendar';
|
||||
|
||||
// specific views
|
||||
@import 'views/account';
|
||||
@import 'views/assets';
|
||||
@import 'views/updates';
|
||||
@import 'views/dashboard';
|
||||
@import 'views/export';
|
||||
@import 'views/index';
|
||||
@import 'views/import';
|
||||
@import 'views/outline';
|
||||
@import 'views/settings';
|
||||
@import 'views/static-pages';
|
||||
@import 'views/subsection';
|
||||
@import 'views/unit';
|
||||
@import 'views/users';
|
||||
@import 'views/checklists';
|
||||
|
||||
@import 'assets/content-types';
|
||||
|
||||
// xblock-related
|
||||
@import 'module/module-styles.scss';
|
||||
@import 'descriptor/module-styles.scss';
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// studio - elements - alerts, notifications, prompts
|
||||
// ====================
|
||||
|
||||
// notifications
|
||||
.wrapper-notification {
|
||||
@include clearfix();
|
||||
@@ -1,4 +1,6 @@
|
||||
//studio global footer
|
||||
// studio - elements - global footer
|
||||
// ====================
|
||||
|
||||
.wrapper-footer {
|
||||
margin: ($baseline*1.5) 0 $baseline 0;
|
||||
padding: $baseline;
|
||||
76
cms/static/sass/elements/_forms.scss
Normal file
76
cms/static/sass/elements/_forms.scss
Normal file
@@ -0,0 +1,76 @@
|
||||
// studio - elements - forms
|
||||
// ====================
|
||||
|
||||
// forms - general
|
||||
input[type="text"],
|
||||
input[type="email"],
|
||||
input[type="password"],
|
||||
textarea.text {
|
||||
padding: 6px 8px 8px;
|
||||
@include box-sizing(border-box);
|
||||
border: 1px solid $mediumGrey;
|
||||
border-radius: 2px;
|
||||
@include linear-gradient($lightGrey, tint($lightGrey, 90%));
|
||||
background-color: $lightGrey;
|
||||
@include box-shadow(0 1px 2px rgba(0, 0, 0, .1) inset);
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
font-size: 11px;
|
||||
color: $baseFontColor;
|
||||
outline: 0;
|
||||
|
||||
&::-webkit-input-placeholder,
|
||||
&:-moz-placeholder,
|
||||
&:-ms-input-placeholder {
|
||||
color: #979faf;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
@include linear-gradient($paleYellow, tint($paleYellow, 90%));
|
||||
outline: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// forms - specific
|
||||
input.search {
|
||||
padding: 6px 15px 8px 30px;
|
||||
@include box-sizing(border-box);
|
||||
border: 1px solid $darkGrey;
|
||||
border-radius: 20px;
|
||||
background: url(../img/search-icon.png) no-repeat 8px 7px #edf1f5;
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
color: $baseFontColor;
|
||||
outline: 0;
|
||||
|
||||
&::-webkit-input-placeholder {
|
||||
color: #979faf;
|
||||
}
|
||||
}
|
||||
|
||||
label {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
code {
|
||||
padding: 0 4px;
|
||||
border-radius: 3px;
|
||||
background: #eee;
|
||||
font-family: Monaco, monospace;
|
||||
}
|
||||
|
||||
.CodeMirror {
|
||||
font-size: 13px;
|
||||
border: 1px solid $darkGrey;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.text-editor {
|
||||
width: 100%;
|
||||
min-height: 80px;
|
||||
padding: 10px;
|
||||
@include box-sizing(border-box);
|
||||
border: 1px solid $mediumGrey;
|
||||
@include linear-gradient(top, rgba(255, 255, 255, 0), rgba(255, 255, 255, 0.3));
|
||||
background-color: #edf1f5;
|
||||
@include box-shadow(0 1px 2px rgba(0, 0, 0, 0.1) inset);
|
||||
font-family: Monaco, monospace;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// studio global header and navigation
|
||||
// studio - elements - global header
|
||||
// ====================
|
||||
|
||||
.wrapper-header {
|
||||
@@ -1,3 +1,6 @@
|
||||
// studio - elements - JQUI calendar
|
||||
// ====================
|
||||
|
||||
.ui-datepicker {
|
||||
border-color: $darkGrey;
|
||||
border-radius: 2px;
|
||||
@@ -1,3 +1,6 @@
|
||||
// studio - elements - modal windows
|
||||
// ====================
|
||||
|
||||
.modal-cover {
|
||||
display: none;
|
||||
position: fixed;
|
||||
24
cms/static/sass/elements/_navigation.scss
Normal file
24
cms/static/sass/elements/_navigation.scss
Normal file
@@ -0,0 +1,24 @@
|
||||
// studio - elements - navigation
|
||||
// ====================
|
||||
|
||||
// common
|
||||
|
||||
// ====================
|
||||
|
||||
// primary
|
||||
|
||||
// ====================
|
||||
|
||||
// right hand side
|
||||
|
||||
// ====================
|
||||
|
||||
// tabs
|
||||
|
||||
// ====================
|
||||
|
||||
// dropdown
|
||||
|
||||
// ====================
|
||||
|
||||
//
|
||||
@@ -1,5 +1,6 @@
|
||||
// Studio - Sign In/Up
|
||||
// studio - views - sign up/in
|
||||
// ====================
|
||||
|
||||
body.signup, body.signin {
|
||||
|
||||
.wrapper-content {
|
||||
@@ -1,4 +1,8 @@
|
||||
.uploads {
|
||||
// studio - views - assets
|
||||
// ====================
|
||||
|
||||
body.course.uploads {
|
||||
|
||||
input.asset-search-input {
|
||||
float: left;
|
||||
width: 260px;
|
||||
347
cms/static/sass/views/_checklists.scss
Normal file
347
cms/static/sass/views/_checklists.scss
Normal file
@@ -0,0 +1,347 @@
|
||||
// Studio - Course Settings
|
||||
// ====================
|
||||
body.course.checklists {
|
||||
|
||||
.content-primary, .content-supplementary {
|
||||
@include box-sizing(border-box);
|
||||
float: left;
|
||||
}
|
||||
|
||||
.content-primary {
|
||||
width: flex-grid(9, 12);
|
||||
margin-right: flex-gutter();
|
||||
}
|
||||
|
||||
// checklists - general
|
||||
.course-checklist {
|
||||
@extend .window;
|
||||
margin: 0 0 ($baseline*2) 0;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
// visual status
|
||||
.viz-checklist-status {
|
||||
@include text-hide();
|
||||
@include size(100%,($baseline/4));
|
||||
position: relative;
|
||||
display: block;
|
||||
margin: 0;
|
||||
background: $gray-l4;
|
||||
|
||||
.viz-checklist-status-value {
|
||||
@include transition(width 2s ease-in-out .25s);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 0%;
|
||||
height: ($baseline/4);
|
||||
background: $green;
|
||||
|
||||
.int {
|
||||
@include text-sr();
|
||||
}
|
||||
}
|
||||
}
|
||||
// <span class="viz viz-checklist-status"><span class="viz value viz-checklist-status-value"><span class="int">0</span>% of checklist completed</span></span>
|
||||
|
||||
// header/title
|
||||
header {
|
||||
@include clearfix();
|
||||
@include box-shadow(inset 0 -1px 1px $shadow-l1);
|
||||
margin-bottom: 0;
|
||||
border-bottom: 1px solid $gray-l3;
|
||||
padding: $baseline ($baseline*1.5);
|
||||
|
||||
.checklist-title {
|
||||
@include transition(color .15s .25s ease-in-out);
|
||||
width: flex-grid(6, 9);
|
||||
margin: 0 flex-gutter() 0 0;
|
||||
float: left;
|
||||
|
||||
.ui-toggle-expansion {
|
||||
@include transition(rotate .15s ease-in-out .25s);
|
||||
@include font-size(14);
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin-right: ($baseline/2);
|
||||
color: $gray-l4;
|
||||
}
|
||||
|
||||
&.is-selectable {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: $blue;
|
||||
|
||||
.ui-toggle-expansion {
|
||||
color: $blue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.checklist-status {
|
||||
@include font-size(13);
|
||||
width: flex-grid(3, 9);
|
||||
float: right;
|
||||
margin-top: ($baseline/2);
|
||||
text-align: right;
|
||||
color: $gray-l2;
|
||||
|
||||
|
||||
.icon-confirm {
|
||||
@include font-size(20);
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin-left: ($baseline/2);
|
||||
color: $gray-l4;
|
||||
}
|
||||
|
||||
.status-count {
|
||||
@include font-size(16);
|
||||
margin-left: ($baseline/4);
|
||||
margin-right: ($baseline/4);
|
||||
color: $gray-d3;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.status-amount {
|
||||
@include font-size(16);
|
||||
margin-left: ($baseline/4);
|
||||
color: $gray-d3;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// checklist actions
|
||||
.course-checklist-actions {
|
||||
@include clearfix();
|
||||
@include box-shadow(inset 0 1px 1px $shadow-l1);
|
||||
@include transition(border .15s ease-in-out .25s);
|
||||
border-top: 1px solid $gray-l2;
|
||||
padding: $baseline ($baseline*1.5);
|
||||
background: $gray-l4;
|
||||
|
||||
.action-primary {
|
||||
@include green-button();
|
||||
float: left;
|
||||
|
||||
.icon-add {
|
||||
@include font-size(12);
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin-right: ($baseline/4);
|
||||
}
|
||||
}
|
||||
|
||||
.action-secondary {
|
||||
@include font-size(14);
|
||||
@include grey-button();
|
||||
font-weight: 400;
|
||||
float: right;
|
||||
|
||||
.icon-delete {
|
||||
@include font-size(12);
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin-right: ($baseline/4);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// state - collapsed
|
||||
&.is-collapsed {
|
||||
|
||||
header {
|
||||
@include box-shadow(none);
|
||||
|
||||
.checklist-title {
|
||||
|
||||
.ui-toggle-expansion {
|
||||
@include transform(rotate(-90deg));
|
||||
@include transform-origin(50% 50%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.list-tasks {
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// state - completed
|
||||
&.is-completed {
|
||||
|
||||
.viz-checklist-status {
|
||||
|
||||
.viz-checklist-status-value {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
header {
|
||||
|
||||
.checklist-title, .icon-confirm {
|
||||
color: $green;
|
||||
}
|
||||
|
||||
.checklist-status {
|
||||
|
||||
.status-count, .status-amount, .icon-confirm {
|
||||
color: $green;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// state - not available
|
||||
.is-not-available {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// list of tasks
|
||||
.list-tasks {
|
||||
height: auto;
|
||||
overflow: hidden;
|
||||
|
||||
.task {
|
||||
@include transition(background .15s ease-in-out .25s);
|
||||
@include transition(border .15s ease-in-out .25s);
|
||||
@include clearfix();
|
||||
position: relative;
|
||||
border-top: 1px solid $white;
|
||||
border-bottom: 1px solid $gray-l5;
|
||||
padding: $baseline ($baseline*1.5);
|
||||
background: $white;
|
||||
opacity: 1.0;
|
||||
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.task-input {
|
||||
display: inline-block;
|
||||
vertical-align: text-top;
|
||||
float: left;
|
||||
margin: ($baseline/2) flex-gutter() 0 0;
|
||||
}
|
||||
|
||||
.task-details {
|
||||
display: inline-block;
|
||||
vertical-align: text-top;
|
||||
float: left;
|
||||
width: flex-grid(6,9);
|
||||
font-weight: 500;
|
||||
|
||||
.task-name {
|
||||
@include transition(color .15s .25s ease-in-out);
|
||||
vertical-align: baseline;
|
||||
cursor: pointer;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.task-description {
|
||||
@include transition(color .15s .25s ease-in-out);
|
||||
@include font-size(14);
|
||||
color: $gray-l2;
|
||||
}
|
||||
|
||||
.task-support {
|
||||
@include transition(opacity .15s .25s ease-in-out);
|
||||
@include font-size(12);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.task-actions {
|
||||
@include transition(opacity .15s .25s ease-in-out);
|
||||
@include clearfix();
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
float: right;
|
||||
width: flex-grid(2,9);
|
||||
margin: ($baseline/2) 0 0 flex-gutter();
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
text-align: right;
|
||||
|
||||
.action-primary {
|
||||
@include blue-button;
|
||||
@include transition(all .15s);
|
||||
@include font-size(12);
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.action-secondary {
|
||||
@include font-size(13);
|
||||
margin-top: ($baseline/2);
|
||||
}
|
||||
}
|
||||
|
||||
// state - hover
|
||||
&:hover {
|
||||
background: $blue-l5;
|
||||
border-bottom-color: $blue-l4;
|
||||
border-top-color: $blue-l4;
|
||||
opacity: 1.0;
|
||||
|
||||
.task-details {
|
||||
|
||||
.task-support {
|
||||
opacity: 1.0;
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.task-actions {
|
||||
opacity: 1.0;
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// state - completed
|
||||
&.is-completed {
|
||||
background: $gray-l6;
|
||||
border-top-color: $gray-l5;
|
||||
border-bottom-color: $gray-l5;
|
||||
|
||||
.task-name {
|
||||
color: $gray-l2;
|
||||
}
|
||||
|
||||
.task-actions {
|
||||
|
||||
.action-primary {
|
||||
@include grey-button;
|
||||
@include font-size(12);
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: $gray-l5;
|
||||
border-bottom-color: $gray-l4;
|
||||
border-top-color: $gray-l4;
|
||||
|
||||
.task-details {
|
||||
opacity:1.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content-supplementary {
|
||||
width: flex-grid(3, 12);
|
||||
}
|
||||
}
|
||||
124
cms/static/sass/views/_dashboard.scss
Normal file
124
cms/static/sass/views/_dashboard.scss
Normal file
@@ -0,0 +1,124 @@
|
||||
// studio - views - user dashboard
|
||||
// ====================
|
||||
|
||||
body.dashboard {
|
||||
|
||||
.my-classes {
|
||||
margin-top: $baseline;
|
||||
}
|
||||
|
||||
.class-list {
|
||||
margin-top: 20px;
|
||||
border-radius: 3px;
|
||||
border: 1px solid $darkGrey;
|
||||
background: #fff;
|
||||
@include box-shadow(0 1px 2px rgba(0, 0, 0, .1));
|
||||
|
||||
li {
|
||||
position: relative;
|
||||
border-bottom: 1px solid $mediumGrey;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.class-link {
|
||||
z-index: 100;
|
||||
display: block;
|
||||
padding: 20px 25px;
|
||||
line-height: 1.3;
|
||||
|
||||
&:hover {
|
||||
background: $paleYellow;
|
||||
|
||||
+ .view-live-button {
|
||||
opacity: 1.0;
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.class-name {
|
||||
display: block;
|
||||
font-size: 19px;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.detail {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
margin-right: 20px;
|
||||
color: #3c3c3c;
|
||||
}
|
||||
|
||||
// view live button
|
||||
.view-live-button {
|
||||
z-index: 10000;
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
right: $baseline;
|
||||
padding: ($baseline/4) ($baseline/2);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
|
||||
&:hover {
|
||||
opacity: 1.0;
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.new-course {
|
||||
padding: 15px 25px;
|
||||
margin-top: 20px;
|
||||
border-radius: 3px;
|
||||
border: 1px solid $darkGrey;
|
||||
background: #fff;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, .1);
|
||||
@include clearfix;
|
||||
|
||||
.row {
|
||||
margin-bottom: 15px;
|
||||
@include clearfix;
|
||||
}
|
||||
|
||||
.column {
|
||||
float: left;
|
||||
width: 48%;
|
||||
}
|
||||
|
||||
.column:first-child {
|
||||
margin-right: 4%;
|
||||
}
|
||||
|
||||
.course-info {
|
||||
width: 600px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.new-course-org,
|
||||
.new-course-number,
|
||||
.new-course-name {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.new-course-name {
|
||||
font-size: 19px;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.new-course-save {
|
||||
@include blue-button;
|
||||
}
|
||||
|
||||
.new-course-cancel {
|
||||
@include white-button;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,8 @@
|
||||
.export {
|
||||
// studio - views - course export
|
||||
// ====================
|
||||
|
||||
body.course.export {
|
||||
|
||||
.export-overview {
|
||||
@extend .window;
|
||||
@include clearfix;
|
||||
@@ -118,6 +122,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,4 +1,8 @@
|
||||
.import {
|
||||
// studio - views - course import
|
||||
// ====================
|
||||
|
||||
body.course.import {
|
||||
|
||||
.import-overview {
|
||||
@extend .window;
|
||||
@include clearfix;
|
||||
@@ -1,5 +1,7 @@
|
||||
// how it works/not signed in index
|
||||
.index {
|
||||
// studio - views - how it works
|
||||
// ====================
|
||||
|
||||
body.index {
|
||||
|
||||
&.not-signedin {
|
||||
|
||||
680
cms/static/sass/views/_outline.scss
Normal file
680
cms/static/sass/views/_outline.scss
Normal file
@@ -0,0 +1,680 @@
|
||||
// studio - views - course outline
|
||||
// ====================
|
||||
|
||||
body.course.outline {
|
||||
|
||||
input.courseware-unit-search-input {
|
||||
float: left;
|
||||
width: 260px;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.branch {
|
||||
|
||||
.section-item {
|
||||
@include clearfix();
|
||||
|
||||
.details {
|
||||
display: block;
|
||||
float: left;
|
||||
margin-bottom: 0;
|
||||
width: 650px;
|
||||
}
|
||||
|
||||
.gradable-status {
|
||||
float: right;
|
||||
position: relative;
|
||||
top: -4px;
|
||||
right: 50px;
|
||||
width: 145px;
|
||||
|
||||
.status-label {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: -5px;
|
||||
display: none;
|
||||
width: 110px;
|
||||
padding: 5px 40px 5px 10px;
|
||||
@include border-radius(3px);
|
||||
color: $lightGrey;
|
||||
text-align: right;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
.menu-toggle {
|
||||
z-index: 10;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 5px;
|
||||
padding: 5px;
|
||||
color: $mediumGrey;
|
||||
|
||||
&:hover, &.is-active {
|
||||
color: $blue;
|
||||
}
|
||||
}
|
||||
|
||||
.menu {
|
||||
z-index: 1;
|
||||
display: none;
|
||||
opacity: 0.0;
|
||||
position: absolute;
|
||||
top: -1px;
|
||||
left: 5px;
|
||||
margin: 0;
|
||||
padding: 8px 12px;
|
||||
background: $white;
|
||||
border: 1px solid $mediumGrey;
|
||||
font-size: 12px;
|
||||
@include border-radius(4px);
|
||||
@include box-shadow(0 1px 2px rgba(0, 0, 0, .2));
|
||||
@include transition(opacity .15s);
|
||||
|
||||
|
||||
li {
|
||||
width: 115px;
|
||||
margin-bottom: 3px;
|
||||
padding-bottom: 3px;
|
||||
border-bottom: 1px solid $lightGrey;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
border: none;
|
||||
|
||||
a {
|
||||
color: $darkGrey;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
color: $blue;
|
||||
|
||||
&.is-selected {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// dropdown state
|
||||
&.is-active {
|
||||
|
||||
.menu {
|
||||
z-index: 1000;
|
||||
display: block;
|
||||
opacity: 1.0;
|
||||
}
|
||||
|
||||
.menu-toggle {
|
||||
z-index: 10000;
|
||||
}
|
||||
}
|
||||
|
||||
// set state
|
||||
&.is-set {
|
||||
|
||||
.menu-toggle {
|
||||
color: $blue;
|
||||
}
|
||||
|
||||
.status-label {
|
||||
display: block;
|
||||
color: $blue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.courseware-section {
|
||||
position: relative;
|
||||
background: #fff;
|
||||
border-radius: 3px;
|
||||
border: 1px solid $mediumGrey;
|
||||
margin-top: 15px;
|
||||
padding-bottom: 12px;
|
||||
@include box-shadow(0 1px 1px rgba(0, 0, 0, 0.1));
|
||||
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
&.collapsed {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
label {
|
||||
float: left;
|
||||
line-height: 29px;
|
||||
}
|
||||
|
||||
.datepair {
|
||||
float: left;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.section-published-date {
|
||||
position: absolute;
|
||||
top: 19px;
|
||||
right: 90px;
|
||||
padding: 4px 10px;
|
||||
border-radius: 3px;
|
||||
background: $lightGrey;
|
||||
text-align: right;
|
||||
|
||||
.published-status {
|
||||
font-size: 12px;
|
||||
margin-right: 15px;
|
||||
|
||||
strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.schedule-button {
|
||||
@include blue-button;
|
||||
}
|
||||
|
||||
.edit-button {
|
||||
@include blue-button;
|
||||
}
|
||||
|
||||
.schedule-button,
|
||||
.edit-button {
|
||||
font-size: 11px;
|
||||
padding: 3px 15px 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.datepair .date,
|
||||
.datepair .time {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
border: none;
|
||||
background: none;
|
||||
@include box-shadow(none);
|
||||
font-size: 13px;
|
||||
font-weight: bold;
|
||||
color: $blue;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.datepair .date {
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.datepair .time {
|
||||
width: 65px;
|
||||
}
|
||||
|
||||
&.collapsed .subsection-list,
|
||||
.collapsed .subsection-list,
|
||||
.collapsed > ol {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
header {
|
||||
min-height: 75px;
|
||||
@include clearfix();
|
||||
|
||||
.item-details, .section-published-date {
|
||||
|
||||
}
|
||||
|
||||
.item-details {
|
||||
display: inline-block;
|
||||
padding: 20px 0 10px 0;
|
||||
@include clearfix();
|
||||
|
||||
.section-name {
|
||||
float: left;
|
||||
margin-right: 10px;
|
||||
width: 350px;
|
||||
font-size: 19px;
|
||||
font-weight: bold;
|
||||
color: $blue;
|
||||
}
|
||||
|
||||
.section-name-span {
|
||||
cursor: pointer;
|
||||
@include transition(color .15s);
|
||||
|
||||
&:hover {
|
||||
color: $orange;
|
||||
}
|
||||
}
|
||||
|
||||
.section-name-edit {
|
||||
position: relative;
|
||||
width: 400px;
|
||||
background: $white;
|
||||
|
||||
input {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.save-button {
|
||||
@include blue-button;
|
||||
padding: 7px 20px 7px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.cancel-button {
|
||||
@include white-button;
|
||||
padding: 7px 20px 7px;
|
||||
}
|
||||
}
|
||||
|
||||
.section-published-date {
|
||||
float: right;
|
||||
width: 265px;
|
||||
margin-right: 220px;
|
||||
@include border-radius(3px);
|
||||
background: $lightGrey;
|
||||
|
||||
.published-status {
|
||||
font-size: 12px;
|
||||
margin-right: 15px;
|
||||
|
||||
strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.schedule-button {
|
||||
@include blue-button;
|
||||
}
|
||||
|
||||
.edit-button {
|
||||
@include blue-button;
|
||||
}
|
||||
|
||||
.schedule-button,
|
||||
.edit-button {
|
||||
font-size: 11px;
|
||||
padding: 3px 15px 5px;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.gradable-status {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 70px;
|
||||
width: 145px;
|
||||
|
||||
.status-label {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 2px;
|
||||
display: none;
|
||||
width: 100px;
|
||||
padding: 10px 35px 10px 10px;
|
||||
@include border-radius(3px);
|
||||
background: $lightGrey;
|
||||
color: $lightGrey;
|
||||
text-align: right;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
.menu-toggle {
|
||||
z-index: 10;
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 5px;
|
||||
padding: 5px;
|
||||
color: $lightGrey;
|
||||
|
||||
&:hover, &.is-active {
|
||||
color: $blue;
|
||||
}
|
||||
}
|
||||
|
||||
.menu {
|
||||
z-index: 1;
|
||||
display: none;
|
||||
opacity: 0.0;
|
||||
position: absolute;
|
||||
top: -1px;
|
||||
left: 2px;
|
||||
margin: 0;
|
||||
padding: 8px 12px;
|
||||
background: $white;
|
||||
border: 1px solid $mediumGrey;
|
||||
font-size: 12px;
|
||||
@include border-radius(4px);
|
||||
@include box-shadow(0 1px 2px rgba(0, 0, 0, .2));
|
||||
@include transition(opacity .15s);
|
||||
@include transition(display .15s);
|
||||
|
||||
|
||||
li {
|
||||
width: 115px;
|
||||
margin-bottom: 3px;
|
||||
padding-bottom: 3px;
|
||||
border-bottom: 1px solid $lightGrey;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
border: none;
|
||||
|
||||
a {
|
||||
color: $darkGrey;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
|
||||
&.is-selected {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// dropdown state
|
||||
&.is-active {
|
||||
|
||||
.menu {
|
||||
z-index: 1000;
|
||||
display: block;
|
||||
opacity: 1.0;
|
||||
}
|
||||
|
||||
|
||||
.menu-toggle {
|
||||
z-index: 10000;
|
||||
}
|
||||
}
|
||||
|
||||
// set state
|
||||
&.is-set {
|
||||
|
||||
.menu-toggle {
|
||||
color: $blue;
|
||||
}
|
||||
|
||||
.status-label {
|
||||
display: block;
|
||||
color: $blue;
|
||||
}
|
||||
}
|
||||
|
||||
float: left;
|
||||
padding: 21px 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
.item-actions {
|
||||
margin-top: 21px;
|
||||
margin-right: 12px;
|
||||
|
||||
.edit-button,
|
||||
.delete-button {
|
||||
margin-top: -3px;
|
||||
}
|
||||
}
|
||||
|
||||
.expand-collapse-icon {
|
||||
float: left;
|
||||
margin: 29px 6px 16px 16px;
|
||||
@include transition(none);
|
||||
|
||||
&.expand {
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
&.collapsed {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.drag-handle {
|
||||
margin-left: 11px;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 19px;
|
||||
font-weight: 700;
|
||||
color: $blue;
|
||||
}
|
||||
|
||||
.section-name-span {
|
||||
cursor: pointer;
|
||||
@include transition(color .15s);
|
||||
|
||||
&:hover {
|
||||
color: $orange;
|
||||
}
|
||||
}
|
||||
|
||||
.section-name-form {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.section-name-edit {
|
||||
input {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.save-button {
|
||||
@include blue-button;
|
||||
padding: 7px 20px 7px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.cancel-button {
|
||||
@include white-button;
|
||||
padding: 7px 20px 7px;
|
||||
}
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 12px;
|
||||
color: #878e9d;
|
||||
|
||||
strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.list-header {
|
||||
@include linear-gradient(top, transparent, rgba(0, 0, 0, .1));
|
||||
background-color: #ced2db;
|
||||
border-radius: 3px 3px 0 0;
|
||||
}
|
||||
|
||||
.subsection-list {
|
||||
margin: 0 12px;
|
||||
|
||||
> ol {
|
||||
@include tree-view;
|
||||
border-top-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.new-section {
|
||||
|
||||
header {
|
||||
height: auto;
|
||||
@include clearfix();
|
||||
}
|
||||
|
||||
.expand-collapse-icon {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.item-details {
|
||||
padding: 25px 0 0 0;
|
||||
|
||||
.section-name {
|
||||
float: none;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.toggle-button-sections {
|
||||
display: none;
|
||||
position: relative;
|
||||
float: right;
|
||||
margin-top: 10px;
|
||||
|
||||
font-size: 13px;
|
||||
color: $darkGrey;
|
||||
|
||||
&.is-shown {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.ss-icon {
|
||||
@include border-radius(20px);
|
||||
position: relative;
|
||||
top: -1px;
|
||||
display: inline-block;
|
||||
margin-right: 2px;
|
||||
line-height: 5px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.label {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
.new-section-name,
|
||||
.new-subsection-name-input {
|
||||
width: 515px;
|
||||
}
|
||||
|
||||
.new-section-name-save,
|
||||
.new-subsection-name-save {
|
||||
@include blue-button;
|
||||
padding: 4px 20px 7px;
|
||||
margin: 0 5px;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
.new-section-name-cancel,
|
||||
.new-subsection-name-cancel {
|
||||
@include white-button;
|
||||
padding: 4px 20px 7px;
|
||||
color: #8891a1 !important;
|
||||
}
|
||||
|
||||
.dummy-calendar {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 55px;
|
||||
left: 110px;
|
||||
z-index: 9999;
|
||||
border: 1px solid #3C3C3C;
|
||||
@include box-shadow(0 1px 15px rgba(0, 0, 0, .2));
|
||||
}
|
||||
|
||||
.preview {
|
||||
background: url(../img/preview.jpg) center top no-repeat;
|
||||
}
|
||||
|
||||
.edit-subsection-publish-settings {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 100px;
|
||||
left: 50%;
|
||||
z-index: 99999;
|
||||
width: 600px;
|
||||
margin-left: -300px;
|
||||
background: #fff;
|
||||
text-align: center;
|
||||
|
||||
.settings {
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 34px;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.picker {
|
||||
margin: 30px 0 65px;
|
||||
}
|
||||
|
||||
.description {
|
||||
margin-top: 30px;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
strong {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.start-date,
|
||||
.start-time {
|
||||
font-size: 19px;
|
||||
}
|
||||
|
||||
.save-button {
|
||||
@include blue-button;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.cancel-button {
|
||||
@include white-button;
|
||||
}
|
||||
|
||||
.save-button,
|
||||
.cancel-button {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.collapse-all-button {
|
||||
float: right;
|
||||
margin-top: 10px;
|
||||
font-size: 13px;
|
||||
color: $darkGrey;
|
||||
}
|
||||
|
||||
// sort/drag and drop
|
||||
.ui-droppable {
|
||||
@include transition (padding 0.5s ease-in-out 0s);
|
||||
min-height: 20px;
|
||||
padding: 0;
|
||||
|
||||
&.dropover {
|
||||
padding: 15px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.ui-draggable-dragging {
|
||||
@include box-shadow(0 1px 2px rgba(0, 0, 0, .3));
|
||||
border: 1px solid $darkGrey;
|
||||
opacity : 0.2;
|
||||
&:hover {
|
||||
opacity : 1.0;
|
||||
.section-item {
|
||||
background: $yellow !important;
|
||||
}
|
||||
}
|
||||
|
||||
// hiding unit button - temporary fix until this semantically corrected
|
||||
.new-unit-item {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
ol.ui-droppable .branch:first-child .section-item {
|
||||
border-top: none;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
// Studio - Course Settings
|
||||
// studio - views - course settings
|
||||
// ====================
|
||||
|
||||
body.course.settings {
|
||||
|
||||
.content-primary, .content-supplementary {
|
||||
@@ -1,4 +1,8 @@
|
||||
.static-pages {
|
||||
// studio - views - course static pages
|
||||
// ====================
|
||||
|
||||
body.course.static-pages {
|
||||
|
||||
.new-static-page-button {
|
||||
@include grey-button;
|
||||
display: block;
|
||||
@@ -16,6 +20,51 @@
|
||||
margin: 0 0 5px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.wrapper-component-editor {
|
||||
z-index: 9999;
|
||||
position: relative;
|
||||
background: $lightBluishGrey2;
|
||||
}
|
||||
|
||||
.component-editor {
|
||||
@include edit-box;
|
||||
@include box-shadow(none);
|
||||
display: none;
|
||||
padding: 20px;
|
||||
border-radius: 2px 2px 0 0;
|
||||
|
||||
.metadata_edit {
|
||||
margin-bottom: 20px;
|
||||
font-size: 13px;
|
||||
|
||||
li {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: inline-block;
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin-bottom: 10px;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
h5 {
|
||||
margin-bottom: 8px;
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.save-button {
|
||||
margin-top: 10px;
|
||||
margin: 15px 8px 0 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.component-editor {
|
||||
@@ -35,6 +84,7 @@
|
||||
}
|
||||
|
||||
.component {
|
||||
position: relative;
|
||||
border: 1px solid $mediumGrey;
|
||||
border-top: none;
|
||||
|
||||
@@ -56,10 +106,13 @@
|
||||
}
|
||||
|
||||
.drag-handle {
|
||||
position: absolute;
|
||||
display: block;
|
||||
top: 0;
|
||||
right: 0;
|
||||
z-index: 11;
|
||||
width: 35px;
|
||||
height: 100%;
|
||||
border: none;
|
||||
background: url(../img/drag-handles.png) center no-repeat #fff;
|
||||
|
||||
@@ -69,6 +122,7 @@
|
||||
}
|
||||
|
||||
.component-actions {
|
||||
position: absolute;
|
||||
top: 26px;
|
||||
right: 44px;
|
||||
}
|
||||
372
cms/static/sass/views/_subsection.scss
Normal file
372
cms/static/sass/views/_subsection.scss
Normal file
@@ -0,0 +1,372 @@
|
||||
// studio - views - course subsection
|
||||
// ====================
|
||||
|
||||
body.course.subsection {
|
||||
|
||||
.unit-settings {
|
||||
.window-contents {
|
||||
padding: 10px 20px;
|
||||
}
|
||||
|
||||
.unit-actions {
|
||||
border-bottom: none;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.published-alert {
|
||||
display: none;
|
||||
padding: 10px;
|
||||
border: 1px solid #edbd3c;
|
||||
border-radius: 3px;
|
||||
background: #fbf6e1;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
|
||||
div {
|
||||
margin-top: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
input[type="radio"] {
|
||||
margin-right: 7px;
|
||||
}
|
||||
|
||||
.status {
|
||||
font-size: 12px;
|
||||
|
||||
strong {
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
.preview-button, .view-button {
|
||||
@include white-button;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.publish-button {
|
||||
@include orange-button;
|
||||
}
|
||||
|
||||
.delete-button {
|
||||
@include blue-button;
|
||||
}
|
||||
|
||||
.delete-draft {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.delete-button,
|
||||
.preview-button,
|
||||
.publish-button,
|
||||
.view-button {
|
||||
font-size: 11px;
|
||||
margin-top: 10px;
|
||||
padding: 6px 15px 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.unit-history {
|
||||
&.collapsed {
|
||||
h4 {
|
||||
border-bottom: none;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.window-contents {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
ol {
|
||||
border: 1px solid #ced2db;
|
||||
|
||||
li {
|
||||
display: block;
|
||||
padding: 6px 8px 8px 10px;
|
||||
background: #edf1f5;
|
||||
font-size: 12px;
|
||||
|
||||
&:hover {
|
||||
background: #fffcf1;
|
||||
|
||||
.item-actions {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
&.checked {
|
||||
background: #d1dae3;
|
||||
}
|
||||
|
||||
.item-actions {
|
||||
display: none;
|
||||
}
|
||||
|
||||
input[type="radio"] {
|
||||
margin-right: 7px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.unit-location {
|
||||
.url {
|
||||
width: 100%;
|
||||
margin-bottom: 10px;
|
||||
@include box-shadow(none);
|
||||
}
|
||||
|
||||
.draft-tag,
|
||||
.hidden-tag,
|
||||
.private-tag,
|
||||
.has-new-draft-tag {
|
||||
font-size: 8px;
|
||||
}
|
||||
|
||||
.window-contents > ol {
|
||||
@include tree-view;
|
||||
|
||||
.section-item {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
font-size: 11px;
|
||||
padding: 2px 8px 4px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
@include box-sizing(border-box);
|
||||
}
|
||||
|
||||
ol {
|
||||
.section-item {
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.new-unit-item {
|
||||
margin-left: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
ol ol {
|
||||
.section-item {
|
||||
padding-left: 34px;
|
||||
}
|
||||
|
||||
.new-unit-item {
|
||||
margin: 0 0 10px 41px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.subsection-body {
|
||||
padding: 32px 40px;
|
||||
@include clearfix;
|
||||
|
||||
> div {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
input {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.sortable-unit-list {
|
||||
ol {
|
||||
@include tree-view;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.subsection-name-input {
|
||||
label {
|
||||
display: block;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.scheduled-date-input,
|
||||
.due-date-input {
|
||||
@include clearfix;
|
||||
|
||||
.date-input,
|
||||
.time-input {
|
||||
display: inline-block;
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.inherits-check {
|
||||
label {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
.notice {
|
||||
margin-top: 6px;
|
||||
font-size: 11px;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.due-date-input {
|
||||
label {
|
||||
display: inline-block !important;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
a {
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.date-setter {
|
||||
@include clearfix;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.remove-date {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.row.visibility {
|
||||
label {
|
||||
display: inline-block !important;
|
||||
margin-right: 10px;
|
||||
line-height: 21px;
|
||||
}
|
||||
|
||||
a {
|
||||
display: inline-block;
|
||||
height: 31px;
|
||||
margin-right: 8px;
|
||||
vertical-align: middle;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
line-height: 31px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.large-toggle {
|
||||
width: 41px;
|
||||
background: url(../img/large-toggles.png) no-repeat;
|
||||
background-position: 0 -50px;
|
||||
|
||||
.hidden {
|
||||
background-position: 0 -5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.gradable {
|
||||
|
||||
label {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.gradable-status {
|
||||
position: relative;
|
||||
top: -4px;
|
||||
display: inline-block;
|
||||
margin-left: 10px;
|
||||
width: 65%;
|
||||
|
||||
.status-label {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
color: $blue;
|
||||
border: none;
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.menu-toggle {
|
||||
z-index: 100;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
background: transparent;
|
||||
|
||||
&:hover, &.is-active {
|
||||
color: $blue;
|
||||
}
|
||||
}
|
||||
|
||||
.menu {
|
||||
z-index: 1;
|
||||
position: absolute;
|
||||
top: -12px;
|
||||
left: -7px;
|
||||
display: none;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 8px 12px;
|
||||
opacity: 0.0;
|
||||
background: $white;
|
||||
border: 1px solid $mediumGrey;
|
||||
font-size: 12px;
|
||||
@include border-radius(4px);
|
||||
@include box-shadow(0 1px 2px rgba(0, 0, 0, .2));
|
||||
@include transition(opacity .15s);
|
||||
|
||||
|
||||
li {
|
||||
margin-bottom: 3px;
|
||||
padding-bottom: 3px;
|
||||
border-bottom: 1px solid $lightGrey;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
|
||||
&.is-selected {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// dropdown state
|
||||
&.is-active {
|
||||
|
||||
.menu {
|
||||
z-index: 10000;
|
||||
display: block;
|
||||
opacity: 1.0;
|
||||
}
|
||||
|
||||
.menu-toggle {
|
||||
z-index: 1000;
|
||||
}
|
||||
}
|
||||
|
||||
// set state
|
||||
&.is-set {
|
||||
|
||||
.menu-toggle {
|
||||
color: $blue;
|
||||
}
|
||||
|
||||
.status-label {
|
||||
display: block;
|
||||
color: $blue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
681
cms/static/sass/views/_unit.scss
Normal file
681
cms/static/sass/views/_unit.scss
Normal file
@@ -0,0 +1,681 @@
|
||||
// studio - views - unit
|
||||
// ====================
|
||||
|
||||
body.course.unit {
|
||||
|
||||
.unit .main-wrapper {
|
||||
@include clearfix();
|
||||
margin: 40px;
|
||||
}
|
||||
|
||||
//Problem Selector tab menu requirements
|
||||
.js .tabs .tab {
|
||||
display: none;
|
||||
}
|
||||
//end problem selector reqs
|
||||
|
||||
.main-column {
|
||||
clear: both;
|
||||
float: left;
|
||||
width: 70%;
|
||||
}
|
||||
|
||||
.unit-body.published {
|
||||
.components > li {
|
||||
border: none;
|
||||
|
||||
.rendered-component {
|
||||
padding: 0 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.unit-body {
|
||||
|
||||
.unit-name-input {
|
||||
padding: 20px 40px;
|
||||
|
||||
label {
|
||||
display: block;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.breadcrumbs {
|
||||
border-radius: 3px 3px 0 0;
|
||||
border-bottom: 1px solid #cbd1db;
|
||||
@include linear-gradient(top, rgba(255, 255, 255, .3), rgba(255, 255, 255, 0) 70%);
|
||||
background-color: #edf1f5;
|
||||
@include box-shadow(0 1px 0 rgba(255, 255, 255, .7) inset);
|
||||
@include clearfix;
|
||||
|
||||
li {
|
||||
float: left;
|
||||
}
|
||||
|
||||
a,
|
||||
.current-page {
|
||||
display: block;
|
||||
padding: 15px 35px 15px 30px;
|
||||
font-size: 14px;
|
||||
background: url(../img/breadcrumb-arrow.png) no-repeat right center;
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 30px 40px 30px 0;
|
||||
color: #646464;
|
||||
font-size: 19px;
|
||||
font-weight: 300;
|
||||
letter-spacing: 1px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.components {
|
||||
|
||||
> li {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
margin: 20px 40px;
|
||||
|
||||
|
||||
|
||||
.title {
|
||||
margin: 0 0 15px 0;
|
||||
color: $mediumGrey;
|
||||
|
||||
.value {
|
||||
}
|
||||
}
|
||||
|
||||
&.new-component-item {
|
||||
margin: 20px 0px;
|
||||
border-top: 1px solid $mediumGrey;
|
||||
box-shadow: 0 2px 1px rgba(182, 182, 182, 0.75) inset;
|
||||
background-color: $lightGrey;
|
||||
margin-bottom: 0px;
|
||||
padding-bottom: 20px;
|
||||
|
||||
.new-component-button {
|
||||
display: block;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
color: #edf1f5;
|
||||
}
|
||||
|
||||
h5 {
|
||||
margin: 20px 0px;
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.rendered-component {
|
||||
display: none;
|
||||
background: #fff;
|
||||
border-radius: 3px 3px 0 0;
|
||||
}
|
||||
|
||||
.new-component-type {
|
||||
|
||||
a,
|
||||
li {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
a {
|
||||
border: 1px solid $mediumGrey;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
color: #fff;
|
||||
margin-right: 15px;
|
||||
margin-bottom: 20px;
|
||||
border-radius: 8px;
|
||||
font-size: 15px;
|
||||
line-height: 14px;
|
||||
text-align: center;
|
||||
@include box-shadow(0 1px 1px rgba(0, 0, 0, .2), 0 1px 0 rgba(255, 255, 255, .4) inset);
|
||||
|
||||
.name {
|
||||
position: absolute;
|
||||
bottom: 5px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
@include box-sizing(border-box);
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.new-component-templates {
|
||||
display: none;
|
||||
margin: 20px 40px 20px 40px;
|
||||
border-radius: 3px;
|
||||
border: 1px solid $mediumGrey;
|
||||
background-color: #fff;
|
||||
@include box-shadow(0 1px 1px rgba(0, 0, 0, .2), 0 1px 0 rgba(255, 255, 255, .4) inset);
|
||||
@include clearfix;
|
||||
|
||||
.cancel-button {
|
||||
margin: 20px 0px 10px 10px;
|
||||
@include white-button;
|
||||
}
|
||||
|
||||
.problem-type-tabs {
|
||||
display: none;
|
||||
}
|
||||
|
||||
// specific menu types
|
||||
&.new-component-problem {
|
||||
padding-bottom:10px;
|
||||
|
||||
.ss-icon, .editor-indicator {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.problem-type-tabs {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.new-component-type,
|
||||
.new-component-template {
|
||||
@include clearfix;
|
||||
|
||||
a {
|
||||
position: relative;
|
||||
border: 1px solid $darkGreen;
|
||||
background: tint($green,20%);
|
||||
color: #fff;
|
||||
|
||||
&:hover {
|
||||
background: $brightGreen;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.problem-type-tabs {
|
||||
list-style-type: none;
|
||||
border-radius: 0;
|
||||
width: 100%;
|
||||
@include linear-gradient(top, rgba(255, 255, 255, .4), rgba(255, 255, 255, 0));
|
||||
background-color: $lightBluishGrey;
|
||||
@include box-shadow(0 1px 0 rgba(255, 255, 255, 0.2) inset, 0 -1px 0 rgba(0, 0, 0, 0.2) inset);
|
||||
|
||||
li:first-child {
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
li {
|
||||
float:left;
|
||||
display:inline-block;
|
||||
text-align:center;
|
||||
width: auto;
|
||||
@include linear-gradient(top, rgba(255, 255, 255, .4), rgba(255, 255, 255, 0));
|
||||
background-color: tint($lightBluishGrey, 10%);
|
||||
@include box-shadow(0 1px 0 rgba(255, 255, 255, 0.2) inset, 0 -1px 0 rgba(0, 0, 0, 0.2) inset);
|
||||
opacity:.8;
|
||||
|
||||
&:hover {
|
||||
opacity:1;
|
||||
background-color: tint($lightBluishGrey, 20%);
|
||||
}
|
||||
|
||||
&.ui-state-active {
|
||||
border: 0px;
|
||||
@include active;
|
||||
opacity:1;
|
||||
}
|
||||
}
|
||||
|
||||
a{
|
||||
display: block;
|
||||
padding: 15px 25px;
|
||||
font-size: 15px;
|
||||
line-height: 16px;
|
||||
text-align: center;
|
||||
color: #3c3c3c;
|
||||
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
.new-component-template {
|
||||
|
||||
a {
|
||||
background: #fff;
|
||||
border: 0px;
|
||||
color: #3c3c3c;
|
||||
@include transition (none);
|
||||
|
||||
&:hover {
|
||||
background: tint($green,30%);
|
||||
color: #fff;
|
||||
@include transition(background-color .15s);
|
||||
}
|
||||
}
|
||||
|
||||
li {
|
||||
border:none;
|
||||
border-bottom: 1px dashed $lightGrey;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
li:first-child {
|
||||
a {
|
||||
border-top: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
li:nth-child(2) {
|
||||
a {
|
||||
border-radius: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
@include clearfix();
|
||||
display: block;
|
||||
padding: 7px 20px;
|
||||
border-bottom: none;
|
||||
font-weight: 500;
|
||||
|
||||
.name {
|
||||
float: left;
|
||||
|
||||
.ss-icon {
|
||||
@include transition(opacity .15s);
|
||||
display: inline-block;
|
||||
top: 1px;
|
||||
margin-right: 5px;
|
||||
opacity: 0.5;
|
||||
width: 17;
|
||||
height: 21px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
.editor-indicator {
|
||||
@include transition(opacity .15s);
|
||||
float: right;
|
||||
position: relative;
|
||||
top: 3px;
|
||||
font-size: 12px;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.ss-icon, .editor-indicator {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: #fff;
|
||||
|
||||
.ss-icon {
|
||||
opacity: 1.0;
|
||||
}
|
||||
|
||||
.editor-indicator {
|
||||
opacity: 1.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// specific editor types
|
||||
.empty {
|
||||
|
||||
a {
|
||||
line-height: 1.4;
|
||||
font-weight: 400;
|
||||
background: #fff;
|
||||
color: #3c3c3c;
|
||||
|
||||
|
||||
&:hover {
|
||||
background: tint($green,30%);
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.new-component {
|
||||
text-align: center;
|
||||
|
||||
h5 {
|
||||
color: $darkGreen;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.component {
|
||||
border: 1px solid $lightBluishGrey2;
|
||||
border-radius: 3px;
|
||||
background: #fff;
|
||||
@include transition(none);
|
||||
|
||||
&:hover {
|
||||
border-color: #6696d7;
|
||||
|
||||
.drag-handle {
|
||||
background-color: $blue;
|
||||
border-color: $blue;
|
||||
}
|
||||
}
|
||||
|
||||
&.editing {
|
||||
border: 1px solid $lightBluishGrey2;
|
||||
z-index: auto;
|
||||
|
||||
.drag-handle,
|
||||
.component-actions {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.component-placeholder {
|
||||
border-color: #6696d7;
|
||||
}
|
||||
|
||||
.drag-handle {
|
||||
position: absolute;
|
||||
display: block;
|
||||
top: -1px;
|
||||
right: -16px;
|
||||
z-index: 10;
|
||||
width: 15px;
|
||||
height: 100%;
|
||||
border-radius: 0 3px 3px 0;
|
||||
border: 1px solid $lightBluishGrey2;
|
||||
background: url(../img/white-drag-handles.png) center no-repeat $lightBluishGrey2;
|
||||
cursor: move;
|
||||
@include transition(none);
|
||||
}
|
||||
}
|
||||
|
||||
.xmodule_display {
|
||||
padding: 40px 20px 20px;
|
||||
overflow-x: auto;
|
||||
|
||||
h1 {
|
||||
float: none;
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.wrapper-component-editor {
|
||||
z-index: 9999;
|
||||
position: relative;
|
||||
background: $lightBluishGrey2;
|
||||
}
|
||||
|
||||
.component-editor {
|
||||
@include edit-box;
|
||||
@include box-shadow(none);
|
||||
display: none;
|
||||
padding: 20px;
|
||||
border-radius: 2px 2px 0 0;
|
||||
|
||||
.metadata_edit {
|
||||
margin-bottom: 20px;
|
||||
font-size: 13px;
|
||||
|
||||
li {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: inline-block;
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin-bottom: 10px;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
h5 {
|
||||
margin-bottom: 8px;
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.save-button {
|
||||
margin-top: 10px;
|
||||
margin: 15px 8px 0 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.unit-settings {
|
||||
.window-contents {
|
||||
padding: 10px 20px;
|
||||
}
|
||||
|
||||
.unit-actions {
|
||||
border-bottom: none;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.published-alert {
|
||||
display: none;
|
||||
padding: 10px;
|
||||
border: 1px solid #edbd3c;
|
||||
border-radius: 3px;
|
||||
background: #fbf6e1;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
|
||||
div {
|
||||
margin-top: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
input[type="radio"] {
|
||||
margin-right: 7px;
|
||||
}
|
||||
|
||||
.status {
|
||||
font-size: 12px;
|
||||
|
||||
strong {
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
.preview-button, .view-button {
|
||||
@include white-button;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.publish-button {
|
||||
@include orange-button;
|
||||
}
|
||||
|
||||
.delete-button {
|
||||
@include blue-button;
|
||||
}
|
||||
|
||||
.delete-draft {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.delete-button,
|
||||
.preview-button,
|
||||
.publish-button,
|
||||
.view-button {
|
||||
font-size: 11px;
|
||||
margin-top: 10px;
|
||||
padding: 6px 15px 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.unit-history {
|
||||
&.collapsed {
|
||||
h4 {
|
||||
border-bottom: none;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.window-contents {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
ol {
|
||||
border: 1px solid #ced2db;
|
||||
|
||||
li {
|
||||
display: block;
|
||||
padding: 6px 8px 8px 10px;
|
||||
background: #edf1f5;
|
||||
font-size: 12px;
|
||||
|
||||
&:hover {
|
||||
background: #fffcf1;
|
||||
|
||||
.item-actions {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
&.checked {
|
||||
background: #d1dae3;
|
||||
}
|
||||
|
||||
.item-actions {
|
||||
display: none;
|
||||
}
|
||||
|
||||
input[type="radio"] {
|
||||
margin-right: 7px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.unit-location {
|
||||
.url {
|
||||
width: 100%;
|
||||
margin-bottom: 10px;
|
||||
@include box-shadow(none);
|
||||
}
|
||||
|
||||
.draft-tag,
|
||||
.hidden-tag,
|
||||
.private-tag,
|
||||
.has-new-draft-tag {
|
||||
font-size: 8px;
|
||||
}
|
||||
|
||||
.window-contents > ol {
|
||||
@include tree-view;
|
||||
|
||||
.section-item {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
font-size: 11px;
|
||||
padding: 2px 8px 4px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
@include box-sizing(border-box);
|
||||
}
|
||||
|
||||
ol {
|
||||
.section-item {
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.new-unit-item {
|
||||
margin-left: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
ol ol {
|
||||
.section-item {
|
||||
padding-left: 34px;
|
||||
}
|
||||
|
||||
.new-unit-item {
|
||||
margin: 0 0 10px 41px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.edit-state-draft {
|
||||
.visibility,
|
||||
|
||||
.edit-draft-message,
|
||||
.view-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.published-alert {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.edit-state-public {
|
||||
.delete-draft,
|
||||
.component-actions,
|
||||
.new-component-item,
|
||||
.editing-draft-alert,
|
||||
.publish-draft-message,
|
||||
.preview-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.published-alert {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.drag-handle {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.edit-state-private {
|
||||
.delete-draft,
|
||||
.publish-draft,
|
||||
.editing-draft-alert,
|
||||
.create-draft,
|
||||
.view-button {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// editing units from courseware
|
||||
body.unit {
|
||||
|
||||
.component {
|
||||
padding-top: 30px;
|
||||
|
||||
.component-actions {
|
||||
@include box-sizing(border-box);
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
padding: 15px;
|
||||
top: 0;
|
||||
left: 0;
|
||||
border-bottom: 1px solid $lightBluishGrey2;
|
||||
background: $lightGrey;
|
||||
}
|
||||
|
||||
&.editing {
|
||||
padding-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,8 @@
|
||||
.course-info {
|
||||
// studio - views - course updates
|
||||
// ====================
|
||||
|
||||
body.course.updates {
|
||||
|
||||
h2 {
|
||||
margin-bottom: 24px;
|
||||
font-size: 22px;
|
||||
@@ -1,4 +1,8 @@
|
||||
.users {
|
||||
// studio - views - course users
|
||||
// ====================
|
||||
|
||||
body.course.users {
|
||||
|
||||
.new-user-form {
|
||||
display: none;
|
||||
padding: 15px 20px;
|
||||
@@ -1,7 +1,7 @@
|
||||
<%inherit file="base.html" />
|
||||
<%! from django.core.urlresolvers import reverse %>
|
||||
<%block name="bodyclass">is-signedin course uploads</%block>
|
||||
<%block name="title">Uploads & Files</%block>
|
||||
<%block name="title">Files & Uploads</%block>
|
||||
|
||||
<%namespace name='static' file='static_content.html'/>
|
||||
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
<script src="${static.url('js/vendor/jquery.leanModal.min.js')}"></script>
|
||||
<script src="${static.url('js/vendor/jquery.tablednd.js')}"></script>
|
||||
<script src="${static.url('js/vendor/jquery.form.js')}"></script>
|
||||
<script src="${static.url('js/vendor/jquery.smooth-scroll.min.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/vendor/CodeMirror/htmlmixed.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/vendor/CodeMirror/css.js')}"></script>
|
||||
<script type="text/javascript">
|
||||
|
||||
74
cms/templates/checklists.html
Normal file
74
cms/templates/checklists.html
Normal file
@@ -0,0 +1,74 @@
|
||||
<%inherit file="base.html" />
|
||||
<%! from django.core.urlresolvers import reverse %>
|
||||
<%block name="title">Course Checklists</%block>
|
||||
<%block name="bodyclass">is-signedin course uxdesign checklists</%block>
|
||||
|
||||
<%namespace name='static' file='static_content.html'/>
|
||||
<%block name="jsextra">
|
||||
|
||||
<script type="text/javascript" src="${static.url('js/template_loader.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/views/checklists_view.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/models/checklists.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/views/server_error.js')}"></script>
|
||||
|
||||
<script type="text/javascript">
|
||||
$(document).ready(function () {
|
||||
var checklistCollection = new CMS.Models.ChecklistCollection();
|
||||
checklistCollection.url = "${reverse('checklists_updates', kwargs=dict(org=context_course.location.org, course=context_course.location.course, name=context_course.location.name))}";
|
||||
|
||||
var editor = new CMS.Views.Checklists({
|
||||
el: $('.course-checklists'),
|
||||
collection: checklistCollection
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
</script>
|
||||
</%block>
|
||||
|
||||
|
||||
<%block name="content">
|
||||
<div class="wrapper-mast wrapper">
|
||||
<header class="mast has-actions has-subtitle">
|
||||
<div class="title">
|
||||
<span class="title-sub">Tools</span>
|
||||
<h1 class="title-1">Course Checklists</h1>
|
||||
</div>
|
||||
</header>
|
||||
</div>
|
||||
|
||||
<div class="wrapper-content wrapper">
|
||||
<section class="content">
|
||||
<article class="content-primary" role="main">
|
||||
<form id="course-checklists" class="course-checklists" method="post" action="">
|
||||
<h2 class="title title-3 sr">Current Checklists</h2>
|
||||
</form>
|
||||
</article>
|
||||
|
||||
<aside class="content-supplementary" role="complimentary">
|
||||
<div class="bit">
|
||||
<h3 class="title title-3">What are checklists?</h3>
|
||||
<p>
|
||||
Running a course on edX is a complex undertaking. Course checklists are designed to help you understand and keep track of all the steps necessary to get your course ready for students.
|
||||
</p>
|
||||
<p>
|
||||
These checklists are shared among your course team, and any changes you make are immediately visible to other members of the team and saved automatically.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="bit">
|
||||
<h3 class="title title-3">Studio checklists</h3>
|
||||
<nav class="nav-page checklists-current">
|
||||
<ol>
|
||||
% for checklist in checklists:
|
||||
<li class="nav-item">
|
||||
<a rel="view" href="${'#course-checklist' + str(loop.index)}">${checklist['short_description']}</a>
|
||||
</li>
|
||||
% endfor
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
</aside>
|
||||
</section>
|
||||
</div>
|
||||
</%block>
|
||||
@@ -2,7 +2,7 @@
|
||||
<%namespace name='static' file='static_content.html'/>
|
||||
|
||||
<!-- TODO decode course # from context_course into title -->
|
||||
<%block name="title">Updates</%block>
|
||||
<%block name="title">Course Updates</%block>
|
||||
<%block name="bodyclass">is-signedin course course-info updates</%block>
|
||||
|
||||
|
||||
|
||||
@@ -31,18 +31,6 @@
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<div id="policy-to-delete" style="display:none">
|
||||
</div>
|
||||
|
||||
<div id="add-new-policy-element-template" style="display:none">
|
||||
<li class="policy-list-element new-policy-list-element">
|
||||
<input type="text" class="policy-list-name" autocomplete="off" size="15"/>: <input type="text" class="policy-list-value" size=40 autocomplete="off"/>
|
||||
<a href="#" class="save-button">Save</a>
|
||||
<a href="#" class="cancel-button">Cancel</a>
|
||||
<a href="#" class="delete-icon remove-policy-data"></a>
|
||||
</li>
|
||||
</div>
|
||||
|
||||
<div class="sidebar">
|
||||
<div class="unit-settings window id-holder" data-id="${subsection.location}">
|
||||
<h4 class="header">Subsection Settings</h4>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<%namespace name='static' file='static_content.html'/>
|
||||
|
||||
<%! from django.core.urlresolvers import reverse %>
|
||||
<%block name="title">Export Course</%block>
|
||||
<%block name="title">Course Export</%block>
|
||||
<%block name="bodyclass">is-signedin course tools export</%block>
|
||||
|
||||
<%block name="content">
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<%namespace name='static' file='static_content.html'/>
|
||||
|
||||
<%! from django.core.urlresolvers import reverse %>
|
||||
<%block name="title">Import Course</%block>
|
||||
<%block name="title">Course Import</%block>
|
||||
<%block name="bodyclass">is-signedin course tools import</%block>
|
||||
|
||||
<%block name="content">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<%inherit file="base.html" />
|
||||
|
||||
<%block name="title">Courses</%block>
|
||||
<%block name="title">My Courses</%block>
|
||||
<%block name="bodyclass">is-signedin index dashboard</%block>
|
||||
|
||||
<%block name="header_extras">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<%inherit file="base.html" />
|
||||
<%block name="title">Course Staff Manager</%block>
|
||||
<%block name="title">Course Team Settings</%block>
|
||||
<%block name="bodyclass">is-signedin course users settings team</%block>
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<%inherit file="base.html" />
|
||||
<%block name="title">Schedule & Details</%block>
|
||||
<%block name="title">Schedule & Details Settings</%block>
|
||||
<%block name="bodyclass">is-signedin course schedule settings</%block>
|
||||
|
||||
<%namespace name='static' file='static_content.html'/>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<%inherit file="base.html" />
|
||||
<%block name="title">Grading</%block>
|
||||
<%block name="title">Grading Settings</%block>
|
||||
<%block name="bodyclass">is-signedin course grading settings</%block>
|
||||
|
||||
<%namespace name='static' file='static_content.html'/>
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
This unit was originally published on ${published_date}.
|
||||
% endif
|
||||
</p>
|
||||
<a href="${published_preview_link}" target="_blank" class="alert-action secondary">Preview the published version</a>
|
||||
<a href="${published_preview_link}" target="_blank" class="alert-action secondary">View the Live Version</a>
|
||||
</div>
|
||||
<div class="main-column">
|
||||
<article class="unit-body window">
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
<div class="wrapper wrapper-nav-sub">
|
||||
<div class="nav-sub">
|
||||
<ul>
|
||||
<li class="nav-item nav-course-tools-checklists"><a href="${reverse('checklists', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, name=ctx_loc.name))}">Checklists</a></li>
|
||||
<li class="nav-item nav-course-tools-import"><a href="${reverse('import_course', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, name=ctx_loc.name))}">Import</a></li>
|
||||
<li class="nav-item nav-course-tools-export"><a href="${reverse('export_course', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, name=ctx_loc.name))}">Export</a></li>
|
||||
</ul>
|
||||
|
||||
55
cms/urls.py
55
cms/urls.py
@@ -42,36 +42,52 @@ urlpatterns = ('',
|
||||
'contentstore.views.remove_user', name='remove_user'),
|
||||
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<name>[^/]+)/remove_user$',
|
||||
'contentstore.views.remove_user', name='remove_user'),
|
||||
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/info/(?P<name>[^/]+)$', 'contentstore.views.course_info', name='course_info'),
|
||||
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/course_info/updates/(?P<provided_id>.*)$', 'contentstore.views.course_info_updates', name='course_info'),
|
||||
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/settings-details/(?P<name>[^/]+)$', 'contentstore.views.get_course_settings', name='course_settings'),
|
||||
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/settings-grading/(?P<name>[^/]+)$', 'contentstore.views.course_config_graders_page', name='course_settings'),
|
||||
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/settings-details/(?P<name>[^/]+)/section/(?P<section>[^/]+).*$', 'contentstore.views.course_settings_updates', name='course_settings'),
|
||||
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/settings-grading/(?P<name>[^/]+)/(?P<grader_index>.*)$', 'contentstore.views.course_grader_updates', name='course_settings'),
|
||||
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/info/(?P<name>[^/]+)$',
|
||||
'contentstore.views.course_info', name='course_info'),
|
||||
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/course_info/updates/(?P<provided_id>.*)$',
|
||||
'contentstore.views.course_info_updates', name='course_info_json'),
|
||||
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/settings-details/(?P<name>[^/]+)$',
|
||||
'contentstore.views.get_course_settings', name='settings_details'),
|
||||
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/settings-grading/(?P<name>[^/]+)$',
|
||||
'contentstore.views.course_config_graders_page', name='settings_grading'),
|
||||
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/settings-details/(?P<name>[^/]+)/section/(?P<section>[^/]+).*$',
|
||||
'contentstore.views.course_settings_updates', name='course_settings'),
|
||||
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/settings-grading/(?P<name>[^/]+)/(?P<grader_index>.*)$',
|
||||
'contentstore.views.course_grader_updates', name='course_settings'),
|
||||
# This is the URL to initially render the course advanced settings.
|
||||
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/settings-advanced/(?P<name>[^/]+)$', 'contentstore.views.course_config_advanced_page', name='course_advanced_settings'),
|
||||
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/settings-advanced/(?P<name>[^/]+)$',
|
||||
'contentstore.views.course_config_advanced_page', name='course_advanced_settings'),
|
||||
# This is the URL used by BackBone for updating and re-fetching the model.
|
||||
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/settings-advanced/(?P<name>[^/]+)/update.*$', 'contentstore.views.course_advanced_updates', name='course_advanced_settings_updates'),
|
||||
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/settings-advanced/(?P<name>[^/]+)/update.*$',
|
||||
'contentstore.views.course_advanced_updates', name='course_advanced_settings_updates'),
|
||||
|
||||
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/(?P<category>[^/]+)/(?P<name>[^/]+)/gradeas.*$', 'contentstore.views.assignment_type_update', name='assignment_type_update'),
|
||||
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/(?P<category>[^/]+)/(?P<name>[^/]+)/gradeas.*$',
|
||||
'contentstore.views.assignment_type_update', name='assignment_type_update'),
|
||||
|
||||
url(r'^pages/(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<coursename>[^/]+)$', 'contentstore.views.static_pages',
|
||||
url(r'^pages/(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<coursename>[^/]+)$',
|
||||
'contentstore.views.static_pages',
|
||||
name='static_pages'),
|
||||
url(r'^edit_static/(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<coursename>[^/]+)$', 'contentstore.views.edit_static', name='edit_static'),
|
||||
url(r'^edit_tabs/(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<coursename>[^/]+)$', 'contentstore.views.edit_tabs', name='edit_tabs'),
|
||||
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/assets/(?P<name>[^/]+)$', 'contentstore.views.asset_index', name='asset_index'),
|
||||
url(r'^edit_static/(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<coursename>[^/]+)$',
|
||||
'contentstore.views.edit_static', name='edit_static'),
|
||||
url(r'^edit_tabs/(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<coursename>[^/]+)$',
|
||||
'contentstore.views.edit_tabs', name='edit_tabs'),
|
||||
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/assets/(?P<name>[^/]+)$',
|
||||
'contentstore.views.asset_index', name='asset_index'),
|
||||
|
||||
# this is a generic method to return the data/metadata associated with a xmodule
|
||||
url(r'^module_info/(?P<module_location>.*)$', 'contentstore.views.module_info', name='module_info'),
|
||||
url(r'^module_info/(?P<module_location>.*)$',
|
||||
'contentstore.views.module_info', name='module_info'),
|
||||
|
||||
|
||||
# temporary landing page for a course
|
||||
url(r'^edge/(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<coursename>[^/]+)$', 'contentstore.views.landing', name='landing'),
|
||||
url(r'^edge/(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<coursename>[^/]+)$',
|
||||
'contentstore.views.landing', name='landing'),
|
||||
|
||||
url(r'^not_found$', 'contentstore.views.not_found', name='not_found'),
|
||||
url(r'^server_error$', 'contentstore.views.server_error', name='server_error'),
|
||||
|
||||
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/assets/(?P<name>[^/]+)$', 'contentstore.views.asset_index', name='asset_index'),
|
||||
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/assets/(?P<name>[^/]+)$',
|
||||
'contentstore.views.asset_index', name='asset_index'),
|
||||
|
||||
# temporary landing page for edge
|
||||
url(r'^edge$', 'contentstore.views.edge', name='edge'),
|
||||
@@ -83,6 +99,9 @@ urlpatterns = ('',
|
||||
|
||||
# User creation and updating views
|
||||
urlpatterns += (
|
||||
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/checklists/(?P<name>[^/]+)$', 'contentstore.views.get_checklists', name='checklists'),
|
||||
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/checklists/(?P<name>[^/]+)/update(/)?(?P<checklist_index>.+)?.*$',
|
||||
'contentstore.views.update_checklist', name='checklists_updates'),
|
||||
url(r'^howitworks$', 'contentstore.views.howitworks', name='howitworks'),
|
||||
url(r'^signup$', 'contentstore.views.signup', name='signup'),
|
||||
|
||||
@@ -100,12 +119,12 @@ urlpatterns += (
|
||||
)
|
||||
|
||||
if settings.ENABLE_JASMINE:
|
||||
## Jasmine
|
||||
# # Jasmine
|
||||
urlpatterns = urlpatterns + (url(r'^_jasmine/', include('django_jasmine.urls')),)
|
||||
|
||||
urlpatterns = patterns(*urlpatterns)
|
||||
|
||||
#Custom error pages
|
||||
# Custom error pages
|
||||
handler404 = 'contentstore.views.render_404'
|
||||
handler500 = 'contentstore.views.render_500'
|
||||
|
||||
|
||||
@@ -15,6 +15,24 @@ from .models import CourseUserGroup
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# tl;dr: global state is bad. capa reseeds random every time a problem is loaded. Even
|
||||
# if and when that's fixed, it's a good idea to have a local generator to avoid any other
|
||||
# code that messes with the global random module.
|
||||
_local_random = None
|
||||
|
||||
def local_random():
|
||||
"""
|
||||
Get the local random number generator. In a function so that we don't run
|
||||
random.Random() at import time.
|
||||
"""
|
||||
# ironic, isn't it?
|
||||
global _local_random
|
||||
|
||||
if _local_random is None:
|
||||
_local_random = random.Random()
|
||||
|
||||
return _local_random
|
||||
|
||||
def is_course_cohorted(course_id):
|
||||
"""
|
||||
Given a course id, return a boolean for whether or not the course is
|
||||
@@ -129,13 +147,7 @@ def get_cohort(user, course_id):
|
||||
return None
|
||||
|
||||
# Put user in a random group, creating it if needed
|
||||
choice = random.randrange(0, n)
|
||||
group_name = choices[choice]
|
||||
|
||||
# Victor: we are seeing very strange behavior on prod, where almost all users
|
||||
# end up in the same group. Log at INFO to try to figure out what's going on.
|
||||
log.info("DEBUG: adding user {0} to cohort {1}. choice={2}".format(
|
||||
user, group_name,choice))
|
||||
group_name = local_random().choice(choices)
|
||||
|
||||
group, created = CourseUserGroup.objects.get_or_create(
|
||||
course_id=course_id,
|
||||
|
||||
@@ -75,10 +75,15 @@ class UserProfile(models.Model):
|
||||
GENDER_CHOICES = (('m', 'Male'), ('f', 'Female'), ('o', 'Other'))
|
||||
gender = models.CharField(blank=True, null=True, max_length=6, db_index=True,
|
||||
choices=GENDER_CHOICES)
|
||||
LEVEL_OF_EDUCATION_CHOICES = (('p_se', 'Doctorate in science or engineering'),
|
||||
('p_oth', 'Doctorate in another field'),
|
||||
|
||||
# [03/21/2013] removed these, but leaving comment since there'll still be
|
||||
# p_se and p_oth in the existing data in db.
|
||||
# ('p_se', 'Doctorate in science or engineering'),
|
||||
# ('p_oth', 'Doctorate in another field'),
|
||||
LEVEL_OF_EDUCATION_CHOICES = (('p', 'Doctorate'),
|
||||
('m', "Master's or professional degree"),
|
||||
('b', "Bachelor's degree"),
|
||||
('a', "Associate's degree"),
|
||||
('hs', "Secondary/high school"),
|
||||
('jhs', "Junior secondary/junior high/middle school"),
|
||||
('el', "Elementary/primary school"),
|
||||
|
||||
0
common/djangoapps/student/tests/__init__.py
Normal file
0
common/djangoapps/student/tests/__init__.py
Normal file
59
common/djangoapps/student/tests/factories.py
Normal file
59
common/djangoapps/student/tests/factories.py
Normal file
@@ -0,0 +1,59 @@
|
||||
from student.models import (User, UserProfile, Registration,
|
||||
CourseEnrollmentAllowed, CourseEnrollment)
|
||||
from django.contrib.auth.models import Group
|
||||
from datetime import datetime
|
||||
from factory import Factory, SubFactory
|
||||
from uuid import uuid4
|
||||
|
||||
|
||||
class GroupFactory(Factory):
|
||||
FACTORY_FOR = Group
|
||||
|
||||
name = 'staff_MITx/999/Robot_Super_Course'
|
||||
|
||||
|
||||
class UserProfileFactory(Factory):
|
||||
FACTORY_FOR = UserProfile
|
||||
|
||||
user = None
|
||||
name = 'Robot Test'
|
||||
level_of_education = None
|
||||
gender = 'm'
|
||||
mailing_address = None
|
||||
goals = 'World domination'
|
||||
|
||||
|
||||
class RegistrationFactory(Factory):
|
||||
FACTORY_FOR = Registration
|
||||
|
||||
user = None
|
||||
activation_key = uuid4().hex
|
||||
|
||||
|
||||
class UserFactory(Factory):
|
||||
FACTORY_FOR = User
|
||||
|
||||
username = 'robot'
|
||||
email = 'robot+test@edx.org'
|
||||
password = 'test'
|
||||
first_name = 'Robot'
|
||||
last_name = 'Test'
|
||||
is_staff = False
|
||||
is_active = True
|
||||
is_superuser = False
|
||||
last_login = datetime(2012, 1, 1)
|
||||
date_joined = datetime(2011, 1, 1)
|
||||
|
||||
|
||||
class CourseEnrollmentFactory(Factory):
|
||||
FACTORY_FOR = CourseEnrollment
|
||||
|
||||
user = SubFactory(UserFactory)
|
||||
course_id = 'edX/toy/2012_Fall'
|
||||
|
||||
|
||||
class CourseEnrollmentAllowedFactory(Factory):
|
||||
FACTORY_FOR = CourseEnrollmentAllowed
|
||||
|
||||
email = 'test@edx.org'
|
||||
course_id = 'edX/test/2012_Fall'
|
||||
@@ -9,8 +9,8 @@ import logging
|
||||
from django.test import TestCase
|
||||
from mock import Mock
|
||||
|
||||
from .models import unique_id_for_user
|
||||
from .views import process_survey_link, _cert_info
|
||||
from student.models import unique_id_for_user
|
||||
from student.views import process_survey_link, _cert_info
|
||||
|
||||
COURSE_1 = 'edX/toy/2012_Fall'
|
||||
COURSE_2 = 'edx/full/6.002_Spring_2012'
|
||||
@@ -311,7 +311,7 @@ def change_enrollment(request):
|
||||
course = course_from_id(course_id)
|
||||
except ItemNotFoundError:
|
||||
log.warning("User {0} tried to enroll in non-existent course {1}"
|
||||
.format(user.username, enrollment.course_id))
|
||||
.format(user.username, course_id))
|
||||
return {'success': False, 'error': 'The course requested does not exist.'}
|
||||
|
||||
if not has_access(user, course, 'enroll'):
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from lettuce import before, after, world
|
||||
from splinter.browser import Browser
|
||||
from logging import getLogger
|
||||
import time
|
||||
|
||||
# Let the LMS and CMS do their one-time setup
|
||||
# For example, setting up mongo caches
|
||||
@@ -16,6 +15,9 @@ from django.core.management import call_command
|
||||
|
||||
@before.harvest
|
||||
def initial_setup(server):
|
||||
'''
|
||||
Launch the browser once before executing the tests
|
||||
'''
|
||||
# Launch the browser app (choose one of these below)
|
||||
world.browser = Browser('chrome')
|
||||
# world.browser = Browser('phantomjs')
|
||||
@@ -24,14 +26,18 @@ def initial_setup(server):
|
||||
|
||||
@before.each_scenario
|
||||
def reset_data(scenario):
|
||||
# Clean out the django test database defined in the
|
||||
# envs/acceptance.py file: mitx_all/db/test_mitx.db
|
||||
'''
|
||||
Clean out the django test database defined in the
|
||||
envs/acceptance.py file: mitx_all/db/test_mitx.db
|
||||
'''
|
||||
logger.debug("Flushing the test database...")
|
||||
call_command('flush', interactive=False)
|
||||
|
||||
|
||||
@after.all
|
||||
def teardown_browser(total):
|
||||
# Quit firefox
|
||||
'''
|
||||
Quit the browser after executing the tests
|
||||
'''
|
||||
world.browser.quit()
|
||||
pass
|
||||
|
||||
@@ -1,190 +1,64 @@
|
||||
from student.models import User, UserProfile, Registration
|
||||
from django.contrib.auth.models import Group
|
||||
from datetime import datetime
|
||||
from factory import Factory
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from time import gmtime
|
||||
from uuid import uuid4
|
||||
from xmodule.timeparse import stringify_time
|
||||
from xmodule.modulestore.inheritance import own_metadata
|
||||
'''
|
||||
Factories are defined in other modules and absorbed here into the
|
||||
lettuce world so that they can be used by both unit tests
|
||||
and integration / BDD tests.
|
||||
'''
|
||||
import student.tests.factories as sf
|
||||
import xmodule.modulestore.tests.factories as xf
|
||||
from lettuce import world
|
||||
|
||||
|
||||
class GroupFactory(Factory):
|
||||
FACTORY_FOR = Group
|
||||
|
||||
name = 'staff_MITx/999/Robot_Super_Course'
|
||||
|
||||
|
||||
class UserProfileFactory(Factory):
|
||||
FACTORY_FOR = UserProfile
|
||||
|
||||
user = None
|
||||
name = 'Robot Test'
|
||||
level_of_education = None
|
||||
gender = 'm'
|
||||
mailing_address = None
|
||||
goals = 'World domination'
|
||||
|
||||
|
||||
class RegistrationFactory(Factory):
|
||||
FACTORY_FOR = Registration
|
||||
|
||||
user = None
|
||||
activation_key = uuid4().hex
|
||||
|
||||
|
||||
class UserFactory(Factory):
|
||||
FACTORY_FOR = User
|
||||
|
||||
username = 'robot'
|
||||
email = 'robot+test@edx.org'
|
||||
password = 'test'
|
||||
first_name = 'Robot'
|
||||
last_name = 'Test'
|
||||
is_staff = False
|
||||
is_active = True
|
||||
is_superuser = False
|
||||
last_login = datetime(2012, 1, 1)
|
||||
date_joined = datetime(2011, 1, 1)
|
||||
|
||||
|
||||
def XMODULE_COURSE_CREATION(class_to_create, **kwargs):
|
||||
return XModuleCourseFactory._create(class_to_create, **kwargs)
|
||||
|
||||
|
||||
def XMODULE_ITEM_CREATION(class_to_create, **kwargs):
|
||||
return XModuleItemFactory._create(class_to_create, **kwargs)
|
||||
|
||||
|
||||
class XModuleCourseFactory(Factory):
|
||||
@world.absorb
|
||||
class UserFactory(sf.UserFactory):
|
||||
"""
|
||||
Factory for XModule courses.
|
||||
User account for lms / cms
|
||||
"""
|
||||
|
||||
ABSTRACT_FACTORY = True
|
||||
_creation_function = (XMODULE_COURSE_CREATION,)
|
||||
|
||||
@classmethod
|
||||
def _create(cls, target_class, *args, **kwargs):
|
||||
|
||||
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')
|
||||
|
||||
# Write the data to the mongo datastore
|
||||
new_course = store.clone_item(template, location)
|
||||
|
||||
# This metadata code was copied from cms/djangoapps/contentstore/views.py
|
||||
if display_name is not None:
|
||||
new_course.display_name = display_name
|
||||
|
||||
new_course.lms.start = gmtime()
|
||||
new_course.tabs = [{"type": "courseware"},
|
||||
{"type": "course_info", "name": "Course Info"},
|
||||
{"type": "discussion", "name": "Discussion"},
|
||||
{"type": "wiki", "name": "Wiki"},
|
||||
{"type": "progress", "name": "Progress"}]
|
||||
|
||||
# Update the data in the mongo datastore
|
||||
store.update_metadata(new_course.location.url(), own_metadata(new_course))
|
||||
|
||||
return new_course
|
||||
|
||||
|
||||
class Course:
|
||||
pass
|
||||
|
||||
|
||||
class CourseFactory(XModuleCourseFactory):
|
||||
FACTORY_FOR = Course
|
||||
|
||||
template = 'i4x://edx/templates/course/Empty'
|
||||
org = 'MITx'
|
||||
number = '999'
|
||||
display_name = 'Robot Super Course'
|
||||
|
||||
|
||||
class XModuleItemFactory(Factory):
|
||||
@world.absorb
|
||||
class UserProfileFactory(sf.UserProfileFactory):
|
||||
"""
|
||||
Factory for XModule items.
|
||||
Demographics etc for the User
|
||||
"""
|
||||
|
||||
ABSTRACT_FACTORY = True
|
||||
_creation_function = (XMODULE_ITEM_CREATION,)
|
||||
|
||||
@classmethod
|
||||
def _create(cls, target_class, *args, **kwargs):
|
||||
"""
|
||||
Uses *kwargs*:
|
||||
|
||||
*parent_location* (required): the location of the parent module
|
||||
(e.g. the parent course or section)
|
||||
|
||||
*template* (required): the template to create the item from
|
||||
(e.g. i4x://templates/section/Empty)
|
||||
|
||||
*data* (optional): the data for the item
|
||||
(e.g. XML problem definition for a problem item)
|
||||
|
||||
*display_name* (optional): the display name of the item
|
||||
|
||||
*metadata* (optional): dictionary of metadata attributes
|
||||
|
||||
*target_class* is ignored
|
||||
"""
|
||||
|
||||
DETACHED_CATEGORIES = ['about', 'static_tab', 'course_info']
|
||||
|
||||
parent_location = Location(kwargs.get('parent_location'))
|
||||
template = Location(kwargs.get('template'))
|
||||
data = kwargs.get('data')
|
||||
display_name = kwargs.get('display_name')
|
||||
metadata = kwargs.get('metadata', {})
|
||||
|
||||
store = modulestore('direct')
|
||||
|
||||
# This code was based off that in cms/djangoapps/contentstore/views.py
|
||||
parent = store.get_item(parent_location)
|
||||
|
||||
# If a display name is set, use that
|
||||
dest_name = display_name.replace(" ", "_") if display_name is not None else uuid4().hex
|
||||
dest_location = parent_location._replace(category=template.category,
|
||||
name=dest_name)
|
||||
|
||||
new_item = store.clone_item(template, dest_location)
|
||||
|
||||
# replace the display name with an optional parameter passed in from the caller
|
||||
if display_name is not None:
|
||||
new_item.display_name = display_name
|
||||
|
||||
# Add additional metadata or override current metadata
|
||||
item_metadata = own_metadata(new_item)
|
||||
item_metadata.update(metadata)
|
||||
store.update_metadata(new_item.location.url(), item_metadata)
|
||||
|
||||
# replace the data with the optional *data* parameter
|
||||
if data is not None:
|
||||
store.update_item(new_item.location, data)
|
||||
|
||||
if new_item.location.category not in DETACHED_CATEGORIES:
|
||||
store.update_children(parent_location, parent.children + [new_item.location.url()])
|
||||
|
||||
return new_item
|
||||
|
||||
|
||||
class Item:
|
||||
pass
|
||||
|
||||
|
||||
class ItemFactory(XModuleItemFactory):
|
||||
FACTORY_FOR = Item
|
||||
@world.absorb
|
||||
class RegistrationFactory(sf.RegistrationFactory):
|
||||
"""
|
||||
Activation key for registering the user account
|
||||
"""
|
||||
pass
|
||||
|
||||
parent_location = 'i4x://MITx/999/course/Robot_Super_Course'
|
||||
template = 'i4x://edx/templates/chapter/Empty'
|
||||
display_name = 'Section One'
|
||||
|
||||
@world.absorb
|
||||
class GroupFactory(sf.GroupFactory):
|
||||
"""
|
||||
Groups for user permissions for courses
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
@world.absorb
|
||||
class CourseEnrollmentAllowedFactory(sf.CourseEnrollmentAllowed):
|
||||
"""
|
||||
Users allowed to enroll in the course outside of the usual window
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
@world.absorb
|
||||
class CourseFactory(xf.CourseFactory):
|
||||
"""
|
||||
Courseware courses
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
@world.absorb
|
||||
class ItemFactory(xf.ItemFactory):
|
||||
"""
|
||||
Everything included inside a course
|
||||
"""
|
||||
pass
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
from lettuce import world, step
|
||||
from .factories import *
|
||||
from lettuce.django import django_url
|
||||
from django.conf import settings
|
||||
from django.http import HttpRequest
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.auth import authenticate, login
|
||||
from django.contrib.auth.middleware import AuthenticationMiddleware
|
||||
from django.contrib.sessions.middleware import SessionMiddleware
|
||||
from student.models import CourseEnrollment
|
||||
from urllib import quote_plus
|
||||
from nose.tools import assert_equals
|
||||
@@ -9,6 +14,7 @@ from bs4 import BeautifulSoup
|
||||
import time
|
||||
import re
|
||||
import os.path
|
||||
from selenium.common.exceptions import WebDriverException
|
||||
|
||||
from logging import getLogger
|
||||
logger = getLogger(__name__)
|
||||
@@ -24,6 +30,11 @@ def reload_the_page(step):
|
||||
world.browser.reload()
|
||||
|
||||
|
||||
@step('I press the browser back button$')
|
||||
def browser_back(step):
|
||||
world.browser.driver.back()
|
||||
|
||||
|
||||
@step('I (?:visit|access|open) the homepage$')
|
||||
def i_visit_the_homepage(step):
|
||||
world.browser.visit(django_url('/'))
|
||||
@@ -77,7 +88,7 @@ def the_page_title_should_contain(step, title):
|
||||
@step('I am a logged in user$')
|
||||
def i_am_logged_in_user(step):
|
||||
create_user('robot')
|
||||
log_in('robot@edx.org', 'test')
|
||||
log_in('robot', 'test')
|
||||
|
||||
|
||||
@step('I am not logged in$')
|
||||
@@ -92,7 +103,7 @@ def i_am_staff_for_course_by_id(step, course_id):
|
||||
|
||||
@step('I log in$')
|
||||
def i_log_in(step):
|
||||
log_in('robot@edx.org', 'test')
|
||||
log_in('robot', 'test')
|
||||
|
||||
|
||||
@step(u'I am an edX user$')
|
||||
@@ -119,38 +130,46 @@ def create_user(uname):
|
||||
portal_user.set_password('test')
|
||||
portal_user.save()
|
||||
|
||||
registration = RegistrationFactory(user=portal_user)
|
||||
registration = world.RegistrationFactory(user=portal_user)
|
||||
registration.register(portal_user)
|
||||
registration.activate()
|
||||
|
||||
user_profile = UserProfileFactory(user=portal_user)
|
||||
user_profile = world.UserProfileFactory(user=portal_user)
|
||||
|
||||
|
||||
@world.absorb
|
||||
def log_in(email, password):
|
||||
world.browser.cookies.delete()
|
||||
world.browser.visit(django_url('/'))
|
||||
world.browser.is_element_present_by_css('header.global', 10)
|
||||
world.browser.click_link_by_href('#login-modal')
|
||||
def log_in(username, password):
|
||||
'''
|
||||
Log the user in programatically
|
||||
'''
|
||||
|
||||
# Wait for the login dialog to load
|
||||
# This is complicated by the fact that sometimes a second #login_form
|
||||
# dialog loads, while the first one remains hidden.
|
||||
# We give them both time to load, starting with the second one.
|
||||
world.browser.is_element_present_by_css('section.content-wrapper form#login_form', wait_time=4)
|
||||
world.browser.is_element_present_by_css('form#login_form', wait_time=2)
|
||||
# Authenticate the user
|
||||
user = authenticate(username=username, password=password)
|
||||
assert(user is not None and user.is_active)
|
||||
|
||||
# For some reason, the page sometimes includes two #login_form
|
||||
# elements, the first of which is not visible.
|
||||
# To avoid this, we always select the last of the two #login_form dialogs
|
||||
login_form = world.browser.find_by_css('form#login_form').last
|
||||
# Send a fake HttpRequest to log the user in
|
||||
# We need to process the request using
|
||||
# Session middleware and Authentication middleware
|
||||
# to ensure that session state can be stored
|
||||
request = HttpRequest()
|
||||
SessionMiddleware().process_request(request)
|
||||
AuthenticationMiddleware().process_request(request)
|
||||
login(request, user)
|
||||
|
||||
login_form.find_by_name('email').fill(email)
|
||||
login_form.find_by_name('password').fill(password)
|
||||
login_form.find_by_name('submit').click()
|
||||
# Save the session
|
||||
request.session.save()
|
||||
|
||||
# wait for the page to redraw
|
||||
assert world.browser.is_element_present_by_css('.content-wrapper', wait_time=10)
|
||||
# Retrieve the sessionid and add it to the browser's cookies
|
||||
cookie_dict = {settings.SESSION_COOKIE_NAME: request.session.session_key}
|
||||
try:
|
||||
world.browser.cookies.add(cookie_dict)
|
||||
|
||||
# WebDriver has an issue where we cannot set cookies
|
||||
# before we make a GET request, so if we get an error,
|
||||
# we load the '/' page and try again
|
||||
except:
|
||||
world.browser.visit(django_url('/'))
|
||||
world.browser.cookies.add(cookie_dict)
|
||||
|
||||
|
||||
@world.absorb
|
||||
@@ -207,6 +226,7 @@ def save_the_course_content(path='/tmp'):
|
||||
u = world.browser.url
|
||||
section_url = u[u.find('courseware/') + 11:]
|
||||
|
||||
|
||||
if not os.path.exists(path):
|
||||
os.makedirs(path)
|
||||
|
||||
@@ -214,3 +234,15 @@ def save_the_course_content(path='/tmp'):
|
||||
f = open('%s/%s' % (path, filename), 'w')
|
||||
f.write(output)
|
||||
f.close
|
||||
|
||||
@world.absorb
|
||||
def css_click(css_selector):
|
||||
try:
|
||||
world.browser.find_by_css(css_selector).click()
|
||||
|
||||
except WebDriverException:
|
||||
# Occassionally, MathJax or other JavaScript can cover up
|
||||
# an element temporarily.
|
||||
# If this happens, wait a second, then try again
|
||||
time.sleep(1)
|
||||
world.browser.find_by_css(css_selector).click()
|
||||
|
||||
@@ -1,17 +1,25 @@
|
||||
import time
|
||||
import datetime
|
||||
import re
|
||||
import calendar
|
||||
import dateutil.parser
|
||||
|
||||
|
||||
def time_to_date(time_obj):
|
||||
"""
|
||||
Convert a time.time_struct to a true universal time (can pass to js Date constructor)
|
||||
Convert a time.time_struct to a true universal time (can pass to js Date
|
||||
constructor)
|
||||
"""
|
||||
# TODO change to using the isoformat() function on datetime. js date can parse those
|
||||
return calendar.timegm(time_obj) * 1000
|
||||
|
||||
|
||||
def time_to_isodate(source):
|
||||
'''Convert to an iso date'''
|
||||
if isinstance(source, time.struct_time):
|
||||
return time.strftime('%Y-%m-%dT%H:%M:%SZ', source)
|
||||
elif isinstance(source, datetime):
|
||||
return source.isoformat() + 'Z'
|
||||
|
||||
|
||||
def jsdate_to_time(field):
|
||||
"""
|
||||
Convert a universal time (iso format) or msec since epoch to a time obj
|
||||
@@ -19,8 +27,7 @@ def jsdate_to_time(field):
|
||||
if field is None:
|
||||
return field
|
||||
elif isinstance(field, basestring):
|
||||
# ISO format but ignores time zone assuming it's Z.
|
||||
d = datetime.datetime(*map(int, re.split('[^\d]', field)[:6])) # stop after seconds. Debatable
|
||||
d = dateutil.parser.parse(field)
|
||||
return d.utctimetuple()
|
||||
elif isinstance(field, (int, long, float)):
|
||||
return time.gmtime(field / 1000)
|
||||
|
||||
@@ -17,6 +17,7 @@ from capa.correctmap import CorrectMap
|
||||
from capa.util import convert_files_to_filenames
|
||||
from capa.xqueue_interface import dateformat
|
||||
|
||||
|
||||
class ResponseTest(unittest.TestCase):
|
||||
""" Base class for tests of capa responses."""
|
||||
|
||||
@@ -39,12 +40,13 @@ class ResponseTest(unittest.TestCase):
|
||||
for input_str in correct_answers:
|
||||
result = problem.grade_answers({'1_2_1': input_str}).get_correctness('1_2_1')
|
||||
self.assertEqual(result, 'correct',
|
||||
msg="%s should be marked correct" % str(input_str))
|
||||
msg="%s should be marked correct" % str(input_str))
|
||||
|
||||
for input_str in incorrect_answers:
|
||||
result = problem.grade_answers({'1_2_1': input_str}).get_correctness('1_2_1')
|
||||
self.assertEqual(result, 'incorrect',
|
||||
msg="%s should be marked incorrect" % str(input_str))
|
||||
msg="%s should be marked incorrect" % str(input_str))
|
||||
|
||||
|
||||
class MultiChoiceResponseTest(ResponseTest):
|
||||
from response_xml_factory import MultipleChoiceResponseXMLFactory
|
||||
@@ -60,7 +62,7 @@ class MultiChoiceResponseTest(ResponseTest):
|
||||
|
||||
def test_named_multiple_choice_grade(self):
|
||||
problem = self.build_problem(choices=[False, True, False],
|
||||
choice_names=["foil_1", "foil_2", "foil_3"])
|
||||
choice_names=["foil_1", "foil_2", "foil_3"])
|
||||
|
||||
# Ensure that we get the expected grades
|
||||
self.assert_grade(problem, 'choice_foil_1', 'incorrect')
|
||||
@@ -91,7 +93,7 @@ class TrueFalseResponseTest(ResponseTest):
|
||||
|
||||
def test_named_true_false_grade(self):
|
||||
problem = self.build_problem(choices=[False, True, True],
|
||||
choice_names=['foil_1','foil_2','foil_3'])
|
||||
choice_names=['foil_1', 'foil_2', 'foil_3'])
|
||||
|
||||
# Check the results
|
||||
# Mark correct if and only if ALL (and only) correct chocies selected
|
||||
@@ -107,6 +109,7 @@ class TrueFalseResponseTest(ResponseTest):
|
||||
self.assert_grade(problem, 'choice_foil_4', 'incorrect')
|
||||
self.assert_grade(problem, 'not_a_choice', 'incorrect')
|
||||
|
||||
|
||||
class ImageResponseTest(ResponseTest):
|
||||
from response_xml_factory import ImageResponseXMLFactory
|
||||
xml_factory_class = ImageResponseXMLFactory
|
||||
@@ -118,7 +121,7 @@ class ImageResponseTest(ResponseTest):
|
||||
# Anything inside the rectangle (and along the borders) is correct
|
||||
# Everything else is incorrect
|
||||
correct_inputs = ["[12,19]", "[10,10]", "[20,20]",
|
||||
"[10,15]", "[20,15]", "[15,10]", "[15,20]"]
|
||||
"[10,15]", "[20,15]", "[15,10]", "[15,20]"]
|
||||
incorrect_inputs = ["[4,6]", "[25,15]", "[15,40]", "[15,4]"]
|
||||
self.assert_multiple_grade(problem, correct_inputs, incorrect_inputs)
|
||||
|
||||
@@ -145,7 +148,7 @@ class ImageResponseTest(ResponseTest):
|
||||
|
||||
def test_multiple_regions_grade(self):
|
||||
# Define multiple regions that the user can select
|
||||
region_str="[[[10,10], [20,10], [20, 30]], [[100,100], [120,100], [120,150]]]"
|
||||
region_str = "[[[10,10], [20,10], [20, 30]], [[100,100], [120,100], [120,150]]]"
|
||||
|
||||
# Expect that only points inside the regions are marked correct
|
||||
problem = self.build_problem(regions=region_str)
|
||||
@@ -155,7 +158,7 @@ class ImageResponseTest(ResponseTest):
|
||||
|
||||
def test_region_and_rectangle_grade(self):
|
||||
rectangle_str = "(100,100)-(200,200)"
|
||||
region_str="[[10,10], [20,10], [20, 30]]"
|
||||
region_str = "[[10,10], [20,10], [20, 30]]"
|
||||
|
||||
# Expect that only points inside the rectangle or region are marked correct
|
||||
problem = self.build_problem(regions=region_str, rectangle=rectangle_str)
|
||||
@@ -171,85 +174,85 @@ class SymbolicResponseTest(unittest.TestCase):
|
||||
test_lcp = lcp.LoncapaProblem(open(symbolicresponse_file).read(), '1', system=test_system)
|
||||
correct_answers = {'1_2_1': 'cos(theta)*[[1,0],[0,1]] + i*sin(theta)*[[0,1],[1,0]]',
|
||||
'1_2_1_dynamath': '''
|
||||
<math xmlns="http://www.w3.org/1998/Math/MathML">
|
||||
<mstyle displaystyle="true">
|
||||
<mrow>
|
||||
<mi>cos</mi>
|
||||
<mrow>
|
||||
<mo>(</mo>
|
||||
<mi>θ</mi>
|
||||
<mo>)</mo>
|
||||
</mrow>
|
||||
</mrow>
|
||||
<mo>⋅</mo>
|
||||
<mrow>
|
||||
<mo>[</mo>
|
||||
<mtable>
|
||||
<mtr>
|
||||
<mtd>
|
||||
<mn>1</mn>
|
||||
</mtd>
|
||||
<mtd>
|
||||
<mn>0</mn>
|
||||
</mtd>
|
||||
</mtr>
|
||||
<mtr>
|
||||
<mtd>
|
||||
<mn>0</mn>
|
||||
</mtd>
|
||||
<mtd>
|
||||
<mn>1</mn>
|
||||
</mtd>
|
||||
</mtr>
|
||||
</mtable>
|
||||
<mo>]</mo>
|
||||
</mrow>
|
||||
<mo>+</mo>
|
||||
<mi>i</mi>
|
||||
<mo>⋅</mo>
|
||||
<mrow>
|
||||
<mi>sin</mi>
|
||||
<mrow>
|
||||
<mo>(</mo>
|
||||
<mi>θ</mi>
|
||||
<mo>)</mo>
|
||||
</mrow>
|
||||
</mrow>
|
||||
<mo>⋅</mo>
|
||||
<mrow>
|
||||
<mo>[</mo>
|
||||
<mtable>
|
||||
<mtr>
|
||||
<mtd>
|
||||
<mn>0</mn>
|
||||
</mtd>
|
||||
<mtd>
|
||||
<mn>1</mn>
|
||||
</mtd>
|
||||
</mtr>
|
||||
<mtr>
|
||||
<mtd>
|
||||
<mn>1</mn>
|
||||
</mtd>
|
||||
<mtd>
|
||||
<mn>0</mn>
|
||||
</mtd>
|
||||
</mtr>
|
||||
</mtable>
|
||||
<mo>]</mo>
|
||||
</mrow>
|
||||
</mstyle>
|
||||
</math>
|
||||
''',
|
||||
<math xmlns="http://www.w3.org/1998/Math/MathML">
|
||||
<mstyle displaystyle="true">
|
||||
<mrow>
|
||||
<mi>cos</mi>
|
||||
<mrow>
|
||||
<mo>(</mo>
|
||||
<mi>θ</mi>
|
||||
<mo>)</mo>
|
||||
</mrow>
|
||||
</mrow>
|
||||
<mo>⋅</mo>
|
||||
<mrow>
|
||||
<mo>[</mo>
|
||||
<mtable>
|
||||
<mtr>
|
||||
<mtd>
|
||||
<mn>1</mn>
|
||||
</mtd>
|
||||
<mtd>
|
||||
<mn>0</mn>
|
||||
</mtd>
|
||||
</mtr>
|
||||
<mtr>
|
||||
<mtd>
|
||||
<mn>0</mn>
|
||||
</mtd>
|
||||
<mtd>
|
||||
<mn>1</mn>
|
||||
</mtd>
|
||||
</mtr>
|
||||
</mtable>
|
||||
<mo>]</mo>
|
||||
</mrow>
|
||||
<mo>+</mo>
|
||||
<mi>i</mi>
|
||||
<mo>⋅</mo>
|
||||
<mrow>
|
||||
<mi>sin</mi>
|
||||
<mrow>
|
||||
<mo>(</mo>
|
||||
<mi>θ</mi>
|
||||
<mo>)</mo>
|
||||
</mrow>
|
||||
</mrow>
|
||||
<mo>⋅</mo>
|
||||
<mrow>
|
||||
<mo>[</mo>
|
||||
<mtable>
|
||||
<mtr>
|
||||
<mtd>
|
||||
<mn>0</mn>
|
||||
</mtd>
|
||||
<mtd>
|
||||
<mn>1</mn>
|
||||
</mtd>
|
||||
</mtr>
|
||||
<mtr>
|
||||
<mtd>
|
||||
<mn>1</mn>
|
||||
</mtd>
|
||||
<mtd>
|
||||
<mn>0</mn>
|
||||
</mtd>
|
||||
</mtr>
|
||||
</mtable>
|
||||
<mo>]</mo>
|
||||
</mrow>
|
||||
</mstyle>
|
||||
</math>
|
||||
''',
|
||||
}
|
||||
wrong_answers = {'1_2_1': '2',
|
||||
'1_2_1_dynamath': '''
|
||||
<math xmlns="http://www.w3.org/1998/Math/MathML">
|
||||
<mstyle displaystyle="true">
|
||||
<mn>2</mn>
|
||||
</mstyle>
|
||||
</math>''',
|
||||
}
|
||||
<mstyle displaystyle="true">
|
||||
<mn>2</mn>
|
||||
</mstyle>
|
||||
</math>''',
|
||||
}
|
||||
self.assertEquals(test_lcp.grade_answers(correct_answers).get_correctness('1_2_1'), 'correct')
|
||||
self.assertEquals(test_lcp.grade_answers(wrong_answers).get_correctness('1_2_1'), 'incorrect')
|
||||
|
||||
@@ -260,7 +263,7 @@ class OptionResponseTest(ResponseTest):
|
||||
|
||||
def test_grade(self):
|
||||
problem = self.build_problem(options=["first", "second", "third"],
|
||||
correct_option="second")
|
||||
correct_option="second")
|
||||
|
||||
# Assert that we get the expected grades
|
||||
self.assert_grade(problem, "first", "incorrect")
|
||||
@@ -281,9 +284,9 @@ class FormulaResponseTest(ResponseTest):
|
||||
|
||||
# The expected solution is numerically equivalent to x+2y
|
||||
problem = self.build_problem(sample_dict=sample_dict,
|
||||
num_samples=10,
|
||||
tolerance=0.01,
|
||||
answer="x+2*y")
|
||||
num_samples=10,
|
||||
tolerance=0.01,
|
||||
answer="x+2*y")
|
||||
|
||||
# Expect an equivalent formula to be marked correct
|
||||
# 2x - x + y + y = x + 2y
|
||||
@@ -297,33 +300,31 @@ class FormulaResponseTest(ResponseTest):
|
||||
|
||||
def test_hint(self):
|
||||
# Sample variables x and y in the range [-10, 10]
|
||||
sample_dict = {'x': (-10, 10), 'y': (-10,10) }
|
||||
sample_dict = {'x': (-10, 10), 'y': (-10, 10)}
|
||||
|
||||
# Give a hint if the user leaves off the coefficient
|
||||
# or leaves out x
|
||||
hints = [('x + 3*y', 'y_coefficient', 'Check the coefficient of y'),
|
||||
('2*y', 'missing_x', 'Try including the variable x')]
|
||||
|
||||
('2*y', 'missing_x', 'Try including the variable x')]
|
||||
|
||||
# The expected solution is numerically equivalent to x+2y
|
||||
problem = self.build_problem(sample_dict=sample_dict,
|
||||
num_samples=10,
|
||||
tolerance=0.01,
|
||||
answer="x+2*y",
|
||||
hints=hints)
|
||||
num_samples=10,
|
||||
tolerance=0.01,
|
||||
answer="x+2*y",
|
||||
hints=hints)
|
||||
|
||||
# Expect to receive a hint if we add an extra y
|
||||
input_dict = {'1_2_1': "x + 2*y + y"}
|
||||
correct_map = problem.grade_answers(input_dict)
|
||||
self.assertEquals(correct_map.get_hint('1_2_1'),
|
||||
'Check the coefficient of y')
|
||||
'Check the coefficient of y')
|
||||
|
||||
# Expect to receive a hint if we leave out x
|
||||
input_dict = {'1_2_1': "2*y"}
|
||||
correct_map = problem.grade_answers(input_dict)
|
||||
self.assertEquals(correct_map.get_hint('1_2_1'),
|
||||
'Try including the variable x')
|
||||
|
||||
'Try including the variable x')
|
||||
|
||||
def test_script(self):
|
||||
# Calculate the answer using a script
|
||||
@@ -334,10 +335,10 @@ class FormulaResponseTest(ResponseTest):
|
||||
|
||||
# The expected solution is numerically equivalent to 2*x
|
||||
problem = self.build_problem(sample_dict=sample_dict,
|
||||
num_samples=10,
|
||||
tolerance=0.01,
|
||||
answer="$calculated_ans",
|
||||
script=script)
|
||||
num_samples=10,
|
||||
tolerance=0.01,
|
||||
answer="$calculated_ans",
|
||||
script=script)
|
||||
|
||||
# Expect that the inputs are graded correctly
|
||||
self.assert_grade(problem, '2*x', 'correct')
|
||||
@@ -348,7 +349,6 @@ class StringResponseTest(ResponseTest):
|
||||
from response_xml_factory import StringResponseXMLFactory
|
||||
xml_factory_class = StringResponseXMLFactory
|
||||
|
||||
|
||||
def test_case_sensitive(self):
|
||||
problem = self.build_problem(answer="Second", case_sensitive=True)
|
||||
|
||||
@@ -372,23 +372,23 @@ class StringResponseTest(ResponseTest):
|
||||
|
||||
def test_hints(self):
|
||||
hints = [("wisconsin", "wisc", "The state capital of Wisconsin is Madison"),
|
||||
("minnesota", "minn", "The state capital of Minnesota is St. Paul")]
|
||||
("minnesota", "minn", "The state capital of Minnesota is St. Paul")]
|
||||
|
||||
problem = self.build_problem(answer="Michigan",
|
||||
case_sensitive=False,
|
||||
hints=hints)
|
||||
case_sensitive=False,
|
||||
hints=hints)
|
||||
|
||||
# We should get a hint for Wisconsin
|
||||
input_dict = {'1_2_1': 'Wisconsin'}
|
||||
correct_map = problem.grade_answers(input_dict)
|
||||
self.assertEquals(correct_map.get_hint('1_2_1'),
|
||||
"The state capital of Wisconsin is Madison")
|
||||
"The state capital of Wisconsin is Madison")
|
||||
|
||||
# We should get a hint for Minnesota
|
||||
input_dict = {'1_2_1': 'Minnesota'}
|
||||
correct_map = problem.grade_answers(input_dict)
|
||||
self.assertEquals(correct_map.get_hint('1_2_1'),
|
||||
"The state capital of Minnesota is St. Paul")
|
||||
"The state capital of Minnesota is St. Paul")
|
||||
|
||||
# We should NOT get a hint for Michigan (the correct answer)
|
||||
input_dict = {'1_2_1': 'Michigan'}
|
||||
@@ -400,6 +400,7 @@ class StringResponseTest(ResponseTest):
|
||||
correct_map = problem.grade_answers(input_dict)
|
||||
self.assertEquals(correct_map.get_hint('1_2_1'), "")
|
||||
|
||||
|
||||
class CodeResponseTest(ResponseTest):
|
||||
from response_xml_factory import CodeResponseXMLFactory
|
||||
xml_factory_class = CodeResponseXMLFactory
|
||||
@@ -409,9 +410,9 @@ class CodeResponseTest(ResponseTest):
|
||||
|
||||
grader_payload = json.dumps({"grader": "ps04/grade_square.py"})
|
||||
self.problem = self.build_problem(initial_display="def square(x):",
|
||||
answer_display="answer",
|
||||
grader_payload=grader_payload,
|
||||
num_responses=2)
|
||||
answer_display="answer",
|
||||
grader_payload=grader_payload,
|
||||
num_responses=2)
|
||||
|
||||
@staticmethod
|
||||
def make_queuestate(key, time):
|
||||
@@ -442,7 +443,6 @@ class CodeResponseTest(ResponseTest):
|
||||
|
||||
self.assertEquals(self.problem.is_queued(), True)
|
||||
|
||||
|
||||
def test_update_score(self):
|
||||
'''
|
||||
Test whether LoncapaProblem.update_score can deliver queued result to the right subproblem
|
||||
@@ -495,7 +495,6 @@ class CodeResponseTest(ResponseTest):
|
||||
else:
|
||||
self.assertTrue(self.problem.correct_map.is_queued(test_id)) # Should be queued, message undelivered
|
||||
|
||||
|
||||
def test_recentmost_queuetime(self):
|
||||
'''
|
||||
Test whether the LoncapaProblem knows about the time of queue requests
|
||||
@@ -538,13 +537,14 @@ class CodeResponseTest(ResponseTest):
|
||||
self.assertEquals(answers_converted['1_3_1'], ['answer1', 'answer2', 'answer3'])
|
||||
self.assertEquals(answers_converted['1_4_1'], [fp.name, fp.name])
|
||||
|
||||
|
||||
class ChoiceResponseTest(ResponseTest):
|
||||
from response_xml_factory import ChoiceResponseXMLFactory
|
||||
xml_factory_class = ChoiceResponseXMLFactory
|
||||
|
||||
def test_radio_group_grade(self):
|
||||
problem = self.build_problem(choice_type='radio',
|
||||
choices=[False, True, False])
|
||||
choices=[False, True, False])
|
||||
|
||||
# Check that we get the expected results
|
||||
self.assert_grade(problem, 'choice_0', 'incorrect')
|
||||
@@ -554,10 +554,9 @@ class ChoiceResponseTest(ResponseTest):
|
||||
# No choice 3 exists --> mark incorrect
|
||||
self.assert_grade(problem, 'choice_3', 'incorrect')
|
||||
|
||||
|
||||
def test_checkbox_group_grade(self):
|
||||
problem = self.build_problem(choice_type='checkbox',
|
||||
choices=[False, True, True])
|
||||
choices=[False, True, True])
|
||||
|
||||
# Check that we get the expected results
|
||||
# (correct if and only if BOTH correct choices chosen)
|
||||
@@ -581,14 +580,15 @@ class JavascriptResponseTest(ResponseTest):
|
||||
os.system("coffee -c %s" % (coffee_file_path))
|
||||
|
||||
problem = self.build_problem(generator_src="test_problem_generator.js",
|
||||
grader_src="test_problem_grader.js",
|
||||
display_class="TestProblemDisplay",
|
||||
display_src="test_problem_display.js",
|
||||
param_dict={'value': '4'})
|
||||
grader_src="test_problem_grader.js",
|
||||
display_class="TestProblemDisplay",
|
||||
display_src="test_problem_display.js",
|
||||
param_dict={'value': '4'})
|
||||
|
||||
# Test that we get graded correctly
|
||||
self.assert_grade(problem, json.dumps({0:4}), "correct")
|
||||
self.assert_grade(problem, json.dumps({0:5}), "incorrect")
|
||||
self.assert_grade(problem, json.dumps({0: 4}), "correct")
|
||||
self.assert_grade(problem, json.dumps({0: 5}), "incorrect")
|
||||
|
||||
|
||||
class NumericalResponseTest(ResponseTest):
|
||||
from response_xml_factory import NumericalResponseXMLFactory
|
||||
@@ -596,27 +596,26 @@ class NumericalResponseTest(ResponseTest):
|
||||
|
||||
def test_grade_exact(self):
|
||||
problem = self.build_problem(question_text="What is 2 + 2?",
|
||||
explanation="The answer is 4",
|
||||
answer=4)
|
||||
explanation="The answer is 4",
|
||||
answer=4)
|
||||
correct_responses = ["4", "4.0", "4.00"]
|
||||
incorrect_responses = ["", "3.9", "4.1", "0"]
|
||||
self.assert_multiple_grade(problem, correct_responses, incorrect_responses)
|
||||
|
||||
|
||||
def test_grade_decimal_tolerance(self):
|
||||
problem = self.build_problem(question_text="What is 2 + 2 approximately?",
|
||||
explanation="The answer is 4",
|
||||
answer=4,
|
||||
tolerance=0.1)
|
||||
explanation="The answer is 4",
|
||||
answer=4,
|
||||
tolerance=0.1)
|
||||
correct_responses = ["4.0", "4.00", "4.09", "3.91"]
|
||||
incorrect_responses = ["", "4.11", "3.89", "0"]
|
||||
self.assert_multiple_grade(problem, correct_responses, incorrect_responses)
|
||||
|
||||
def test_grade_percent_tolerance(self):
|
||||
problem = self.build_problem(question_text="What is 2 + 2 approximately?",
|
||||
explanation="The answer is 4",
|
||||
answer=4,
|
||||
tolerance="10%")
|
||||
explanation="The answer is 4",
|
||||
answer=4,
|
||||
tolerance="10%")
|
||||
correct_responses = ["4.0", "4.3", "3.7", "4.30", "3.70"]
|
||||
incorrect_responses = ["", "4.5", "3.5", "0"]
|
||||
self.assert_multiple_grade(problem, correct_responses, incorrect_responses)
|
||||
@@ -624,9 +623,9 @@ class NumericalResponseTest(ResponseTest):
|
||||
def test_grade_with_script(self):
|
||||
script_text = "computed_response = math.sqrt(4)"
|
||||
problem = self.build_problem(question_text="What is sqrt(4)?",
|
||||
explanation="The answer is 2",
|
||||
answer="$computed_response",
|
||||
script=script_text)
|
||||
explanation="The answer is 2",
|
||||
answer="$computed_response",
|
||||
script=script_text)
|
||||
correct_responses = ["2", "2.0"]
|
||||
incorrect_responses = ["", "2.01", "1.99", "0"]
|
||||
self.assert_multiple_grade(problem, correct_responses, incorrect_responses)
|
||||
@@ -634,10 +633,10 @@ class NumericalResponseTest(ResponseTest):
|
||||
def test_grade_with_script_and_tolerance(self):
|
||||
script_text = "computed_response = math.sqrt(4)"
|
||||
problem = self.build_problem(question_text="What is sqrt(4)?",
|
||||
explanation="The answer is 2",
|
||||
answer="$computed_response",
|
||||
tolerance="0.1",
|
||||
script=script_text)
|
||||
explanation="The answer is 2",
|
||||
answer="$computed_response",
|
||||
tolerance="0.1",
|
||||
script=script_text)
|
||||
correct_responses = ["2", "2.0", "2.05", "1.95"]
|
||||
incorrect_responses = ["", "2.11", "1.89", "0"]
|
||||
self.assert_multiple_grade(problem, correct_responses, incorrect_responses)
|
||||
@@ -651,7 +650,6 @@ class NumericalResponseTest(ResponseTest):
|
||||
self.assert_multiple_grade(problem, correct_responses, incorrect_responses)
|
||||
|
||||
|
||||
|
||||
class CustomResponseTest(ResponseTest):
|
||||
from response_xml_factory import CustomResponseXMLFactory
|
||||
xml_factory_class = CustomResponseXMLFactory
|
||||
@@ -692,7 +690,6 @@ class CustomResponseTest(ResponseTest):
|
||||
overall_msg = correctmap.get_overall_message()
|
||||
self.assertEqual(overall_msg, "Overall message")
|
||||
|
||||
|
||||
def test_function_code_single_input(self):
|
||||
|
||||
# For function code, we pass in these arguments:
|
||||
@@ -746,7 +743,7 @@ class CustomResponseTest(ResponseTest):
|
||||
""")
|
||||
|
||||
problem = self.build_problem(script=script, cfn="check_func",
|
||||
expect="42", num_inputs=2)
|
||||
expect="42", num_inputs=2)
|
||||
|
||||
# Correct answer -- expect both inputs marked correct
|
||||
input_dict = {'1_2_1': '42', '1_2_2': '42'}
|
||||
@@ -768,7 +765,6 @@ class CustomResponseTest(ResponseTest):
|
||||
correctness = correct_map.get_correctness('1_2_2')
|
||||
self.assertEqual(correctness, 'incorrect')
|
||||
|
||||
|
||||
def test_function_code_multiple_inputs(self):
|
||||
|
||||
# If the <customresponse> has multiple inputs associated with it,
|
||||
@@ -794,10 +790,10 @@ class CustomResponseTest(ResponseTest):
|
||||
""")
|
||||
|
||||
problem = self.build_problem(script=script,
|
||||
cfn="check_func", num_inputs=3)
|
||||
cfn="check_func", num_inputs=3)
|
||||
|
||||
# Grade the inputs (one input incorrect)
|
||||
input_dict = {'1_2_1': '-999', '1_2_2': '2', '1_2_3': '3' }
|
||||
input_dict = {'1_2_1': '-999', '1_2_2': '2', '1_2_3': '3'}
|
||||
correct_map = problem.grade_answers(input_dict)
|
||||
|
||||
# Expect that we receive the overall message (for the whole response)
|
||||
@@ -813,7 +809,6 @@ class CustomResponseTest(ResponseTest):
|
||||
self.assertEqual(correct_map.get_msg('1_2_2'), 'Feedback 2')
|
||||
self.assertEqual(correct_map.get_msg('1_2_3'), 'Feedback 3')
|
||||
|
||||
|
||||
def test_multiple_inputs_return_one_status(self):
|
||||
# When given multiple inputs, the 'answer_given' argument
|
||||
# to the check_func() is a list of inputs
|
||||
@@ -835,10 +830,10 @@ class CustomResponseTest(ResponseTest):
|
||||
""")
|
||||
|
||||
problem = self.build_problem(script=script,
|
||||
cfn="check_func", num_inputs=3)
|
||||
cfn="check_func", num_inputs=3)
|
||||
|
||||
# Grade the inputs (one input incorrect)
|
||||
input_dict = {'1_2_1': '-999', '1_2_2': '2', '1_2_3': '3' }
|
||||
input_dict = {'1_2_1': '-999', '1_2_2': '2', '1_2_3': '3'}
|
||||
correct_map = problem.grade_answers(input_dict)
|
||||
|
||||
# Everything marked incorrect
|
||||
@@ -847,7 +842,7 @@ class CustomResponseTest(ResponseTest):
|
||||
self.assertEqual(correct_map.get_correctness('1_2_3'), 'incorrect')
|
||||
|
||||
# Grade the inputs (everything correct)
|
||||
input_dict = {'1_2_1': '1', '1_2_2': '2', '1_2_3': '3' }
|
||||
input_dict = {'1_2_1': '1', '1_2_2': '2', '1_2_3': '3'}
|
||||
correct_map = problem.grade_answers(input_dict)
|
||||
|
||||
# Everything marked incorrect
|
||||
@@ -902,13 +897,13 @@ class SchematicResponseTest(ResponseTest):
|
||||
# To test that the context is set up correctly,
|
||||
# we create a script that sets *correct* to true
|
||||
# if and only if we find the *submission* (list)
|
||||
script="correct = ['correct' if 'test' in submission[0] else 'incorrect']"
|
||||
script = "correct = ['correct' if 'test' in submission[0] else 'incorrect']"
|
||||
problem = self.build_problem(answer=script)
|
||||
|
||||
# The actual dictionary would contain schematic information
|
||||
# sent from the JavaScript simulation
|
||||
submission_dict = {'test': 'test'}
|
||||
input_dict = { '1_2_1': json.dumps(submission_dict) }
|
||||
input_dict = {'1_2_1': json.dumps(submission_dict)}
|
||||
correct_map = problem.grade_answers(input_dict)
|
||||
|
||||
# Expect that the problem is graded as true
|
||||
@@ -916,6 +911,7 @@ class SchematicResponseTest(ResponseTest):
|
||||
# is what we expect)
|
||||
self.assertEqual(correct_map.get_correctness('1_2_1'), 'correct')
|
||||
|
||||
|
||||
class AnnotationResponseTest(ResponseTest):
|
||||
from response_xml_factory import AnnotationResponseXMLFactory
|
||||
xml_factory_class = AnnotationResponseXMLFactory
|
||||
@@ -924,18 +920,18 @@ class AnnotationResponseTest(ResponseTest):
|
||||
(correct, partially, incorrect) = ('correct', 'partially-correct', 'incorrect')
|
||||
|
||||
answer_id = '1_2_1'
|
||||
options = (('x', correct),('y', partially),('z', incorrect))
|
||||
make_answer = lambda option_ids: {answer_id: json.dumps({'options': option_ids })}
|
||||
options = (('x', correct), ('y', partially), ('z', incorrect))
|
||||
make_answer = lambda option_ids: {answer_id: json.dumps({'options': option_ids})}
|
||||
|
||||
tests = [
|
||||
{'correctness': correct, 'points': 2,'answers': make_answer([0]) },
|
||||
{'correctness': partially, 'points': 1, 'answers': make_answer([1]) },
|
||||
{'correctness': incorrect, 'points': 0, 'answers': make_answer([2]) },
|
||||
{'correctness': incorrect, 'points': 0, 'answers': make_answer([0,1,2]) },
|
||||
{'correctness': incorrect, 'points': 0, 'answers': make_answer([]) },
|
||||
{'correctness': incorrect, 'points': 0, 'answers': make_answer('') },
|
||||
{'correctness': incorrect, 'points': 0, 'answers': make_answer(None) },
|
||||
{'correctness': incorrect, 'points': 0, 'answers': {answer_id: 'null' } },
|
||||
{'correctness': correct, 'points': 2, 'answers': make_answer([0])},
|
||||
{'correctness': partially, 'points': 1, 'answers': make_answer([1])},
|
||||
{'correctness': incorrect, 'points': 0, 'answers': make_answer([2])},
|
||||
{'correctness': incorrect, 'points': 0, 'answers': make_answer([0, 1, 2])},
|
||||
{'correctness': incorrect, 'points': 0, 'answers': make_answer([])},
|
||||
{'correctness': incorrect, 'points': 0, 'answers': make_answer('')},
|
||||
{'correctness': incorrect, 'points': 0, 'answers': make_answer(None)},
|
||||
{'correctness': incorrect, 'points': 0, 'answers': {answer_id: 'null'}},
|
||||
]
|
||||
|
||||
for (index, test) in enumerate(tests):
|
||||
|
||||
@@ -40,8 +40,21 @@ class ConditionalModule(ConditionalFields, XModule):
|
||||
poll_answer - map to `poll_answer` module attribute
|
||||
voted - map to `voted` module attribute
|
||||
|
||||
<conditional> tag attributes:
|
||||
sources - location id of modules, separated by ';'
|
||||
<show> tag attributes:
|
||||
sources - location id of required modules, separated by ';'
|
||||
|
||||
You can add you own rules for <conditional> tag, like
|
||||
"completed", "attempted" etc. To do that yo must extend
|
||||
`ConditionalModule.conditions_map` variable and add pair:
|
||||
my_attr: my_property/my_method
|
||||
|
||||
After that you can use it:
|
||||
<conditional my_attr="some value" ...>
|
||||
...
|
||||
</conditional>
|
||||
|
||||
And my_property/my_method will be called for required modules.
|
||||
|
||||
"""
|
||||
|
||||
js = {'coffee': [resource_string(__name__, 'js/src/javascript_loader.coffee'),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import logging
|
||||
from cStringIO import StringIO
|
||||
from math import exp, erf
|
||||
from math import exp
|
||||
from lxml import etree
|
||||
from path import path # NOTE (THK): Only used for detecting presence of syllabus
|
||||
import requests
|
||||
@@ -12,14 +12,9 @@ from xmodule.seq_module import SequenceDescriptor, SequenceModule
|
||||
from xmodule.timeparse import parse_time
|
||||
from xmodule.util.decorators import lazyproperty
|
||||
from xmodule.graders import grader_from_conf
|
||||
from datetime import datetime
|
||||
import json
|
||||
import logging
|
||||
import requests
|
||||
import time
|
||||
import copy
|
||||
|
||||
from xblock.core import Scope, ModelType, List, String, Object, Boolean
|
||||
from xblock.core import Scope, List, String, Object, Boolean
|
||||
from .fields import Date
|
||||
|
||||
|
||||
@@ -29,30 +24,30 @@ log = logging.getLogger(__name__)
|
||||
class StringOrDate(Date):
|
||||
def from_json(self, value):
|
||||
"""
|
||||
Parse an optional metadata key containing a time: if present, complain
|
||||
if it doesn't parse.
|
||||
Return None if not present or invalid.
|
||||
Parse an optional metadata key containing a time or a string:
|
||||
if present, assume it's a string if it doesn't parse.
|
||||
"""
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
return time.strptime(value, self.time_format)
|
||||
result = super(StringOrDate, self).from_json(value)
|
||||
except ValueError:
|
||||
return value
|
||||
if result is None:
|
||||
return value
|
||||
else:
|
||||
return result
|
||||
|
||||
def to_json(self, value):
|
||||
"""
|
||||
Convert a time struct to a string
|
||||
Convert a time struct or string to a string.
|
||||
"""
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
return time.strftime(self.time_format, value)
|
||||
except (ValueError, TypeError):
|
||||
result = super(StringOrDate, self).to_json(value)
|
||||
except:
|
||||
return value
|
||||
|
||||
if result is None:
|
||||
return value
|
||||
else:
|
||||
return result
|
||||
|
||||
|
||||
edx_xml_parser = etree.XMLParser(dtd_validation=False, load_dtd=False,
|
||||
@@ -60,6 +55,7 @@ edx_xml_parser = etree.XMLParser(dtd_validation=False, load_dtd=False,
|
||||
|
||||
_cached_toc = {}
|
||||
|
||||
|
||||
class Textbook(object):
|
||||
def __init__(self, title, book_url):
|
||||
self.title = title
|
||||
@@ -179,7 +175,7 @@ class CourseFields(object):
|
||||
allow_anonymous_to_peers = Boolean(scope=Scope.settings, default=False)
|
||||
advanced_modules = List(help="Beta modules used in your course", scope=Scope.settings)
|
||||
has_children = True
|
||||
|
||||
checklists = List(scope=Scope.settings)
|
||||
info_sidebar_name = String(scope=Scope.settings, default='Course Handouts')
|
||||
|
||||
# An extra property is used rather than the wiki_slug/number because
|
||||
@@ -367,7 +363,7 @@ class CourseDescriptor(CourseFields, SequenceDescriptor):
|
||||
textbooks.append((textbook.get('title'), textbook.get('book_url')))
|
||||
xml_object.remove(textbook)
|
||||
|
||||
#Load the wiki tag if it exists
|
||||
# Load the wiki tag if it exists
|
||||
wiki_slug = None
|
||||
wiki_tag = xml_object.find("wiki")
|
||||
if wiki_tag is not None:
|
||||
@@ -675,7 +671,7 @@ class CourseDescriptor(CourseFields, SequenceDescriptor):
|
||||
# *end* of the same day, not the same time. It's going to be used as the
|
||||
# end of the exam overall, so we don't want the exam to disappear too soon.
|
||||
# It's also used optionally as the registration end date, so time matters there too.
|
||||
self.last_eligible_appointment_date = self._try_parse_time('Last_Eligible_Appointment_Date') # or self.first_eligible_appointment_date
|
||||
self.last_eligible_appointment_date = self._try_parse_time('Last_Eligible_Appointment_Date') # or self.first_eligible_appointment_date
|
||||
if self.last_eligible_appointment_date is None:
|
||||
raise ValueError("Last appointment date must be specified")
|
||||
self.registration_start_date = self._try_parse_time('Registration_Start_Date') or time.gmtime(0)
|
||||
|
||||
@@ -4,27 +4,35 @@ import re
|
||||
|
||||
from datetime import timedelta
|
||||
from xblock.core import ModelType
|
||||
import datetime
|
||||
import dateutil.parser
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Date(ModelType):
|
||||
time_format = "%Y-%m-%dT%H:%M"
|
||||
|
||||
def from_json(self, value):
|
||||
'''
|
||||
Date fields know how to parse and produce json (iso) compatible formats.
|
||||
'''
|
||||
# NB: these are copies of util.converters.*
|
||||
def from_json(self, field):
|
||||
"""
|
||||
Parse an optional metadata key containing a time: if present, complain
|
||||
if it doesn't parse.
|
||||
Return None if not present or invalid.
|
||||
"""
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
return time.strptime(value, self.time_format)
|
||||
except ValueError as e:
|
||||
msg = "Field {0} has bad value '{1}': '{2}'".format(
|
||||
self._name, value, e)
|
||||
if field is None:
|
||||
return field
|
||||
elif isinstance(field, basestring):
|
||||
d = dateutil.parser.parse(field)
|
||||
return d.utctimetuple()
|
||||
elif isinstance(field, (int, long, float)):
|
||||
return time.gmtime(field / 1000)
|
||||
elif isinstance(field, time.struct_time):
|
||||
return field
|
||||
else:
|
||||
msg = "Field {0} has bad value '{1}'".format(
|
||||
self._name, field)
|
||||
log.warning(msg)
|
||||
return None
|
||||
|
||||
@@ -34,8 +42,11 @@ class Date(ModelType):
|
||||
"""
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
return time.strftime(self.time_format, value)
|
||||
if isinstance(value, time.struct_time):
|
||||
# struct_times are always utc
|
||||
return time.strftime('%Y-%m-%dT%H:%M:%SZ', value)
|
||||
elif isinstance(value, datetime.datetime):
|
||||
return value.isoformat() + 'Z'
|
||||
|
||||
|
||||
TIMEDELTA_REGEX = re.compile(r'^((?P<days>\d+?) day(?:s?))?(\s)?((?P<hours>\d+?) hour(?:s?))?(\s)?((?P<minutes>\d+?) minute(?:s)?)?(\s)?((?P<seconds>\d+?) second(?:s)?)?$')
|
||||
@@ -66,4 +77,4 @@ class Timedelta(ModelType):
|
||||
cur_value = getattr(value, attr, 0)
|
||||
if cur_value > 0:
|
||||
values.append("%d %s" % (cur_value, attr))
|
||||
return ' '.join(values)
|
||||
return ' '.join(values)
|
||||
|
||||
@@ -8,7 +8,7 @@ from collections import namedtuple
|
||||
from fs.osfs import OSFS
|
||||
from itertools import repeat
|
||||
from path import path
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import datetime
|
||||
|
||||
from importlib import import_module
|
||||
from xmodule.errortracker import null_error_tracker, exc_info_to_str
|
||||
@@ -246,6 +246,7 @@ class MongoModuleStore(ModuleStoreBase):
|
||||
self.fs_root = path(fs_root)
|
||||
self.error_tracker = error_tracker
|
||||
self.render_template = render_template
|
||||
self.ignore_write_events_on_courses = []
|
||||
|
||||
def get_metadata_inheritance_tree(self, location):
|
||||
'''
|
||||
@@ -330,6 +331,11 @@ class MongoModuleStore(ModuleStoreBase):
|
||||
|
||||
return tree
|
||||
|
||||
def refresh_cached_metadata_inheritance_tree(self, location):
|
||||
pseudo_course_id = '/'.join([location.org, location.course])
|
||||
if pseudo_course_id not in self.ignore_write_events_on_courses:
|
||||
self.get_cached_metadata_inheritance_tree(location, force_refresh = True)
|
||||
|
||||
def clear_cached_metadata_inheritance_tree(self, location):
|
||||
key_name = '{0}/{1}'.format(location.org, location.course)
|
||||
if self.metadata_inheritance_cache is not None:
|
||||
@@ -376,7 +382,7 @@ class MongoModuleStore(ModuleStoreBase):
|
||||
|
||||
return data
|
||||
|
||||
def _load_item(self, item, data_cache):
|
||||
def _load_item(self, item, data_cache, should_apply_metadata_inheritence=True):
|
||||
"""
|
||||
Load an XModuleDescriptor from item, using the children stored in data_cache
|
||||
"""
|
||||
@@ -388,7 +394,10 @@ class MongoModuleStore(ModuleStoreBase):
|
||||
|
||||
resource_fs = OSFS(root)
|
||||
|
||||
metadata_inheritance_tree = self.get_cached_metadata_inheritance_tree(Location(item['location']))
|
||||
metadata_inheritance_tree = None
|
||||
|
||||
if should_apply_metadata_inheritence:
|
||||
metadata_inheritance_tree = self.get_cached_metadata_inheritance_tree(Location(item['location']))
|
||||
|
||||
# TODO (cdodge): When the 'split module store' work has been completed, we should remove
|
||||
# the 'metadata_inheritance_tree' parameter
|
||||
@@ -410,7 +419,10 @@ class MongoModuleStore(ModuleStoreBase):
|
||||
"""
|
||||
data_cache = self._cache_children(items, depth)
|
||||
|
||||
return [self._load_item(item, data_cache) for item in items]
|
||||
# if we are loading a course object, if we're not prefetching children (depth != 0) then don't
|
||||
# bother with the metadata inheritence
|
||||
return [self._load_item(item, data_cache,
|
||||
should_apply_metadata_inheritence=(item['location']['category'] != 'course' or depth != 0)) for item in items]
|
||||
|
||||
def get_courses(self):
|
||||
'''
|
||||
@@ -493,10 +505,12 @@ class MongoModuleStore(ModuleStoreBase):
|
||||
try:
|
||||
source_item = self.collection.find_one(location_to_query(source))
|
||||
source_item['_id'] = Location(location).dict()
|
||||
self.collection.insert(source_item,
|
||||
self.collection.insert(
|
||||
source_item,
|
||||
# Must include this to avoid the django debug toolbar (which defines the deprecated "safe=False")
|
||||
# from overriding our default value set in the init method.
|
||||
safe=self.collection.safe)
|
||||
safe=self.collection.safe
|
||||
)
|
||||
item = self._load_items([source_item])[0]
|
||||
|
||||
# VS[compat] cdodge: This is a hack because static_tabs also have references from the course module, so
|
||||
@@ -518,7 +532,7 @@ class MongoModuleStore(ModuleStoreBase):
|
||||
raise DuplicateItemError(location)
|
||||
|
||||
# recompute (and update) the metadata inheritance tree which is cached
|
||||
self.get_cached_metadata_inheritance_tree(Location(location), force_refresh = True)
|
||||
self.refresh_cached_metadata_inheritance_tree(Location(location))
|
||||
|
||||
def get_course_for_item(self, location, depth=0):
|
||||
'''
|
||||
@@ -588,7 +602,7 @@ class MongoModuleStore(ModuleStoreBase):
|
||||
|
||||
self._update_single_item(location, {'definition.children': children})
|
||||
# recompute (and update) the metadata inheritance tree which is cached
|
||||
self.get_cached_metadata_inheritance_tree(Location(location), force_refresh = True)
|
||||
self.refresh_cached_metadata_inheritance_tree(Location(location))
|
||||
|
||||
def update_metadata(self, location, metadata):
|
||||
"""
|
||||
@@ -614,7 +628,7 @@ class MongoModuleStore(ModuleStoreBase):
|
||||
|
||||
self._update_single_item(location, {'metadata': metadata})
|
||||
# recompute (and update) the metadata inheritance tree which is cached
|
||||
self.get_cached_metadata_inheritance_tree(loc, force_refresh = True)
|
||||
self.refresh_cached_metadata_inheritance_tree(loc)
|
||||
|
||||
def delete_item(self, location):
|
||||
"""
|
||||
@@ -637,8 +651,7 @@ class MongoModuleStore(ModuleStoreBase):
|
||||
# from overriding our default value set in the init method.
|
||||
safe=self.collection.safe)
|
||||
# recompute (and update) the metadata inheritance tree which is cached
|
||||
self.get_cached_metadata_inheritance_tree(Location(location), force_refresh = True)
|
||||
|
||||
self.refresh_cached_metadata_inheritance_tree(Location(location))
|
||||
|
||||
def get_parent_locations(self, location, course_id):
|
||||
'''Find all locations that are the parents of this location in this
|
||||
|
||||
@@ -25,8 +25,7 @@ class XModuleCourseFactory(Factory):
|
||||
|
||||
@classmethod
|
||||
def _create(cls, target_class, *args, **kwargs):
|
||||
# This logic was taken from the create_new_course method in
|
||||
# cms/djangoapps/contentstore/views.py
|
||||
|
||||
template = Location('i4x', 'edx', 'templates', 'course', 'Empty')
|
||||
org = kwargs.get('org')
|
||||
number = kwargs.get('number')
|
||||
@@ -43,8 +42,7 @@ class XModuleCourseFactory(Factory):
|
||||
if display_name is not None:
|
||||
new_course.display_name = display_name
|
||||
|
||||
new_course.start = gmtime()
|
||||
|
||||
new_course.lms.start = gmtime()
|
||||
new_course.tabs = [{"type": "courseware"},
|
||||
{"type": "course_info", "name": "Course Info"},
|
||||
{"type": "discussion", "name": "Discussion"},
|
||||
@@ -81,21 +79,41 @@ class XModuleItemFactory(Factory):
|
||||
@classmethod
|
||||
def _create(cls, target_class, *args, **kwargs):
|
||||
"""
|
||||
kwargs must include parent_location, template. Can contain display_name
|
||||
target_class is ignored
|
||||
Uses *kwargs*:
|
||||
|
||||
*parent_location* (required): the location of the parent module
|
||||
(e.g. the parent course or section)
|
||||
|
||||
*template* (required): the template to create the item from
|
||||
(e.g. i4x://templates/section/Empty)
|
||||
|
||||
*data* (optional): the data for the item
|
||||
(e.g. XML problem definition for a problem item)
|
||||
|
||||
*display_name* (optional): the display name of the item
|
||||
|
||||
*metadata* (optional): dictionary of metadata attributes
|
||||
|
||||
*target_class* is ignored
|
||||
"""
|
||||
|
||||
DETACHED_CATEGORIES = ['about', 'static_tab', 'course_info']
|
||||
|
||||
parent_location = Location(kwargs.get('parent_location'))
|
||||
template = Location(kwargs.get('template'))
|
||||
data = kwargs.get('data')
|
||||
display_name = kwargs.get('display_name')
|
||||
metadata = kwargs.get('metadata', {})
|
||||
|
||||
store = modulestore('direct')
|
||||
|
||||
# This code was based off that in cms/djangoapps/contentstore/views.py
|
||||
parent = store.get_item(parent_location)
|
||||
dest_location = parent_location._replace(category=template.category, name=uuid4().hex)
|
||||
|
||||
# If a display name is set, use that
|
||||
dest_name = display_name.replace(" ", "_") if display_name is not None else uuid4().hex
|
||||
dest_location = parent_location._replace(category=template.category,
|
||||
name=dest_name)
|
||||
|
||||
new_item = store.clone_item(template, dest_location)
|
||||
|
||||
@@ -103,7 +121,14 @@ class XModuleItemFactory(Factory):
|
||||
if display_name is not None:
|
||||
new_item.display_name = display_name
|
||||
|
||||
store.update_metadata(new_item.location.url(), own_metadata(new_item))
|
||||
# Add additional metadata or override current metadata
|
||||
item_metadata = own_metadata(new_item)
|
||||
item_metadata.update(metadata)
|
||||
store.update_metadata(new_item.location.url(), item_metadata)
|
||||
|
||||
# replace the data with the optional *data* parameter
|
||||
if data is not None:
|
||||
store.update_item(new_item.location, data)
|
||||
|
||||
if new_item.location.category not in DETACHED_CATEGORIES:
|
||||
store.update_children(parent_location, parent.children + [new_item.location.url()])
|
||||
|
||||
@@ -4,6 +4,8 @@ import mimetypes
|
||||
from lxml.html import rewrite_links as lxml_rewrite_links
|
||||
from path import path
|
||||
|
||||
from xblock.core import Scope
|
||||
|
||||
from .xml import XMLModuleStore
|
||||
from .exceptions import DuplicateItemError
|
||||
from xmodule.modulestore import Location
|
||||
@@ -201,100 +203,127 @@ def import_from_xml(store, data_dir, course_dirs=None,
|
||||
course_items = []
|
||||
for course_id in module_store.modules.keys():
|
||||
|
||||
course_data_path = None
|
||||
course_location = None
|
||||
if target_location_namespace is not None:
|
||||
pseudo_course_id = '/'.join([target_location_namespace.org, target_location_namespace.course])
|
||||
else:
|
||||
course_id_components = course_id.split('/')
|
||||
pseudo_course_id = '/'.join([course_id_components[0], course_id_components[1]])
|
||||
|
||||
if verbose:
|
||||
log.debug("Scanning {0} for course module...".format(course_id))
|
||||
try:
|
||||
# turn off all write signalling while importing as this is a high volume operation
|
||||
if pseudo_course_id not in store.ignore_write_events_on_courses:
|
||||
store.ignore_write_events_on_courses.append(pseudo_course_id)
|
||||
|
||||
# Quick scan to get course module as we need some info from there. Also we need to make sure that the
|
||||
# course module is committed first into the store
|
||||
for module in module_store.modules[course_id].itervalues():
|
||||
if module.category == 'course':
|
||||
course_data_path = path(data_dir) / module.data_dir
|
||||
course_location = module.location
|
||||
|
||||
module = remap_namespace(module, target_location_namespace)
|
||||
|
||||
# cdodge: more hacks (what else). Seems like we have a problem when importing a course (like 6.002) which
|
||||
# does not have any tabs defined in the policy file. The import goes fine and then displays fine in LMS,
|
||||
# but if someone tries to add a new tab in the CMS, then the LMS barfs because it expects that -
|
||||
# if there is *any* tabs - then there at least needs to be some predefined ones
|
||||
if module.tabs is None or len(module.tabs) == 0:
|
||||
module.tabs = [{"type": "courseware"},
|
||||
{"type": "course_info", "name": "Course Info"},
|
||||
{"type": "discussion", "name": "Discussion"},
|
||||
{"type": "wiki", "name": "Wiki"}] # note, add 'progress' when we can support it on Edge
|
||||
|
||||
|
||||
if hasattr(module, 'data'):
|
||||
store.update_item(module.location, module.data)
|
||||
store.update_children(module.location, module.children)
|
||||
store.update_metadata(module.location, dict(own_metadata(module)))
|
||||
|
||||
# a bit of a hack, but typically the "course image" which is shown on marketing pages is hard coded to /images/course_image.jpg
|
||||
# so let's make sure we import in case there are no other references to it in the modules
|
||||
verify_content_links(module, course_data_path, static_content_store, '/static/images/course_image.jpg')
|
||||
|
||||
course_items.append(module)
|
||||
|
||||
|
||||
# then import all the static content
|
||||
if static_content_store is not None:
|
||||
_namespace_rename = target_location_namespace if target_location_namespace is not None else course_location
|
||||
|
||||
# first pass to find everything in /static/
|
||||
import_static_content(module_store.modules[course_id], course_location, course_data_path, static_content_store,
|
||||
_namespace_rename, subpath='static', verbose=verbose)
|
||||
|
||||
# finally loop through all the modules
|
||||
for module in module_store.modules[course_id].itervalues():
|
||||
|
||||
if module.category == 'course':
|
||||
# we've already saved the course module up at the top of the loop
|
||||
# so just skip over it in the inner loop
|
||||
continue
|
||||
|
||||
# remap module to the new namespace
|
||||
if target_location_namespace is not None:
|
||||
module = remap_namespace(module, target_location_namespace)
|
||||
course_data_path = None
|
||||
course_location = None
|
||||
|
||||
if verbose:
|
||||
log.debug('importing module location {0}'.format(module.location))
|
||||
log.debug("Scanning {0} for course module...".format(course_id))
|
||||
|
||||
if hasattr(module, 'data'):
|
||||
module_data = module.data
|
||||
# Quick scan to get course module as we need some info from there. Also we need to make sure that the
|
||||
# course module is committed first into the store
|
||||
for module in module_store.modules[course_id].itervalues():
|
||||
if module.category == 'course':
|
||||
course_data_path = path(data_dir) / module.data_dir
|
||||
course_location = module.location
|
||||
|
||||
# cdodge: now go through any link references to '/static/' and make sure we've imported
|
||||
# it as a StaticContent asset
|
||||
try:
|
||||
remap_dict = {}
|
||||
module = remap_namespace(module, target_location_namespace)
|
||||
|
||||
# use the rewrite_links as a utility means to enumerate through all links
|
||||
# in the module data. We use that to load that reference into our asset store
|
||||
# IMPORTANT: There appears to be a bug in lxml.rewrite_link which makes us not be able to
|
||||
# do the rewrites natively in that code.
|
||||
# For example, what I'm seeing is <img src='foo.jpg' /> -> <img src='bar.jpg'>
|
||||
# Note the dropped element closing tag. This causes the LMS to fail when rendering modules - that's
|
||||
# no good, so we have to do this kludge
|
||||
if isinstance(module_data, str) or isinstance(module_data, unicode): # some module 'data' fields are non strings which blows up the link traversal code
|
||||
lxml_rewrite_links(module_data, lambda link: verify_content_links(module, course_data_path,
|
||||
static_content_store, link, remap_dict))
|
||||
# cdodge: more hacks (what else). Seems like we have a problem when importing a course (like 6.002) which
|
||||
# does not have any tabs defined in the policy file. The import goes fine and then displays fine in LMS,
|
||||
# but if someone tries to add a new tab in the CMS, then the LMS barfs because it expects that -
|
||||
# if there is *any* tabs - then there at least needs to be some predefined ones
|
||||
if module.tabs is None or len(module.tabs) == 0:
|
||||
module.tabs = [{"type": "courseware"},
|
||||
{"type": "course_info", "name": "Course Info"},
|
||||
{"type": "discussion", "name": "Discussion"},
|
||||
{"type": "wiki", "name": "Wiki"}] # note, add 'progress' when we can support it on Edge
|
||||
|
||||
for key in remap_dict.keys():
|
||||
module_data = module_data.replace(key, remap_dict[key])
|
||||
|
||||
except Exception, e:
|
||||
logging.exception("failed to rewrite links on {0}. Continuing...".format(module.location))
|
||||
if hasattr(module, 'data'):
|
||||
store.update_item(module.location, module.data)
|
||||
store.update_children(module.location, module.children)
|
||||
store.update_metadata(module.location, dict(own_metadata(module)))
|
||||
|
||||
store.update_item(module.location, module_data)
|
||||
# a bit of a hack, but typically the "course image" which is shown on marketing pages is hard coded to /images/course_image.jpg
|
||||
# so let's make sure we import in case there are no other references to it in the modules
|
||||
verify_content_links(module, course_data_path, static_content_store, '/static/images/course_image.jpg')
|
||||
|
||||
if hasattr(module, 'children') and module.children != []:
|
||||
store.update_children(module.location, module.children)
|
||||
course_items.append(module)
|
||||
|
||||
# NOTE: It's important to use own_metadata here to avoid writing
|
||||
# inherited metadata everywhere.
|
||||
store.update_metadata(module.location, dict(own_metadata(module)))
|
||||
|
||||
# then import all the static content
|
||||
if static_content_store is not None:
|
||||
_namespace_rename = target_location_namespace if target_location_namespace is not None else course_location
|
||||
|
||||
# first pass to find everything in /static/
|
||||
import_static_content(module_store.modules[course_id], course_location, course_data_path, static_content_store,
|
||||
_namespace_rename, subpath='static', verbose=verbose)
|
||||
|
||||
# finally loop through all the modules
|
||||
for module in module_store.modules[course_id].itervalues():
|
||||
|
||||
if module.category == 'course':
|
||||
# we've already saved the course module up at the top of the loop
|
||||
# so just skip over it in the inner loop
|
||||
continue
|
||||
|
||||
# remap module to the new namespace
|
||||
if target_location_namespace is not None:
|
||||
module = remap_namespace(module, target_location_namespace)
|
||||
|
||||
if verbose:
|
||||
log.debug('importing module location {0}'.format(module.location))
|
||||
|
||||
content = {}
|
||||
for field in module.fields:
|
||||
if field.scope != Scope.content:
|
||||
continue
|
||||
try:
|
||||
content[field.name] = module._model_data[field.name]
|
||||
except KeyError:
|
||||
# Ignore any missing keys in _model_data
|
||||
pass
|
||||
|
||||
if 'data' in content:
|
||||
module_data = content['data']
|
||||
|
||||
# cdodge: now go through any link references to '/static/' and make sure we've imported
|
||||
# it as a StaticContent asset
|
||||
try:
|
||||
remap_dict = {}
|
||||
|
||||
# use the rewrite_links as a utility means to enumerate through all links
|
||||
# in the module data. We use that to load that reference into our asset store
|
||||
# IMPORTANT: There appears to be a bug in lxml.rewrite_link which makes us not be able to
|
||||
# do the rewrites natively in that code.
|
||||
# For example, what I'm seeing is <img src='foo.jpg' /> -> <img src='bar.jpg'>
|
||||
# Note the dropped element closing tag. This causes the LMS to fail when rendering modules - that's
|
||||
# no good, so we have to do this kludge
|
||||
if isinstance(module_data, str) or isinstance(module_data, unicode): # some module 'data' fields are non strings which blows up the link traversal code
|
||||
lxml_rewrite_links(module_data, lambda link: verify_content_links(module, course_data_path,
|
||||
static_content_store, link, remap_dict))
|
||||
|
||||
for key in remap_dict.keys():
|
||||
module_data = module_data.replace(key, remap_dict[key])
|
||||
|
||||
except Exception, e:
|
||||
logging.exception("failed to rewrite links on {0}. Continuing...".format(module.location))
|
||||
|
||||
store.update_item(module.location, content)
|
||||
|
||||
if hasattr(module, 'children') and module.children != []:
|
||||
store.update_children(module.location, module.children)
|
||||
|
||||
# NOTE: It's important to use own_metadata here to avoid writing
|
||||
# inherited metadata everywhere.
|
||||
store.update_metadata(module.location, dict(own_metadata(module)))
|
||||
finally:
|
||||
# turn back on all write signalling
|
||||
if pseudo_course_id in store.ignore_write_events_on_courses:
|
||||
store.ignore_write_events_on_courses.remove(pseudo_course_id)
|
||||
store.refresh_cached_metadata_inheritance_tree(target_location_namespace if
|
||||
target_location_namespace is not None else course_location)
|
||||
|
||||
return module_store, course_items
|
||||
|
||||
|
||||
@@ -2,5 +2,123 @@
|
||||
metadata:
|
||||
display_name: Empty
|
||||
start: 2020-10-10T10:00
|
||||
checklists: [
|
||||
{"short_description" : "Getting Started With Studio",
|
||||
"items" : [{"short_description": "Add Course Team Members",
|
||||
"long_description": "Grant your collaborators permission to edit your course so you can work together.",
|
||||
"is_checked": false,
|
||||
"action_url": "ManageUsers",
|
||||
"action_text": "Edit Course Team",
|
||||
"action_external": false},
|
||||
{"short_description": "Set Important Dates for Your Course",
|
||||
"long_description": "Establish your course's student enrollment and launch dates on the Schedule and Details page.",
|
||||
"is_checked": false,
|
||||
"action_url": "SettingsDetails",
|
||||
"action_text": "Edit Course Details & Schedule",
|
||||
"action_external": false},
|
||||
{"short_description": "Draft Your Course's Grading Policy",
|
||||
"long_description": "Set up your assignment types and grading policy even if you haven't created all your assignments.",
|
||||
"is_checked": false,
|
||||
"action_url": "SettingsGrading",
|
||||
"action_text": "Edit Grading Settings",
|
||||
"action_external": false},
|
||||
{"short_description": "Explore the Other Studio Checklists",
|
||||
"long_description": "Discover other available course authoring tools, and find help when you need it.",
|
||||
"is_checked": false,
|
||||
"action_url": "",
|
||||
"action_text": "",
|
||||
"action_external": false}]
|
||||
},
|
||||
{"short_description" : "Draft a Rough Course Outline",
|
||||
"items" : [{"short_description": "Create Your First Section and Subsection",
|
||||
"long_description": "Use your course outline to build your first Section and Subsection.",
|
||||
"is_checked": false,
|
||||
"action_url": "CourseOutline",
|
||||
"action_text": "Edit Course Outline",
|
||||
"action_external": false},
|
||||
{"short_description": "Set Section Release Dates",
|
||||
"long_description": "Specify the release dates for each Section in your course. Sections become visible to students on their release dates.",
|
||||
"is_checked": false,
|
||||
"action_url": "CourseOutline",
|
||||
"action_text": "Edit Course Outline",
|
||||
"action_external": false},
|
||||
{"short_description": "Designate a Subsection as Graded",
|
||||
"long_description": "Set a Subsection to be graded as a specific assignment type. Assignments within graded Subsections count toward a student's final grade.",
|
||||
"is_checked": false,
|
||||
"action_url": "CourseOutline",
|
||||
"action_text": "Edit Course Outline",
|
||||
"action_external": false},
|
||||
{"short_description": "Reordering Course Content",
|
||||
"long_description": "Use drag and drop to reorder the content in your course.",
|
||||
"is_checked": false,
|
||||
"action_url": "CourseOutline",
|
||||
"action_text": "Edit Course Outline",
|
||||
"action_external": false},
|
||||
{"short_description": "Renaming Sections",
|
||||
"long_description": "Rename Sections by clicking the Section name from the Course Outline.",
|
||||
"is_checked": false,
|
||||
"action_url": "CourseOutline",
|
||||
"action_text": "Edit Course Outline",
|
||||
"action_external": false},
|
||||
{"short_description": "Deleting Course Content",
|
||||
"long_description": "Delete Sections, Subsections, or Units you don't need anymore. Be careful, as there is no Undo function.",
|
||||
"is_checked": false,
|
||||
"action_url": "CourseOutline",
|
||||
"action_text": "Edit Course Outline",
|
||||
"action_external": false},
|
||||
{"short_description": "Add an Instructor-Only Section to Your Outline",
|
||||
"long_description": "Some course authors find using a section for unsorted, in-progress work useful. To do this, create a section and set the release date to the distant future.",
|
||||
"is_checked": false,
|
||||
"action_url": "CourseOutline",
|
||||
"action_text": "Edit Course Outline",
|
||||
"action_external": false}]
|
||||
},
|
||||
{"short_description" : "Explore edX's Support Tools",
|
||||
"items" : [{"short_description": "Explore the Studio Help Forum",
|
||||
"long_description": "Access the Studio Help forum from the menu that appears when you click your user name in the top right corner of Studio.",
|
||||
"is_checked": false,
|
||||
"action_url": "http://help.edge.edx.org/",
|
||||
"action_text": "Visit Studio Help",
|
||||
"action_external": true},
|
||||
{"short_description": "Enroll in edX 101",
|
||||
"long_description": "Register for edX 101, edX's primer for course creation.",
|
||||
"is_checked": false,
|
||||
"action_url": "https://edge.edx.org/courses/edX/edX101/How_to_Create_an_edX_Course/about",
|
||||
"action_text": "Register for edX 101",
|
||||
"action_external": true},
|
||||
{"short_description": "Download the Studio Documentation",
|
||||
"long_description": "Download the searchable Studio reference documentation in PDF form.",
|
||||
"is_checked": false,
|
||||
"action_url": "http://help.edge.edx.org/help/assets/8ccd409f979c8639dd463e126eb840dc67f09098/Getting_Started_with_Studio.pdf",
|
||||
"action_text": "Download Documentation",
|
||||
"action_external": true}]
|
||||
},
|
||||
{"short_description" : "Draft Your Course About Page",
|
||||
"items" : [{"short_description": "Draft a Course Description",
|
||||
"long_description": "Courses on edX have an About page that includes a course video, description, and more. Draft the text students will read before deciding to enroll in your course.",
|
||||
"is_checked": false,
|
||||
"action_url": "SettingsDetails",
|
||||
"action_text": "Edit Course Schedule & Details",
|
||||
"action_external": false},
|
||||
{"short_description": "Add Staff Bios",
|
||||
"long_description": "Showing prospective students who their instructor will be is helpful. Include staff bios on the course About page.",
|
||||
"is_checked": false,
|
||||
"action_url": "SettingsDetails",
|
||||
"action_text": "Edit Course Schedule & Details",
|
||||
"action_external": false},
|
||||
{"short_description": "Add Course FAQs",
|
||||
"long_description": "Include a short list of frequently asked questions about your course.",
|
||||
"is_checked": false,
|
||||
"action_url": "SettingsDetails",
|
||||
"action_text": "Edit Course Schedule & Details",
|
||||
"action_external": false},
|
||||
{"short_description": "Add Course Prerequisites",
|
||||
"long_description": "Let students know what knowledge and/or skills they should have before they enroll in your course.",
|
||||
"is_checked": false,
|
||||
"action_url": "SettingsDetails",
|
||||
"action_text": "Edit Course Schedule & Details",
|
||||
"action_external": false}]
|
||||
}
|
||||
]
|
||||
data: { 'textbooks' : [ ], 'wiki_slug' : null }
|
||||
children: []
|
||||
|
||||
7
common/static/js/vendor/jquery.smooth-scroll.min.js
vendored
Executable file
7
common/static/js/vendor/jquery.smooth-scroll.min.js
vendored
Executable file
@@ -0,0 +1,7 @@
|
||||
/*!
|
||||
* Smooth Scroll - v1.4.10 - 2013-03-02
|
||||
* https://github.com/kswedberg/jquery-smooth-scroll
|
||||
* Copyright (c) 2013 Karl Swedberg
|
||||
* Licensed MIT (https://github.com/kswedberg/jquery-smooth-scroll/blob/master/LICENSE-MIT)
|
||||
*/
|
||||
(function(l){function t(l){return l.replace(/(:|\.)/g,"\\$1")}var e="1.4.10",o={exclude:[],excludeWithin:[],offset:0,direction:"top",scrollElement:null,scrollTarget:null,beforeScroll:function(){},afterScroll:function(){},easing:"swing",speed:400,autoCoefficent:2},r=function(t){var e=[],o=!1,r=t.dir&&"left"==t.dir?"scrollLeft":"scrollTop";return this.each(function(){if(this!=document&&this!=window){var t=l(this);t[r]()>0?e.push(this):(t[r](1),o=t[r]()>0,o&&e.push(this),t[r](0))}}),e.length||this.each(function(){"BODY"===this.nodeName&&(e=[this])}),"first"===t.el&&e.length>1&&(e=[e[0]]),e};l.fn.extend({scrollable:function(l){var t=r.call(this,{dir:l});return this.pushStack(t)},firstScrollable:function(l){var t=r.call(this,{el:"first",dir:l});return this.pushStack(t)},smoothScroll:function(e){e=e||{};var o=l.extend({},l.fn.smoothScroll.defaults,e),r=l.smoothScroll.filterPath(location.pathname);return this.unbind("click.smoothscroll").bind("click.smoothscroll",function(e){var n=this,s=l(this),c=o.exclude,i=o.excludeWithin,a=0,f=0,h=!0,u={},d=location.hostname===n.hostname||!n.hostname,m=o.scrollTarget||(l.smoothScroll.filterPath(n.pathname)||r)===r,p=t(n.hash);if(o.scrollTarget||d&&m&&p){for(;h&&c.length>a;)s.is(t(c[a++]))&&(h=!1);for(;h&&i.length>f;)s.closest(i[f++]).length&&(h=!1)}else h=!1;h&&(e.preventDefault(),l.extend(u,o,{scrollTarget:o.scrollTarget||p,link:n}),l.smoothScroll(u))}),this}}),l.smoothScroll=function(t,e){var o,r,n,s,c=0,i="offset",a="scrollTop",f={},h={};"number"==typeof t?(o=l.fn.smoothScroll.defaults,n=t):(o=l.extend({},l.fn.smoothScroll.defaults,t||{}),o.scrollElement&&(i="position","static"==o.scrollElement.css("position")&&o.scrollElement.css("position","relative"))),o=l.extend({link:null},o),a="left"==o.direction?"scrollLeft":a,o.scrollElement?(r=o.scrollElement,c=r[a]()):r=l("html, body").firstScrollable(),o.beforeScroll.call(r,o),n="number"==typeof t?t:e||l(o.scrollTarget)[i]()&&l(o.scrollTarget)[i]()[o.direction]||0,f[a]=n+c+o.offset,s=o.speed,"auto"===s&&(s=f[a]||r.scrollTop(),s/=o.autoCoefficent),h={duration:s,easing:o.easing,complete:function(){o.afterScroll.call(o.link,o)}},o.step&&(h.step=o.step),r.length?r.stop().animate(f,h):o.afterScroll.call(o.link,o)},l.smoothScroll.version=e,l.smoothScroll.filterPath=function(l){return l.replace(/^\//,"").replace(/(index|default).[a-zA-Z]{3,4}$/,"").replace(/\/$/,"")},l.fn.smoothScroll.defaults=o})(jQuery);
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user