Commit 66ba0f9b by Brian Talbot

studio - resolving local merge with master

parents c901cecd 8c1b7a71
...@@ -41,7 +41,8 @@ disable= ...@@ -41,7 +41,8 @@ disable=
# R0902: Too many instance attributes # R0902: Too many instance attributes
# R0903: Too few public methods (1/2) # R0903: Too few public methods (1/2)
# R0904: Too many public methods # R0904: Too many public methods
W0141,W0142,R0201,R0901,R0902,R0903,R0904 # R0913: Too many arguments
W0141,W0142,R0201,R0901,R0902,R0903,R0904,R0913
[REPORTS] [REPORTS]
...@@ -137,7 +138,7 @@ bad-names=foo,bar,baz,toto,tutu,tata ...@@ -137,7 +138,7 @@ bad-names=foo,bar,baz,toto,tutu,tata
# Regular expression which should only match functions or classes name which do # Regular expression which should only match functions or classes name which do
# not require a docstring # not require a docstring
no-docstring-rgx=__.*__ no-docstring-rgx=(__.*__|test_.*)
[MISCELLANEOUS] [MISCELLANEOUS]
......
from xmodule.modulestore.exceptions import ItemNotFoundError from xmodule.modulestore.exceptions import ItemNotFoundError
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from lxml import html, etree from lxml import html
import re import re
from django.http import HttpResponseBadRequest from django.http import HttpResponseBadRequest
import logging import logging
import django.utils
## TODO store as array of { date, content } and override course_info_module.definition_from_xml # # 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 # # This should be in a class which inherits from XmlDescriptor
log = logging.getLogger(__name__)
def get_course_updates(location): def get_course_updates(location):
...@@ -26,9 +28,11 @@ 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. # purely to handle free formed updates not done via editor. Actually kills them, but at least doesn't break.
try: try:
course_html_parsed = etree.fromstring(course_updates.data) course_html_parsed = html.fromstring(course_updates.data)
except etree.XMLSyntaxError: except:
course_html_parsed = etree.fromstring("<ol></ol>") log.error("Cannot parse: " + course_updates.data)
escaped = django.utils.html.escape(course_updates.data)
course_html_parsed = html.fromstring("<ol><li>" + escaped + "</li></ol>")
# Confirm that root is <ol>, iterate over <li>, pull out <h2> subs and then rest of val # Confirm that root is <ol>, iterate over <li>, pull out <h2> subs and then rest of val
course_upd_collection = [] course_upd_collection = []
...@@ -64,9 +68,11 @@ def update_course_updates(location, update, passed_id=None): ...@@ -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. # purely to handle free formed updates not done via editor. Actually kills them, but at least doesn't break.
try: try:
course_html_parsed = etree.fromstring(course_updates.data) course_html_parsed = html.fromstring(course_updates.data)
except etree.XMLSyntaxError: except:
course_html_parsed = etree.fromstring("<ol></ol>") log.error("Cannot parse: " + course_updates.data)
escaped = django.utils.html.escape(course_updates.data)
course_html_parsed = html.fromstring("<ol><li>" + escaped + "</li></ol>")
# No try/catch b/c failure generates an error back to client # 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>') 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): ...@@ -85,12 +91,19 @@ def update_course_updates(location, update, passed_id=None):
passed_id = course_updates.location.url() + "/" + str(idx) passed_id = course_updates.location.url() + "/" + str(idx)
# update db record # 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) modulestore('direct').update_item(location, course_updates.data)
return {"id" : passed_id, if (len(new_html_parsed) == 1):
"date" : update['date'], content = new_html_parsed[0].tail
"content" :update['content']} 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): def delete_course_update(location, update, passed_id):
""" """
...@@ -108,9 +121,11 @@ 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 # 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. # purely to handle free formed updates not done via editor. Actually kills them, but at least doesn't break.
try: try:
course_html_parsed = etree.fromstring(course_updates.data) course_html_parsed = html.fromstring(course_updates.data)
except etree.XMLSyntaxError: except:
course_html_parsed = etree.fromstring("<ol></ol>") log.error("Cannot parse: " + course_updates.data)
escaped = django.utils.html.escape(course_updates.data)
course_html_parsed = html.fromstring("<ol><li>" + escaped + "</li></ol>")
if course_html_parsed.tag == 'ol': if course_html_parsed.tag == 'ol':
# ??? Should this use the id in the json or in the url or does it matter? # ??? 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): ...@@ -121,7 +136,7 @@ def delete_course_update(location, update, passed_id):
course_html_parsed.remove(element_to_delete) course_html_parsed.remove(element_to_delete)
# update db record # update db record
course_updates.data = etree.tostring(course_html_parsed) course_updates.data = html.tostring(course_html_parsed)
store = modulestore('direct') store = modulestore('direct')
store.update_item(location, course_updates.data) store.update_item(location, course_updates.data)
...@@ -132,7 +147,6 @@ def get_idx(passed_id): ...@@ -132,7 +147,6 @@ def get_idx(passed_id):
""" """
From the url w/ idx appended, get the idx. 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: if idx_matcher:
return int(idx_matcher.group(1)) return int(idx_matcher.group(1))
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
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 ...@@ -7,8 +7,6 @@ from selenium.common.exceptions import WebDriverException, StaleElementReference
from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By 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.modulestore.django import _MODULESTORES, modulestore
from xmodule.templates import update_templates from xmodule.templates import update_templates
from auth.authz import get_user_by_email from auth.authz import get_user_by_email
...@@ -61,7 +59,7 @@ def create_studio_user( ...@@ -61,7 +59,7 @@ def create_studio_user(
email='robot+studio@edx.org', email='robot+studio@edx.org',
password='test', password='test',
is_staff=False): is_staff=False):
studio_user = UserFactory.build( studio_user = world.UserFactory.build(
username=uname, username=uname,
email=email, email=email,
password=password, password=password,
...@@ -69,11 +67,11 @@ def create_studio_user( ...@@ -69,11 +67,11 @@ def create_studio_user(
studio_user.set_password(password) studio_user.set_password(password)
studio_user.save() studio_user.save()
registration = RegistrationFactory(user=studio_user) registration = world.RegistrationFactory(user=studio_user)
registration.register(studio_user) registration.register(studio_user)
registration.activate() registration.activate()
user_profile = UserProfileFactory(user=studio_user) user_profile = world.UserProfileFactory(user=studio_user)
def flush_xmodule_store(): def flush_xmodule_store():
...@@ -175,11 +173,11 @@ def log_into_studio( ...@@ -175,11 +173,11 @@ def log_into_studio(
def create_a_course(): 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 # Add the user to the instructor group of the course
# so they will have the permissions to see it in studio # 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 = get_user_by_email('robot+studio@edx.org')
u.groups.add(g) u.groups.add(g)
u.save() u.save()
......
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()
from lettuce import world, step from lettuce import world, step
from terrain.factories import *
from common import * from common import *
from nose.tools import assert_true, assert_false, assert_equal from nose.tools import assert_true, assert_false, assert_equal
...@@ -10,15 +9,15 @@ logger = getLogger(__name__) ...@@ -10,15 +9,15 @@ logger = getLogger(__name__)
@step(u'I have a course with no sections$') @step(u'I have a course with no sections$')
def have_a_course(step): def have_a_course(step):
clear_courses() clear_courses()
course = CourseFactory.create() course = world.CourseFactory.create()
@step(u'I have a course with 1 section$') @step(u'I have a course with 1 section$')
def have_a_course_with_1_section(step): def have_a_course_with_1_section(step):
clear_courses() clear_courses()
course = CourseFactory.create() course = world.CourseFactory.create()
section = ItemFactory.create(parent_location=course.location) section = world.ItemFactory.create(parent_location=course.location)
subsection1 = ItemFactory.create( subsection1 = world.ItemFactory.create(
parent_location=section.location, parent_location=section.location,
template='i4x://edx/templates/sequential/Empty', template='i4x://edx/templates/sequential/Empty',
display_name='Subsection One',) display_name='Subsection One',)
...@@ -27,20 +26,20 @@ def have_a_course_with_1_section(step): ...@@ -27,20 +26,20 @@ def have_a_course_with_1_section(step):
@step(u'I have a course with multiple sections$') @step(u'I have a course with multiple sections$')
def have_a_course_with_two_sections(step): def have_a_course_with_two_sections(step):
clear_courses() clear_courses()
course = CourseFactory.create() course = world.CourseFactory.create()
section = ItemFactory.create(parent_location=course.location) section = world.ItemFactory.create(parent_location=course.location)
subsection1 = ItemFactory.create( subsection1 = world.ItemFactory.create(
parent_location=section.location, parent_location=section.location,
template='i4x://edx/templates/sequential/Empty', template='i4x://edx/templates/sequential/Empty',
display_name='Subsection One',) display_name='Subsection One',)
section2 = ItemFactory.create( section2 = world.ItemFactory.create(
parent_location=course.location, parent_location=course.location,
display_name='Section Two',) display_name='Section Two',)
subsection2 = ItemFactory.create( subsection2 = world.ItemFactory.create(
parent_location=section2.location, parent_location=section2.location,
template='i4x://edx/templates/sequential/Empty', template='i4x://edx/templates/sequential/Empty',
display_name='Subsection Alpha',) display_name='Subsection Alpha',)
subsection3 = ItemFactory.create( subsection3 = world.ItemFactory.create(
parent_location=section2.location, parent_location=section2.location,
template='i4x://edx/templates/sequential/Empty', template='i4x://edx/templates/sequential/Empty',
display_name='Subsection Beta',) display_name='Subsection Beta',)
......
""" 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)
\ No newline at end of file
...@@ -101,6 +101,20 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -101,6 +101,20 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
self.assertEqual(reverse_tabs, course_tabs) 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): def test_delete(self):
import_from_xml(modulestore(), 'common/test/data/', ['full']) import_from_xml(modulestore(), 'common/test/data/', ['full'])
......
...@@ -22,25 +22,61 @@ from xmodule.modulestore.tests.factories import CourseFactory ...@@ -22,25 +22,61 @@ from xmodule.modulestore.tests.factories import CourseFactory
from models.settings.course_metadata import CourseMetadata from models.settings.course_metadata import CourseMetadata
from xmodule.modulestore.xml_importer import import_from_xml from xmodule.modulestore.xml_importer import import_from_xml
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
import time
# YYYY-MM-DDThh:mm:ss.s+/-HH:MM # YYYY-MM-DDThh:mm:ss.s+/-HH:MM
class ConvertersTestCase(TestCase): class ConvertersTestCase(TestCase):
@staticmethod @staticmethod
def struct_to_datetime(struct_time): 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, return datetime.datetime(struct_time.tm_year, struct_time.tm_mon,
struct_time.tm_min, struct_time.tm_sec, tzinfo=UTC()) 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): def compare_dates(self, date1, date2, expected_delta):
dt1 = ConvertersTestCase.struct_to_datetime(date1) dt1 = ConvertersTestCase.struct_to_datetime(date1)
dt2 = ConvertersTestCase.struct_to_datetime(date2) 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): 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)) '''Test conversion from iso compatible date strings to struct_time'''
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-01"),
self.compare_dates(converters.jsdate_to_time("2013-01-01T00:00"), converters.jsdate_to_time("2012-12-31T23:59"), datetime.timedelta(minutes=1)) converters.jsdate_to_time("2012-12-31"),
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)) 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): class CourseTestCase(ModuleStoreTestCase):
...@@ -104,7 +140,7 @@ class CourseDetailsTestCase(CourseTestCase): ...@@ -104,7 +140,7 @@ class CourseDetailsTestCase(CourseTestCase):
self.assertIsNone(jsondetails['effort'], "effort somehow initialized") self.assertIsNone(jsondetails['effort'], "effort somehow initialized")
def test_update_and_fetch(self): 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 = CourseDetails.fetch(self.course_location)
jsondetails.syllabus = "<a href='foo'>bar</a>" jsondetails.syllabus = "<a href='foo'>bar</a>"
# encode - decode to convert date fields and other data which changes form # encode - decode to convert date fields and other data which changes form
...@@ -182,7 +218,7 @@ class CourseDetailsViewTest(CourseTestCase): ...@@ -182,7 +218,7 @@ class CourseDetailsViewTest(CourseTestCase):
details_encoded = jsdate_to_time(details[field]) details_encoded = jsdate_to_time(details[field])
dt2 = ConvertersTestCase.struct_to_datetime(details_encoded) 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) self.assertEqual(dt1 - dt2, expected_delta, str(dt1) + "!=" + str(dt2) + " at " + context)
else: else:
self.fail(field + " missing from encoded but in details at " + context) self.fail(field + " missing from encoded but in details at " + context)
...@@ -269,7 +305,7 @@ class CourseMetadataEditingTest(CourseTestCase): ...@@ -269,7 +305,7 @@ class CourseMetadataEditingTest(CourseTestCase):
CourseTestCase.setUp(self) CourseTestCase.setUp(self)
# add in the full class too # add in the full class too
import_from_xml(modulestore(), 'common/test/data/', ['full']) 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): def test_fetch_initial_fields(self):
......
'''unit tests for course_info views and models.'''
from contentstore.tests.test_course_settings import CourseTestCase from contentstore.tests.test_course_settings import CourseTestCase
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
import json import json
class CourseUpdateTest(CourseTestCase): class CourseUpdateTest(CourseTestCase):
'''The do all and end all of unit test cases.'''
def test_course_update(self): def test_course_update(self):
'''Go through each interface and ensure it works.'''
# first get the update to force the creation # first get the update to force the creation
url = reverse('course_info', kwargs={'org': self.course_location.org, 'course': self.course_location.course, url = reverse('course_info',
'name': self.course_location.name}) kwargs={'org': self.course_location.org,
'course': self.course_location.course,
'name': self.course_location.name})
self.client.get(url) 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, payload = {'content': content,
'date': 'January 8, 2013'} 'date': 'January 8, 2013'}
url = reverse('course_info', kwargs={'org': self.course_location.org, 'course': self.course_location.course, url = reverse('course_info_json',
'provided_id': ''}) kwargs={'org': self.course_location.org,
'course': self.course_location.course,
'provided_id': ''})
resp = self.client.post(url, json.dumps(payload), "application/json") resp = self.client.post(url, json.dumps(payload), "application/json")
payload = json.loads(resp.content) payload = json.loads(resp.content)
self.assertHTMLEqual(content, payload['content'], "single iframe") self.assertHTMLEqual(payload['content'], content)
url = reverse('course_info', kwargs={'org': self.course_location.org, 'course': self.course_location.course, first_update_url = reverse('course_info_json',
'provided_id': payload['id']}) kwargs={'org': self.course_location.org,
content += '<div>div <p>p</p></div>' 'course': self.course_location.course,
'provided_id': payload['id']})
content += '<div>div <p>p<br/></p></div>'
payload['content'] = content 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")
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") 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)
self.assertHTMLEqual(content, json.loads(resp.content)['content'], "iframe w/ div") # 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)
from contentstore import utils """ Tests for utils. """
from contentstore import utils
import mock import mock
from django.test import TestCase from django.test import TestCase
from xmodule.modulestore.tests.factories import CourseFactory
from .utils import ModuleStoreTestCase
class LMSLinksTestCase(TestCase): class LMSLinksTestCase(TestCase):
""" Tests for LMS links. """
def about_page_test(self): def about_page_test(self):
""" Get URL for about page. """
location = 'i4x', 'mitX', '101', 'course', 'test' location = 'i4x', 'mitX', '101', 'course', 'test'
utils.get_course_id = mock.Mock(return_value="mitX/101/test") utils.get_course_id = mock.Mock(return_value="mitX/101/test")
link = utils.get_lms_link_for_about_page(location) link = utils.get_lms_link_for_about_page(location)
self.assertEquals(link, "//localhost:8000/courses/mitX/101/test/about") 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' location = 'i4x', 'mitX', '101', 'vertical', 'contacting_us'
utils.get_course_id = mock.Mock(return_value="mitX/101/test") utils.get_course_id = mock.Mock(return_value="mitX/101/test")
link = utils.get_lms_link_for_item(location, False) 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") 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) 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)
)
\ No newline at end of file
...@@ -2,6 +2,7 @@ from django.conf import settings ...@@ -2,6 +2,7 @@ from django.conf import settings
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.modulestore.exceptions import ItemNotFoundError from xmodule.modulestore.exceptions import ItemNotFoundError
from django.core.urlresolvers import reverse
DIRECT_ONLY_CATEGORIES = ['course', 'chapter', 'sequential', 'about', 'static_tab', 'course_info'] DIRECT_ONLY_CATEGORIES = ['course', 'chapter', 'sequential', 'about', 'static_tab', 'course_info']
...@@ -158,3 +159,35 @@ def update_item(location, value): ...@@ -158,3 +159,35 @@ def update_item(location, value):
get_modulestore(location).delete_item(location) get_modulestore(location).delete_item(location)
else: else:
get_modulestore(location).update_item(location, value) 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"
...@@ -3,19 +3,24 @@ from contentstore.utils import get_modulestore ...@@ -3,19 +3,24 @@ from contentstore.utils import get_modulestore
from xmodule.x_module import XModuleDescriptor from xmodule.x_module import XModuleDescriptor
from xmodule.modulestore.inheritance import own_metadata from xmodule.modulestore.inheritance import own_metadata
from xblock.core import Scope from xblock.core import Scope
from xmodule.course_module import CourseDescriptor
class CourseMetadata(object): class CourseMetadata(object):
''' '''
For CRUD operations on metadata fields which do not have specific editors on the other pages including any user generated ones. For CRUD operations on metadata fields which do not have specific editors
The objects have no predefined attrs but instead are obj encodings of the editable metadata. 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 @classmethod
def fetch(cls, course_location): 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): if not isinstance(course_location, Location):
course_location = Location(course_location) course_location = Location(course_location)
...@@ -29,7 +34,7 @@ class CourseMetadata(object): ...@@ -29,7 +34,7 @@ class CourseMetadata(object):
continue continue
if field.name not in cls.FILTERED_LIST: if field.name not in cls.FILTERED_LIST:
course[field.name] = field.read_from(descriptor) course[field.name] = field.read_json(descriptor)
return course return course
...@@ -51,22 +56,26 @@ class CourseMetadata(object): ...@@ -51,22 +56,26 @@ class CourseMetadata(object):
if hasattr(descriptor, k) and getattr(descriptor, k) != v: if hasattr(descriptor, k) and getattr(descriptor, k) != v:
dirty = True 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: elif hasattr(descriptor.lms, k) and getattr(descriptor.lms, k) != k:
dirty = True dirty = True
setattr(descriptor.lms, k, v) value = getattr(CourseDescriptor.lms, k).from_json(v)
setattr(descriptor.lms, k, value)
if dirty: 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 # Could just generate and return a course obj w/o doing any db reads,
# it persisted correctly # but I put the reads in as a means to confirm it persisted correctly
return cls.fetch(course_location) return cls.fetch(course_location)
@classmethod @classmethod
def delete_key(cls, course_location, payload): 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) descriptor = get_modulestore(course_location).get_item(course_location)
...@@ -76,6 +85,7 @@ class CourseMetadata(object): ...@@ -76,6 +85,7 @@ class CourseMetadata(object):
elif hasattr(descriptor.lms, key): elif hasattr(descriptor.lms, key):
delattr(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) return cls.fetch(course_location)
...@@ -127,8 +127,7 @@ DEBUG_TOOLBAR_PANELS = ( ...@@ -127,8 +127,7 @@ DEBUG_TOOLBAR_PANELS = (
'debug_toolbar.panels.sql.SQLDebugPanel', 'debug_toolbar.panels.sql.SQLDebugPanel',
'debug_toolbar.panels.signals.SignalDebugPanel', 'debug_toolbar.panels.signals.SignalDebugPanel',
'debug_toolbar.panels.logger.LoggingPanel', 'debug_toolbar.panels.logger.LoggingPanel',
# This is breaking Mongo updates-- Christina is investigating. 'debug_toolbar_mongo.panel.MongoDebugPanel',
# 'debug_toolbar_mongo.panel.MongoDebugPanel',
# Enabling the profiler has a weird bug as of django-debug-toolbar==0.9.4 and # Enabling the profiler has a weird bug as of django-debug-toolbar==0.9.4 and
# Django=1.3.1/1.4 where requests to views get duplicated (your method gets # Django=1.3.1/1.4 where requests to views get duplicated (your method gets
...@@ -143,4 +142,4 @@ DEBUG_TOOLBAR_CONFIG = { ...@@ -143,4 +142,4 @@ DEBUG_TOOLBAR_CONFIG = {
# To see stacktraces for MongoDB queries, set this to True. # To see stacktraces for MongoDB queries, set this to True.
# Stacktraces slow down page loads drastically (for pages with lots of queries). # Stacktraces slow down page loads drastically (for pages with lots of queries).
# DEBUG_TOOLBAR_MONGO_STACKTRACES = False DEBUG_TOOLBAR_MONGO_STACKTRACES = False
<% var allChecked = itemsChecked == items.length; %>
<section
<% if (allChecked) { %>
class="course-checklist is-completed"
<% } else { %>
class="course-checklist"
<% } %>
id="<%= 'course-checklist' + checklistIndex %>">
<% var widthPercentage = 'width:' + percentChecked + '%;'; %>
<span class="viz viz-checklist-status"><span class="viz value viz-checklist-status-value" style="<%= widthPercentage %>">
<span class="int"><%= percentChecked %></span>% of checklist completed</span></span>
<header>
<h3 class="checklist-title title-2 is-selectable" title="Collapse/Expand this Checklist">
<i class="ss-icon ss-symbolicons-standard icon-arrow ui-toggle-expansion">&#x25BE;</i>
<%= checklistShortDescription %></h3>
<span class="checklist-status status">
Tasks Completed: <span class="status-count"><%= itemsChecked %></span>/<span class="status-amount"><%= items.length %></span>
<i class="ss-icon ss-symbolicons-standard icon-confirm">&#x2713;</i>
</span>
</header>
<ul class="list list-tasks">
<% var taskIndex = 0; %>
<% _.each(items, function(item) { %>
<% var checked = item['is_checked']; %>
<li
<% if (checked) { %>
class="task is-completed"
<% } else { %>
class="task"
<% } %>
>
<% var taskId = 'course-checklist' + checklistIndex + '-task' + taskIndex; %>
<input type="checkbox" class="task-input" data-checklist="<%= checklistIndex %>" data-task="<%= taskIndex %>"
name="<%= taskId %>" id="<%= taskId %>"
<% if (checked) { %>
checked="checked"
<% } %>
>
<label class="task-details" for="<%= taskId %>">
<h4 class="task-name title title-3"><%= item['short_description'] %></h4>
<p class="task-description"><%= item['long_description'] %></p>
</label>
<% if (item['action_text'] !== '' && item['action_url'] !== '') { %>
<ul class="list-actions task-actions">
<li>
<a href="<%= item['action_url'] %>" class="action action-primary"
<% if (item['action_external']) { %>
rel="external" title="This link will open in a new browser window/tab"
<% } %>
><%= item['action_text'] %></a>
</li>
</ul>
<% } %>
</li>
<% taskIndex+=1; }) %>
</ul>
</section>
\ No newline at end of file
{ {
"js_files": [ "static_files": [
"/static/js/vendor/RequireJS.js", "js/vendor/RequireJS.js",
"/static/js/vendor/jquery.min.js", "js/vendor/jquery.min.js",
"/static/js/vendor/jquery-ui.min.js", "js/vendor/jquery-ui.min.js",
"/static/js/vendor/jquery.ui.draggable.js", "js/vendor/jquery.ui.draggable.js",
"/static/js/vendor/jquery.cookie.js", "js/vendor/jquery.cookie.js",
"/static/js/vendor/json2.js", "js/vendor/json2.js",
"/static/js/vendor/underscore-min.js", "js/vendor/underscore-min.js",
"/static/js/vendor/backbone-min.js" "js/vendor/backbone-min.js"
] ]
} }
...@@ -34,7 +34,10 @@ class CMS.Views.UnitEdit extends Backbone.View ...@@ -34,7 +34,10 @@ class CMS.Views.UnitEdit extends Backbone.View
@$('.components').sortable( @$('.components').sortable(
handle: '.drag-handle' 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' helper: 'clone'
opacity: '0.5' opacity: '0.5'
placeholder: 'component-placeholder' placeholder: 'component-placeholder'
...@@ -109,7 +112,14 @@ class CMS.Views.UnitEdit extends Backbone.View ...@@ -109,7 +112,14 @@ class CMS.Views.UnitEdit extends Backbone.View
id: $component.data('id') id: $component.data('id')
}, => }, =>
$component.remove() $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) -> deleteDraft: (event) ->
...@@ -157,7 +167,7 @@ class CMS.Views.UnitEdit extends Backbone.View ...@@ -157,7 +167,7 @@ class CMS.Views.UnitEdit extends Backbone.View
class CMS.Views.UnitEdit.NameEdit extends Backbone.View class CMS.Views.UnitEdit.NameEdit extends Backbone.View
events: events:
"keyup .unit-display-name-input": "saveName" 'change .unit-display-name-input': 'saveName'
initialize: => initialize: =>
@model.on('change:metadata', @render) @model.on('change:metadata', @render)
...@@ -180,29 +190,10 @@ class CMS.Views.UnitEdit.NameEdit extends Backbone.View ...@@ -180,29 +190,10 @@ class CMS.Views.UnitEdit.NameEdit extends Backbone.View
# Treat the metadata dictionary as immutable # Treat the metadata dictionary as immutable
metadata = $.extend({}, @model.get('metadata')) metadata = $.extend({}, @model.get('metadata'))
metadata.display_name = @$('.unit-display-name-input').val() 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) $('.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 class CMS.Views.UnitEdit.LocationState extends Backbone.View
initialize: => initialize: =>
@model.on('change:state', @render) @model.on('change:state', @render)
......
...@@ -14,10 +14,6 @@ $(document).ready(function () { ...@@ -14,10 +14,6 @@ $(document).ready(function () {
// scopes (namely the course-info tab) // scopes (namely the course-info tab)
window.$modalCover = $modalCover; 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); $body.append($modalCover);
$newComponentItem = $('.new-component-item'); $newComponentItem = $('.new-component-item');
$newComponentTypePicker = $('.new-component'); $newComponentTypePicker = $('.new-component');
...@@ -76,10 +72,7 @@ $(document).ready(function () { ...@@ -76,10 +72,7 @@ $(document).ready(function () {
}); });
// general link management - new window/tab // general link management - new window/tab
$('a[rel="external"]').attr('title', 'This link will open in a new browser window/tab').click(function (e) { $('a[rel="external"]').attr('title', 'This link will open in a new browser window/tab').bind('click', linkNewWindow);
window.open($(this).attr('href'));
e.preventDefault();
});
// general link management - lean modal window // 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' }); $('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 () { ...@@ -87,6 +80,10 @@ $(document).ready(function () {
(e).preventDefault(); (e).preventDefault();
}); });
// general link management - smooth scrolling page links
$('a[rel*="view"]').bind('click', linkSmoothScroll);
// toggling overview section details // toggling overview section details
$(function () { $(function () {
if ($('.courseware-section').length > 0) { if ($('.courseware-section').length > 0) {
...@@ -95,9 +92,9 @@ $(document).ready(function () { ...@@ -95,9 +92,9 @@ $(document).ready(function () {
}); });
$('.toggle-button-sections').bind('click', toggleSections); $('.toggle-button-sections').bind('click', toggleSections);
// autosave when a field is updated on the subsection page // autosave when leaving input field
$body.on('keyup', '.subsection-display-name-input, .unit-subtitle, .policy-list-value', checkForNewValue); $body.on('change', '.subsection-display-name-input', saveSubsection);
$('.subsection-display-name-input, .unit-subtitle, .policy-list-name, .policy-list-value').each(function (i) { $('.subsection-display-name-input').each(function () {
this.val = $(this).val(); this.val = $(this).val();
}); });
$("#start_date, #start_time, #due_date, #due_time").bind('change', autosaveInput); $("#start_date, #start_time, #due_date, #due_time").bind('change', autosaveInput);
...@@ -113,11 +110,6 @@ $(document).ready(function () { ...@@ -113,11 +110,6 @@ $(document).ready(function () {
// add new/delete subsection // add new/delete subsection
$('.new-subsection-item').bind('click', addNewSubsection); $('.new-subsection-item').bind('click', addNewSubsection);
$('.delete-subsection-button').bind('click', deleteSubsection); $('.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); $('.sync-date').bind('click', syncReleaseDate);
...@@ -156,10 +148,27 @@ $(document).ready(function () { ...@@ -156,10 +148,27 @@ $(document).ready(function () {
}); });
}); });
// function collapseAll(e) { function linkSmoothScroll(e) {
// $('.branch').addClass('collapsed'); (e).preventDefault();
// $('.expand-collapse-icon').removeClass('collapse').addClass('expand');
// } $.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) { function toggleSections(e) {
e.preventDefault(); e.preventDefault();
...@@ -219,56 +228,6 @@ function syncReleaseDate(e) { ...@@ -219,56 +228,6 @@ function syncReleaseDate(e) {
$("#start_time").val(""); $("#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) { function getEdxTimeFromDateTimeVals(date_val, time_val, format) {
var edxTimeStr = null; var edxTimeStr = null;
...@@ -294,31 +253,6 @@ function getEdxTimeFromDateTimeInputs(date_id, time_id, format) { ...@@ -294,31 +253,6 @@ function getEdxTimeFromDateTimeInputs(date_id, time_id, format) {
return getEdxTimeFromDateTimeVals(input_date, input_time, 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) { function autosaveInput(e) {
if (this.saveTimer) { if (this.saveTimer) {
clearTimeout(this.saveTimer); clearTimeout(this.saveTimer);
...@@ -332,6 +266,7 @@ function autosaveInput(e) { ...@@ -332,6 +266,7 @@ function autosaveInput(e) {
} }
function saveSubsection() { 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')) { if ($changedInput && !$changedInput.hasClass('no-spinner')) {
$spinner.css({ $spinner.css({
'position': 'absolute', 'position': 'absolute',
...@@ -354,20 +289,6 @@ function saveSubsection() { ...@@ -354,20 +289,6 @@ function saveSubsection() {
metadata[$(el).data("metadata-name")] = el.value; 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 // 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 // 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 // so make sure we're passing back the correct format
......
// 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);
}
});
// <!-- from https://github.com/Gazler/Underscore-Template-Loader/blob/master/index.html --> // <!-- 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) // 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. // so this only loads the lazily loaded ones.
(function() { (function () {
if (typeof window.templateLoader == 'function') return; if (typeof window.templateLoader == 'function') return;
var templateLoader = { var templateLoader = {
templateVersion: "0.0.16", templateVersion: "0.0.15",
templates: {}, templates: {},
loadRemoteTemplate: function(templateName, filename, callback) { // Control whether template caching in local memory occurs. Caching screws up development but may
if (!this.templates[templateName]) { // be a good optimization in production (it works fairly well).
var self = this; cacheTemplates: false,
jQuery.ajax({url : filename, loadRemoteTemplate: function (templateName, filename, callback) {
success : function(data) { if (!this.templates[templateName]) {
self.addTemplate(templateName, data); var self = this;
self.saveLocalTemplates(); jQuery.ajax({url: filename,
callback(data); success: function (data) {
}, self.addTemplate(templateName, data);
error : function(xhdr, textStatus, errorThrown) { self.saveLocalTemplates();
console.log(textStatus); }, callback(data);
dataType : "html" },
}) error: function (xhdr, textStatus, errorThrown) {
} console.log(textStatus);
else { },
callback(this.templates[templateName]); dataType: "html"
} })
}, }
else {
addTemplate: function(templateName, data) { callback(this.templates[templateName]);
// 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; 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
localStorageAvailable: function() { // if it maintains a separate cache of uncompiled ones
try { this.templates[templateName] = data;
// window.cachetemplates is global set in base.js to turn caching on/off },
return window.cachetemplates && 'localStorage' in window && window['localStorage'] !== null;
} catch (e) { localStorageAvailable: function () {
return false; 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); 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) { loadLocalTemplates: function () {
var templates = localStorage.getItem("templates"); if (this.localStorageAvailable()) {
if (templates) { var templateVersion = localStorage.getItem("templateVersion");
templates = JSON.parse(templates); if (templateVersion && templateVersion == this.templateVersion) {
for (var x in templates) { var templates = localStorage.getItem("templates");
if (!this.templates[x]) { if (templates) {
this.addTemplate(x, templates[x]); 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");
} }
}
} }
}
else {
localStorage.removeItem("templates");
localStorage.removeItem("templateVersion");
}
} }
}
}; };
templateLoader.loadLocalTemplates(); templateLoader.loadLocalTemplates();
window.templateLoader = templateLoader; window.templateLoader = templateLoader;
})(); })();
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
});
}
});
\ No newline at end of file
...@@ -142,8 +142,11 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({ ...@@ -142,8 +142,11 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
onDelete: function(event) { onDelete: function(event) {
event.preventDefault(); 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); var targetModel = this.eventModel(event);
this.modelDom(event).remove(); this.modelDom(event).remove();
var cacheThis = this; var cacheThis = this;
......
...@@ -25,11 +25,14 @@ CMS.Views.ValidatingView = Backbone.View.extend({ ...@@ -25,11 +25,14 @@ CMS.Views.ValidatingView = Backbone.View.extend({
for (var field in error) { for (var field in error) {
var ele = this.$el.find('#' + this.fieldToSelectorMap[field]); var ele = this.$el.find('#' + this.fieldToSelectorMap[field]);
this._cacheValidationErrors.push(ele); this._cacheValidationErrors.push(ele);
if ($(ele).is('div')) { var inputElements = 'input, textarea';
if ($(ele).is(inputElements)) {
$(ele).addClass('error');
}
else {
// put error on the contained inputs // put error on the contained inputs
$(ele).find('input, textarea').addClass('error'); $(ele).find(inputElements).addClass('error');
} }
else $(ele).addClass('error');
$(ele).parent().append(this.errorTemplate({message : error[field]})); $(ele).parent().append(this.errorTemplate({message : error[field]}));
} }
}, },
......
// studio base styling // studio - base styling
// ==================== // ====================
// basic reset // basic setup
html { html {
font-size: 62.5%; font-size: 62.5%;
overflow-y: scroll; overflow-y: scroll;
...@@ -9,7 +9,7 @@ html { ...@@ -9,7 +9,7 @@ html {
body { body {
@include font-size(16); @include font-size(16);
min-width: 980px; min-width: $fg-min-width;
background: $gray-l5; background: $gray-l5;
line-height: 1.6; line-height: 1.6;
color: $baseFontColor; color: $baseFontColor;
...@@ -214,7 +214,7 @@ h1 { ...@@ -214,7 +214,7 @@ h1 {
color: $gray-l2; color: $gray-l2;
} }
.title, .title-1 { .title-1 {
@include font-size(32); @include font-size(32);
margin: 0; margin: 0;
padding: 0; padding: 0;
...@@ -283,8 +283,8 @@ h1 { ...@@ -283,8 +283,8 @@ h1 {
.title-3 { .title-3 {
@include font-size(16); @include font-size(16);
margin: 0 0 ($baseline/4) 0; margin: 0 0 ($baseline/2) 0;
font-weight: 500; font-weight: 600;
} }
.title-4 { .title-4 {
...@@ -327,7 +327,8 @@ h1 { ...@@ -327,7 +327,8 @@ h1 {
} }
} }
.nav-related { // navigation
.nav-related, .nav-page {
.nav-item { .nav-item {
margin-bottom: ($baseline/4); margin-bottom: ($baseline/4);
...@@ -350,10 +351,11 @@ h1 { ...@@ -350,10 +351,11 @@ h1 {
// layout - grandfathered // layout - grandfathered
.main-wrapper { .main-wrapper {
position: relative; position: relative;
margin: 0 40px; margin: 40px;
} }
.inner-wrapper { .inner-wrapper {
@include clearfix();
position: relative; position: relative;
max-width: 1280px; max-width: 1280px;
margin: auto; margin: auto;
...@@ -363,6 +365,12 @@ h1 { ...@@ -363,6 +365,12 @@ h1 {
} }
} }
.main-column {
clear: both;
float: left;
width: 70%;
}
.sidebar { .sidebar {
float: right; float: right;
width: 28%; width: 28%;
...@@ -378,109 +386,6 @@ h1 { ...@@ -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 // UI - actions
.new-unit-item, .new-unit-item,
.new-subsection-item, .new-subsection-item,
...@@ -787,6 +692,10 @@ hr.divide { ...@@ -787,6 +692,10 @@ hr.divide {
word-wrap: break-word; word-wrap: break-word;
} }
hr.divider {
@extend .sr;
}
// ==================== // ====================
// js dependant // js dependant
...@@ -861,14 +770,4 @@ body.hide-wip { ...@@ -861,14 +770,4 @@ body.hide-wip {
.wip-box { .wip-box {
display: none; display: none;
} }
}
// ====================
// needed fudges for now
body.dashboard {
.my-classes {
margin-top: $baseline;
}
} }
\ No newline at end of file
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;
}
}
}
}
// studio - utilities - mixins and extends
// ====================
@mixin clearfix { @mixin clearfix {
&:after { &:after {
content: ''; content: '';
......
.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;
}
\ No newline at end of file
// 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;
}
}
\ No newline at end of file
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;
}
}
}
.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;
}
}
\ No newline at end of file
.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;
}
}
\ No newline at end of file
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;
}
}
}
}
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;
}
}
}
// studio - utilities - reset
// ====================
// * {
// @include box-sizing(border-box);
// }
html, body, div, span, applet, object, iframe, html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre, h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code, a, abbr, acronym, address, big, cite, code,
...@@ -18,7 +25,7 @@ time, mark, audio, video { ...@@ -18,7 +25,7 @@ time, mark, audio, video {
font: inherit; font: inherit;
vertical-align: baseline; vertical-align: baseline;
} }
/* HTML5 display-role reset for older browsers */
article, aside, details, figcaption, figure, article, aside, details, figcaption, figure,
footer, header, hgroup, menu, nav, section { footer, header, hgroup, menu, nav, section {
display: block; display: block;
...@@ -38,12 +45,6 @@ q:before, q:after { ...@@ -38,12 +45,6 @@ q:before, q:after {
content: none; content: none;
} }
/* remember to define visible focus styles!
:focus {
outline: ?????;
} */
/* remember to highlight inserts somehow! */
ins { ins {
text-decoration: none; text-decoration: none;
} }
...@@ -56,10 +57,11 @@ table { ...@@ -56,10 +57,11 @@ table {
border-spacing: 0; 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 { .ui-tabs {
padding: 0; padding: 0;
white-space: normal; white-space: normal;
...@@ -118,10 +120,7 @@ from the tabs component (used in the add component problem tab menu) ...@@ -118,10 +120,7 @@ from the tabs component (used in the add component problem tab menu)
padding: 0; padding: 0;
} }
/* reapplying the tab styles from unit.scss after // reapplying the tab styles from unit.scss after removing jquery ui ui-lightness styling
removing jquery ui ui-lightness styling
*/
.problem-type-tabs { .problem-type-tabs {
border:none; border:none;
list-style-type: none; list-style-type: none;
...@@ -146,26 +145,4 @@ removing jquery ui ui-lightness styling ...@@ -146,26 +145,4 @@ removing jquery ui ui-lightness styling
border: 0px; 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;
}
}
*/
} }
\ No newline at end of file
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;
}
}
}
}
}
}
}
}
}
// studio - utilities - variables
// ====================
$baseline: 20px; $baseline: 20px;
// grid // grid
...@@ -12,12 +15,19 @@ $fg-min-width: 900px; ...@@ -12,12 +15,19 @@ $fg-min-width: 900px;
// type // type
$sans-serif: 'Open Sans', $verdana; $sans-serif: 'Open Sans', $verdana;
$body-line-height: golden-ratio(.875em, 1); $body-line-height: golden-ratio(.875em, 1);
$error-red: rgb(253, 87, 87);
// colors - new for re-org // colors - new for re-org
$black: rgb(0,0,0); $black: rgb(0,0,0);
$black-t0: rgba(0,0,0,0.125); $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: 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: rgb(127,127,127);
$gray-l1: tint($gray,20%); $gray-l1: tint($gray,20%);
...@@ -25,6 +35,7 @@ $gray-l2: tint($gray,40%); ...@@ -25,6 +35,7 @@ $gray-l2: tint($gray,40%);
$gray-l3: tint($gray,60%); $gray-l3: tint($gray,60%);
$gray-l4: tint($gray,80%); $gray-l4: tint($gray,80%);
$gray-l5: tint($gray,90%); $gray-l5: tint($gray,90%);
$gray-l6: tint($gray,95%);
$gray-d1: shade($gray,20%); $gray-d1: shade($gray,20%);
$gray-d2: shade($gray,40%); $gray-d2: shade($gray,40%);
$gray-d3: shade($gray,60%); $gray-d3: shade($gray,60%);
...@@ -40,6 +51,12 @@ $blue-d1: shade($blue,20%); ...@@ -40,6 +51,12 @@ $blue-d1: shade($blue,20%);
$blue-d2: shade($blue,40%); $blue-d2: shade($blue,40%);
$blue-d3: shade($blue,60%); $blue-d3: shade($blue,60%);
$blue-d4: shade($blue,80%); $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: rgb(183, 37, 103);
$pink-l1: tint($pink,20%); $pink-l1: tint($pink,20%);
...@@ -51,6 +68,29 @@ $pink-d1: shade($pink,20%); ...@@ -51,6 +68,29 @@ $pink-d1: shade($pink,20%);
$pink-d2: shade($pink,40%); $pink-d2: shade($pink,40%);
$pink-d3: shade($pink,60%); $pink-d3: shade($pink,60%);
$pink-d4: shade($pink,80%); $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: rgb(37, 184, 90);
$green-l1: tint($green,20%); $green-l1: tint($green,20%);
...@@ -62,6 +102,12 @@ $green-d1: shade($green,20%); ...@@ -62,6 +102,12 @@ $green-d1: shade($green,20%);
$green-d2: shade($green,40%); $green-d2: shade($green,40%);
$green-d3: shade($green,60%); $green-d3: shade($green,60%);
$green-d4: shade($green,80%); $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: rgb(231, 214, 143);
$yellow-l1: tint($yellow,20%); $yellow-l1: tint($yellow,20%);
...@@ -73,6 +119,29 @@ $yellow-d1: shade($yellow,20%); ...@@ -73,6 +119,29 @@ $yellow-d1: shade($yellow,20%);
$yellow-d2: shade($yellow,40%); $yellow-d2: shade($yellow,40%);
$yellow-d3: shade($yellow,60%); $yellow-d3: shade($yellow,60%);
$yellow-d4: shade($yellow,80%); $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: rgba(0,0,0,0.2);
$shadow-l1: rgba(0,0,0,0.1); $shadow-l1: rgba(0,0,0,0.1);
...@@ -81,8 +150,6 @@ $shadow-d1: rgba(0,0,0,0.4); ...@@ -81,8 +150,6 @@ $shadow-d1: rgba(0,0,0,0.4);
// colors - inherited // colors - inherited
$baseFontColor: #3c3c3c; $baseFontColor: #3c3c3c;
$offBlack: #3c3c3c; $offBlack: #3c3c3c;
$orange: #edbd3c;
$red: #b20610;
$green: #108614; $green: #108614;
$lightGrey: #edf1f5; $lightGrey: #edf1f5;
$mediumGrey: #b0b6c2; $mediumGrey: #b0b6c2;
...@@ -95,4 +162,5 @@ $brightGreen: rgb(22, 202, 87); ...@@ -95,4 +162,5 @@ $brightGreen: rgb(22, 202, 87);
$disabledGreen: rgb(124, 206, 153); $disabledGreen: rgb(124, 206, 153);
$darkGreen: rgb(52, 133, 76); $darkGreen: rgb(52, 133, 76);
$lightBluishGrey: rgb(197, 207, 223); $lightBluishGrey: rgb(197, 207, 223);
$lightBluishGrey2: rgb(213, 220, 228); $lightBluishGrey2: rgb(213, 220, 228);
\ No newline at end of file $error-red: rgb(253, 87, 87);
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;
}
}
}
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;
}
}
}
}
}
}
}
}
}
}
// studio - css architecture
// ====================
// bourbon libs and resets
@import 'bourbon/bourbon'; @import 'bourbon/bourbon';
@import 'bourbon/addons/button'; @import 'bourbon/addons/button';
@import 'vendor/normalize'; @import 'vendor/normalize';
@import 'keyframes';
@import 'reset'; @import 'reset';
// utilities
@import 'variables';
@import 'mixins'; @import 'mixins';
@import 'cms_mixins';
// assets
@import 'assets/fonts';
@import 'assets/graphics';
@import 'assets/keyframes';
// 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 "fonts"; @import 'assets/content-types';
@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';
@import 'tender-widget';
@import 'content-types';
// xblock-related
@import 'module/module-styles.scss'; @import 'module/module-styles.scss';
@import 'descriptor/module-styles.scss'; @import 'descriptor/module-styles.scss';
// studio - elements - alerts, notifications, prompts
// ====================
// notifications // notifications
.wrapper-notification { .wrapper-notification {
@include clearfix(); @include clearfix();
......
//studio global footer // studio - elements - global footer
// ====================
.wrapper-footer { .wrapper-footer {
margin: ($baseline*1.5) 0 $baseline 0; margin: ($baseline*1.5) 0 $baseline 0;
padding: $baseline; padding: $baseline;
......
// 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;
}
\ No newline at end of file
// studio global header and navigation // studio - elements - global header
// ==================== // ====================
.wrapper-header { .wrapper-header {
......
// studio - elements - JQUI calendar
// ====================
.ui-datepicker { .ui-datepicker {
border-color: $darkGrey; border-color: $darkGrey;
border-radius: 2px; border-radius: 2px;
......
// studio - elements - modal windows
// ====================
.modal-cover { .modal-cover {
display: none; display: none;
position: fixed; position: fixed;
......
// studio - elements - navigation
// ====================
// common
// ====================
// primary
// ====================
// right hand side
// ====================
// tabs
// ====================
// dropdown
// ====================
//
\ No newline at end of file
// Studio - Sign In/Up // studio - views - sign up/in
// ==================== // ====================
body.signup, body.signin { body.signup, body.signin {
.wrapper-content { .wrapper-content {
......
.uploads { // studio - views - assets
// ====================
body.course.uploads {
input.asset-search-input { input.asset-search-input {
float: left; float: left;
width: 260px; width: 260px;
......
// 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);
}
}
\ No newline at end of file
.class-list { // studio - views - user dashboard
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 { body.dashboard {
z-index: 100;
display: block; .my-classes {
padding: 20px 25px; margin-top: $baseline;
line-height: 1.3; }
&:hover {
background: $paleYellow;
+ .view-live-button { .class-list {
opacity: 1.0; margin-top: 20px;
pointer-events: auto; 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 { .class-name {
display: block; display: block;
font-size: 19px; font-size: 19px;
font-weight: 300; font-weight: 300;
} }
.detail { .detail {
font-size: 14px; font-size: 14px;
font-weight: 400; font-weight: 400;
margin-right: 20px; margin-right: 20px;
color: #3c3c3c; color: #3c3c3c;
} }
// view live button // view live button
.view-live-button { .view-live-button {
z-index: 10000; z-index: 10000;
position: absolute; position: absolute;
top: 15px; top: 15px;
right: $baseline; right: $baseline;
padding: ($baseline/4) ($baseline/2); padding: ($baseline/4) ($baseline/2);
opacity: 0; opacity: 0;
pointer-events: none; pointer-events: none;
&:hover { &:hover {
opacity: 1.0; opacity: 1.0;
pointer-events: auto; pointer-events: auto;
}
} }
} }
}
.new-course {
.new-course { padding: 15px 25px;
padding: 15px 25px; margin-top: 20px;
margin-top: 20px; border-radius: 3px;
border-radius: 3px; border: 1px solid $darkGrey;
border: 1px solid $darkGrey; background: #fff;
background: #fff; box-shadow: 0 1px 2px rgba(0, 0, 0, .1);
box-shadow: 0 1px 2px rgba(0, 0, 0, .1);
@include clearfix;
.row {
margin-bottom: 15px;
@include clearfix; @include clearfix;
}
.column { .row {
float: left; margin-bottom: 15px;
width: 48%; @include clearfix;
} }
.column:first-child { .column {
margin-right: 4%; float: left;
} width: 48%;
}
.course-info { .column:first-child {
width: 600px; margin-right: 4%;
} }
label { .course-info {
display: block; width: 600px;
font-size: 13px; }
font-weight: 700;
}
.new-course-org, label {
.new-course-number, display: block;
.new-course-name { font-size: 13px;
width: 100%; font-weight: 700;
} }
.new-course-name { .new-course-org,
font-size: 19px; .new-course-number,
font-weight: 300; .new-course-name {
} width: 100%;
}
.new-course-save { .new-course-name {
@include blue-button; font-size: 19px;
} font-weight: 300;
}
.new-course-save {
@include blue-button;
}
.new-course-cancel { .new-course-cancel {
@include white-button; @include white-button;
}
} }
} }
\ No newline at end of file
.export { // studio - views - course export
// ====================
body.course.export {
.export-overview { .export-overview {
@extend .window; @extend .window;
@include clearfix; @include clearfix;
...@@ -118,6 +122,4 @@ ...@@ -118,6 +122,4 @@
} }
} }
} }
} }
\ No newline at end of file
.import { // studio - views - course import
// ====================
body.course.import {
.import-overview { .import-overview {
@extend .window; @extend .window;
@include clearfix; @include clearfix;
......
// how it works/not signed in index // studio - views - how it works
.index { // ====================
body.index {
&.not-signedin { &.not-signedin {
......
// Studio - Course Settings // studio - views - course settings
// ==================== // ====================
body.course.settings { body.course.settings {
.content-primary, .content-supplementary { .content-primary, .content-supplementary {
......
.static-pages { // studio - views - course static pages
// ====================
body.course.static-pages {
.new-static-page-button { .new-static-page-button {
@include grey-button; @include grey-button;
display: block; display: block;
...@@ -16,6 +20,51 @@ ...@@ -16,6 +20,51 @@
margin: 0 0 5px 0; 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 { .component-editor {
...@@ -35,6 +84,7 @@ ...@@ -35,6 +84,7 @@
} }
.component { .component {
position: relative;
border: 1px solid $mediumGrey; border: 1px solid $mediumGrey;
border-top: none; border-top: none;
...@@ -56,10 +106,13 @@ ...@@ -56,10 +106,13 @@
} }
.drag-handle { .drag-handle {
position: absolute;
display: block;
top: 0; top: 0;
right: 0; right: 0;
z-index: 11; z-index: 11;
width: 35px; width: 35px;
height: 100%;
border: none; border: none;
background: url(../img/drag-handles.png) center no-repeat #fff; background: url(../img/drag-handles.png) center no-repeat #fff;
...@@ -69,6 +122,7 @@ ...@@ -69,6 +122,7 @@
} }
.component-actions { .component-actions {
position: absolute;
top: 26px; top: 26px;
right: 44px; right: 44px;
} }
......
.course-info { // studio - views - course updates
// ====================
body.course.updates {
h2 { h2 {
margin-bottom: 24px; margin-bottom: 24px;
font-size: 22px; font-size: 22px;
......
.users { // studio - views - course users
// ====================
body.course.users {
.new-user-form { .new-user-form {
display: none; display: none;
padding: 15px 20px; padding: 15px 20px;
......
<%inherit file="base.html" />
<%block name="title">Page Not Found</%block>
<%block name="content">
<div class="wrapper-content wrapper">
<section class="content">
<h1>Page not found</h1>
<p>The page that you were looking for was not found. Go back to the <a href="/">homepage</a> or let us know about any pages that may have been moved at <a href="mailto:technical@edx.org">technical@edx.org</a>.</p>
</section>
</div>
</%block>
\ No newline at end of file
<%inherit file="base.html" />
<%block name="title">Server Error</%block>
<%block name="content">
<div class="wrapper-content wrapper">
<section class="content">
<h1>Currently the <em>edX</em> servers are down</h1>
<p>Our staff is currently working to get the site back up as soon as possible. Please email us at <a href="mailto:technical@edx.org">technical@edx.org</a> to report any problems or downtime.</p>
</section>
</div>
</%block>
\ No newline at end of file
<%inherit file="base.html" /> <%inherit file="base.html" />
<%! from django.core.urlresolvers import reverse %> <%! from django.core.urlresolvers import reverse %>
<%block name="bodyclass">is-signedin course uploads</%block> <%block name="bodyclass">is-signedin course uploads</%block>
<%block name="title">Uploads &amp; Files</%block> <%block name="title">Files &amp; Uploads</%block>
<%namespace name='static' file='static_content.html'/> <%namespace name='static' file='static_content.html'/>
......
...@@ -45,6 +45,7 @@ ...@@ -45,6 +45,7 @@
<script src="${static.url('js/vendor/jquery.leanModal.min.js')}"></script> <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.tablednd.js')}"></script>
<script src="${static.url('js/vendor/jquery.form.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/htmlmixed.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/CodeMirror/css.js')}"></script> <script type="text/javascript" src="${static.url('js/vendor/CodeMirror/css.js')}"></script>
<script type="text/javascript"> <script type="text/javascript">
......
<%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 @@ ...@@ -2,7 +2,7 @@
<%namespace name='static' file='static_content.html'/> <%namespace name='static' file='static_content.html'/>
<!-- TODO decode course # from context_course into title --> <!-- 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> <%block name="bodyclass">is-signedin course course-info updates</%block>
......
...@@ -31,18 +31,6 @@ ...@@ -31,18 +31,6 @@
</article> </article>
</div> </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"/>:&nbsp;<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="sidebar">
<div class="unit-settings window id-holder" data-id="${subsection.location}"> <div class="unit-settings window id-holder" data-id="${subsection.location}">
<h4 class="header">Subsection Settings</h4> <h4 class="header">Subsection Settings</h4>
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
<%namespace name='static' file='static_content.html'/> <%namespace name='static' file='static_content.html'/>
<%! from django.core.urlresolvers import reverse %> <%! 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="bodyclass">is-signedin course tools export</%block>
<%block name="content"> <%block name="content">
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
<%namespace name='static' file='static_content.html'/> <%namespace name='static' file='static_content.html'/>
<%! from django.core.urlresolvers import reverse %> <%! 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="bodyclass">is-signedin course tools import</%block>
<%block name="content"> <%block name="content">
......
<%inherit file="base.html" /> <%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="bodyclass">is-signedin index dashboard</%block>
<%block name="header_extras"> <%block name="header_extras">
......
<%inherit file="base.html" /> <%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> <%block name="bodyclass">is-signedin course users settings team</%block>
......
<%inherit file="base.html" /> <%inherit file="base.html" />
<%block name="title">Schedule &amp; Details</%block> <%block name="title">Schedule &amp; Details Settings</%block>
<%block name="bodyclass">is-signedin course schedule settings</%block> <%block name="bodyclass">is-signedin course schedule settings</%block>
<%namespace name='static' file='static_content.html'/> <%namespace name='static' file='static_content.html'/>
......
<%inherit file="base.html" /> <%inherit file="base.html" />
<%block name="title">Grading</%block> <%block name="title">Grading Settings</%block>
<%block name="bodyclass">is-signedin course grading settings</%block> <%block name="bodyclass">is-signedin course grading settings</%block>
<%namespace name='static' file='static_content.html'/> <%namespace name='static' file='static_content.html'/>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment