Commit 62523760 by Chris Dodge

Merge branch 'feature/cale/cms-master' of github.com:MITx/mitx into feature/cdodge/export

Conflicts:
	common/lib/xmodule/xmodule/contentstore/content.py
	common/lib/xmodule/xmodule/contentstore/mongo.py
parents 10b417c8 53b73ca3
...@@ -55,6 +55,39 @@ def create_new_course_group(creator, location, role): ...@@ -55,6 +55,39 @@ def create_new_course_group(creator, location, role):
return return
'''
This is to be called only by either a command line code path or through a app which has already
asserted permissions
'''
def _delete_course_group(location):
# remove all memberships
instructors = Group.objects.get(name=get_course_groupname_for_role(location, INSTRUCTOR_ROLE_NAME))
for user in instructors.user_set.all():
user.groups.remove(instructors)
user.save()
staff = Group.objects.get(name=get_course_groupname_for_role(location, STAFF_ROLE_NAME))
for user in staff.user_set.all():
user.groups.remove(staff)
user.save()
'''
This is to be called only by either a command line code path or through an app which has already
asserted permissions to do this action
'''
def _copy_course_group(source, dest):
instructors = Group.objects.get(name=get_course_groupname_for_role(source, INSTRUCTOR_ROLE_NAME))
new_instructors_group = Group.objects.get(name=get_course_groupname_for_role(dest, INSTRUCTOR_ROLE_NAME))
for user in instructors.user_set.all():
user.groups.add(new_instructors_group)
user.save()
staff = Group.objects.get(name=get_course_groupname_for_role(source, STAFF_ROLE_NAME))
new_staff_group = Group.objects.get(name=get_course_groupname_for_role(dest, STAFF_ROLE_NAME))
for user in staff.user_set.all():
user.groups.add(new_staff_group)
user.save()
def add_user_to_course_group(caller, user, location, role): def add_user_to_course_group(caller, user, location, role):
# only admins can add/remove other users # only admins can add/remove other users
......
...@@ -4,6 +4,7 @@ from xmodule.modulestore.django import modulestore ...@@ -4,6 +4,7 @@ from xmodule.modulestore.django import modulestore
from lxml import etree from lxml import etree
import re import re
from django.http import HttpResponseBadRequest from django.http import HttpResponseBadRequest
import logging
## 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
...@@ -23,27 +24,28 @@ def get_course_updates(location): ...@@ -23,27 +24,28 @@ 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.definition['data'], etree.XMLParser(remove_blank_text=True)) course_html_parsed = etree.fromstring(course_updates.definition['data'])
except etree.XMLSyntaxError: except etree.XMLSyntaxError:
course_html_parsed = etree.fromstring("<ol></ol>") course_html_parsed = etree.fromstring("<ol></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 = []
if course_html_parsed.tag == 'ol': if course_html_parsed.tag == 'ol':
# 0 is the oldest so that new ones get unique idx # 0 is the newest
for idx, update in enumerate(course_html_parsed.iter("li")): for idx, update in enumerate(course_html_parsed):
if (len(update) == 0): if (len(update) == 0):
continue continue
elif (len(update) == 1): elif (len(update) == 1):
content = update.find("h2").tail # could enforce that update[0].tag == 'h2'
content = update[0].tail
else: else:
content = etree.tostring(update[1]) content = "\n".join([etree.tostring(ele) for ele in update[1:]])
course_upd_collection.append({"id" : location_base + "/" + str(idx), # make the id on the client be 1..len w/ 1 being the oldest and len being the newest
course_upd_collection.append({"id" : location_base + "/" + str(len(course_html_parsed) - idx),
"date" : update.findtext("h2"), "date" : update.findtext("h2"),
"content" : content}) "content" : content})
# return newest to oldest
course_upd_collection.reverse()
return course_upd_collection return course_upd_collection
def update_course_updates(location, update, passed_id=None): def update_course_updates(location, update, passed_id=None):
...@@ -59,43 +61,25 @@ def update_course_updates(location, update, passed_id=None): ...@@ -59,43 +61,25 @@ 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.definition['data'], etree.XMLParser(remove_blank_text=True)) course_html_parsed = etree.fromstring(course_updates.definition['data'])
except etree.XMLSyntaxError: except etree.XMLSyntaxError:
course_html_parsed = etree.fromstring("<ol></ol>") course_html_parsed = etree.fromstring("<ol></ol>")
try: # No try/catch b/c failure generates an error back to client
new_html_parsed = etree.fromstring(update['content'], etree.XMLParser(remove_blank_text=True)) new_html_parsed = etree.fromstring('<li><h2>' + update['date'] + '</h2>' + update['content'] + '</li>')
except etree.XMLSyntaxError:
new_html_parsed = None
# 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
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?
if passed_id: if passed_id:
element = course_html_parsed.findall("li")[get_idx(passed_id)] idx = get_idx(passed_id)
element[0].text = update['date'] # idx is count from end of list
if (len(element) == 1): course_html_parsed[-idx] = new_html_parsed
if new_html_parsed is not None:
element[0].tail = None
element.append(new_html_parsed)
else:
element[0].tail = update['content']
else: else:
if new_html_parsed is not None: course_html_parsed.insert(0, new_html_parsed)
element[1] = new_html_parsed
else: idx = len(course_html_parsed)
element.pop(1)
element[0].tail = update['content']
else:
idx = len(course_html_parsed.findall("li"))
passed_id = course_updates.location.url() + "/" + str(idx) passed_id = course_updates.location.url() + "/" + str(idx)
element = etree.SubElement(course_html_parsed, "li")
date_element = etree.SubElement(element, "h2")
date_element.text = update['date']
if new_html_parsed is not None:
element.append(new_html_parsed)
else:
date_element.tail = update['content']
# update db record # update db record
course_updates.definition['data'] = etree.tostring(course_html_parsed) course_updates.definition['data'] = etree.tostring(course_html_parsed)
...@@ -121,15 +105,17 @@ def delete_course_update(location, update, passed_id): ...@@ -121,15 +105,17 @@ 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.definition['data'], etree.XMLParser(remove_blank_text=True)) course_html_parsed = etree.fromstring(course_updates.definition['data'])
except etree.XMLSyntaxError: except etree.XMLSyntaxError:
course_html_parsed = etree.fromstring("<ol></ol>") course_html_parsed = etree.fromstring("<ol></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?
element_to_delete = course_html_parsed.xpath('/ol/li[position()=' + str(get_idx(passed_id) + 1) + "]") idx = get_idx(passed_id)
if element_to_delete: # idx is count from end of list
course_html_parsed.remove(element_to_delete[0]) element_to_delete = course_html_parsed[-idx]
if element_to_delete is not None:
course_html_parsed.remove(element_to_delete)
# update db record # update db record
course_updates.definition['data'] = etree.tostring(course_html_parsed) course_updates.definition['data'] = etree.tostring(course_html_parsed)
...@@ -143,6 +129,6 @@ def get_idx(passed_id): ...@@ -143,6 +129,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 # 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))
\ No newline at end of file
###
### Script for cloning a course
###
from django.core.management.base import BaseCommand, CommandError
from xmodule.modulestore.store_utilities import clone_course
from xmodule.modulestore.django import modulestore
from xmodule.contentstore.django import contentstore
from xmodule.modulestore import Location
from xmodule.course_module import CourseDescriptor
from auth.authz import _copy_course_group
#
# To run from command line: rake cms:clone SOURCE_LOC=MITx/111/Foo1 DEST_LOC=MITx/135/Foo3
#
class Command(BaseCommand):
help = \
'''Clone a MongoDB backed course to another location'''
def handle(self, *args, **options):
if len(args) != 2:
raise CommandError("clone requires two arguments: <source-location> <dest-location>")
source_location_str = args[0]
dest_location_str = args[1]
ms = modulestore('direct')
cs = contentstore()
print "Cloning course {0} to {1}".format(source_location_str, dest_location_str)
source_location = CourseDescriptor.id_to_location(source_location_str)
dest_location = CourseDescriptor.id_to_location(dest_location_str)
if clone_course(ms, cs, source_location, dest_location):
print "copying User permissions..."
_copy_course_group(source_location, dest_location)
###
### Script for cloning a course
###
from django.core.management.base import BaseCommand, CommandError
from xmodule.modulestore.store_utilities import delete_course
from xmodule.modulestore.django import modulestore
from xmodule.contentstore.django import contentstore
from xmodule.modulestore import Location
from xmodule.course_module import CourseDescriptor
from prompt import query_yes_no
from auth.authz import _delete_course_group
#
# To run from command line: rake cms:delete_course LOC=MITx/111/Foo1
#
class Command(BaseCommand):
help = \
'''Delete a MongoDB backed course'''
def handle(self, *args, **options):
if len(args) != 1:
raise CommandError("delete_course requires one argument: <location>")
loc_str = args[0]
ms = modulestore('direct')
cs = contentstore()
if query_yes_no("Deleting course {0}. Confirm?".format(loc_str), default="no"):
if query_yes_no("Are you sure. This action cannot be undone!", default="no"):
loc = CourseDescriptor.id_to_location(loc_str)
if delete_course(ms, cs, loc) == True:
print 'removing User permissions from course....'
# in the django layer, we need to remove all the user permissions groups associated with this course
_delete_course_group(loc)
import sys
def query_yes_no(question, default="yes"):
"""Ask a yes/no question via raw_input() and return their answer.
"question" is a string that is presented to the user.
"default" is the presumed answer if the user just hits <Enter>.
It must be "yes" (the default), "no" or None (meaning
an answer is required of the user).
The "answer" return value is one of "yes" or "no".
"""
valid = {"yes":True, "y":True, "ye":True,
"no":False, "n":False}
if default == None:
prompt = " [y/n] "
elif default == "yes":
prompt = " [Y/n] "
elif default == "no":
prompt = " [y/N] "
else:
raise ValueError("invalid default answer: '%s'" % default)
while True:
sys.stdout.write(question + prompt)
choice = raw_input().lower()
if default is not None and choice == '':
return valid[default]
elif choice in valid:
return valid[choice]
else:
sys.stdout.write("Please respond with 'yes' or 'no' "\
"(or 'y' or 'n').\n")
\ No newline at end of file
...@@ -40,11 +40,8 @@ def set_module_info(store, location, post_data): ...@@ -40,11 +40,8 @@ def set_module_info(store, location, post_data):
module = store.clone_item(template_location, location) module = store.clone_item(template_location, location)
isNew = True isNew = True
logging.debug('post = {0}'.format(post_data))
if post_data.get('data') is not None: if post_data.get('data') is not None:
data = post_data['data'] data = post_data['data']
logging.debug('data = {0}'.format(data))
store.update_item(location, data) store.update_item(location, data)
# cdodge: note calling request.POST.get('children') will return None if children is an empty array # cdodge: note calling request.POST.get('children') will return None if children is an empty array
......
from django.test.testcases import TestCase
from cms.djangoapps.contentstore import utils
import mock
class LMSLinksTestCase(TestCase):
def about_page_test(self):
location = 'i4x','mitX','101','course', 'test'
utils.get_course_id = mock.Mock(return_value="mitX/101/test")
link = utils.get_lms_link_for_about_page(location)
self.assertEquals(link, "//localhost:8000/courses/mitX/101/test/about")
def ls_link_test(self):
location = 'i4x','mitX','101','vertical', 'contacting_us'
utils.get_course_id = mock.Mock(return_value="mitX/101/test")
link = utils.get_lms_link_for_item(location, False)
self.assertEquals(link, "//localhost:8000/courses/mitX/101/test/jump_to/i4x://mitX/101/vertical/contacting_us")
link = utils.get_lms_link_for_item(location, True)
self.assertEquals(link, "//preview.localhost:8000/courses/mitX/101/test/jump_to/i4x://mitX/101/vertical/contacting_us")
...@@ -13,6 +13,11 @@ from xmodule.modulestore.xml_importer import import_from_xml ...@@ -13,6 +13,11 @@ from xmodule.modulestore.xml_importer import import_from_xml
import copy import copy
from factories import * from factories import *
from xmodule.modulestore.store_utilities import clone_course
from xmodule.modulestore.store_utilities import delete_course
from xmodule.modulestore.django import modulestore
from xmodule.contentstore.django import contentstore
from xmodule.course_module import CourseDescriptor
def parse_json(response): def parse_json(response):
"""Parse response, which is assumed to be json""" """Parse response, which is assumed to be json"""
...@@ -339,4 +344,45 @@ class ContentStoreTest(TestCase): ...@@ -339,4 +344,45 @@ class ContentStoreTest(TestCase):
def test_edit_unit_full(self): def test_edit_unit_full(self):
self.check_edit_unit('full') self.check_edit_unit('full')
def test_clone_course(self):
import_from_xml(modulestore(), 'common/test/data/', ['full'])
resp = self.client.post(reverse('create_new_course'), self.course_data)
self.assertEqual(resp.status_code, 200)
data = parse_json(resp)
self.assertEqual(data['id'], 'i4x://MITx/999/course/Robot_Super_Course')
ms = modulestore('direct')
cs = contentstore()
source_location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
dest_location = CourseDescriptor.id_to_location('MITx/999/Robot_Super_Course')
clone_course(ms, cs, source_location, dest_location)
# now loop through all the units in the course and verify that the clone can render them, which
# means the objects are at least present
items = ms.get_items(Location(['i4x','edX', 'full', 'vertical', None]))
self.assertGreater(len(items), 0)
clone_items = ms.get_items(Location(['i4x', 'MITx','999','vertical', None]))
self.assertGreater(len(clone_items), 0)
for descriptor in items:
new_loc = descriptor.location._replace(org = 'MITx', course='999')
print "Checking {0} should now also be at {1}".format(descriptor.location.url(), new_loc.url())
resp = self.client.get(reverse('edit_unit', kwargs={'location': new_loc.url()}))
self.assertEqual(resp.status_code, 200)
def test_delete_course(self):
import_from_xml(modulestore(), 'common/test/data/', ['full'])
ms = modulestore('direct')
cs = contentstore()
location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
delete_course(ms, cs, location)
items = ms.get_items(Location(['i4x','edX', 'full', 'vertical', None]))
self.assertEqual(len(items), 0)
...@@ -3,6 +3,19 @@ from xmodule.modulestore import Location ...@@ -3,6 +3,19 @@ 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
DIRECT_ONLY_CATEGORIES = ['course', 'chapter', 'sequential', 'about', 'static_tab', 'course_info']
def get_modulestore(location):
"""
Returns the correct modulestore to use for modifying the specified location
"""
if not isinstance(location, Location):
location = Location(location)
if location.category in DIRECT_ONLY_CATEGORIES:
return modulestore('direct')
else:
return modulestore()
def get_course_location_for_item(location): def get_course_location_for_item(location):
''' '''
...@@ -60,20 +73,38 @@ def get_course_for_item(location): ...@@ -60,20 +73,38 @@ def get_course_for_item(location):
def get_lms_link_for_item(location, preview=False): def get_lms_link_for_item(location, preview=False):
location = Location(location)
if settings.LMS_BASE is not None: if settings.LMS_BASE is not None:
lms_link = "//{preview}{lms_base}/courses/{course_id}/jump_to/{location}".format( lms_link = "//{preview}{lms_base}/courses/{course_id}/jump_to/{location}".format(
preview='preview.' if preview else '', preview='preview.' if preview else '',
lms_base=settings.LMS_BASE, lms_base=settings.LMS_BASE,
# TODO: These will need to be changed to point to the particular instance of this problem in the particular course course_id=get_course_id(location),
course_id=modulestore().get_containing_courses(location)[0].id, location=Location(location)
location=location,
) )
else: else:
lms_link = None lms_link = None
return lms_link return lms_link
def get_lms_link_for_about_page(location):
"""
Returns the url to the course about page from the location tuple.
"""
if settings.LMS_BASE is not None:
lms_link = "//{lms_base}/courses/{course_id}/about".format(
lms_base=settings.LMS_BASE,
course_id=get_course_id(location)
)
else:
lms_link = None
return lms_link
def get_course_id(location):
"""
Returns the course_id from a given the location tuple.
"""
# TODO: These will need to be changed to point to the particular instance of this problem in the particular course
return modulestore().get_containing_courses(Location(location))[0].id
class UnitState(object): class UnitState(object):
draft = 'draft' draft = 'draft'
...@@ -103,3 +134,12 @@ def compute_unit_state(unit): ...@@ -103,3 +134,12 @@ def compute_unit_state(unit):
def get_date_display(date): def get_date_display(date):
return date.strftime("%d %B, %Y at %I:%M %p") return date.strftime("%d %B, %Y at %I:%M %p")
def update_item(location, value):
"""
If value is None, delete the db entry. Otherwise, update it using the correct modulestore.
"""
if value is None:
get_modulestore(location).delete_item(location)
else:
get_modulestore(location).update_item(location, value)
\ No newline at end of file
from xmodule.modulestore.django import modulestore
from xmodule.modulestore import Location
from xmodule.modulestore.exceptions import ItemNotFoundError
import json
from json.encoder import JSONEncoder
import time
from contentstore.utils import get_modulestore
from util.converters import jsdate_to_time, time_to_date
from cms.djangoapps.models.settings import course_grading
from cms.djangoapps.contentstore.utils import update_item
import re
import logging
class CourseDetails:
def __init__(self, location):
self.course_location = location # a Location obj
self.start_date = None # 'start'
self.end_date = None # 'end'
self.enrollment_start = None
self.enrollment_end = None
self.syllabus = None # a pdf file asset
self.overview = "" # html to render as the overview
self.intro_video = None # a video pointer
self.effort = None # int hours/week
@classmethod
def fetch(cls, course_location):
"""
Fetch the course details for the given course from persistence and return a CourseDetails model.
"""
if not isinstance(course_location, Location):
course_location = Location(course_location)
course = cls(course_location)
descriptor = get_modulestore(course_location).get_item(course_location)
course.start_date = descriptor.start
course.end_date = descriptor.end
course.enrollment_start = descriptor.enrollment_start
course.enrollment_end = descriptor.enrollment_end
temploc = course_location._replace(category='about', name='syllabus')
try:
course.syllabus = get_modulestore(temploc).get_item(temploc).definition['data']
except ItemNotFoundError:
pass
temploc = temploc._replace(name='overview')
try:
course.overview = get_modulestore(temploc).get_item(temploc).definition['data']
except ItemNotFoundError:
pass
temploc = temploc._replace(name='effort')
try:
course.effort = get_modulestore(temploc).get_item(temploc).definition['data']
except ItemNotFoundError:
pass
temploc = temploc._replace(name='video')
try:
raw_video = get_modulestore(temploc).get_item(temploc).definition['data']
course.intro_video = CourseDetails.parse_video_tag(raw_video)
except ItemNotFoundError:
pass
return course
@classmethod
def update_from_json(cls, jsondict):
"""
Decode the json into CourseDetails and save any changed attrs to the db
"""
## TODO make it an error for this to be undefined & for it to not be retrievable from modulestore
course_location = jsondict['course_location']
## Will probably want to cache the inflight courses because every blur generates an update
descriptor = get_modulestore(course_location).get_item(course_location)
dirty = False
## ??? Will this comparison work?
if 'start_date' in jsondict:
converted = jsdate_to_time(jsondict['start_date'])
else:
converted = None
if converted != descriptor.start:
dirty = True
descriptor.start = converted
if 'end_date' in jsondict:
converted = jsdate_to_time(jsondict['end_date'])
else:
converted = None
if converted != descriptor.end:
dirty = True
descriptor.end = converted
if 'enrollment_start' in jsondict:
converted = jsdate_to_time(jsondict['enrollment_start'])
else:
converted = None
if converted != descriptor.enrollment_start:
dirty = True
descriptor.enrollment_start = converted
if 'enrollment_end' in jsondict:
converted = jsdate_to_time(jsondict['enrollment_end'])
else:
converted = None
if converted != descriptor.enrollment_end:
dirty = True
descriptor.enrollment_end = converted
if dirty:
get_modulestore(course_location).update_metadata(course_location, descriptor.metadata)
# NOTE: below auto writes to the db w/o verifying that any of the fields actually changed
# to make faster, could compare against db or could have client send over a list of which fields changed.
temploc = Location(course_location)._replace(category='about', name='syllabus')
update_item(temploc, jsondict['syllabus'])
temploc = temploc._replace(name='overview')
update_item(temploc, jsondict['overview'])
temploc = temploc._replace(name='effort')
update_item(temploc, jsondict['effort'])
temploc = temploc._replace(name='video')
recomposed_video_tag = CourseDetails.recompose_video_tag(jsondict['intro_video'])
update_item(temploc, recomposed_video_tag)
# Could just generate and return a course obj w/o doing any db reads, but I put the reads in as a means to confirm
# it persisted correctly
return CourseDetails.fetch(course_location)
@staticmethod
def parse_video_tag(raw_video):
"""
Because the client really only wants the author to specify the youtube key, that's all we send to and get from the client.
The problem is that the db stores the html markup as well (which, of course, makes any sitewide changes to how we do videos
next to impossible.)
"""
if not raw_video:
return None
keystring_matcher = re.search('(?<=embed/)[a-zA-Z0-9_-]+', raw_video)
if keystring_matcher is None:
keystring_matcher = re.search('<?=\d+:[a-zA-Z0-9_-]+', raw_video)
if keystring_matcher:
return keystring_matcher.group(0)
else:
logging.warn("ignoring the content because it doesn't not conform to expected pattern: " + raw_video)
return None
@staticmethod
def recompose_video_tag(video_key):
# TODO should this use a mako template? Of course, my hope is that this is a short-term workaround for the db not storing
# the right thing
result = None
if video_key:
result = '<iframe width="560" height="315" src="http://www.youtube.com/embed/' + \
video_key + '?autoplay=1&rel=0" frameborder="0" allowfullscreen=""></iframe>'
return result
# TODO move to a more general util? Is there a better way to do the isinstance model check?
class CourseSettingsEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, CourseDetails) or isinstance(obj, course_grading.CourseGradingModel):
return obj.__dict__
elif isinstance(obj, Location):
return obj.dict()
elif isinstance(obj, time.struct_time):
return time_to_date(obj)
else:
return JSONEncoder.default(self, obj)
...@@ -59,6 +59,14 @@ MODULESTORE = { ...@@ -59,6 +59,14 @@ MODULESTORE = {
} }
} }
CONTENTSTORE = {
'ENGINE': 'xmodule.contentstore.mongo.MongoContentStore',
'OPTIONS': {
'host': 'localhost',
'db' : 'xcontent',
}
}
DATABASES = { DATABASES = {
'default': { 'default': {
'ENGINE': 'django.db.backends.sqlite3', 'ENGINE': 'django.db.backends.sqlite3',
...@@ -77,6 +85,8 @@ DATABASES = { ...@@ -77,6 +85,8 @@ DATABASES = {
} }
} }
LMS_BASE = "localhost:8000"
CACHES = { CACHES = {
# This is the cache used for most things. Askbot will not work without a # This is the cache used for most things. Askbot will not work without a
# functioning cache -- it relies on caching to load its settings in places. # functioning cache -- it relies on caching to load its settings in places.
......
<li class="input input-existing multi course-grading-assignment-list-item">
<div class="row row-col2">
<label for="course-grading-assignment-name">Assignment Type Name:</label>
<div class="field">
<div class="input course-grading-assignment-name">
<input type="text" class="long"
id="course-grading-assignment-name" value="<%= model.get('type') %>">
<span class="tip tip-stacked">e.g. Homework, Labs, Midterm Exams, Final Exam</span>
</div>
</div>
</div>
<div class="row row-col2">
<label for="course-grading-shortname">Abbreviation:</label>
<div class="field">
<div class="input course-grading-shortname">
<input type="text" class="short"
id="course-grading-assignment-shortname"
value="<%= model.get('short_label') %>">
<span class="tip tip-inline">e.g. HW, Midterm, Final</span>
</div>
</div>
</div>
<div class="row row-col2">
<label for="course-grading-gradeweight">Weight of Total
Grade:</label>
<div class="field">
<div class="input course-grading-gradeweight">
<input type="text" class="short"
id="course-grading-assignment-gradeweight"
value = "<%= model.get('weight') %>">
<span class="tip tip-inline">e.g. 25%</span>
</div>
</div>
</div>
<div class="row row-col2">
<label for="course-grading-assignment-totalassignments">Total
Number:</label>
<div class="field">
<div class="input course-grading-totalassignments">
<input type="text" class="short"
id="course-grading-assignment-totalassignments"
value = "<%= model.get('min_count') %>">
<span class="tip tip-inline">total exercises assigned</span>
</div>
</div>
</div>
<div class="row row-col2">
<label for="course-grading-assignment-droppable">Number of
Droppable:</label>
<div class="field">
<div class="input course-grading-droppable">
<input type="text" class="short"
id="course-grading-assignment-droppable"
value = "<%= model.get('drop_count') %>">
<span class="tip tip-inline">total exercises that won't be graded</span>
</div>
</div>
</div>
<a href="#" class="delete-button standard remove-item remove-grading-data"><span class="delete-icon"></span>Delete</a>
</li>
...@@ -3,6 +3,6 @@ ...@@ -3,6 +3,6 @@
"/static/js/vendor/jquery.min.js", "/static/js/vendor/jquery.min.js",
"/static/js/vendor/json2.js", "/static/js/vendor/json2.js",
"/static/js/vendor/underscore-min.js", "/static/js/vendor/underscore-min.js",
"/static/js/vendor/backbone.js" "/static/js/vendor/backbone-min.js"
] ]
} }
...@@ -56,6 +56,7 @@ class CMS.Views.ModuleEdit extends Backbone.View ...@@ -56,6 +56,7 @@ class CMS.Views.ModuleEdit extends Backbone.View
event.preventDefault() event.preventDefault()
data = @module.save() data = @module.save()
data.metadata = @metadata() data.metadata = @metadata()
$modalCover.hide()
@model.save(data).done( => @model.save(data).done( =>
# # showToastMessage("Your changes have been saved.", null, 3) # # showToastMessage("Your changes have been saved.", null, 3)
@module = null @module = null
...@@ -69,9 +70,11 @@ class CMS.Views.ModuleEdit extends Backbone.View ...@@ -69,9 +70,11 @@ class CMS.Views.ModuleEdit extends Backbone.View
event.preventDefault() event.preventDefault()
@$el.removeClass('editing') @$el.removeClass('editing')
@$component_editor().slideUp(150) @$component_editor().slideUp(150)
$modalCover.hide()
clickEditButton: (event) -> clickEditButton: (event) ->
event.preventDefault() event.preventDefault()
@$el.addClass('editing') @$el.addClass('editing')
$modalCover.show()
@$component_editor().slideDown(150) @$component_editor().slideDown(150)
@loadEdit() @loadEdit()
...@@ -33,6 +33,10 @@ class CMS.Views.TabsEdit extends Backbone.View ...@@ -33,6 +33,10 @@ class CMS.Views.TabsEdit extends Backbone.View
) )
$('.new-component-item').before(editor.$el) $('.new-component-item').before(editor.$el)
editor.$el.addClass('new')
setTimeout(=>
editor.$el.removeClass('new')
, 500)
editor.cloneTemplate( editor.cloneTemplate(
@model.get('id'), @model.get('id'),
......
...@@ -4,7 +4,6 @@ class CMS.Views.UnitEdit extends Backbone.View ...@@ -4,7 +4,6 @@ class CMS.Views.UnitEdit extends Backbone.View
'click .new-component .cancel-button': 'closeNewComponent' 'click .new-component .cancel-button': 'closeNewComponent'
'click .new-component-templates .new-component-template a': 'saveNewComponent' 'click .new-component-templates .new-component-template a': 'saveNewComponent'
'click .new-component-templates .cancel-button': 'closeNewComponent' 'click .new-component-templates .cancel-button': 'closeNewComponent'
'click .new-component-button': 'showNewComponentForm'
'click .delete-draft': 'deleteDraft' 'click .delete-draft': 'deleteDraft'
'click .create-draft': 'createDraft' 'click .create-draft': 'createDraft'
'click .publish-draft': 'publishDraft' 'click .publish-draft': 'publishDraft'
...@@ -54,30 +53,20 @@ class CMS.Views.UnitEdit extends Backbone.View ...@@ -54,30 +53,20 @@ class CMS.Views.UnitEdit extends Backbone.View
) )
) )
# New component creation
showNewComponentForm: (event) =>
event.preventDefault()
@$newComponentItem.addClass('adding')
$(event.target).fadeOut(150)
@$newComponentItem.css('height', @$newComponentTypePicker.outerHeight())
@$newComponentTypePicker.slideDown(250)
showComponentTemplates: (event) => showComponentTemplates: (event) =>
event.preventDefault() event.preventDefault()
type = $(event.currentTarget).data('type') type = $(event.currentTarget).data('type')
@$newComponentTypePicker.fadeOut(250) @$newComponentTypePicker.slideUp(250)
@$(".new-component-#{type}").fadeIn(250) @$(".new-component-#{type}").slideDown(250)
closeNewComponent: (event) => closeNewComponent: (event) =>
event.preventDefault() event.preventDefault()
@$newComponentTypePicker.slideUp(250) @$newComponentTypePicker.slideDown(250)
@$newComponentTemplatePickers.slideUp(250) @$newComponentTemplatePickers.slideUp(250)
@$newComponentButton.fadeIn(250)
@$newComponentItem.removeClass('adding') @$newComponentItem.removeClass('adding')
@$newComponentItem.find('.rendered-component').remove() @$newComponentItem.find('.rendered-component').remove()
@$newComponentItem.css('height', @$newComponentButton.outerHeight())
saveNewComponent: (event) => saveNewComponent: (event) =>
event.preventDefault() event.preventDefault()
......
...@@ -2,8 +2,6 @@ var $body; ...@@ -2,8 +2,6 @@ var $body;
var $modal; var $modal;
var $modalCover; var $modalCover;
var $newComponentItem; var $newComponentItem;
var $newComponentStep1;
var $newComponentStep2;
var $changedInput; var $changedInput;
var $spinner; var $spinner;
...@@ -16,6 +14,10 @@ $(document).ready(function() { ...@@ -16,6 +14,10 @@ $(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');
...@@ -39,6 +41,8 @@ $(document).ready(function() { ...@@ -39,6 +41,8 @@ $(document).ready(function() {
$('.unit .item-actions .delete-button').bind('click', deleteUnit); $('.unit .item-actions .delete-button').bind('click', deleteUnit);
$('.new-unit-item').bind('click', createNewUnit); $('.new-unit-item').bind('click', createNewUnit);
$('.collapse-all-button').bind('click', collapseAll);
// autosave when a field is updated on the subsection page // autosave when a field is updated on the subsection page
$body.on('keyup', '.subsection-display-name-input, .unit-subtitle, .policy-list-value', checkForNewValue); $body.on('keyup', '.subsection-display-name-input, .unit-subtitle, .policy-list-value', checkForNewValue);
$('.subsection-display-name-input, .unit-subtitle, .policy-list-name, .policy-list-value').each(function(i) { $('.subsection-display-name-input, .unit-subtitle, .policy-list-name, .policy-list-value').each(function(i) {
...@@ -105,15 +109,12 @@ $(document).ready(function() { ...@@ -105,15 +109,12 @@ $(document).ready(function() {
$('.edit-section-start-cancel').bind('click', cancelSetSectionScheduleDate); $('.edit-section-start-cancel').bind('click', cancelSetSectionScheduleDate);
$('.edit-section-start-save').bind('click', saveSetSectionScheduleDate); $('.edit-section-start-save').bind('click', saveSetSectionScheduleDate);
// modal upload asset dialog. Bind it in the initializer otherwise multiple hanlders will get registered causing
// pretty wacky stuff to happen
$('.file-input').bind('change', startUpload);
$('.upload-modal .choose-file-button').bind('click', showFileSelectionMenu); $('.upload-modal .choose-file-button').bind('click', showFileSelectionMenu);
$body.on('click', '.section-published-date .edit-button', editSectionPublishDate); $body.on('click', '.section-published-date .edit-button', editSectionPublishDate);
$body.on('click', '.section-published-date .schedule-button', editSectionPublishDate); $body.on('click', '.section-published-date .schedule-button', editSectionPublishDate);
$body.on('click', '.edit-subsection-publish-settings .save-button', saveSetSectionScheduleDate); $body.on('click', '.edit-subsection-publish-settings .save-button', saveSetSectionScheduleDate);
$body.on('click', '.edit-subsection-publish-settings .cancel-button', hideModal) $body.on('click', '.edit-subsection-publish-settings .cancel-button', hideModal);
$body.on('change', '.edit-subsection-publish-settings .start-date', function() { $body.on('change', '.edit-subsection-publish-settings .start-date', function() {
if($('.edit-subsection-publish-settings').find('.start-time').val() == '') { if($('.edit-subsection-publish-settings').find('.start-time').val() == '') {
$('.edit-subsection-publish-settings').find('.start-time').val('12:00am'); $('.edit-subsection-publish-settings').find('.start-time').val('12:00am');
...@@ -124,6 +125,11 @@ $(document).ready(function() { ...@@ -124,6 +125,11 @@ $(document).ready(function() {
}); });
}); });
function collapseAll(e) {
$('.branch').addClass('collapsed');
$('.expand-collapse-icon').removeClass('collapse').addClass('expand');
}
function editSectionPublishDate(e) { function editSectionPublishDate(e) {
e.preventDefault(); e.preventDefault();
$modal = $('.edit-subsection-publish-settings').show(); $modal = $('.edit-subsection-publish-settings').show();
...@@ -303,7 +309,7 @@ function checkForNewValue(e) { ...@@ -303,7 +309,7 @@ function checkForNewValue(e) {
this.saveTimer = setTimeout(function() { this.saveTimer = setTimeout(function() {
$changedInput = $(e.target); $changedInput = $(e.target);
saveSubsection() saveSubsection();
this.saveTimer = null; this.saveTimer = null;
}, 500); }, 500);
} }
...@@ -316,7 +322,7 @@ function autosaveInput(e) { ...@@ -316,7 +322,7 @@ function autosaveInput(e) {
this.saveTimer = setTimeout(function() { this.saveTimer = setTimeout(function() {
$changedInput = $(e.target); $changedInput = $(e.target);
saveSubsection() saveSubsection();
this.saveTimer = null; this.saveTimer = null;
}, 500); }, 500);
} }
...@@ -338,23 +344,22 @@ function saveSubsection() { ...@@ -338,23 +344,22 @@ function saveSubsection() {
// pull all 'normalized' metadata editable fields on page // pull all 'normalized' metadata editable fields on page
var metadata_fields = $('input[data-metadata-name]'); var metadata_fields = $('input[data-metadata-name]');
metadata = {}; var metadata = {};
for(var i=0; i< metadata_fields.length;i++) { for(var i=0; i< metadata_fields.length;i++) {
el = metadata_fields[i]; var el = metadata_fields[i];
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) // now add 'free-formed' metadata which are presented to the user as dual input fields (name/value)
$('ol.policy-list > li.policy-list-element').each( function(i, element) { $('ol.policy-list > li.policy-list-element').each( function(i, element) {
name = $(element).children('.policy-list-name').val(); var name = $(element).children('.policy-list-name').val();
val = $(element).children('.policy-list-value').val(); metadata[name] = $(element).children('.policy-list-value').val();
metadata[name] = val;
}); });
// now add any 'removed' policy metadata which is stored in a separate hidden div // now add any 'removed' policy metadata which is stored in a separate hidden div
// 'null' presented to the server means 'remove' // 'null' presented to the server means 'remove'
$("#policy-to-delete > li.policy-list-element").each(function(i, element) { $("#policy-to-delete > li.policy-list-element").each(function(i, element) {
name = $(element).children('.policy-list-name').val(); var name = $(element).children('.policy-list-name').val();
if (name != "") if (name != "")
metadata[name] = null; metadata[name] = null;
}); });
...@@ -390,7 +395,7 @@ function createNewUnit(e) { ...@@ -390,7 +395,7 @@ function createNewUnit(e) {
$.post('/clone_item', $.post('/clone_item',
{'parent_location' : parent, {'parent_location' : parent,
'template' : template, 'template' : template,
'display_name': 'New Unit', 'display_name': 'New Unit'
}, },
function(data) { function(data) {
// redirect to the edit page // redirect to the edit page
...@@ -480,7 +485,7 @@ function displayFinishedUpload(xhr) { ...@@ -480,7 +485,7 @@ function displayFinishedUpload(xhr) {
var template = $('#new-asset-element').html(); var template = $('#new-asset-element').html();
var html = Mustache.to_html(template, resp); var html = Mustache.to_html(template, resp);
$('table > tbody > tr:first').before(html); $('table > tbody').prepend(html);
} }
...@@ -493,6 +498,7 @@ function hideModal(e) { ...@@ -493,6 +498,7 @@ function hideModal(e) {
if(e) { if(e) {
e.preventDefault(); e.preventDefault();
} }
$('.file-input').unbind('change', startUpload);
$modal.hide(); $modal.hide();
$modalCover.hide(); $modalCover.hide();
} }
...@@ -593,9 +599,11 @@ function hideToastMessage(e) { ...@@ -593,9 +599,11 @@ function hideToastMessage(e) {
function addNewSection(e, isTemplate) { function addNewSection(e, isTemplate) {
e.preventDefault(); e.preventDefault();
$(e.target).addClass('disabled');
var $newSection = $($('#new-section-template').html()); var $newSection = $($('#new-section-template').html());
var $cancelButton = $newSection.find('.new-section-name-cancel'); var $cancelButton = $newSection.find('.new-section-name-cancel');
$('.new-courseware-section-button').after($newSection); $('.courseware-overview').prepend($newSection);
$newSection.find('.new-section-name').focus().select(); $newSection.find('.new-section-name').focus().select();
$newSection.find('.section-name-form').bind('submit', saveNewSection); $newSection.find('.section-name-form').bind('submit', saveNewSection);
$cancelButton.bind('click', cancelNewSection); $cancelButton.bind('click', cancelNewSection);
...@@ -632,11 +640,14 @@ function saveNewSection(e) { ...@@ -632,11 +640,14 @@ function saveNewSection(e) {
function cancelNewSection(e) { function cancelNewSection(e) {
e.preventDefault(); e.preventDefault();
$('.new-courseware-section-button').removeClass('disabled');
$(this).parents('section.new-section').remove(); $(this).parents('section.new-section').remove();
} }
function addNewCourse(e) { function addNewCourse(e) {
e.preventDefault(); e.preventDefault();
$(e.target).hide();
var $newCourse = $($('#new-course-template').html()); var $newCourse = $($('#new-course-template').html());
var $cancelButton = $newCourse.find('.new-course-cancel'); var $cancelButton = $newCourse.find('.new-course-cancel');
$('.new-course-button').after($newCourse); $('.new-course-button').after($newCourse);
...@@ -664,7 +675,7 @@ function saveNewCourse(e) { ...@@ -664,7 +675,7 @@ function saveNewCourse(e) {
'template' : template, 'template' : template,
'org' : org, 'org' : org,
'number' : number, 'number' : number,
'display_name': display_name, 'display_name': display_name
}, },
function(data) { function(data) {
if (data.id != undefined) { if (data.id != undefined) {
...@@ -677,6 +688,7 @@ function saveNewCourse(e) { ...@@ -677,6 +688,7 @@ function saveNewCourse(e) {
function cancelNewCourse(e) { function cancelNewCourse(e) {
e.preventDefault(); e.preventDefault();
$('.new-course-button').show();
$(this).parents('section.new-course').remove(); $(this).parents('section.new-course').remove();
} }
...@@ -692,7 +704,7 @@ function addNewSubsection(e) { ...@@ -692,7 +704,7 @@ function addNewSubsection(e) {
var parent = $(this).parents("section.branch").data("id"); var parent = $(this).parents("section.branch").data("id");
$saveButton.data('parent', parent) $saveButton.data('parent', parent);
$saveButton.data('template', $(this).data('template')); $saveButton.data('template', $(this).data('template'));
$newSubsection.find('.new-subsection-form').bind('submit', saveNewSubsection); $newSubsection.find('.new-subsection-form').bind('submit', saveNewSubsection);
...@@ -757,7 +769,7 @@ function saveEditSectionName(e) { ...@@ -757,7 +769,7 @@ function saveEditSectionName(e) {
$spinner.show(); $spinner.show();
if (display_name == '') { if (display_name == '') {
alert("You must specify a name before saving.") alert("You must specify a name before saving.");
return; return;
} }
...@@ -794,13 +806,12 @@ function cancelSetSectionScheduleDate(e) { ...@@ -794,13 +806,12 @@ function cancelSetSectionScheduleDate(e) {
function saveSetSectionScheduleDate(e) { function saveSetSectionScheduleDate(e) {
e.preventDefault(); e.preventDefault();
input_date = $('.edit-subsection-publish-settings .start-date').val(); var input_date = $('.edit-subsection-publish-settings .start-date').val();
input_time = $('.edit-subsection-publish-settings .start-time').val(); var input_time = $('.edit-subsection-publish-settings .start-time').val();
start = getEdxTimeFromDateTimeVals(input_date, input_time); var start = getEdxTimeFromDateTimeVals(input_date, input_time);
id = $modal.attr('data-id'); var id = $modal.attr('data-id');
var $_this = $(this);
// call into server to commit the new order // call into server to commit the new order
$.ajax({ $.ajax({
......
CMS.Models.Location = Backbone.Model.extend({
defaults: {
tag: "",
org: "",
course: "",
category: "",
name: ""
},
toUrl: function(overrides) {
return
(overrides && overrides['tag'] ? overrides['tag'] : this.get('tag')) + "://" +
(overrides && overrides['org'] ? overrides['org'] : this.get('org')) + "/" +
(overrides && overrides['course'] ? overrides['course'] : this.get('course')) + "/" +
(overrides && overrides['category'] ? overrides['category'] : this.get('category')) + "/" +
(overrides && overrides['name'] ? overrides['name'] : this.get('name')) + "/";
},
_tagPattern : /[^:]+/g,
_fieldPattern : new RegExp('[^/]+','g'),
parse: function(payload) {
if (_.isArray(payload)) {
return {
tag: payload[0],
org: payload[1],
course: payload[2],
category: payload[3],
name: payload[4]
}
}
else if (_.isString(payload)) {
this._tagPattern.lastIndex = 0; // odd regex behavior requires this to be reset sometimes
var foundTag = this._tagPattern.exec(payload);
if (foundTag) {
this._fieldPattern.lastIndex = this._tagPattern.lastIndex + 1; // skip over the colon
return {
tag: foundTag[0],
org: this.getNextField(payload),
course: this.getNextField(payload),
category: this.getNextField(payload),
name: this.getNextField(payload)
}
}
else return null;
}
else {
return payload;
}
},
getNextField : function(payload) {
try {
return this._fieldPattern.exec(payload)[0];
}
catch (err) {
return "";
}
}
});
CMS.Models.CourseRelative = Backbone.Model.extend({
defaults: {
course_location : null, // must never be null, but here to doc the field
idx : null // the index making it unique in the containing collection (no implied sort)
}
});
CMS.Models.CourseRelativeCollection = Backbone.Collection.extend({
model : CMS.Models.CourseRelative
});
\ No newline at end of file
if (!CMS.Models['Settings']) CMS.Models.Settings = new Object();
CMS.Models.Settings.CourseDetails = Backbone.Model.extend({
defaults: {
location : null, // the course's Location model, required
start_date: null, // maps to 'start'
end_date: null, // maps to 'end'
enrollment_start: null,
enrollment_end: null,
syllabus: null,
overview: "",
intro_video: null,
effort: null // an int or null
},
// When init'g from html script, ensure you pass {parse: true} as an option (2nd arg to reset)
parse: function(attributes) {
if (attributes['course_location']) {
attributes.location = new CMS.Models.Location(attributes.course_location, {parse:true});
}
if (attributes['start_date']) {
attributes.start_date = new Date(attributes.start_date);
}
if (attributes['end_date']) {
attributes.end_date = new Date(attributes.end_date);
}
if (attributes['enrollment_start']) {
attributes.enrollment_start = new Date(attributes.enrollment_start);
}
if (attributes['enrollment_end']) {
attributes.enrollment_end = new Date(attributes.enrollment_end);
}
return attributes;
},
validate: function(newattrs) {
// Returns either nothing (no return call) so that validate works or an object of {field: errorstring} pairs
// A bit funny in that the video key validation is asynchronous; so, it won't stop the validation.
var errors = {};
if (newattrs.start_date && newattrs.end_date && newattrs.start_date >= newattrs.end_date) {
errors.end_date = "The course end date cannot be before the course start date.";
}
if (newattrs.start_date && newattrs.enrollment_start && newattrs.start_date < newattrs.enrollment_start) {
errors.enrollment_start = "The course start date cannot be before the enrollment start date.";
}
if (newattrs.enrollment_start && newattrs.enrollment_end && newattrs.enrollment_start >= newattrs.enrollment_end) {
errors.enrollment_end = "The enrollment start date cannot be after the enrollment end date.";
}
if (newattrs.end_date && newattrs.enrollment_end && newattrs.end_date < newattrs.enrollment_end) {
errors.enrollment_end = "The enrollment end date cannot be after the course end date.";
}
if (newattrs.intro_video && newattrs.intro_video !== this.get('intro_video')) {
if (this._videokey_illegal_chars.exec(newattrs.intro_video)) {
errors.intro_video = "Key should only contain letters, numbers, _, or -";
}
// TODO check if key points to a real video using google's youtube api
}
if (!_.isEmpty(errors)) return errors;
// NOTE don't return empty errors as that will be interpreted as an error state
},
url: function() {
var location = this.get('location');
return '/' + location.get('org') + "/" + location.get('course') + '/settings/' + location.get('name') + '/section/details';
},
_videokey_illegal_chars : /[^a-zA-Z0-9_-]/g,
save_videosource: function(newsource) {
// newsource either is <video youtube="speed:key, *"/> or just the "speed:key, *" string
// returns the videosource for the preview which iss the key whose speed is closest to 1
if (_.isEmpty(newsource) && !_.isEmpty(this.get('intro_video'))) this.save({'intro_video': null});
// TODO remove all whitespace w/in string
else {
if (this.get('intro_video') !== newsource) this.save('intro_video', newsource);
}
return this.videosourceSample();
},
videosourceSample : function() {
if (this.has('intro_video')) return "http://www.youtube.com/embed/" + this.get('intro_video');
else return "";
}
});
if (!CMS.Models['Settings']) CMS.Models.Settings = new Object();
CMS.Models.Settings.CourseGradingPolicy = Backbone.Model.extend({
defaults : {
course_location : null,
graders : null, // CourseGraderCollection
grade_cutoffs : null, // CourseGradeCutoff model
grace_period : null // either null or { hours: n, minutes: m, ...}
},
parse: function(attributes) {
if (attributes['course_location']) {
attributes.course_location = new CMS.Models.Location(attributes.course_location, {parse:true});
}
if (attributes['graders']) {
var graderCollection;
if (this.has('graders')) {
graderCollection = this.get('graders');
graderCollection.reset(attributes.graders);
}
else {
graderCollection = new CMS.Models.Settings.CourseGraderCollection(attributes.graders);
graderCollection.course_location = attributes['course_location'] || this.get('course_location');
}
attributes.graders = graderCollection;
}
return attributes;
},
url : function() {
var location = this.get('course_location');
return '/' + location.get('org') + "/" + location.get('course') + '/settings/' + location.get('name') + '/section/grading';
},
gracePeriodToDate : function() {
var newDate = new Date();
if (this.has('grace_period') && this.get('grace_period')['hours'])
newDate.setHours(this.get('grace_period')['hours']);
else newDate.setHours(0);
if (this.has('grace_period') && this.get('grace_period')['minutes'])
newDate.setMinutes(this.get('grace_period')['minutes']);
else newDate.setMinutes(0);
if (this.has('grace_period') && this.get('grace_period')['seconds'])
newDate.setSeconds(this.get('grace_period')['seconds']);
else newDate.setSeconds(0);
return newDate;
},
dateToGracePeriod : function(date) {
return {hours : date.getHours(), minutes : date.getMinutes(), seconds : date.getSeconds() };
}
});
CMS.Models.Settings.CourseGrader = Backbone.Model.extend({
defaults: {
"type" : "", // must be unique w/in collection (ie. w/in course)
"min_count" : 1,
"drop_count" : 0,
"short_label" : "", // what to use in place of type if space is an issue
"weight" : 0 // int 0..100
},
parse : function(attrs) {
if (attrs['weight']) {
if (!_.isNumber(attrs.weight)) attrs.weight = parseInt(attrs.weight);
}
if (attrs['min_count']) {
if (!_.isNumber(attrs.min_count)) attrs.min_count = parseInt(attrs.min_count);
}
if (attrs['drop_count']) {
if (!_.isNumber(attrs.drop_count)) attrs.drop_count = parseInt(attrs.drop_count);
}
return attrs;
},
validate : function(attrs) {
var errors = {};
if (attrs['type']) {
if (_.isEmpty(attrs['type'])) {
errors.type = "The assignment type must have a name.";
}
else {
// FIXME somehow this.collection is unbound sometimes. I can't track down when
var existing = this.collection && this.collection.some(function(other) { return (other != this) && (other.get('type') == attrs['type']);}, this);
if (existing) {
errors.type = "There's already another assignment type with this name.";
}
}
}
if (attrs['weight']) {
if (!isFinite(attrs.weight) || /\D+/.test(attrs.weight)) {
errors.weight = "Please enter an integer between 0 and 100.";
}
else {
attrs.weight = parseInt(attrs.weight); // see if this ensures value saved is int
if (this.collection && attrs.weight > 0) {
// FIXME b/c saves don't update the models if validation fails, we should
// either revert the field value to the one in the model and make them make room
// or figure out a wholistic way to balance the vals across the whole
// if ((this.collection.sumWeights() + attrs.weight - this.get('weight')) > 100)
// errors.weight = "The weights cannot add to more than 100.";
}
}}
if (attrs['min_count']) {
if (!isFinite(attrs.min_count) || /\D+/.test(attrs.min_count)) {
errors.min_count = "Please enter an integer.";
}
else attrs.min_count = parseInt(attrs.min_count);
}
if (attrs['drop_count']) {
if (!isFinite(attrs.drop_count) || /\D+/.test(attrs.drop_count)) {
errors.drop_count = "Please enter an integer.";
}
else attrs.drop_count = parseInt(attrs.drop_count);
}
if (attrs['min_count'] && attrs['drop_count'] && attrs.drop_count > attrs.min_count) {
errors.drop_count = "Cannot drop more " + attrs.type + " than will assigned.";
}
if (!_.isEmpty(errors)) return errors;
}
});
CMS.Models.Settings.CourseGraderCollection = Backbone.Collection.extend({
model : CMS.Models.Settings.CourseGrader,
course_location : null, // must be set to a Location object
url : function() {
return '/' + this.course_location.get('org') + "/" + this.course_location.get('course') + '/grades/' + this.course_location.get('name') + '/';
},
sumWeights : function() {
return this.reduce(function(subtotal, grader) { return subtotal + grader.get('weight'); }, 0);
}
});
\ No newline at end of file
if (!CMS.Models['Settings']) CMS.Models.Settings = new Object();
CMS.Models.Settings.CourseSettings = Backbone.Model.extend({
// a container for the models representing the n possible tabbed states
defaults: {
courseLocation: null,
// NOTE: keep these sync'd w/ the data-section names in settings-page-menu
details: null,
faculty: null,
grading: null,
problems: null,
discussions: null
},
retrieve: function(submodel, callback) {
if (this.get(submodel)) callback();
else {
var cachethis = this;
switch (submodel) {
case 'details':
var details = new CMS.Models.Settings.CourseDetails({location: this.get('courseLocation')});
details.fetch( {
success : function(model) {
cachethis.set('details', model);
callback(model);
}
});
break;
case 'grading':
var grading = new CMS.Models.Settings.CourseGradingPolicy({course_location: this.get('courseLocation')});
grading.fetch( {
success : function(model) {
cachethis.set('grading', model);
callback(model);
}
});
break;
default:
break;
}
}
}
})
\ No newline at end of file
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
if (typeof window.templateLoader == 'function') return; if (typeof window.templateLoader == 'function') return;
var templateLoader = { var templateLoader = {
templateVersion: "0.0.8", templateVersion: "0.0.12",
templates: {}, templates: {},
loadRemoteTemplate: function(templateName, filename, callback) { loadRemoteTemplate: function(templateName, filename, callback) {
if (!this.templates[templateName]) { if (!this.templates[templateName]) {
...@@ -35,7 +35,8 @@ ...@@ -35,7 +35,8 @@
localStorageAvailable: function() { localStorageAvailable: function() {
try { try {
return 'localStorage' in window && window['localStorage'] !== null; // window.cachetemplates is global set in base.js to turn caching on/off
return window.cachetemplates && 'localStorage' in window && window['localStorage'] !== null;
} catch (e) { } catch (e) {
return false; return false;
} }
......
...@@ -62,6 +62,7 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({ ...@@ -62,6 +62,7 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
}, },
onNew: function(event) { onNew: function(event) {
event.preventDefault();
var self = this; var self = this;
// create new obj, insert into collection, and render this one ele overriding the hidden attr // create new obj, insert into collection, and render this one ele overriding the hidden attr
var newModel = new CMS.Models.CourseUpdate(); var newModel = new CMS.Models.CourseUpdate();
...@@ -69,6 +70,9 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({ ...@@ -69,6 +70,9 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
var $newForm = $(this.template({ updateModel : newModel })); var $newForm = $(this.template({ updateModel : newModel }));
var updateEle = this.$el.find("#course-update-list");
$(updateEle).prepend($newForm);
var $textArea = $newForm.find(".new-update-content").first(); var $textArea = $newForm.find(".new-update-content").first();
if (this.$codeMirror == null ) { if (this.$codeMirror == null ) {
this.$codeMirror = CodeMirror.fromTextArea($textArea.get(0), { this.$codeMirror = CodeMirror.fromTextArea($textArea.get(0), {
...@@ -78,8 +82,6 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({ ...@@ -78,8 +82,6 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
}); });
} }
var updateEle = this.$el.find("#course-update-list");
$(updateEle).prepend($newForm);
$newForm.addClass('editing'); $newForm.addClass('editing');
this.$currentPost = $newForm.closest('li'); this.$currentPost = $newForm.closest('li');
...@@ -93,15 +95,19 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({ ...@@ -93,15 +95,19 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
}, },
onSave: function(event) { onSave: function(event) {
event.preventDefault();
var targetModel = this.eventModel(event); var targetModel = this.eventModel(event);
console.log(this.contentEntry(event).val());
targetModel.set({ date : this.dateEntry(event).val(), content : this.$codeMirror.getValue() }); targetModel.set({ date : this.dateEntry(event).val(), content : this.$codeMirror.getValue() });
// push change to display, hide the editor, submit the change // push change to display, hide the editor, submit the change
targetModel.save({}, {error : function(model, xhr) {
// TODO use a standard component
window.alert(xhr.responseText);
}});
this.closeEditor(this); this.closeEditor(this);
targetModel.save();
}, },
onCancel: function(event) { onCancel: function(event) {
event.preventDefault();
// change editor contents back to model values and hide the editor // change editor contents back to model values and hide the editor
$(this.editor(event)).hide(); $(this.editor(event)).hide();
var targetModel = this.eventModel(event); var targetModel = this.eventModel(event);
...@@ -109,6 +115,7 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({ ...@@ -109,6 +115,7 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
}, },
onEdit: function(event) { onEdit: function(event) {
event.preventDefault();
var self = this; var self = this;
this.$currentPost = $(event.target).closest('li'); this.$currentPost = $(event.target).closest('li');
this.$currentPost.addClass('editing'); this.$currentPost.addClass('editing');
...@@ -131,6 +138,7 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({ ...@@ -131,6 +138,7 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
}, },
onDelete: function(event) { onDelete: function(event) {
event.preventDefault();
// TODO ask for confirmation // TODO ask for confirmation
// remove the dom element and delete the model // remove the dom element and delete the model
var targetModel = this.eventModel(event); var targetModel = this.eventModel(event);
...@@ -158,6 +166,8 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({ ...@@ -158,6 +166,8 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
self.$currentPost.find('form').hide(); self.$currentPost.find('form').hide();
window.$modalCover.unbind('click'); window.$modalCover.unbind('click');
window.$modalCover.hide(); window.$modalCover.hide();
this.$codeMirror = null;
self.$currentPost.find('.CodeMirror').remove();
}, },
// Dereferencing from events to screen elements // Dereferencing from events to screen elements
...@@ -271,5 +281,7 @@ CMS.Views.ClassInfoHandoutsView = Backbone.View.extend({ ...@@ -271,5 +281,7 @@ CMS.Views.ClassInfoHandoutsView = Backbone.View.extend({
this.$form.hide(); this.$form.hide();
window.$modalCover.unbind('click'); window.$modalCover.unbind('click');
window.$modalCover.hide(); window.$modalCover.hide();
self.$form.find('.CodeMirror').remove();
this.$codeMirror = null;
} }
}); });
\ No newline at end of file
CMS.Models.AssignmentGrade = Backbone.Model.extend({
defaults : {
graderType : null, // the type label (string). May be "Not Graded" which implies None. I'd like to use id but that's ephemeral
location : null // A location object
},
initialize : function(attrs) {
if (attrs['assignmentUrl']) {
this.set('location', new CMS.Models.Location(attrs['assignmentUrl'], {parse: true}));
}
},
parse : function(attrs) {
if (attrs && attrs['location']) {
attrs.location = new CMS.Models.Location(attrs['location'], {parse: true});
}
},
urlRoot : function() {
if (this.has('location')) {
var location = this.get('location');
return '/' + location.get('org') + "/" + location.get('course') + '/' + location.get('category') + '/'
+ location.get('name') + '/gradeas/';
}
else return "";
}
});
CMS.Views.OverviewAssignmentGrader = Backbone.View.extend({
// instantiate w/ { graders : CourseGraderCollection, el : <the gradable-status div> }
events : {
"click .menu-toggle" : "showGradeMenu",
"click .menu li" : "selectGradeType"
},
initialize : function() {
// call template w/ {assignmentType : formatname, graders : CourseGraderCollection instance }
this.template = _.template(
// TODO move to a template file
'<h4 class="status-label"><%= assignmentType %></h4>' +
'<a data-tooltip="Mark/unmark this subsection as graded" class="menu-toggle" href="#">' +
'<% if (!hideSymbol) {%><span class="ss-icon ss-standard">&#x2713;</span><%};%>' +
'</a>' +
'<ul class="menu">' +
'<% graders.each(function(option) { %>' +
'<li><a <% if (option.get("type") == assignmentType) {%>class="is-selected" <%}%> href="#"><%= option.get("type") %></a></li>' +
'<% }) %>' +
'<li><a class="gradable-status-notgraded" href="#">Not Graded</a></li>' +
'</ul>');
this.assignmentGrade = new CMS.Models.AssignmentGrade({
assignmentUrl : this.$el.closest('.id-holder').data('id'),
graderType : this.$el.data('initial-status')});
// TODO throw exception if graders is null
this.graders = this.options['graders'];
var cachethis = this;
// defining here to get closure around this
this.removeMenu = function(e) {
e.preventDefault();
cachethis.$el.removeClass('is-active');
$(document).off('click', cachethis.removeMenu);
}
this.hideSymbol = this.options['hideSymbol'];
this.render();
},
render : function() {
this.$el.html(this.template({ assignmentType : this.assignmentGrade.get('graderType'), graders : this.graders,
hideSymbol : this.hideSymbol }));
if (this.assignmentGrade.has('graderType') && this.assignmentGrade.get('graderType') != "Not Graded") {
this.$el.addClass('is-set');
}
else {
this.$el.removeClass('is-set');
}
},
showGradeMenu : function(e) {
e.preventDefault();
// I sure hope this doesn't break anything but it's needed to keep the removeMenu from activating
e.stopPropagation();
// nasty global event trap :-(
$(document).on('click', this.removeMenu);
this.$el.addClass('is-active');
},
selectGradeType : function(e) {
e.preventDefault();
this.removeMenu(e);
// TODO I'm not happy with this string fetch via the html for what should be an id. I'd rather use the id attr
// of the CourseGradingPolicy model or null for Not Graded (NOTE, change template's if check for is-selected accordingly)
this.assignmentGrade.save('graderType', $(e.target).text());
this.render();
}
})
\ No newline at end of file
...@@ -5,14 +5,6 @@ ...@@ -5,14 +5,6 @@
background-color: #fff; background-color: #fff;
} }
.upload-button {
@include blue-button;
float: left;
margin-right: 20px;
padding: 8px 30px 10px;
font-size: 12px;
}
.asset-library { .asset-library {
@include clearfix; @include clearfix;
......
...@@ -6,11 +6,15 @@ ...@@ -6,11 +6,15 @@
body { body {
min-width: 980px; min-width: 980px;
background: #f3f4f5; background: rgb(240, 241, 245);
font-family: 'Open Sans', sans-serif;
font-size: 16px; font-size: 16px;
line-height: 1.6; line-height: 1.6;
color: #3c3c3c; color: $baseFontColor;
}
body,
input {
font-family: 'Open Sans', sans-serif;
} }
a { a {
...@@ -26,7 +30,8 @@ a { ...@@ -26,7 +30,8 @@ a {
h1 { h1 {
float: left; float: left;
font-size: 28px; font-size: 28px;
margin: 36px 6px; font-weight: 300;
margin: 24px 6px;
} }
.waiting { .waiting {
...@@ -34,8 +39,7 @@ h1 { ...@@ -34,8 +39,7 @@ h1 {
} }
.page-actions { .page-actions {
float: right; margin-bottom: 30px;
margin-top: 42px;
} }
.main-wrapper { .main-wrapper {
...@@ -53,13 +57,6 @@ h1 { ...@@ -53,13 +57,6 @@ h1 {
} }
} }
.window {
background: #fff;
border: 1px solid $darkGrey;
border-radius: 3px;
@include box-shadow(0 1px 2px rgba(0, 0, 0, .1));
}
.sidebar { .sidebar {
float: right; float: right;
width: 28%; width: 28%;
...@@ -80,17 +77,18 @@ footer { ...@@ -80,17 +77,18 @@ footer {
input[type="text"], input[type="text"],
input[type="email"], input[type="email"],
input[type="password"] { input[type="password"],
textarea.text {
padding: 6px 8px 8px; padding: 6px 8px 8px;
@include box-sizing(border-box); @include box-sizing(border-box);
border: 1px solid #b0b6c2; border: 1px solid $mediumGrey;
border-radius: 2px; border-radius: 2px;
@include linear-gradient(top, rgba(255, 255, 255, 0), rgba(255, 255, 255, .3)); @include linear-gradient($lightGrey, tint($lightGrey, 90%));
background-color: #edf1f5; background-color: $lightGrey;
@include box-shadow(0 1px 2px rgba(0, 0, 0, .1) inset); @include box-shadow(0 1px 2px rgba(0, 0, 0, .1) inset);
font-family: 'Open Sans', sans-serif; font-family: 'Open Sans', sans-serif;
font-size: 11px; font-size: 11px;
color: #3c3c3c; color: $baseFontColor;
outline: 0; outline: 0;
&::-webkit-input-placeholder, &::-webkit-input-placeholder,
...@@ -98,6 +96,11 @@ input[type="password"] { ...@@ -98,6 +96,11 @@ input[type="password"] {
&:-ms-input-placeholder { &:-ms-input-placeholder {
color: #979faf; color: #979faf;
} }
&:focus {
@include linear-gradient($paleYellow, tint($paleYellow, 90%));
outline: 0;
}
} }
input.search { input.search {
...@@ -107,7 +110,7 @@ input.search { ...@@ -107,7 +110,7 @@ input.search {
border-radius: 20px; border-radius: 20px;
background: url(../img/search-icon.png) no-repeat 8px 7px #edf1f5; background: url(../img/search-icon.png) no-repeat 8px 7px #edf1f5;
font-family: 'Open Sans', sans-serif; font-family: 'Open Sans', sans-serif;
color: #3c3c3c; color: $baseFontColor;
outline: 0; outline: 0;
&::-webkit-input-placeholder { &::-webkit-input-placeholder {
...@@ -126,12 +129,18 @@ code { ...@@ -126,12 +129,18 @@ code {
font-family: Monaco, monospace; font-family: Monaco, monospace;
} }
.CodeMirror {
font-size: 13px;
border: 1px solid $darkGrey;
background: #fff;
}
.text-editor { .text-editor {
width: 100%; width: 100%;
min-height: 80px; min-height: 80px;
padding: 10px; padding: 10px;
@include box-sizing(border-box); @include box-sizing(border-box);
border: 1px solid #b0b6c2; border: 1px solid $mediumGrey;
@include linear-gradient(top, rgba(255, 255, 255, 0), rgba(255, 255, 255, 0.3)); @include linear-gradient(top, rgba(255, 255, 255, 0), rgba(255, 255, 255, 0.3));
background-color: #edf1f5; background-color: #edf1f5;
@include box-shadow(0 1px 2px rgba(0, 0, 0, 0.1) inset); @include box-shadow(0 1px 2px rgba(0, 0, 0, 0.1) inset);
...@@ -173,27 +182,32 @@ code { ...@@ -173,27 +182,32 @@ code {
padding: 10px 0; padding: 10px 0;
} }
.details {
display: none;
margin-bottom: 30px;
font-size: 14px;
}
.window { .window {
margin-bottom: 20px; margin-bottom: 20px;
border: 1px solid $mediumGrey;
border-radius: 3px;
background: #fff;
@include box-shadow(0 1px 1px rgba(0, 0, 0, 0.1));
.window-contents { .window-contents {
padding: 20px; padding: 20px;
} }
.details {
margin-bottom: 30px;
font-size: 14px;
}
h4 { h4 {
padding: 6px 14px; padding: 6px 14px;
border-bottom: 1px solid #cbd1db; border-bottom: 1px solid $mediumGrey;
border-radius: 3px 3px 0 0; border-radius: 2px 2px 0 0;
@include linear-gradient(top, rgba(255, 255, 255, .3), rgba(255, 255, 255, 0) 70%); @include linear-gradient(top, rgba(255, 255, 255, .4), rgba(255, 255, 255, 0));
background-color: #edf1f5; background-color: $lightBluishGrey;
@include box-shadow(0 1px 0 rgba(255, 255, 255, .7) inset); @include box-shadow(0 1px 0 rgba(255, 255, 255, 0.2) inset);
font-size: 14px; font-size: 14px;
font-weight: 600; text-shadow: 0 1px 0 rgba(255, 255, 255, 0.3);
} }
label { label {
...@@ -345,8 +359,9 @@ body.show-wip { ...@@ -345,8 +359,9 @@ body.show-wip {
} }
.new-button { .new-button {
@include grey-button; @include green-button;
padding: 20px 0; font-size: 13px;
padding: 8px 20px 10px;
text-align: center; text-align: center;
&.big { &.big {
...@@ -368,3 +383,38 @@ body.show-wip { ...@@ -368,3 +383,38 @@ body.show-wip {
margin-right: 4px; margin-right: 4px;
} }
} }
.delete-button.standard {
&:hover {
background-color: tint($orange, 75%);
}
}
.tooltip {
position: absolute;
top: 0;
left: 0;
z-index: 99999;
padding: 0 10px;
border-radius: 3px;
background: rgba(0, 0, 0, 0.85);
font-size: 11px;
font-weight: normal;
line-height: 26px;
color: #fff;
pointer-events: none;
opacity: 0;
@include transition(opacity 0.1s ease-out);
&:after {
content: '▾';
display: block;
position: absolute;
bottom: -14px;
left: 50%;
margin-left: -7px;
font-size: 20px;
color: rgba(0, 0, 0, 0.85);
}
}
\ No newline at end of file
...@@ -47,18 +47,33 @@ ...@@ -47,18 +47,33 @@
} }
} }
@mixin green-button {
@include button;
border: 1px solid #0d7011;
border-radius: 3px;
@include linear-gradient(top, rgba(255, 255, 255, .3), rgba(255, 255, 255, 0));
background-color: $green;
color: #fff;
&:hover {
background-color: #129416;
color: #fff;
}
}
@mixin white-button { @mixin white-button {
@include button; @include button;
border: 1px solid $darkGrey; border: 1px solid $mediumGrey;
border-radius: 3px; border-radius: 3px;
@include linear-gradient(top, rgba(255, 255, 255, 0.6), rgba(255, 255, 255, 0)); @include linear-gradient(top, rgba(255, 255, 255, 0.6), rgba(255, 255, 255, 0));
background-color: #dfe5eb; background-color: #dfe5eb;
@include box-shadow(0 1px 0 rgba(255, 255, 255, .3) inset); @include box-shadow(0 1px 0 rgba(255, 255, 255, .3) inset);
color: #778192; color: rgb(92, 103, 122);
text-shadow: 0 1px 0 rgba(255, 255, 255, .5);
&:hover { &:hover {
background-color: #f2f6f9; background-color: rgb(222, 236, 247);
color: #778192; color: rgb(92, 103, 122);
} }
} }
...@@ -92,6 +107,28 @@ ...@@ -92,6 +107,28 @@
} }
} }
@mixin green-button {
@include button;
border: 1px solid $darkGreen;
border-radius: 3px;
@include linear-gradient(top, rgba(255, 255, 255, .3), rgba(255, 255, 255, 0));
background-color: $green;
@include box-shadow(0 1px 0 rgba(255, 255, 255, .3) inset);
color: #fff;
&:hover {
background-color: $brightGreen;
color: #fff;
}
&.disabled {
border: 1px solid $disabledGreen !important;
background: $disabledGreen !important;
color: #fff !important;
@include box-shadow(none);
}
}
@mixin dark-grey-button { @mixin dark-grey-button {
@include button; @include button;
border: 1px solid #1c1e20; border: 1px solid #1c1e20;
...@@ -109,18 +146,17 @@ ...@@ -109,18 +146,17 @@
@mixin edit-box { @mixin edit-box {
padding: 15px 20px; padding: 15px 20px;
border-radius: 3px; border-radius: 3px;
border: 1px solid #5597dd; background-color: $lightBluishGrey2;
@include linear-gradient(top, rgba(255, 255, 255, 0.3), rgba(255, 255, 255, 0)); color: #3c3c3c;
background-color: #5597dd;
@include box-shadow(0 1px 0 rgba(255, 255, 255, .2) inset); @include box-shadow(0 1px 0 rgba(255, 255, 255, .2) inset);
label { label {
color: #fff; color: $baseFontColor;
} }
input, input,
textarea { textarea {
border: 1px solid #3c3c3c; border: 1px solid $darkGrey;
} }
textarea { textarea {
...@@ -140,21 +176,19 @@ ...@@ -140,21 +176,19 @@
} }
.save-button { .save-button {
@include orange-button; @include blue-button;
border-color: #3c3c3c;
margin-top: 0; margin-top: 0;
} }
.cancel-button { .cancel-button {
@include white-button; @include white-button;
border-color: #30649C;
margin-top: 0; margin-top: 0;
} }
} }
@mixin tree-view { @mixin tree-view {
border: 1px solid #ced2db; border: 1px solid $mediumGrey;
background: #edf1f5; background: $lightGrey;
.branch { .branch {
margin-bottom: 10px; margin-bottom: 10px;
...@@ -200,15 +234,10 @@ ...@@ -200,15 +234,10 @@
content: "- draft"; content: "- draft";
} }
.public-item:after {
content: "- public";
}
.private-item:after { .private-item:after {
content: "- private"; content: "- private";
} }
.public-item,
.private-item { .private-item {
color: #a4aab7; color: #a4aab7;
} }
...@@ -219,7 +248,11 @@ ...@@ -219,7 +248,11 @@
} }
a { a {
color: #2c2e33; color: $baseFontColor;
&.new-unit-item {
color: #6d788b;
}
} }
ol { ol {
...@@ -242,3 +275,14 @@ ...@@ -242,3 +275,14 @@
} }
} }
} }
@mixin sr-text {
border: 0;
clip: rect(0 0 0 0);
height: 1px;
margin: -1px;
overflow: hidden;
padding: 0;
position: absolute;
width: 1px;
}
\ No newline at end of file
input.courseware-unit-search-input { input.courseware-unit-search-input {
float: left; float: left;
width: 260px; width: 260px;
background-color: #fff; background-color: #fff;
} }
.courseware-overview { .branch {
.section-item {
@include clearfix();
.details {
display: block;
float: left;
margin-bottom: 0;
width: 650px;
}
.gradable-status {
float: right;
position: relative;
top: -4px;
right: 50px;
width: 145px;
.status-label {
position: absolute;
top: 2px;
right: -5px;
display: none;
width: 110px;
padding: 5px 40px 5px 10px;
@include border-radius(3px);
color: $lightGrey;
text-align: right;
font-size: 12px;
font-weight: bold;
line-height: 16px;
}
.menu-toggle {
z-index: 10;
position: absolute;
top: 0;
right: 5px;
padding: 5px;
color: $mediumGrey;
&:hover, &.is-active {
color: $blue;
}
}
.menu {
z-index: 1;
display: none;
opacity: 0.0;
position: absolute;
top: -1px;
left: 5px;
margin: 0;
padding: 8px 12px;
background: $white;
border: 1px solid $mediumGrey;
font-size: 12px;
@include border-radius(4px);
@include box-shadow(0 1px 2px rgba(0, 0, 0, .2));
@include transition(opacity .15s);
li {
width: 115px;
margin-bottom: 3px;
padding-bottom: 3px;
border-bottom: 1px solid $lightGrey;
&:last-child {
margin-bottom: 0;
padding-bottom: 0;
border: none;
a {
color: $darkGrey;
}
}
}
a {
color: $blue;
&.is-selected {
font-weight: bold;
}
}
}
// dropdown state
&.is-active {
.menu {
z-index: 1000;
display: block;
opacity: 1.0;
}
.menu-toggle {
z-index: 10000;
}
}
// set state
&.is-set {
.menu-toggle {
color: $blue;
}
.status-label {
display: block;
color: $blue;
}
}
}
}
}
}
.courseware-section { .courseware-section {
position: relative; position: relative;
background: #fff; background: #fff;
border: 1px solid $darkGrey;
border-radius: 3px; border-radius: 3px;
margin: 10px 0; border: 1px solid $mediumGrey;
margin-top: 15px;
padding-bottom: 12px; padding-bottom: 12px;
@include box-shadow(0 1px 2px rgba(0, 0, 0, .1)); @include box-shadow(0 1px 1px rgba(0, 0, 0, 0.1));
&:first-child { &:first-child {
margin-top: 0; margin-top: 0;
...@@ -49,7 +167,7 @@ input.courseware-unit-search-input { ...@@ -49,7 +167,7 @@ input.courseware-unit-search-input {
margin-right: 15px; margin-right: 15px;
strong { strong {
font-weight: 700; font-weight: bold;
} }
} }
...@@ -76,7 +194,7 @@ input.courseware-unit-search-input { ...@@ -76,7 +194,7 @@ input.courseware-unit-search-input {
background: none; background: none;
@include box-shadow(none); @include box-shadow(none);
font-size: 13px; font-size: 13px;
font-weight: 700; font-weight: bold;
color: $blue; color: $blue;
cursor: pointer; cursor: pointer;
} }
...@@ -96,12 +214,199 @@ input.courseware-unit-search-input { ...@@ -96,12 +214,199 @@ input.courseware-unit-search-input {
} }
header { header {
height: 75px; min-height: 75px;
@include clearfix();
.item-details, .section-published-date {
}
.item-details { .item-details {
display: inline-block;
padding: 20px 0 10px 0;
@include clearfix();
.section-name {
float: left;
margin-right: 10px;
width: 350px;
font-size: 19px;
font-weight: bold;
color: $blue;
}
.section-name-span {
cursor: pointer;
@include transition(color .15s);
&:hover {
color: $orange;
}
}
.section-name-edit {
position: relative;
width: 400px;
background: $white;
input {
font-size: 16px;
}
.save-button {
@include blue-button;
padding: 7px 20px 7px;
margin-right: 5px;
}
.cancel-button {
@include white-button;
padding: 7px 20px 7px;
}
}
.section-published-date {
float: right;
width: 265px;
margin-right: 220px;
@include border-radius(3px);
background: $lightGrey;
.published-status {
font-size: 12px;
margin-right: 15px;
strong {
font-weight: bold;
}
}
.schedule-button {
@include blue-button;
}
.edit-button {
@include blue-button;
}
.schedule-button,
.edit-button {
font-size: 11px;
padding: 3px 15px 5px;
}
}
.gradable-status {
position: absolute;
top: 20px;
right: 70px;
width: 145px;
.status-label {
position: absolute;
top: 0;
right: 2px;
display: none;
width: 100px;
padding: 10px 35px 10px 10px;
@include border-radius(3px);
background: $lightGrey;
color: $lightGrey;
text-align: right;
font-size: 12px;
font-weight: bold;
line-height: 16px;
}
.menu-toggle {
z-index: 10;
position: absolute;
top: 2px;
right: 5px;
padding: 5px;
color: $lightGrey;
&:hover, &.is-active {
color: $blue;
}
}
.menu {
z-index: 1;
display: none;
opacity: 0.0;
position: absolute;
top: -1px;
left: 2px;
margin: 0;
padding: 8px 12px;
background: $white;
border: 1px solid $mediumGrey;
font-size: 12px;
@include border-radius(4px);
@include box-shadow(0 1px 2px rgba(0, 0, 0, .2));
@include transition(opacity .15s);
@include transition(display .15s);
li {
width: 115px;
margin-bottom: 3px;
padding-bottom: 3px;
border-bottom: 1px solid $lightGrey;
&:last-child {
margin-bottom: 0;
padding-bottom: 0;
border: none;
a {
color: $darkGrey;
}
}
}
a {
&.is-selected {
font-weight: bold;
}
}
}
// dropdown state
&.is-active {
.menu {
z-index: 1000;
display: block;
opacity: 1.0;
}
.menu-toggle {
z-index: 10000;
}
}
// set state
&.is-set {
.menu-toggle {
color: $blue;
}
.status-label {
display: block;
color: $blue;
}
}
float: left; float: left;
padding: 21px 0 0; padding: 21px 0 0;
} }
}
.item-actions { .item-actions {
margin-top: 21px; margin-top: 21px;
...@@ -139,6 +444,10 @@ input.courseware-unit-search-input { ...@@ -139,6 +444,10 @@ input.courseware-unit-search-input {
} }
} }
.section-name-form {
margin-bottom: 15px;
}
.section-name-edit { .section-name-edit {
input { input {
font-size: 16px; font-size: 16px;
...@@ -146,13 +455,13 @@ input.courseware-unit-search-input { ...@@ -146,13 +455,13 @@ input.courseware-unit-search-input {
.save-button { .save-button {
@include blue-button; @include blue-button;
padding: 10px 20px; padding: 7px 20px 7px;
margin-right: 5px; margin-right: 5px;
} }
.cancel-button { .cancel-button {
@include white-button; @include white-button;
padding: 10px 20px; padding: 7px 20px 7px;
} }
} }
...@@ -161,7 +470,7 @@ input.courseware-unit-search-input { ...@@ -161,7 +470,7 @@ input.courseware-unit-search-input {
color: #878e9d; color: #878e9d;
strong { strong {
font-weight: 700; font-weight: bold;
} }
} }
...@@ -183,7 +492,7 @@ input.courseware-unit-search-input { ...@@ -183,7 +492,7 @@ input.courseware-unit-search-input {
&.new-section { &.new-section {
header { header {
height: auto; height: auto;
@include clearfix; @include clearfix();
} }
.expand-collapse-icon { .expand-collapse-icon {
...@@ -192,6 +501,17 @@ input.courseware-unit-search-input { ...@@ -192,6 +501,17 @@ input.courseware-unit-search-input {
} }
} }
.collapse-all-button {
float: right;
margin-top: 10px;
font-size: 13px;
color: $darkGrey;
.collapse-all-icon {
margin-right: 6px;
}
}
.new-section-name, .new-section-name,
.new-subsection-name-input { .new-subsection-name-input {
width: 515px; width: 515px;
...@@ -200,7 +520,7 @@ input.courseware-unit-search-input { ...@@ -200,7 +520,7 @@ input.courseware-unit-search-input {
.new-section-name-save, .new-section-name-save,
.new-subsection-name-save { .new-subsection-name-save {
@include blue-button; @include blue-button;
padding: 6px 20px 8px; padding: 4px 20px 7px;
margin: 0 5px; margin: 0 5px;
color: #fff !important; color: #fff !important;
} }
...@@ -208,7 +528,7 @@ input.courseware-unit-search-input { ...@@ -208,7 +528,7 @@ input.courseware-unit-search-input {
.new-section-name-cancel, .new-section-name-cancel,
.new-subsection-name-cancel { .new-subsection-name-cancel {
@include white-button; @include white-button;
padding: 6px 20px 8px; padding: 4px 20px 7px;
color: #8891a1 !important; color: #8891a1 !important;
} }
...@@ -292,3 +612,10 @@ input.courseware-unit-search-input { ...@@ -292,3 +612,10 @@ input.courseware-unit-search-input {
font-size: 16px; font-size: 16px;
} }
} }
.collapse-all-button {
float: right;
margin-top: 10px;
font-size: 13px;
color: $darkGrey;
}
\ No newline at end of file
...@@ -36,13 +36,6 @@ ...@@ -36,13 +36,6 @@
} }
} }
.new-course-button {
@include grey-button;
display: block;
padding: 20px;
text-align: center;
}
.new-course { .new-course {
padding: 15px 25px; padding: 15px 25px;
margin-top: 20px; margin-top: 20px;
...@@ -89,7 +82,6 @@ ...@@ -89,7 +82,6 @@
.new-course-save { .new-course-save {
@include blue-button; @include blue-button;
// padding: ;
} }
.new-course-cancel { .new-course-cancel {
......
...@@ -34,6 +34,14 @@ ...@@ -34,6 +34,14 @@
background: url(../img/video-icon.png) no-repeat; background: url(../img/video-icon.png) no-repeat;
} }
.upload-icon {
display: inline-block;
width: 22px;
height: 13px;
margin-right: 5px;
background: url(../img/upload-icon.png) no-repeat;
}
.list-icon { .list-icon {
display: inline-block; display: inline-block;
width: 14px; width: 14px;
...@@ -56,6 +64,27 @@ ...@@ -56,6 +64,27 @@
background: url(../img/home-icon.png) no-repeat; background: url(../img/home-icon.png) no-repeat;
} }
.small-home-icon {
display: inline-block;
width: 16px;
height: 14px;
background: url(../img/small-home-icon.png) no-repeat;
}
.log-out-icon {
display: inline-block;
width: 15px;
height: 13px;
background: url(../img/log-out-icon.png) no-repeat;
}
.collapse-all-icon {
display: inline-block;
width: 15px;
height: 9px;
background: url(../img/collapse-all-icon.png) no-repeat;
}
.calendar-icon { .calendar-icon {
display: inline-block; display: inline-block;
width: 12px; width: 12px;
......
...@@ -5,18 +5,15 @@ body.no-header { ...@@ -5,18 +5,15 @@ body.no-header {
} }
@mixin active { @mixin active {
@include linear-gradient(top, rgba(0, 0, 0, 0.6), rgba(0, 0, 0, 0.3)); @include linear-gradient(top, rgba(255, 255, 255, .4), rgba(255, 255, 255, 0));
@include box-shadow(0 2px 8px rgba(0, 0, 0, .7) inset); background-color: rgba(255, 255, 255, .3);
@include box-shadow(0 -1px 0 rgba(0, 0, 0, .2) inset, 0 1px 0 #fff inset);
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
} }
.primary-header { .primary-header {
width: 100%; width: 100%;
height: 36px; margin-bottom: 30px;
border-bottom: 1px solid #2c2e33;
@include linear-gradient(top, #686b76, #54565e);
font-size: 13px;
color: #fff;
@include box-shadow(0 1px 1px rgba(0, 0, 0, 0.2), 0 -1px 0px rgba(255, 255, 255, 0.05) inset);
&.active-tab-courseware #courseware-tab { &.active-tab-courseware #courseware-tab {
@include active; @include active;
...@@ -34,23 +31,16 @@ body.no-header { ...@@ -34,23 +31,16 @@ body.no-header {
@include active; @include active;
} }
&.active-tab-import #import-tab { &.active-tab-settings #settings-tab {
@include active; @include active;
} }
#import-tab { &.active-tab-import #import-tab {
@include box-shadow(1px 0 0 #787981 inset, -1px 0 0 #3d3e44 inset, 1px 0 0 #787981, -1px 0 0 #3d3e44); @include active;
}
.left {
width: 750px;
} }
.class-name { &.active-tab-export #export-tab {
max-width: 350px; @include active;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
} }
.drop-icon { .drop-icon {
...@@ -63,26 +53,57 @@ body.no-header { ...@@ -63,26 +53,57 @@ body.no-header {
line-height: 18px; line-height: 18px;
} }
a, .class-nav-bar {
.username { clear: both;
@include linear-gradient(top, rgba(255, 255, 255, .4), rgba(255, 255, 255, 0));
background-color: $lightBluishGrey;
@include box-shadow(0 1px 0 rgba(255, 255, 255, 0.2) inset, 0 -1px 0 rgba(0, 0, 0, 0.2) inset);
}
.class-nav {
@include clearfix;
a {
float: left; float: left;
display: inline-block; display: inline-block;
height: 29px; padding: 15px 25px 17px;
padding: 7px 15px 0; font-size: 15px;
color: #e4e6ee; color: #3c3c3c;
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.3);
&:hover {
@include linear-gradient(top, rgba(255, 255, 255, .2), rgba(255, 255, 255, 0));
}
} }
.class-nav, li {
.class-nav li {
float: left; float: left;
} }
}
.class {
@include clearfix;
height: 100%;
font-size: 12px;
color: rgb(163, 171, 184);
@include linear-gradient(top, rgba(255, 255, 255, 0), rgba(255, 255, 255, .1));
background-color: rgb(47, 53, 63);
a { a {
@include box-shadow(1px 0 0 #787981 inset, -1px 0 0 #3d3e44 inset, 1px 0 0 #787981, -1px 0 0 #3d3e44); display: inline-block;
height: 20px;
padding: 5px 10px 6px;
color: rgb(163, 171, 184);
}
&:hover { .home {
background: rgba(255, 255, 255, .1); position: relative;
top: 1px;
} }
.log-out {
position: relative;
top: 3px;
}
} }
} }
\ No newline at end of file
...@@ -7,7 +7,20 @@ ...@@ -7,7 +7,20 @@
} }
.unit-body { .unit-body {
padding: 30px 40px; padding: 0;
.details {
display: block !important;
h2 {
margin: 0 0 5px 0;
}
}
}
.component-editor {
border: none;
border-radius: 0;
} }
.components > li { .components > li {
...@@ -36,7 +49,7 @@ ...@@ -36,7 +49,7 @@
} }
.drag-handle { .drag-handle {
background: url(../img/drag-handles.png) center no-repeat $lightGrey; background: url(../img/drag-handles.png) center no-repeat #fff;
} }
} }
...@@ -46,10 +59,10 @@ ...@@ -46,10 +59,10 @@
z-index: 11; z-index: 11;
width: 35px; width: 35px;
border: none; border: none;
background: url(../img/drag-handles.png) center no-repeat $lightGrey; background: url(../img/drag-handles.png) center no-repeat #fff;
&:hover { &:hover {
background: url(../img/drag-handles.png) center no-repeat $lightGrey; background: url(../img/drag-handles.png) center no-repeat #fff;
} }
} }
...@@ -60,16 +73,24 @@ ...@@ -60,16 +73,24 @@
} }
.component.editing { .component.editing {
border-left: 1px solid $mediumGrey;
border-right: 1px solid $mediumGrey;
.xmodule_display { .xmodule_display {
display: none; display: none;
} }
} }
.new .xmodule_display {
background: $yellow;
}
.xmodule_display { .xmodule_display {
padding: 20px 20px 22px; padding: 20px 20px 22px;
font-size: 24px; font-size: 24px;
font-weight: 300; font-weight: 300;
background: $lightGrey; background: #fff;
@include transition(background-color 3s);
} }
.static-page-item { .static-page-item {
......
...@@ -137,8 +137,7 @@ ...@@ -137,8 +137,7 @@
a { a {
font-size: 11px; font-size: 11px;
font-weight: 700; font-weight: bold;
line-height: 31px;
text-transform: uppercase; text-transform: uppercase;
} }
...@@ -180,3 +179,109 @@ ...@@ -180,3 +179,109 @@
} }
} }
} }
.gradable {
label {
display: inline-block;
vertical-align: top;
}
.gradable-status {
position: relative;
top: -4px;
display: inline-block;
margin-left: 10px;
width: 65%;
.status-label {
margin: 0;
padding: 0;
background: transparent;
color: $blue;
border: none;
font-size: 11px;
font-weight: bold;
text-transform: uppercase;
}
.menu-toggle {
z-index: 100;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 20px;
background: transparent;
&:hover, &.is-active {
color: $blue;
}
}
.menu {
z-index: 1;
position: absolute;
top: -12px;
left: -7px;
display: none;
width: 100%;
margin: 0;
padding: 8px 12px;
opacity: 0.0;
background: $white;
border: 1px solid $mediumGrey;
font-size: 12px;
@include border-radius(4px);
@include box-shadow(0 1px 2px rgba(0, 0, 0, .2));
@include transition(opacity .15s);
li {
margin-bottom: 3px;
padding-bottom: 3px;
border-bottom: 1px solid $lightGrey;
&:last-child {
margin-bottom: 0;
padding-bottom: 0;
border: none;
}
}
a {
&.is-selected {
font-weight: bold;
}
}
}
// dropdown state
&.is-active {
.menu {
z-index: 10000;
display: block;
opacity: 1.0;
}
.menu-toggle {
z-index: 1000;
}
}
// set state
&.is-set {
.menu-toggle {
color: $blue;
}
.status-label {
display: block;
color: $blue;
}
}
}
}
\ No newline at end of file
...@@ -42,7 +42,7 @@ ...@@ -42,7 +42,7 @@
} }
h2 { h2 {
margin: 30px 40px; margin: 30px 40px 30px 0;
color: #646464; color: #646464;
font-size: 19px; font-size: 19px;
font-weight: 300; font-weight: 300;
...@@ -57,14 +57,10 @@ ...@@ -57,14 +57,10 @@
margin: 20px 40px; margin: 20px 40px;
&.new-component-item { &.new-component-item {
padding: 0; padding: 20px;
border: none; border: none;
border-radius: 0; border-radius: 3px;
background: $lightGrey;
&.adding {
background-color: $blue;
border-color: #437fbf;
}
.new-component-button { .new-component-button {
display: block; display: block;
...@@ -85,12 +81,13 @@ ...@@ -85,12 +81,13 @@
border-radius: 3px 3px 0 0; border-radius: 3px 3px 0 0;
} }
.new-component-type, .new-component-template { .new-component-type {
@include clearfix; a,
li {
display: inline-block;
}
a { a {
position: relative;
float: left;
width: 100px; width: 100px;
height: 100px; height: 100px;
margin-right: 10px; margin-right: 10px;
...@@ -98,14 +95,8 @@ ...@@ -98,14 +95,8 @@
border-radius: 8px; border-radius: 8px;
font-size: 13px; font-size: 13px;
line-height: 14px; line-height: 14px;
color: #fff;
text-align: center; text-align: center;
@include box-shadow(0 1px 1px rgba(0, 0, 0, .4), 0 1px 0 rgba(255, 255, 255, .4) inset); @include box-shadow(0 1px 1px rgba(0, 0, 0, .2), 0 1px 0 rgba(255, 255, 255, .4) inset);
@include transition(background-color .15s);
&:hover {
background-color: rgba(255, 255, 255, .2);
}
.name { .name {
position: absolute; position: absolute;
...@@ -118,23 +109,62 @@ ...@@ -118,23 +109,62 @@
} }
} }
.new-component-type,
.new-component-template {
@include clearfix;
a {
position: relative;
border: 1px solid $darkGreen;
background: $green;
color: #fff;
@include transition(background-color .15s);
&:hover {
background: $brightGreen;
}
}
}
.new-component-template { .new-component-template {
margin-bottom: 20px;
li:first-child {
a {
border-radius: 3px 3px 0 0;
}
}
li:last-child {
a { a {
height: 60px; border-radius: 0 0 3px 3px;
} }
} }
.new-component, a {
display: block;
padding: 7px 20px;
border-bottom: none;
font-weight: 300;
}
}
.new-component {
text-align: center;
h5 {
color: $green;
}
}
.new-component-templates { .new-component-templates {
display: none; display: none;
position: absolute;
width: 100%;
padding: 20px; padding: 20px;
@include clearfix; @include clearfix;
.cancel-button { .cancel-button {
@include blue-button; @include white-button;
border-color: #30649c;
} }
} }
} }
...@@ -142,7 +172,7 @@ ...@@ -142,7 +172,7 @@
} }
.component { .component {
border: 1px solid #d1ddec; border: 1px solid $lightBluishGrey2;
border-radius: 3px; border-radius: 3px;
background: #fff; background: #fff;
@include transition(none); @include transition(none);
...@@ -157,7 +187,8 @@ ...@@ -157,7 +187,8 @@
} }
&.editing { &.editing {
border-color: #6696d7; border: 1px solid $lightBluishGrey2;
z-index: 9999;
.drag-handle, .drag-handle,
.component-actions { .component-actions {
...@@ -173,11 +204,6 @@ ...@@ -173,11 +204,6 @@
position: absolute; position: absolute;
top: 7px; top: 7px;
right: 9px; right: 9px;
@include transition(opacity .15s);
a {
color: $darkGrey;
}
} }
.drag-handle { .drag-handle {
...@@ -189,10 +215,10 @@ ...@@ -189,10 +215,10 @@
width: 15px; width: 15px;
height: 100%; height: 100%;
border-radius: 0 3px 3px 0; border-radius: 0 3px 3px 0;
border: 1px solid #d1ddec; border: 1px solid $lightBluishGrey2;
background: url(../img/white-drag-handles.png) center no-repeat #d1ddec; background: url(../img/white-drag-handles.png) center no-repeat $lightBluishGrey2;
cursor: move; cursor: move;
@include transition(all .15s); @include transition(none);
} }
} }
...@@ -205,9 +231,6 @@ ...@@ -205,9 +231,6 @@
display: none; display: none;
padding: 20px; padding: 20px;
border-radius: 2px 2px 0 0; border-radius: 2px 2px 0 0;
@include linear-gradient(top, rgba(0, 0, 0, 0), rgba(0, 0, 0, .1));
background-color: $blue;
color: #fff;
@include box-shadow(none); @include box-shadow(none);
.metadata_edit { .metadata_edit {
...@@ -224,12 +247,6 @@ ...@@ -224,12 +247,6 @@
} }
} }
.CodeMirror {
border: 1px solid #3c3c3c;
background: #fff;
color: #3c3c3c;
}
h3 { h3 {
margin-bottom: 10px; margin-bottom: 10px;
font-size: 18px; font-size: 18px;
...@@ -446,3 +463,26 @@ ...@@ -446,3 +463,26 @@
display: none; display: none;
} }
} }
// editing units from courseware
body.unit {
.component {
padding-top: 30px;
.component-actions {
@include box-sizing(border-box);
position: absolute;
width: 811px;
padding: 15px;
top: 0;
left: 0;
border-bottom: 1px solid $lightBluishGrey2;
background: $lightGrey;
}
&.editing {
padding-top: 0;
}
}
}
.users { .users {
.user-overview {
@extend .window;
padding: 30px 40px;
}
.new-user-button {
@include grey-button;
margin: 5px 8px;
padding: 3px 10px 4px 10px;
font-size: 12px;
.plus-icon {
position: relative;
top: 1px;
}
}
.list-header {
@include linear-gradient(top, transparent, rgba(0, 0, 0, .1));
background-color: #ced2db;
border-radius: 3px 3px 0 0;
}
.new-user-form { .new-user-form {
display: none; display: none;
padding: 15px 20px; padding: 15px 20px;
background: $mediumGrey; background-color: $lightBluishGrey2;
#result { #result {
display: none; display: none;
...@@ -55,21 +32,22 @@ ...@@ -55,21 +32,22 @@
.add-button { .add-button {
@include blue-button; @include blue-button;
padding: 5px 20px 9px;
} }
.cancel-button { .cancel-button {
@include white-button; @include white-button;
padding: 5px 20px 9px;
} }
} }
.user-list { .user-list {
border: 1px solid $mediumGrey; border: 1px solid $mediumGrey;
border-top: none; background: #fff;
background: $lightGrey;
li { li {
position: relative; position: relative;
padding: 10px 20px; padding: 20px;
border-bottom: 1px solid $mediumGrey; border-bottom: 1px solid $mediumGrey;
&:last-child { &:last-child {
...@@ -81,12 +59,19 @@ ...@@ -81,12 +59,19 @@
} }
.user-name { .user-name {
width: 30%; margin-right: 10px;
font-weight: 700; font-size: 24px;
font-weight: 300;
}
.user-email {
font-size: 14px;
font-style: italic;
color: $mediumGrey;
} }
.item-actions { .item-actions {
top: 9px; top: 24px;
} }
} }
} }
......
...@@ -10,13 +10,28 @@ $fg-min-width: 810px; ...@@ -10,13 +10,28 @@ $fg-min-width: 810px;
$sans-serif: 'Open Sans', $verdana; $sans-serif: 'Open Sans', $verdana;
$body-line-height: golden-ratio(.875em, 1); $body-line-height: golden-ratio(.875em, 1);
$white: rgb(255,255,255);
$black: rgb(0,0,0);
$pink: rgb(182,37,104); $pink: rgb(182,37,104);
$error-red: rgb(253, 87, 87); $error-red: rgb(253, 87, 87);
$baseFontColor: #3c3c3c;
$offBlack: #3c3c3c;
$black: rgb(0,0,0);
$white: rgb(255,255,255);
$blue: #5597dd; $blue: #5597dd;
$orange: #edbd3c; $orange: #edbd3c;
$red: #b20610;
$green: #108614;
$lightGrey: #edf1f5; $lightGrey: #edf1f5;
$mediumGrey: #ced2db; $mediumGrey: #b0b6c2;
$darkGrey: #8891a1; $darkGrey: #8891a1;
$extraDarkGrey: #3d4043; $extraDarkGrey: #3d4043;
$paleYellow: #fffcf1; $paleYellow: #fffcf1;
$yellow: rgb(255, 254, 223);
$green: rgb(37, 184, 90);
$brightGreen: rgb(22, 202, 87);
$disabledGreen: rgb(124, 206, 153);
$darkGreen: rgb(52, 133, 76);
$lightBluishGrey: rgb(197, 207, 223);
$lightBluishGrey2: rgb(213, 220, 228);
...@@ -18,13 +18,13 @@ ...@@ -18,13 +18,13 @@
@import "static-pages"; @import "static-pages";
@import "users"; @import "users";
@import "import"; @import "import";
@import "settings";
@import "course-info"; @import "course-info";
@import "landing"; @import "landing";
@import "graphics"; @import "graphics";
@import "modal"; @import "modal";
@import "alerts"; @import "alerts";
@import "login"; @import "login";
@import "lms";
@import 'jquery-ui-calendar'; @import 'jquery-ui-calendar';
@import 'content-types'; @import 'content-types';
......
...@@ -21,7 +21,7 @@ ...@@ -21,7 +21,7 @@
</div> </div>
</td> </td>
<td class="name-col"> <td class="name-col">
<a href="{{url}}" class="filename">{{displayname}}</a> <a data-tooltip="Open/download this file" href="{{url}}" class="filename">{{displayname}}</a>
<div class="embeddable-xml"></div> <div class="embeddable-xml"></div>
</td> </td>
<td class="date-col"> <td class="date-col">
...@@ -35,9 +35,10 @@ ...@@ -35,9 +35,10 @@
<div class="main-wrapper"> <div class="main-wrapper">
<div class="inner-wrapper"> <div class="inner-wrapper">
<h1>Asset Library</h1>
<div class="page-actions"> <div class="page-actions">
<a href="#" class="upload-button">Upload New File</a> <a href="#" class="upload-button new-button">
<span class="upload-icon"></span>Upload New Asset
</a>
<input type="text" class="asset-search-input search wip-box" placeholder="search assets" style="display:none"/> <input type="text" class="asset-search-input search wip-box" placeholder="search assets" style="display:none"/>
</div> </div>
<article class="asset-library"> <article class="asset-library">
...@@ -61,7 +62,7 @@ ...@@ -61,7 +62,7 @@
</div> </div>
</td> </td>
<td class="name-col"> <td class="name-col">
<a href="${asset['url']}" class="filename">${asset['displayname']}</a> <a data-tooltip="Open/download this file" href="${asset['url']}" class="filename">${asset['displayname']}</a>
<div class="embeddable-xml"></div> <div class="embeddable-xml"></div>
</td> </td>
<td class="date-col"> <td class="date-col">
......
...@@ -9,6 +9,9 @@ ...@@ -9,6 +9,9 @@
<%static:css group='base-style'/> <%static:css group='base-style'/>
<link rel="stylesheet" type="text/css" href="${static.url('js/vendor/markitup/skins/simple/style.css')}" /> <link rel="stylesheet" type="text/css" href="${static.url('js/vendor/markitup/skins/simple/style.css')}" />
<link rel="stylesheet" type="text/css" href="${static.url('js/vendor/markitup/sets/wiki/style.css')}" /> <link rel="stylesheet" type="text/css" href="${static.url('js/vendor/markitup/sets/wiki/style.css')}" />
<link rel="stylesheet" type="text/css" href="${static.url('css/vendor/symbolset.ss-standard.css')}" />
<link rel="stylesheet" type="text/css" href="${static.url('css/vendor/symbolset.ss-symbolicons-block.css')}" />
<title><%block name="title"></%block></title> <title><%block name="title"></%block></title>
<meta name="viewport" content="width=device-width,initial-scale=1"> <meta name="viewport" content="width=device-width,initial-scale=1">
...@@ -26,6 +29,9 @@ ...@@ -26,6 +29,9 @@
<script type="text/javascript" src="${static.url('js/vendor/backbone-min.js')}"></script> <script type="text/javascript" src="${static.url('js/vendor/backbone-min.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/markitup/jquery.markitup.js')}"></script> <script type="text/javascript" src="${static.url('js/vendor/markitup/jquery.markitup.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/markitup/sets/wiki/set.js')}"></script> <script type="text/javascript" src="${static.url('js/vendor/markitup/sets/wiki/set.js')}"></script>
<script src="${static.url('js/vendor/symbolset.ss-standard.js')}"></script>
<script src="${static.url('js/vendor/symbolset.ss-symbolicons.js')}"></script>
<%static:js group='main'/> <%static:js group='main'/>
<%static:js group='module-js'/> <%static:js group='module-js'/>
<script src="${static.url('js/vendor/jquery.inlineedit.js')}"></script> <script src="${static.url('js/vendor/jquery.inlineedit.js')}"></script>
......
...@@ -9,5 +9,5 @@ ...@@ -9,5 +9,5 @@
<a href="#" class="edit-button standard"><span class="edit-icon"></span>Edit</a> <a href="#" class="edit-button standard"><span class="edit-icon"></span>Edit</a>
<a href="#" class="delete-button standard"><span class="delete-icon"></span>Delete</a> <a href="#" class="delete-button standard"><span class="delete-icon"></span>Delete</a>
</div> </div>
<a href="#" class="drag-handle"></a> <a data-tooltip="Drag to reorder" href="#" class="drag-handle"></a>
${preview} ${preview}
\ No newline at end of file
...@@ -17,13 +17,18 @@ ...@@ -17,13 +17,18 @@
<%block name="content"> <%block name="content">
<div class="main-wrapper"> <div class="main-wrapper">
<div class="inner-wrapper"> <div class="inner-wrapper">
<div> <article class="unit-body">
<h1>Static Tabs</h1>
</div>
<article class="unit-body window">
<div class="details"> <div class="details">
<p>Here you can add and manage additional pages for your course. These pages will be added to the primary navigation menu alongside Courseware, Course Info, Discussion, etc.</p> <h2>Here you can add and manage additional pages for your course</h2>
<p>These pages will be added to the primary navigation menu alongside Courseware, Course Info, Discussion, etc.</p>
</div>
<div class="page-actions">
<a href="#" class="new-button new-tab">
<span class="plus-icon white"></span>New Page
</a>
</div> </div>
<div class="tab-list"> <div class="tab-list">
<ol class='components'> <ol class='components'>
% for id in components: % for id in components:
...@@ -31,9 +36,7 @@ ...@@ -31,9 +36,7 @@
% endfor % endfor
<li class="new-component-item"> <li class="new-component-item">
<a href="#" class="new-button big new-tab">
<span class="plus-icon"></span>New Tab
</a>
</li> </li>
</ol> </ol>
</div> </div>
......
...@@ -31,21 +31,6 @@ ...@@ -31,21 +31,6 @@
<label>Units:</label> <label>Units:</label>
${units.enum_units(subsection, subsection_units=subsection_units)} ${units.enum_units(subsection, subsection_units=subsection_units)}
</div> </div>
<div>
<label>Policy:</label>
<ol class='policy-list'>
% for policy_name in policy_metadata.keys():
<li class="policy-list-element">
<input type="text" class="policy-list-name" name="${policy_name}" value="${policy_name}" disabled size="15"/>:&nbsp;<input type="text" class="policy-list-value" name="${policy_metadata[policy_name]}" value="${policy_metadata[policy_name]}" size="40"/>
<a href="#" class="cancel-button">Cancel</a>
<a href="#" class="delete-icon remove-policy-data"></a>
</li>
% endfor
<a href="#" class="new-policy-item add-policy-data">
<span class="plus-icon-small"></span>New Policy Data
</a>
</ol>
</div>
</article> </article>
</div> </div>
...@@ -62,7 +47,7 @@ ...@@ -62,7 +47,7 @@
</div> </div>
<div class="sidebar"> <div class="sidebar">
<div class="unit-settings window"> <div class="unit-settings window id-holder" data-id="${subsection.location}">
<h4>Subsection Settings</h4> <h4>Subsection Settings</h4>
<div class="window-contents"> <div class="window-contents">
<div class="scheduled-date-input row"> <div class="scheduled-date-input row">
...@@ -84,6 +69,13 @@ ...@@ -84,6 +69,13 @@
<a href="#" class="sync-date no-spinner">Sync to ${parent_item.display_name}.</a></p> <a href="#" class="sync-date no-spinner">Sync to ${parent_item.display_name}.</a></p>
% endif % endif
</div> </div>
<div class="row gradable">
<label>Graded as:</label>
<div class="gradable-status" data-initial-status="${subsection.metadata.get('format', 'Not Graded')}">
</div>
<div class="due-date-input row"> <div class="due-date-input row">
<label>Due date:</label> <label>Due date:</label>
<a href="#" class="set-date">Set a due date</a> <a href="#" class="set-date">Set a due date</a>
...@@ -116,6 +108,10 @@ ...@@ -116,6 +108,10 @@
<script src="${static.url('js/vendor/timepicker/jquery.timepicker.js')}"></script> <script src="${static.url('js/vendor/timepicker/jquery.timepicker.js')}"></script>
<script src="${static.url('js/vendor/timepicker/datepair.js')}"></script> <script src="${static.url('js/vendor/timepicker/datepair.js')}"></script>
<script src="${static.url('js/vendor/date.js')}"></script> <script src="${static.url('js/vendor/date.js')}"></script>
<script type="text/javascript" src="${static.url('js/models/course_relative.js')}"></script>
<script type="text/javascript" src="${static.url('js/views/grader-select-view.js')}"></script>
<script type="text/javascript" src="${static.url('js/models/settings/course_grading_policy.js')}"></script>
<script type="text/javascript"> <script type="text/javascript">
$(document).ready(function() { $(document).ready(function() {
// expand the due-date area if the values are set // expand the due-date area if the values are set
...@@ -124,6 +120,23 @@ ...@@ -124,6 +120,23 @@
$('.set-date').hide(); $('.set-date').hide();
$block.find('.date-setter').show(); $block.find('.date-setter').show();
} }
// TODO figure out whether these should be in window or someplace else or whether they're only needed as local vars
// I believe that current (New Section/New Subsection) cause full page reloads which means these aren't needed globally
// but we really should change that behavior.
if (!window.graderTypes) {
window.graderTypes = new CMS.Models.Settings.CourseGraderCollection();
window.graderTypes.course_location = new CMS.Models.Location('${parent_location}');
window.graderTypes.reset(${course_graders|n});
}
$(".gradable-status").each(function(index, ele) {
var gradeView = new CMS.Views.OverviewAssignmentGrader({
el : ele,
graders : window.graderTypes,
hideSymbol : true
});
});
}) })
</script> </script>
</%block> </%block>
...@@ -4,7 +4,7 @@ ${content} ...@@ -4,7 +4,7 @@ ${content}
<a href="#" class="edit-button"><span class="edit-icon white"></span>Edit</a> <a href="#" class="edit-button"><span class="edit-icon white"></span>Edit</a>
<a href="#" class="delete-button"><span class="delete-icon white"></span>Delete</a> <a href="#" class="delete-button"><span class="delete-icon white"></span>Delete</a>
</div> </div>
<a href="#" class="drag-handle"></a> <a data-tooltip="Drag to reorder" href="#" class="drag-handle"></a>
<div class="component-editor"> <div class="component-editor">
<h5>Edit Video Component</h5> <h5>Edit Video Component</h5>
<textarea class="component-source"><video youtube="1.50:q1xkuPsOY6Q,1.25:9WOY2dHz5i4,1.0:4rpg8Bq6hb4,0.75:KLim9Xkp7IY"/></textarea> <textarea class="component-source"><video youtube="1.50:q1xkuPsOY6Q,1.25:9WOY2dHz5i4,1.0:4rpg8Bq6hb4,0.75:KLim9Xkp7IY"/></textarea>
......
...@@ -8,7 +8,6 @@ ...@@ -8,7 +8,6 @@
<%block name="content"> <%block name="content">
<div class="main-wrapper"> <div class="main-wrapper">
<div class="inner-wrapper"> <div class="inner-wrapper">
<h1>Import</h1>
<article class="import-overview"> <article class="import-overview">
<div class="description"> <div class="description">
<h2>Please <a href="https://edge.edx.org/courses/edX/edx101/edX_Studio_Reference/about" target="_blank">read the documentation</a> before attempting an import!</h2> <h2>Please <a href="https://edge.edx.org/courses/edX/edx101/edX_Studio_Reference/about" target="_blank">read the documentation</a> before attempting an import!</h2>
......
...@@ -37,7 +37,7 @@ ...@@ -37,7 +37,7 @@
<h1>My Courses</h1> <h1>My Courses</h1>
<article class="my-classes"> <article class="my-classes">
% if user.is_active: % if user.is_active:
<a href="#" class="new-course-button"><span class="plus-icon"></span> New Course</a> <a href="#" class="new-button new-course-button"><span class="plus-icon white"></span> New Course</a>
<ul class="class-list"> <ul class="class-list">
%for course, url in courses: %for course, url in courses:
<li> <li>
......
...@@ -5,28 +5,28 @@ ...@@ -5,28 +5,28 @@
<%block name="content"> <%block name="content">
<div class="main-wrapper"> <div class="main-wrapper">
<div class="inner-wrapper"> <div class="inner-wrapper">
<h1>Users</h1> <div class="page-actions">
<article class="user-overview">
<div class="details">
<p>The following list of users have been designated as course staff. This means that these users will have permissions to modify course content. You may add additional course staff below, if you are the course instructor. Please note that they must have already registered and verified their account.</p>
</div>
<div class="list-header">
%if allow_actions: %if allow_actions:
<a href="#" class="new-user-button"> <a href="#" class="new-button new-user-button">
<span class="plus-icon"></span>New User <span class="plus-icon white"></span>New User
</a> </a>
%endif %endif
</div> </div>
<div class="details">
<p>The following list of users have been designated as course staff. This means that these users will have permissions to modify course content. You may add additional course staff below, if you are the course instructor. Please note that they must have already registered and verified their account.</p>
</div>
<article class="user-overview">
%if allow_actions: %if allow_actions:
<div class="new-user-form"> <form class="new-user-form">
<div id="result"></div> <div id="result"></div>
<div class="form-elements"> <div class="form-elements">
<label>email: </label><input type="text" id="email" class="email-input" autocomplete="off" placeholder="email@example.com"> <label>email: </label><input type="text" id="email" class="email-input" autocomplete="off" placeholder="email@example.com">
<a href="#" id="add_user" class="add-button">add user</a> <input type="submit" value="Add User" id="add_user" class="add-button" />
<a href="#" class="cancel-button">cancel</a> <input type="button" value="Cancel" class="cancel-button" />
</div>
</div> </div>
</form>
%endif %endif
<div> <div>
<ol class="user-list"> <ol class="user-list">
...@@ -57,6 +57,7 @@ ...@@ -57,6 +57,7 @@
function showNewUserForm(e) { function showNewUserForm(e) {
e.preventDefault(); e.preventDefault();
$newUserForm.slideDown(150); $newUserForm.slideDown(150);
$newUserForm.find('.email-input').focus();
} }
function hideNewUserForm(e) { function hideNewUserForm(e) {
...@@ -66,13 +67,15 @@ ...@@ -66,13 +67,15 @@
$('#email').val(''); $('#email').val('');
} }
$(document).ready(function() { function checkForCancel(e) {
$newUserForm = $('.new-user-form'); if(e.which == 27) {
$newUserForm.find('.cancel-button').bind('click', hideNewUserForm); e.data.$cancelButton.click();
}
}
$('.new-user-button').bind('click', showNewUserForm); function addUser(e) {
e.preventDefault();
$('#add_user').click(function() {
$.ajax({ $.ajax({
url: '${add_user_postback_url}', url: '${add_user_postback_url}',
type: 'POST', type: 'POST',
...@@ -84,8 +87,17 @@ ...@@ -84,8 +87,17 @@
$('#result').show().empty().append(data.ErrMsg); $('#result').show().empty().append(data.ErrMsg);
else else
location.reload(); location.reload();
})
}); });
}
$(document).ready(function() {
$newUserForm = $('.new-user-form');
var $cancelButton = $newUserForm.find('.cancel-button');
$newUserForm.bind('submit', addUser);
$cancelButton.bind('click', hideNewUserForm);
$('.new-user-button').bind('click', showNewUserForm);
$body.bind('keyup', { $cancelButton: $cancelButton }, checkForCancel);
$('.remove-user').click(function() { $('.remove-user').click(function() {
$.ajax({ $.ajax({
......
...@@ -16,13 +16,37 @@ ...@@ -16,13 +16,37 @@
<script src="${static.url('js/vendor/timepicker/jquery.timepicker.js')}"></script> <script src="${static.url('js/vendor/timepicker/jquery.timepicker.js')}"></script>
<script src="${static.url('js/vendor/timepicker/datepair.js')}"></script> <script src="${static.url('js/vendor/timepicker/datepair.js')}"></script>
<script src="${static.url('js/vendor/date.js')}"></script> <script src="${static.url('js/vendor/date.js')}"></script>
<script type="text/javascript" src="${static.url('js/models/course_relative.js')}"></script>
<script type="text/javascript" src="${static.url('js/views/grader-select-view.js')}"></script>
<script type="text/javascript" src="${static.url('js/models/settings/course_grading_policy.js')}"></script>
<script type="text/javascript">
$(document).ready(function(){
// TODO figure out whether these should be in window or someplace else or whether they're only needed as local vars
// I believe that current (New Section/New Subsection) cause full page reloads which means these aren't needed globally
// but we really should change that behavior.
if (!window.graderTypes) {
window.graderTypes = new CMS.Models.Settings.CourseGraderCollection();
window.graderTypes.course_location = new CMS.Models.Location('${parent_location}');
window.graderTypes.reset(${course_graders|n});
}
$(".gradable-status").each(function(index, ele) {
var gradeView = new CMS.Views.OverviewAssignmentGrader({
el : ele,
graders : window.graderTypes
});
});
});
</script>
</%block> </%block>
<%block name="header_extras"> <%block name="header_extras">
<script type="text/template" id="new-section-template"> <script type="text/template" id="new-section-template">
<section class="courseware-section branch new-section"> <section class="courseware-section branch new-section">
<header> <header>
<a href="#" class="expand-collapse-icon collapse"></a> <a href="#" data-tooltip="Collapse/expand this section" class="expand-collapse-icon collapse"></a>
<div class="item-details"> <div class="item-details">
<h3 class="section-name"> <h3 class="section-name">
<form class="section-name-form"> <form class="section-name-form">
...@@ -38,7 +62,7 @@ ...@@ -38,7 +62,7 @@
<script type="text/template" id="blank-slate-template"> <script type="text/template" id="blank-slate-template">
<section class="courseware-section branch new-section"> <section class="courseware-section branch new-section">
<header> <header>
<a href="#" class="expand-collapse-icon collapse"></a> <a href="#" data-tooltip="Collapse/expand this section" class="expand-collapse-icon collapse"></a>
<div class="item-details"> <div class="item-details">
<h3 class="section-name"> <h3 class="section-name">
<span class="section-name-span">Click here to set the section name</span> <span class="section-name-span">Click here to set the section name</span>
...@@ -49,8 +73,8 @@ ...@@ -49,8 +73,8 @@
</form> </form>
</div> </div>
<div class="item-actions"> <div class="item-actions">
<a href="#" class="delete-button delete-section-button"><span class="delete-icon"></span></a> <a href="#" data-tooltip="Delete this section" class="delete-button delete-section-button"><span class="delete-icon"></span></a>
<a href="#" class="drag-handle"></a> <a href="#" data-tooltip="Drag to re-order" class="drag-handle"></a>
</div> </div>
</header> </header>
</section> </section>
...@@ -77,8 +101,6 @@ ...@@ -77,8 +101,6 @@
</ol> </ol>
</li> </li>
</script> </script>
</%block> </%block>
<%block name="content"> <%block name="content">
...@@ -89,7 +111,7 @@ ...@@ -89,7 +111,7 @@
<input class="start-date date" type="text" name="start_date" value="" placeholder="MM/DD/YYYY" class="date" size='15' autocomplete="off"/> <input class="start-date date" type="text" name="start_date" value="" placeholder="MM/DD/YYYY" class="date" size='15' autocomplete="off"/>
<input class="start-time time" type="text" name="start_time" value="" placeholder="HH:MM" class="time" size='10' autocomplete="off"/> <input class="start-time time" type="text" name="start_time" value="" placeholder="HH:MM" class="time" size='10' autocomplete="off"/>
<div class="description"> <div class="description">
<p>On the date set above, this section – <strong class="section-name"></strong> – will be released to students along with the 5 subsections within it. Any units marked private will only be visible to admins.</p> <p>On the date set above, this section – <strong class="section-name"></strong> – will be released to students. Any units marked private will only be visible to admins.</p>
</div> </div>
</div> </div>
<a href="#" class="save-button">Save</a><a href="#" class="cancel-button">Cancel</a> <a href="#" class="save-button">Save</a><a href="#" class="cancel-button">Cancel</a>
...@@ -98,17 +120,19 @@ ...@@ -98,17 +120,19 @@
<div class="main-wrapper"> <div class="main-wrapper">
<div class="inner-wrapper"> <div class="inner-wrapper">
<h1>Courseware</h1> <div class="page-actions">
<div class="page-actions"></div> <a href="#" class="new-button new-courseware-section-button"><span class="plus-icon white"></span> New Section</a>
<a href="#" class="collapse-all-button"><span class="collapse-all-icon"></span>Collapse All</a>
</div>
<article class="courseware-overview" data-course-id="${context_course.location.url()}"> <article class="courseware-overview" data-course-id="${context_course.location.url()}">
<a href="#" class="new-button big new-courseware-section-button"><span class="plus-icon"></span> New Section</a>
% for section in sections: % for section in sections:
<section class="courseware-section branch" data-id="${section.location}"> <section class="courseware-section branch" data-id="${section.location}">
<header> <header>
<a href="#" class="expand-collapse-icon collapse"></a> <a href="#" data-tooltip="Expand/collapse this section" class="expand-collapse-icon collapse"></a>
<div class="item-details" data-id="${section.location}"> <div class="item-details" data-id="${section.location}">
<h3 class="section-name"> <h3 class="section-name">
<span class="section-name-span">${section.display_name}</span> <span data-tooltip="Edit this section's name" class="section-name-span">${section.display_name}</span>
<form class="section-name-edit" style="display:none"> <form class="section-name-edit" style="display:none">
<input type="text" value="${section.display_name}" class="edit-section-name" autocomplete="off"/> <input type="text" value="${section.display_name}" class="edit-section-name" autocomplete="off"/>
<input type="submit" class="save-button edit-section-name-save" value="Save" /> <input type="submit" class="save-button edit-section-name-save" value="Save" />
...@@ -130,9 +154,10 @@ ...@@ -130,9 +154,10 @@
%endif %endif
</div> </div>
</div> </div>
<div class="item-actions"> <div class="item-actions">
<a href="#" class="delete-button delete-section-button"><span class="delete-icon"></span></a> <a href="#" data-tooltip="Delete this section" class="delete-button delete-section-button"><span class="delete-icon"></span></a>
<a href="#" class="drag-handle"></a> <a href="#" data-tooltip="Drag to reorder" class="drag-handle"></a>
</div> </div>
</header> </header>
<div class="subsection-list"> <div class="subsection-list">
...@@ -143,18 +168,22 @@ ...@@ -143,18 +168,22 @@
</div> </div>
<ol data-section-id="${section.location.url()}"> <ol data-section-id="${section.location.url()}">
% for subsection in section.get_children(): % for subsection in section.get_children():
<li class="branch collapsed" data-id="${subsection.location}"> <li class="branch collapsed id-holder" data-id="${subsection.location}">
<div class="section-item"> <div class="section-item">
<div> <div class="details">
<a href="#" class="expand-collapse-icon expand"></a> <a href="#" data-tooltip="Expand/collapse this subsection" class="expand-collapse-icon expand"></a>
<a href="${reverse('edit_subsection', args=[subsection.location])}"> <a href="${reverse('edit_subsection', args=[subsection.location])}">
<span class="folder-icon"></span> <span class="folder-icon"></span>
<span class="subsection-name"><span class="subsection-name-value">${subsection.display_name}</span></span> <span class="subsection-name"><span class="subsection-name-value">${subsection.display_name}</span></span>
</a> </a>
</div> </div>
<div class="gradable-status" data-initial-status="${subsection.metadata.get('format', 'Not Graded')}">
</div>
<div class="item-actions"> <div class="item-actions">
<a href="#" class="delete-button delete-subsection-button"><span class="delete-icon"></span></a> <a href="#" data-tooltip="Delete this subsection" class="delete-button delete-subsection-button"><span class="delete-icon"></span></a>
<a href="#" class="drag-handle"></a> <a href="#" data-tooltip="Drag to reorder" class="drag-handle"></a>
</div> </div>
</div> </div>
${units.enum_units(subsection)} ${units.enum_units(subsection)}
......
...@@ -32,12 +32,9 @@ ...@@ -32,12 +32,9 @@
% for id in components: % for id in components:
<li class="component" data-id="${id}"/> <li class="component" data-id="${id}"/>
% endfor % endfor
<li class="new-component-item"> <li class="new-component-item adding">
<a href="#" class="new-component-button new-button big">
<span class="plus-icon"></span>New Component
</a>
<div class="new-component"> <div class="new-component">
<h5>Select Component Type</h5> <h5>Add New Component</h5>
<ul class="new-component-type"> <ul class="new-component-type">
% for type in sorted(component_templates.keys()): % for type in sorted(component_templates.keys()):
<li> <li>
...@@ -48,7 +45,6 @@ ...@@ -48,7 +45,6 @@
</li> </li>
% endfor % endfor
</ul> </ul>
<a href="#" class="cancel-button">Cancel</a>
</div> </div>
% for type, templates in sorted(component_templates.items()): % for type, templates in sorted(component_templates.items()):
<div class="new-component-templates new-component-${type}"> <div class="new-component-templates new-component-${type}">
...@@ -85,7 +81,11 @@ ...@@ -85,7 +81,11 @@
<p class="publish-draft-message">This is a draft of the published unit. To update the live version, you must <a href="#" class="publish-draft">replace it with this draft</a>.</p> <p class="publish-draft-message">This is a draft of the published unit. To update the live version, you must <a href="#" class="publish-draft">replace it with this draft</a>.</p>
</div> </div>
<div class="row status"> <div class="row status">
<p>This unit is scheduled to be released to <strong>students</strong> on <strong>${release_date}</strong> with the subsection <a href="${reverse('edit_subsection', kwargs={'location': subsection.location})}">"${subsection.display_name}"</a></p> <p>This unit is scheduled to be released to <strong>students</strong>
% if release_date is not None:
on <strong>${release_date}</strong>
% endif
with the subsection <a href="${reverse('edit_subsection', kwargs={'location': subsection.location})}">"${subsection.display_name}"</a></p>
</div> </div>
<div class="row unit-actions"> <div class="row unit-actions">
<a href="#" class="delete-draft delete-button">Delete Draft</a> <a href="#" class="delete-draft delete-button">Delete Draft</a>
......
...@@ -2,29 +2,38 @@ ...@@ -2,29 +2,38 @@
<% active_tab_class = 'active-tab-' + active_tab if active_tab else '' %> <% active_tab_class = 'active-tab-' + active_tab if active_tab else '' %>
<header class="primary-header ${active_tab_class}"> <header class="primary-header ${active_tab_class}">
<nav class="inner-wrapper"> <div class="class">
<div class="inner-wrapper">
<div class="left"> <div class="left">
<a href="/"><span class="home-icon"></span></a>
% if context_course: % if context_course:
<% ctx_loc = context_course.location %> <% ctx_loc = context_course.location %>
<a href="${reverse('course_index', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, name=ctx_loc.name))}" class="class-name">${context_course.display_name}</a> <a href="/" class="home"><span class="small-home-icon"></span></a>
<ul class="class-nav"> <a href="${reverse('course_index', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, name=ctx_loc.name))}" class="class-name">${context_course.display_name}</a>
<li><a href="${reverse('course_index', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, name=ctx_loc.name))}" id='courseware-tab'>Courseware</a></li>
<li><a href="${reverse('course_info', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, name=ctx_loc.name))}" id='courseinfo-tab'>Course Info</a></li>
<li><a href="${reverse('edit_tabs', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, coursename=ctx_loc.name))}" id='pages-tab'>Tabs</a></li>
<li><a href="${reverse('asset_index', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, name=ctx_loc.name))}" id='assets-tab'>Assets</a></li>
<li><a href="${reverse('manage_users', kwargs=dict(location=ctx_loc))}" id='users-tab'>Users</a></li>
<li><a href="${reverse('import_course', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, name=ctx_loc.name))}" id='import-tab'>Import</a></li>
</ul>
% endif % endif
</div> </div>
<div class="right"> <div class="right">
<span class="username">${ user.username }</span> <span class="username">${ user.username }</span>
% if user.is_authenticated(): % if user.is_authenticated():
<a href="${reverse('logout')}">Log out</a> <a href="${reverse('logout')}" class="log-out"><span class="log-out-icon"></span></a>
% else: % else:
<a href="${reverse('login')}">Log in</a> <a href="${reverse('login')}">Log in</a>
% endif % endif
</div> </div>
</div>
</div>
<nav class="class-nav-bar">
% if context_course:
<% ctx_loc = context_course.location %>
<ul class="class-nav inner-wrapper">
<li><a href="${reverse('course_index', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, name=ctx_loc.name))}" id='courseware-tab'>Courseware</a></li>
<li><a href="${reverse('course_info', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, name=ctx_loc.name))}" id='courseinfo-tab'>Course Info</a></li>
<li><a href="${reverse('edit_tabs', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, coursename=ctx_loc.name))}" id='pages-tab'>Pages</a></li>
<li><a href="${reverse('manage_users', kwargs=dict(location=ctx_loc))}" id='users-tab'>Users</a></li>
<li><a href="${reverse('asset_index', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, name=ctx_loc.name))}" id='assets-tab'>Assets</a></li>
<li><a href="${reverse('course_settings', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, name=ctx_loc.name))}" id='settings-tab'>Settings</a></li>
<li><a href="${reverse('import_course', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, name=ctx_loc.name))}" id='import-tab'>Import</a></li>
</ul>
% endif
</nav> </nav>
</header> </header>
<%include file="metadata-edit.html" /> <%include file="metadata-edit.html" />
<section class="html-edit"> <section class="html-edit">
<textarea name="" class="edit-box" rows="8" cols="40">${data}</textarea> <div name="" class="edit-box">${data}</div>
</section> </section>
...@@ -4,7 +4,6 @@ ...@@ -4,7 +4,6 @@
hlskey = hashlib.md5(module.location.url()).hexdigest() hlskey = hashlib.md5(module.location.url()).hexdigest()
%> %>
<section class="metadata_edit"> <section class="metadata_edit">
<h3>Metadata</h3>
<ul> <ul>
% for keyname in editable_metadata_fields: % for keyname in editable_metadata_fields:
<li> <li>
......
...@@ -26,8 +26,8 @@ This def will enumerate through a passed in subsection and list all of the units ...@@ -26,8 +26,8 @@ This def will enumerate through a passed in subsection and list all of the units
</a> </a>
% if actions: % if actions:
<div class="item-actions"> <div class="item-actions">
<a href="#" class="delete-button" data-id="${unit.location}"><span class="delete-icon"></span></a> <a href="#" data-tooltip="Delete this unit" class="delete-button" data-id="${unit.location}"><span class="delete-icon"></span></a>
<a href="#" class="drag-handle"></a> <a href="#" data-tooltip="Drag to sort" class="drag-handle"></a>
</div> </div>
% endif % endif
</div> </div>
......
<li class="video-box"> <li class="video-box">
<div class="thumb"><img src="http://placehold.it/100x65" /></div> <div class="thumb"></div>
<div class="meta"> <div class="meta">
<strong>video-name</strong> 236mb Uploaded 6 hours ago by <em>Anant Agrawal</em> <strong>video-name</strong> 236mb Uploaded 6 hours ago by <em>Anant Agrawal</em>
......
<li class="video-box"> <li class="video-box">
<div class="thumb"><img src="http://placehold.it/155x90" /></div> <div class="thumb"></div>
<div class="meta"> <div class="meta">
<strong>video-name</strong> 236mb <strong>video-name</strong> 236mb
......
...@@ -35,9 +35,13 @@ urlpatterns = ('', ...@@ -35,9 +35,13 @@ urlpatterns = ('',
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<name>[^/]+)/remove_user$', url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<name>[^/]+)/remove_user$',
'contentstore.views.remove_user', name='remove_user'), 'contentstore.views.remove_user', name='remove_user'),
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/info/(?P<name>[^/]+)$', 'contentstore.views.course_info', name='course_info'), url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/info/(?P<name>[^/]+)$', 'contentstore.views.course_info', name='course_info'),
# ??? Is the following necessary or will the one below work w/ id=None if not sent?
# url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/course_info/updates$', 'contentstore.views.course_info_updates', name='course_info'),
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/course_info/updates/(?P<provided_id>.*)$', 'contentstore.views.course_info_updates', name='course_info'), url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/course_info/updates/(?P<provided_id>.*)$', 'contentstore.views.course_info_updates', name='course_info'),
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/settings/(?P<name>[^/]+)$', 'contentstore.views.get_course_settings', name='course_settings'),
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/settings/(?P<name>[^/]+)/section/(?P<section>[^/]+).*$', 'contentstore.views.course_settings_updates', name='course_settings'),
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/grades/(?P<name>[^/]+)/(?P<grader_index>.*)$', 'contentstore.views.course_grader_updates', name='course_settings'),
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/(?P<category>[^/]+)/(?P<name>[^/]+)/gradeas.*$', 'contentstore.views.assignment_type_update', name='assignment_type_update'),
url(r'^pages/(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<coursename>[^/]+)$', 'contentstore.views.static_pages', url(r'^pages/(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<coursename>[^/]+)$', 'contentstore.views.static_pages',
name='static_pages'), name='static_pages'),
url(r'^edit_static/(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<coursename>[^/]+)$', 'contentstore.views.edit_static', name='edit_static'), url(r'^edit_static/(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<coursename>[^/]+)$', 'contentstore.views.edit_static', name='edit_static'),
......
class CourseRelativeMember:
def __init__(self, location, idx):
self.course_location = location # a Location obj
self.idx = idx # which milestone this represents. Hopefully persisted # so we don't have race conditions
### ??? If 2+ courses use the same textbook or other asset, should they point to the same db record?
class linked_asset(CourseRelativeMember):
"""
Something uploaded to our asset lib which has a name/label and location. Here it's tracked by course and index, but
we could replace the label/url w/ a pointer to a real asset and keep the join info here.
"""
def __init__(self, location, idx):
CourseRelativeMember.__init__(self, location, idx)
self.label = ""
self.url = None
class summary_detail_pair(CourseRelativeMember):
"""
A short text with an arbitrary html descriptor used for paired label - details elements.
"""
def __init__(self, location, idx):
CourseRelativeMember.__init__(self, location, idx)
self.summary = ""
self.detail = ""
\ No newline at end of file
import time, datetime
import re
import calendar
def time_to_date(time_obj):
"""
Convert a time.time_struct to a true universal time (can pass to js Date constructor)
"""
# TODO change to using the isoformat() function on datetime. js date can parse those
return calendar.timegm(time_obj) * 1000
def jsdate_to_time(field):
"""
Convert a universal time (iso format) or msec since epoch to a time obj
"""
if field is None:
return field
elif isinstance(field, unicode) or isinstance(field, str): # iso format but ignores time zone assuming it's Z
d=datetime.datetime(*map(int, re.split('[^\d]', field)[:6])) # stop after seconds. Debatable
return d.utctimetuple()
elif isinstance(field, int) or isinstance(field, float):
return time.gmtime(field / 1000)
elif isinstance(field, time.struct_time):
return field
\ No newline at end of file
...@@ -1133,7 +1133,13 @@ class CodeResponse(LoncapaResponse): ...@@ -1133,7 +1133,13 @@ class CodeResponse(LoncapaResponse):
xml = self.xml xml = self.xml
# TODO: XML can override external resource (grader/queue) URL # TODO: XML can override external resource (grader/queue) URL
self.url = xml.get('url', None) self.url = xml.get('url', None)
self.queue_name = xml.get('queuename', self.system.xqueue['default_queuename'])
# We do not support xqueue within Studio.
if self.system.xqueue is not None:
default_queuename = self.system.xqueue['default_queuename']
else:
default_queuename = None
self.queue_name = xml.get('queuename', default_queuename)
# VS[compat]: # VS[compat]:
# Check if XML uses the ExternalResponse format or the generic CodeResponse format # Check if XML uses the ExternalResponse format or the generic CodeResponse format
...@@ -1230,6 +1236,13 @@ class CodeResponse(LoncapaResponse): ...@@ -1230,6 +1236,13 @@ class CodeResponse(LoncapaResponse):
(err, self.answer_id, convert_files_to_filenames(student_answers))) (err, self.answer_id, convert_files_to_filenames(student_answers)))
raise Exception(err) raise Exception(err)
# We do not support xqueue within Studio.
if self.system.xqueue is None:
cmap = CorrectMap()
cmap.set(self.answer_id, queuestate=None,
msg='Error checking problem: no external queueing server is configured.')
return cmap
# Prepare xqueue request # Prepare xqueue request
#------------------------------------------------------------ #------------------------------------------------------------
......
...@@ -12,6 +12,7 @@ ...@@ -12,6 +12,7 @@
<script type="text/javascript" src="<%= common_coffee_root %>/logger.js"></script> <script type="text/javascript" src="<%= common_coffee_root %>/logger.js"></script>
<script type="text/javascript" src="<%= common_js_root %>/vendor/jquery.min.js"></script> <script type="text/javascript" src="<%= common_js_root %>/vendor/jquery.min.js"></script>
<script type="text/javascript" src="<%= common_js_root %>/vendor/jasmine-jquery.js"></script> <script type="text/javascript" src="<%= common_js_root %>/vendor/jasmine-jquery.js"></script>
<script type="text/javascript" src="<%= common_js_root %>/vendor/CodeMirror/codemirror.js"></script>
<script type="text/javascript" src="<%= common_js_root %>/vendor/mathjax-MathJax-c9db6ac/MathJax.js"></script> <script type="text/javascript" src="<%= common_js_root %>/vendor/mathjax-MathJax-c9db6ac/MathJax.js"></script>
<script type="text/javascript"> <script type="text/javascript">
......
...@@ -18,7 +18,7 @@ class StaticContent(object): ...@@ -18,7 +18,7 @@ class StaticContent(object):
self.content_type = content_type self.content_type = content_type
self.data = data self.data = data
self.last_modified_at = last_modified_at self.last_modified_at = last_modified_at
self.thumbnail_location = thumbnail_location self.thumbnail_location = Location(thumbnail_location)
# optional information about where this file was imported from. This is needed to support import/export # optional information about where this file was imported from. This is needed to support import/export
# cycles # cycles
self.import_path = import_path self.import_path = import_path
......
...@@ -31,8 +31,7 @@ class MongoContentStore(ContentStore): ...@@ -31,8 +31,7 @@ class MongoContentStore(ContentStore):
id = content.get_id() id = content.get_id()
# Seems like with the GridFS we can't update existing ID's we have to do a delete/add pair # Seems like with the GridFS we can't update existing ID's we have to do a delete/add pair
if self.fs.exists({"_id" : id}): self.delete(id)
self.fs.delete(id)
with self.fs.new_file(_id = id, filename=content.get_url_path(), content_type=content.content_type, with self.fs.new_file(_id = id, filename=content.get_url_path(), content_type=content.content_type,
displayname=content.name, thumbnail_location=content.thumbnail_location, import_path=content.import_path) as fp: displayname=content.name, thumbnail_location=content.thumbnail_location, import_path=content.import_path) as fp:
...@@ -41,13 +40,16 @@ class MongoContentStore(ContentStore): ...@@ -41,13 +40,16 @@ class MongoContentStore(ContentStore):
return content return content
def delete(self, id):
if self.fs.exists({"_id" : id}):
self.fs.delete(id)
def find(self, location): def find(self, location):
id = StaticContent.get_id_from_location(location) id = StaticContent.get_id_from_location(location)
try: try:
with self.fs.get(id) as fp: with self.fs.get(id) as fp:
return StaticContent(location, fp.displayname, fp.content_type, fp.read(), return StaticContent(location, fp.displayname, fp.content_type, fp.read(),
fp.uploadDate, thumbnail_location = fp.thumbnail_location if 'thumbnail_location' in fp else None, fp.uploadDate, thumbnail_location = fp.thumbnail_location if hasattr(fp, 'thumbnail_location') else None,
import_path = fp.import_path if hasattr(fp, 'import_path') else None) import_path = fp.import_path if hasattr(fp, 'import_path') else None)
except NoFile: except NoFile:
raise NotFoundError() raise NotFoundError()
......
from fs.errors import ResourceNotFoundError from cStringIO import StringIO
import logging
import json
from lxml import etree from lxml import etree
from path import path # NOTE (THK): Only used for detecting presence of syllabus from path import path # NOTE (THK): Only used for detecting presence of syllabus
import requests from xmodule.graders import grader_from_conf
import time
from cStringIO import StringIO
from xmodule.util.decorators import lazyproperty
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.seq_module import SequenceDescriptor, SequenceModule from xmodule.seq_module import SequenceDescriptor, SequenceModule
from xmodule.xml_module import XmlDescriptor
from xmodule.timeparse import parse_time, stringify_time from xmodule.timeparse import parse_time, stringify_time
from xmodule.graders import grader_from_conf from xmodule.util.decorators import lazyproperty
import json
import logging
import requests
import time
import copy
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -92,10 +91,6 @@ class CourseDescriptor(SequenceDescriptor): ...@@ -92,10 +91,6 @@ class CourseDescriptor(SequenceDescriptor):
log.critical(msg) log.critical(msg)
system.error_tracker(msg) system.error_tracker(msg)
self.enrollment_start = self._try_parse_time("enrollment_start")
self.enrollment_end = self._try_parse_time("enrollment_end")
self.end = self._try_parse_time("end")
# NOTE: relies on the modulestore to call set_grading_policy() right after # NOTE: relies on the modulestore to call set_grading_policy() right after
# init. (Modulestore is in charge of figuring out where to load the policy from) # init. (Modulestore is in charge of figuring out where to load the policy from)
...@@ -105,19 +100,11 @@ class CourseDescriptor(SequenceDescriptor): ...@@ -105,19 +100,11 @@ class CourseDescriptor(SequenceDescriptor):
self.set_grading_policy(self.definition['data'].get('grading_policy', None)) self.set_grading_policy(self.definition['data'].get('grading_policy', None))
def defaut_grading_policy(self):
def set_grading_policy(self, course_policy):
if course_policy is None:
course_policy = {}
""" """
The JSON object can have the keys GRADER and GRADE_CUTOFFS. If either is Return a dict which is a copy of the default grading policy
missing, it reverts to the default.
""" """
default = {"GRADER" : [
default_policy_string = """
{
"GRADER" : [
{ {
"type" : "Homework", "type" : "Homework",
"min_count" : 12, "min_count" : 12,
...@@ -129,37 +116,44 @@ class CourseDescriptor(SequenceDescriptor): ...@@ -129,37 +116,44 @@ class CourseDescriptor(SequenceDescriptor):
"type" : "Lab", "type" : "Lab",
"min_count" : 12, "min_count" : 12,
"drop_count" : 2, "drop_count" : 2,
"category" : "Labs",
"weight" : 0.15 "weight" : 0.15
}, },
{ {
"type" : "Midterm", "type" : "Midterm Exam",
"name" : "Midterm Exam",
"short_label" : "Midterm", "short_label" : "Midterm",
"min_count" : 1,
"drop_count" : 0,
"weight" : 0.3 "weight" : 0.3
}, },
{ {
"type" : "Final", "type" : "Final Exam",
"name" : "Final Exam",
"short_label" : "Final", "short_label" : "Final",
"min_count" : 1,
"drop_count" : 0,
"weight" : 0.4 "weight" : 0.4
} }
], ],
"GRADE_CUTOFFS" : { "GRADE_CUTOFFS" : {
"A" : 0.87, "Pass" : 0.5
"B" : 0.7, }}
"C" : 0.6 return copy.deepcopy(default)
}
} def set_grading_policy(self, course_policy):
"""
The JSON object can have the keys GRADER and GRADE_CUTOFFS. If either is
missing, it reverts to the default.
""" """
if course_policy is None:
course_policy = {}
# Load the global settings as a dictionary # Load the global settings as a dictionary
grading_policy = json.loads(default_policy_string) grading_policy = self.defaut_grading_policy()
# Override any global settings with the course settings # Override any global settings with the course settings
grading_policy.update(course_policy) grading_policy.update(course_policy)
# Here is where we should parse any configurations, so that we can fail early # Here is where we should parse any configurations, so that we can fail early
grading_policy['RAW_GRADER'] = grading_policy['GRADER'] # used for cms access
grading_policy['GRADER'] = grader_from_conf(grading_policy['GRADER']) grading_policy['GRADER'] = grader_from_conf(grading_policy['GRADER'])
self._grading_policy = grading_policy self._grading_policy = grading_policy
...@@ -168,8 +162,8 @@ class CourseDescriptor(SequenceDescriptor): ...@@ -168,8 +162,8 @@ class CourseDescriptor(SequenceDescriptor):
@classmethod @classmethod
def read_grading_policy(cls, paths, system): def read_grading_policy(cls, paths, system):
"""Load a grading policy from the specified paths, in order, if it exists.""" """Load a grading policy from the specified paths, in order, if it exists."""
# Default to a blank policy # Default to a blank policy dict
policy_str = '""' policy_str = '{}'
for policy_path in paths: for policy_path in paths:
if not system.resources_fs.exists(policy_path): if not system.resources_fs.exists(policy_path):
...@@ -252,13 +246,53 @@ class CourseDescriptor(SequenceDescriptor): ...@@ -252,13 +246,53 @@ class CourseDescriptor(SequenceDescriptor):
return time.gmtime() > self.start return time.gmtime() > self.start
@property @property
def end(self):
return self._try_parse_time("end")
@end.setter
def end(self, value):
if isinstance(value, time.struct_time):
self.metadata['end'] = stringify_time(value)
@property
def enrollment_start(self):
return self._try_parse_time("enrollment_start")
@enrollment_start.setter
def enrollment_start(self, value):
if isinstance(value, time.struct_time):
self.metadata['enrollment_start'] = stringify_time(value)
@property
def enrollment_end(self):
return self._try_parse_time("enrollment_end")
@enrollment_end.setter
def enrollment_end(self, value):
if isinstance(value, time.struct_time):
self.metadata['enrollment_end'] = stringify_time(value)
@property
def grader(self): def grader(self):
return self._grading_policy['GRADER'] return self._grading_policy['GRADER']
@property @property
def raw_grader(self):
return self._grading_policy['RAW_GRADER']
@raw_grader.setter
def raw_grader(self, value):
# NOTE WELL: this change will not update the processed graders. If we need that, this needs to call grader_from_conf
self._grading_policy['RAW_GRADER'] = value
self.definition['data'].setdefault('grading_policy',{})['GRADER'] = value
@property
def grade_cutoffs(self): def grade_cutoffs(self):
return self._grading_policy['GRADE_CUTOFFS'] return self._grading_policy['GRADE_CUTOFFS']
@grade_cutoffs.setter
def grade_cutoffs(self, value):
self._grading_policy['GRADE_CUTOFFS'] = value
self.definition['data'].setdefault('grading_policy',{})['GRADE_CUTOFFS'] = value
@property @property
def tabs(self): def tabs(self):
""" """
......
<section class="html-edit">
<div name="" class="edit-box" rows="8" cols="40">&lt;problem>
&lt;p>&lt;/p>
&lt;multiplechoiceresponse>
<pre>&lt;problem>
&lt;p>&lt;/p></pre>
<div><foo>bar</foo></div></div>
</section>
\ No newline at end of file
describe 'HTMLEditingDescriptor', ->
describe 'Read data from server, create Editor, and get data back out', ->
it 'Does not munge &lt', ->
# This is a test for Lighthouse #22,
# "html names are automatically converted to the symbols they describe"
# A better test would be a Selenium test to avoid duplicating the
# mako template structure in html-edit-formattingbug.html.
# However, we currently have no working Selenium tests.
loadFixtures 'html-edit-formattingbug.html'
@descriptor = new HTMLEditingDescriptor($('.html-edit'))
data = @descriptor.save().data
expect(data).toEqual("""&lt;problem&gt;
&lt;p&gt;&lt;/p&gt;
&lt;multiplechoiceresponse&gt;
<pre>&lt;problem&gt;
&lt;p&gt;&lt;/p&gt;</pre>
<div><foo>bar</foo></div>""")
\ No newline at end of file
class @HTMLEditingDescriptor class @HTMLEditingDescriptor
constructor: (@element) -> constructor: (@element) ->
@edit_box = CodeMirror.fromTextArea($(".edit-box", @element)[0], { text = $(".edit-box", @element)[0];
replace_func = (elt) -> text.parentNode.replaceChild(elt, text)
@edit_box = CodeMirror(replace_func, {
value: text.innerHTML
mode: "text/html" mode: "text/html"
lineNumbers: true lineNumbers: true
lineWrapping: true lineWrapping: true})
})
save: -> save: ->
data: @edit_box.getValue() data: @edit_box.getValue()
...@@ -51,6 +51,9 @@ class CachingDescriptorSystem(MakoDescriptorSystem): ...@@ -51,6 +51,9 @@ class CachingDescriptorSystem(MakoDescriptorSystem):
self.modulestore = modulestore self.modulestore = modulestore
self.module_data = module_data self.module_data = module_data
self.default_class = default_class self.default_class = default_class
# cdodge: other Systems have a course_id attribute defined. To keep things consistent, let's
# define an attribute here as well, even though it's None
self.course_id = None
def load_item(self, location): def load_item(self, location):
location = Location(location) location = Location(location)
......
import logging
from xmodule.contentstore.content import StaticContent
from xmodule.modulestore import Location
from xmodule.modulestore.mongo import MongoModuleStore
def clone_course(modulestore, contentstore, source_location, dest_location, delete_original=False):
# first check to see if the modulestore is Mongo backed
if not isinstance(modulestore, MongoModuleStore):
raise Exception("Expected a MongoModuleStore in the runtime. Aborting....")
# check to see if the dest_location exists as an empty course
# we need an empty course because the app layers manage the permissions and users
if not modulestore.has_item(dest_location):
raise Exception("An empty course at {0} must have already been created. Aborting...".format(dest_location))
# verify that the dest_location really is an empty course, which means only one
dest_modules = modulestore.get_items([dest_location.tag, dest_location.org, dest_location.course, None, None, None])
if len(dest_modules) != 1:
raise Exception("Course at destination {0} is not an empty course. You can only clone into an empty course. Aborting...".format(dest_location))
# check to see if the source course is actually there
if not modulestore.has_item(source_location):
raise Exception("Cannot find a course at {0}. Aborting".format(source_location))
# Get all modules under this namespace which is (tag, org, course) tuple
modules = modulestore.get_items([source_location.tag, source_location.org, source_location.course, None, None, None])
for module in modules:
original_loc = Location(module.location)
if original_loc.category != 'course':
module.location = module.location._replace(tag = dest_location.tag, org = dest_location.org,
course = dest_location.course)
else:
# on the course module we also have to update the module name
module.location = module.location._replace(tag = dest_location.tag, org = dest_location.org,
course = dest_location.course, name=dest_location.name)
print "Cloning module {0} to {1}....".format(original_loc, module.location)
if 'data' in module.definition:
modulestore.update_item(module.location, module.definition['data'])
# repoint children
if 'children' in module.definition:
new_children = []
for child_loc_url in module.definition['children']:
child_loc = Location(child_loc_url)
child_loc = child_loc._replace(tag = dest_location.tag, org = dest_location.org,
course = dest_location.course)
new_children = new_children + [child_loc.url()]
modulestore.update_children(module.location, new_children)
# save metadata
modulestore.update_metadata(module.location, module.metadata)
# now iterate through all of the assets and clone them
# first the thumbnails
thumbs = contentstore.get_all_content_thumbnails_for_course(source_location)
for thumb in thumbs:
thumb_loc = Location(thumb["_id"])
content = contentstore.find(thumb_loc)
content.location = content.location._replace(org = dest_location.org,
course = dest_location.course)
print "Cloning thumbnail {0} to {1}".format(thumb_loc, content.location)
contentstore.save(content)
# now iterate through all of the assets, also updating the thumbnail pointer
assets = contentstore.get_all_content_for_course(source_location)
for asset in assets:
asset_loc = Location(asset["_id"])
content = contentstore.find(asset_loc)
content.location = content.location._replace(org = dest_location.org,
course = dest_location.course)
# be sure to update the pointer to the thumbnail
if content.thumbnail_location is not None:
content.thumbnail_location = content.thumbnail_location._replace(org = dest_location.org,
course = dest_location.course)
print "Cloning asset {0} to {1}".format(asset_loc, content.location)
contentstore.save(content)
return True
def delete_course(modulestore, contentstore, source_location):
# first check to see if the modulestore is Mongo backed
if not isinstance(modulestore, MongoModuleStore):
raise Exception("Expected a MongoModuleStore in the runtime. Aborting....")
# check to see if the source course is actually there
if not modulestore.has_item(source_location):
raise Exception("Cannot find a course at {0}. Aborting".format(source_location))
# first delete all of the thumbnails
thumbs = contentstore.get_all_content_thumbnails_for_course(source_location)
for thumb in thumbs:
thumb_loc = Location(thumb["_id"])
id = StaticContent.get_id_from_location(thumb_loc)
print "Deleting {0}...".format(id)
contentstore.delete(id)
# then delete all of the assets
assets = contentstore.get_all_content_for_course(source_location)
for asset in assets:
asset_loc = Location(asset["_id"])
id = StaticContent.get_id_from_location(asset_loc)
print "Deleting {0}...".format(id)
contentstore.delete(id)
# then delete all course modules
modules = modulestore.get_items([source_location.tag, source_location.org, source_location.course, None, None, None])
for module in modules:
if module.category != 'course': # save deleting the course module for last
print "Deleting {0}...".format(module.location)
modulestore.delete_item(module.location)
# finally delete the top-level course module itself
print "Deleting {0}...".format(source_location)
modulestore.delete_item(source_location)
return True
\ No newline at end of file
--- ---
metadata: metadata:
display_name: Formula Repsonse display_name: Formula Response
rerandomize: never rerandomize: never
showanswer: always showanswer: always
data: | data: |
......
...@@ -22,7 +22,8 @@ class VideoModule(XModule): ...@@ -22,7 +22,8 @@ class VideoModule(XModule):
resource_string(__name__, 'js/src/video/display.coffee')] + resource_string(__name__, 'js/src/video/display.coffee')] +
[resource_string(__name__, 'js/src/video/display/' + filename) [resource_string(__name__, 'js/src/video/display/' + filename)
for filename for filename
in sorted(resource_listdir(__name__, 'js/src/video/display'))]} in sorted(resource_listdir(__name__, 'js/src/video/display'))
if filename.endswith('.coffee')]}
css = {'scss': [resource_string(__name__, 'css/video/display.scss')]} css = {'scss': [resource_string(__name__, 'css/video/display.scss')]}
js_module_name = "Video" js_module_name = "Video"
......
...@@ -10,10 +10,11 @@ from collections import namedtuple ...@@ -10,10 +10,11 @@ from collections import namedtuple
from pkg_resources import resource_listdir, resource_string, resource_isdir from pkg_resources import resource_listdir, resource_string, resource_isdir
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.timeparse import parse_time from xmodule.timeparse import parse_time, stringify_time
from xmodule.contentstore.content import StaticContent, XASSET_SRCREF_PREFIX from xmodule.contentstore.content import StaticContent, XASSET_SRCREF_PREFIX
from xmodule.modulestore.exceptions import ItemNotFoundError from xmodule.modulestore.exceptions import ItemNotFoundError
import time
log = logging.getLogger('mitx.' + __name__) log = logging.getLogger('mitx.' + __name__)
...@@ -494,6 +495,11 @@ class XModuleDescriptor(Plugin, HTMLSnippet, ResourceTemplates): ...@@ -494,6 +495,11 @@ class XModuleDescriptor(Plugin, HTMLSnippet, ResourceTemplates):
return None return None
return self._try_parse_time('start') return self._try_parse_time('start')
@start.setter
def start(self, value):
if isinstance(value, time.struct_time):
self.metadata['start'] = stringify_time(value)
@property @property
def own_metadata(self): def own_metadata(self):
""" """
......
...@@ -12,10 +12,10 @@ class @TooltipManager ...@@ -12,10 +12,10 @@ class @TooltipManager
'click': @hideTooltip 'click': @hideTooltip
showTooltip: (e) => showTooltip: (e) =>
tooltipText = $(e.target).attr('data-tooltip') $target = $(e.target).closest('[data-tooltip]')
tooltipText = $target.attr('data-tooltip')
@$tooltip.html(tooltipText) @$tooltip.html(tooltipText)
@$body.append(@$tooltip) @$body.append(@$tooltip)
$(e.target).children().css('pointer-events', 'none')
tooltipCoords = tooltipCoords =
x: e.pageX - (@$tooltip.outerWidth() / 2) x: e.pageX - (@$tooltip.outerWidth() / 2)
...@@ -26,8 +26,8 @@ class @TooltipManager ...@@ -26,8 +26,8 @@ class @TooltipManager
'top': tooltipCoords.y 'top': tooltipCoords.y
@tooltipTimer = setTimeout ()=> @tooltipTimer = setTimeout ()=>
@$tooltip.show().css('opacity', 1)
@$tooltip.show().css('opacity', 1)
@tooltipTimer = setTimeout ()=> @tooltipTimer = setTimeout ()=>
@hideTooltip() @hideTooltip()
, 3000 , 3000
......
roots/2012_Fall.xml
\ No newline at end of file
<course org="edX" course="graded" url_name="2012_Fall"/>
\ No newline at end of file
roots/2012_Fall.xml
\ No newline at end of file
<course org="edX" course="sa_test" url_name="2012_Fall"/>
roots/2012_Fall.xml
\ No newline at end of file
<course org="edX" course="test_start_date" url_name="2012_Fall"/>
\ No newline at end of file
roots/2012_Fall.xml
\ No newline at end of file
<course org="edX" course="toy" url_name="2012_Fall"/>
\ No newline at end of file
...@@ -22,7 +22,8 @@ rake test_lms[false] || TESTS_FAILED=1 ...@@ -22,7 +22,8 @@ rake test_lms[false] || TESTS_FAILED=1
rake test_common/lib/capa || TESTS_FAILED=1 rake test_common/lib/capa || TESTS_FAILED=1
rake test_common/lib/xmodule || TESTS_FAILED=1 rake test_common/lib/xmodule || TESTS_FAILED=1
rake phantomjs_jasmine_lms || true rake phantomjs_jasmine_lms || true
rake phantomjs_jasmine_cms || true rake phantomjs_jasmine_cms || TESTS_FAILED=1
rake phantomjs_jasmine_common/lib/xmodule || true
rake coverage:xml coverage:html rake coverage:xml coverage:html
[ $TESTS_FAILED == '0' ] [ $TESTS_FAILED == '0' ]
......
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