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):
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):
# only admins can add/remove other users
......
......@@ -4,6 +4,7 @@ from xmodule.modulestore.django import modulestore
from lxml import etree
import re
from django.http import HttpResponseBadRequest
import logging
## TODO store as array of { date, content } and override course_info_module.definition_from_xml
## This should be in a class which inherits from XmlDescriptor
......@@ -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.
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:
course_html_parsed = etree.fromstring("<ol></ol>")
# Confirm that root is <ol>, iterate over <li>, pull out <h2> subs and then rest of val
course_upd_collection = []
if course_html_parsed.tag == 'ol':
# 0 is the oldest so that new ones get unique idx
for idx, update in enumerate(course_html_parsed.iter("li")):
# 0 is the newest
for idx, update in enumerate(course_html_parsed):
if (len(update) == 0):
continue
elif (len(update) == 1):
content = update.find("h2").tail
# could enforce that update[0].tag == 'h2'
content = update[0].tail
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"),
"content" : content})
# return newest to oldest
course_upd_collection.reverse()
return course_upd_collection
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.
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:
course_html_parsed = etree.fromstring("<ol></ol>")
try:
new_html_parsed = etree.fromstring(update['content'], etree.XMLParser(remove_blank_text=True))
except etree.XMLSyntaxError:
new_html_parsed = None
# No try/catch b/c failure generates an error back to client
new_html_parsed = etree.fromstring('<li><h2>' + update['date'] + '</h2>' + update['content'] + '</li>')
# Confirm that root is <ol>, iterate over <li>, pull out <h2> subs and then rest of val
if course_html_parsed.tag == 'ol':
# ??? Should this use the id in the json or in the url or does it matter?
if passed_id:
element = course_html_parsed.findall("li")[get_idx(passed_id)]
element[0].text = update['date']
if (len(element) == 1):
if new_html_parsed is not None:
element[0].tail = None
element.append(new_html_parsed)
else:
element[0].tail = update['content']
else:
if new_html_parsed is not None:
element[1] = new_html_parsed
else:
element.pop(1)
element[0].tail = update['content']
idx = get_idx(passed_id)
# idx is count from end of list
course_html_parsed[-idx] = new_html_parsed
else:
idx = len(course_html_parsed.findall("li"))
course_html_parsed.insert(0, new_html_parsed)
idx = len(course_html_parsed)
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
course_updates.definition['data'] = etree.tostring(course_html_parsed)
......@@ -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
# purely to handle free formed updates not done via editor. Actually kills them, but at least doesn't break.
try:
course_html_parsed = etree.fromstring(course_updates.definition['data'], etree.XMLParser(remove_blank_text=True))
course_html_parsed = etree.fromstring(course_updates.definition['data'])
except etree.XMLSyntaxError:
course_html_parsed = etree.fromstring("<ol></ol>")
if course_html_parsed.tag == 'ol':
# ??? Should this use the id in the json or in the url or does it matter?
element_to_delete = course_html_parsed.xpath('/ol/li[position()=' + str(get_idx(passed_id) + 1) + "]")
if element_to_delete:
course_html_parsed.remove(element_to_delete[0])
idx = get_idx(passed_id)
# idx is count from end of list
element_to_delete = course_html_parsed[-idx]
if element_to_delete is not None:
course_html_parsed.remove(element_to_delete)
# update db record
course_updates.definition['data'] = etree.tostring(course_html_parsed)
......@@ -143,6 +129,6 @@ def get_idx(passed_id):
From the url w/ idx appended, get the idx.
"""
# TODO compile this regex into a class static and reuse for each call
idx_matcher = re.search(r'.*/(\d)+$', passed_id)
idx_matcher = re.search(r'.*/(\d+)$', passed_id)
if idx_matcher:
return int(idx_matcher.group(1))
\ 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):
module = store.clone_item(template_location, location)
isNew = True
logging.debug('post = {0}'.format(post_data))
if post_data.get('data') is not None:
data = post_data['data']
logging.debug('data = {0}'.format(data))
store.update_item(location, data)
# 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
import copy
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):
"""Parse response, which is assumed to be json"""
......@@ -339,4 +344,45 @@ class ContentStoreTest(TestCase):
def test_edit_unit_full(self):
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
from xmodule.modulestore.django import modulestore
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):
'''
......@@ -60,20 +73,38 @@ def get_course_for_item(location):
def get_lms_link_for_item(location, preview=False):
location = Location(location)
if settings.LMS_BASE is not None:
lms_link = "//{preview}{lms_base}/courses/{course_id}/jump_to/{location}".format(
preview='preview.' if preview else '',
lms_base=settings.LMS_BASE,
# TODO: These will need to be changed to point to the particular instance of this problem in the particular course
course_id=modulestore().get_containing_courses(location)[0].id,
location=location,
course_id=get_course_id(location),
location=Location(location)
)
else:
lms_link = None
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):
draft = 'draft'
......@@ -103,3 +134,12 @@ def compute_unit_state(unit):
def get_date_display(date):
return date.strftime("%d %B, %Y at %I:%M %p")
def update_item(location, value):
"""
If value is None, delete the db entry. Otherwise, update it using the correct modulestore.
"""
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 = {
}
}
CONTENTSTORE = {
'ENGINE': 'xmodule.contentstore.mongo.MongoContentStore',
'OPTIONS': {
'host': 'localhost',
'db' : 'xcontent',
}
}
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
......@@ -77,6 +85,8 @@ DATABASES = {
}
}
LMS_BASE = "localhost:8000"
CACHES = {
# 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.
......
<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 @@
"/static/js/vendor/jquery.min.js",
"/static/js/vendor/json2.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
event.preventDefault()
data = @module.save()
data.metadata = @metadata()
$modalCover.hide()
@model.save(data).done( =>
# # showToastMessage("Your changes have been saved.", null, 3)
@module = null
......@@ -69,9 +70,11 @@ class CMS.Views.ModuleEdit extends Backbone.View
event.preventDefault()
@$el.removeClass('editing')
@$component_editor().slideUp(150)
$modalCover.hide()
clickEditButton: (event) ->
event.preventDefault()
@$el.addClass('editing')
$modalCover.show()
@$component_editor().slideDown(150)
@loadEdit()
......@@ -33,6 +33,10 @@ class CMS.Views.TabsEdit extends Backbone.View
)
$('.new-component-item').before(editor.$el)
editor.$el.addClass('new')
setTimeout(=>
editor.$el.removeClass('new')
, 500)
editor.cloneTemplate(
@model.get('id'),
......
......@@ -4,7 +4,6 @@ class CMS.Views.UnitEdit extends Backbone.View
'click .new-component .cancel-button': 'closeNewComponent'
'click .new-component-templates .new-component-template a': 'saveNewComponent'
'click .new-component-templates .cancel-button': 'closeNewComponent'
'click .new-component-button': 'showNewComponentForm'
'click .delete-draft': 'deleteDraft'
'click .create-draft': 'createDraft'
'click .publish-draft': 'publishDraft'
......@@ -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) =>
event.preventDefault()
type = $(event.currentTarget).data('type')
@$newComponentTypePicker.fadeOut(250)
@$(".new-component-#{type}").fadeIn(250)
@$newComponentTypePicker.slideUp(250)
@$(".new-component-#{type}").slideDown(250)
closeNewComponent: (event) =>
event.preventDefault()
@$newComponentTypePicker.slideUp(250)
@$newComponentTypePicker.slideDown(250)
@$newComponentTemplatePickers.slideUp(250)
@$newComponentButton.fadeIn(250)
@$newComponentItem.removeClass('adding')
@$newComponentItem.find('.rendered-component').remove()
@$newComponentItem.css('height', @$newComponentButton.outerHeight())
saveNewComponent: (event) =>
event.preventDefault()
......
......@@ -2,8 +2,6 @@ var $body;
var $modal;
var $modalCover;
var $newComponentItem;
var $newComponentStep1;
var $newComponentStep2;
var $changedInput;
var $spinner;
......@@ -15,6 +13,10 @@ $(document).ready(function() {
// pipelining (note, this doesn't happen on local runtimes). So if we set it on window, when we can access it from other
// scopes (namely the course-info tab)
window.$modalCover = $modalCover;
// Control whether template caching in local memory occurs (see template_loader.js). Caching screws up development but may
// be a good optimization in production (it works fairly well)
window.cachetemplates = false;
$body.append($modalCover);
$newComponentItem = $('.new-component-item');
......@@ -39,6 +41,8 @@ $(document).ready(function() {
$('.unit .item-actions .delete-button').bind('click', deleteUnit);
$('.new-unit-item').bind('click', createNewUnit);
$('.collapse-all-button').bind('click', collapseAll);
// autosave when a field is updated on the subsection page
$body.on('keyup', '.subsection-display-name-input, .unit-subtitle, .policy-list-value', checkForNewValue);
$('.subsection-display-name-input, .unit-subtitle, .policy-list-name, .policy-list-value').each(function(i) {
......@@ -105,15 +109,12 @@ $(document).ready(function() {
$('.edit-section-start-cancel').bind('click', cancelSetSectionScheduleDate);
$('.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);
$body.on('click', '.section-published-date .edit-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 .cancel-button', hideModal)
$body.on('click', '.edit-subsection-publish-settings .cancel-button', hideModal);
$body.on('change', '.edit-subsection-publish-settings .start-date', function() {
if($('.edit-subsection-publish-settings').find('.start-time').val() == '') {
$('.edit-subsection-publish-settings').find('.start-time').val('12:00am');
......@@ -124,6 +125,11 @@ $(document).ready(function() {
});
});
function collapseAll(e) {
$('.branch').addClass('collapsed');
$('.expand-collapse-icon').removeClass('collapse').addClass('expand');
}
function editSectionPublishDate(e) {
e.preventDefault();
$modal = $('.edit-subsection-publish-settings').show();
......@@ -303,7 +309,7 @@ function checkForNewValue(e) {
this.saveTimer = setTimeout(function() {
$changedInput = $(e.target);
saveSubsection()
saveSubsection();
this.saveTimer = null;
}, 500);
}
......@@ -316,7 +322,7 @@ function autosaveInput(e) {
this.saveTimer = setTimeout(function() {
$changedInput = $(e.target);
saveSubsection()
saveSubsection();
this.saveTimer = null;
}, 500);
}
......@@ -338,23 +344,22 @@ function saveSubsection() {
// pull all 'normalized' metadata editable fields on page
var metadata_fields = $('input[data-metadata-name]');
metadata = {};
var metadata = {};
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;
}
// 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) {
name = $(element).children('.policy-list-name').val();
val = $(element).children('.policy-list-value').val();
metadata[name] = val;
var name = $(element).children('.policy-list-name').val();
metadata[name] = $(element).children('.policy-list-value').val();
});
// now add any 'removed' policy metadata which is stored in a separate hidden div
// 'null' presented to the server means 'remove'
$("#policy-to-delete > li.policy-list-element").each(function(i, element) {
name = $(element).children('.policy-list-name').val();
var name = $(element).children('.policy-list-name').val();
if (name != "")
metadata[name] = null;
});
......@@ -390,7 +395,7 @@ function createNewUnit(e) {
$.post('/clone_item',
{'parent_location' : parent,
'template' : template,
'display_name': 'New Unit',
'display_name': 'New Unit'
},
function(data) {
// redirect to the edit page
......@@ -480,7 +485,7 @@ function displayFinishedUpload(xhr) {
var template = $('#new-asset-element').html();
var html = Mustache.to_html(template, resp);
$('table > tbody > tr:first').before(html);
$('table > tbody').prepend(html);
}
......@@ -493,6 +498,7 @@ function hideModal(e) {
if(e) {
e.preventDefault();
}
$('.file-input').unbind('change', startUpload);
$modal.hide();
$modalCover.hide();
}
......@@ -593,9 +599,11 @@ function hideToastMessage(e) {
function addNewSection(e, isTemplate) {
e.preventDefault();
$(e.target).addClass('disabled');
var $newSection = $($('#new-section-template').html());
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('.section-name-form').bind('submit', saveNewSection);
$cancelButton.bind('click', cancelNewSection);
......@@ -632,11 +640,14 @@ function saveNewSection(e) {
function cancelNewSection(e) {
e.preventDefault();
$('.new-courseware-section-button').removeClass('disabled');
$(this).parents('section.new-section').remove();
}
function addNewCourse(e) {
e.preventDefault();
$(e.target).hide();
var $newCourse = $($('#new-course-template').html());
var $cancelButton = $newCourse.find('.new-course-cancel');
$('.new-course-button').after($newCourse);
......@@ -664,7 +675,7 @@ function saveNewCourse(e) {
'template' : template,
'org' : org,
'number' : number,
'display_name': display_name,
'display_name': display_name
},
function(data) {
if (data.id != undefined) {
......@@ -677,6 +688,7 @@ function saveNewCourse(e) {
function cancelNewCourse(e) {
e.preventDefault();
$('.new-course-button').show();
$(this).parents('section.new-course').remove();
}
......@@ -692,7 +704,7 @@ function addNewSubsection(e) {
var parent = $(this).parents("section.branch").data("id");
$saveButton.data('parent', parent)
$saveButton.data('parent', parent);
$saveButton.data('template', $(this).data('template'));
$newSubsection.find('.new-subsection-form').bind('submit', saveNewSubsection);
......@@ -757,7 +769,7 @@ function saveEditSectionName(e) {
$spinner.show();
if (display_name == '') {
alert("You must specify a name before saving.")
alert("You must specify a name before saving.");
return;
}
......@@ -794,13 +806,12 @@ function cancelSetSectionScheduleDate(e) {
function saveSetSectionScheduleDate(e) {
e.preventDefault();
input_date = $('.edit-subsection-publish-settings .start-date').val();
input_time = $('.edit-subsection-publish-settings .start-time').val();
var input_date = $('.edit-subsection-publish-settings .start-date').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 $_this = $(this);
var id = $modal.attr('data-id');
// call into server to commit the new order
$.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 @@
if (typeof window.templateLoader == 'function') return;
var templateLoader = {
templateVersion: "0.0.8",
templateVersion: "0.0.12",
templates: {},
loadRemoteTemplate: function(templateName, filename, callback) {
if (!this.templates[templateName]) {
......@@ -35,7 +35,8 @@
localStorageAvailable: function() {
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) {
return false;
}
......
......@@ -62,6 +62,7 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
},
onNew: function(event) {
event.preventDefault();
var self = this;
// create new obj, insert into collection, and render this one ele overriding the hidden attr
var newModel = new CMS.Models.CourseUpdate();
......@@ -69,6 +70,9 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
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();
if (this.$codeMirror == null ) {
this.$codeMirror = CodeMirror.fromTextArea($textArea.get(0), {
......@@ -77,9 +81,7 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
lineWrapping: true,
});
}
var updateEle = this.$el.find("#course-update-list");
$(updateEle).prepend($newForm);
$newForm.addClass('editing');
this.$currentPost = $newForm.closest('li');
......@@ -93,15 +95,19 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
},
onSave: function(event) {
event.preventDefault();
var targetModel = this.eventModel(event);
console.log(this.contentEntry(event).val());
targetModel.set({ date : this.dateEntry(event).val(), content : this.$codeMirror.getValue() });
// 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);
targetModel.save();
},
onCancel: function(event) {
event.preventDefault();
// change editor contents back to model values and hide the editor
$(this.editor(event)).hide();
var targetModel = this.eventModel(event);
......@@ -109,6 +115,7 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
},
onEdit: function(event) {
event.preventDefault();
var self = this;
this.$currentPost = $(event.target).closest('li');
this.$currentPost.addClass('editing');
......@@ -131,6 +138,7 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
},
onDelete: function(event) {
event.preventDefault();
// TODO ask for confirmation
// remove the dom element and delete the model
var targetModel = this.eventModel(event);
......@@ -158,6 +166,8 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
self.$currentPost.find('form').hide();
window.$modalCover.unbind('click');
window.$modalCover.hide();
this.$codeMirror = null;
self.$currentPost.find('.CodeMirror').remove();
},
// Dereferencing from events to screen elements
......@@ -271,5 +281,7 @@ CMS.Views.ClassInfoHandoutsView = Backbone.View.extend({
this.$form.hide();
window.$modalCover.unbind('click');
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 @@
background-color: #fff;
}
.upload-button {
@include blue-button;
float: left;
margin-right: 20px;
padding: 8px 30px 10px;
font-size: 12px;
}
.asset-library {
@include clearfix;
......
......@@ -6,11 +6,15 @@
body {
min-width: 980px;
background: #f3f4f5;
font-family: 'Open Sans', sans-serif;
background: rgb(240, 241, 245);
font-size: 16px;
line-height: 1.6;
color: #3c3c3c;
color: $baseFontColor;
}
body,
input {
font-family: 'Open Sans', sans-serif;
}
a {
......@@ -26,7 +30,8 @@ a {
h1 {
float: left;
font-size: 28px;
margin: 36px 6px;
font-weight: 300;
margin: 24px 6px;
}
.waiting {
......@@ -34,8 +39,7 @@ h1 {
}
.page-actions {
float: right;
margin-top: 42px;
margin-bottom: 30px;
}
.main-wrapper {
......@@ -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 {
float: right;
width: 28%;
......@@ -80,17 +77,18 @@ footer {
input[type="text"],
input[type="email"],
input[type="password"] {
input[type="password"],
textarea.text {
padding: 6px 8px 8px;
@include box-sizing(border-box);
border: 1px solid #b0b6c2;
border: 1px solid $mediumGrey;
border-radius: 2px;
@include linear-gradient(top, rgba(255, 255, 255, 0), rgba(255, 255, 255, .3));
background-color: #edf1f5;
@include linear-gradient($lightGrey, tint($lightGrey, 90%));
background-color: $lightGrey;
@include box-shadow(0 1px 2px rgba(0, 0, 0, .1) inset);
font-family: 'Open Sans', sans-serif;
font-size: 11px;
color: #3c3c3c;
color: $baseFontColor;
outline: 0;
&::-webkit-input-placeholder,
......@@ -98,6 +96,11 @@ input[type="password"] {
&:-ms-input-placeholder {
color: #979faf;
}
&:focus {
@include linear-gradient($paleYellow, tint($paleYellow, 90%));
outline: 0;
}
}
input.search {
......@@ -107,7 +110,7 @@ input.search {
border-radius: 20px;
background: url(../img/search-icon.png) no-repeat 8px 7px #edf1f5;
font-family: 'Open Sans', sans-serif;
color: #3c3c3c;
color: $baseFontColor;
outline: 0;
&::-webkit-input-placeholder {
......@@ -126,12 +129,18 @@ code {
font-family: Monaco, monospace;
}
.CodeMirror {
font-size: 13px;
border: 1px solid $darkGrey;
background: #fff;
}
.text-editor {
width: 100%;
min-height: 80px;
padding: 10px;
@include box-sizing(border-box);
border: 1px solid #b0b6c2;
border: 1px solid $mediumGrey;
@include linear-gradient(top, rgba(255, 255, 255, 0), rgba(255, 255, 255, 0.3));
background-color: #edf1f5;
@include box-shadow(0 1px 2px rgba(0, 0, 0, 0.1) inset);
......@@ -173,27 +182,32 @@ code {
padding: 10px 0;
}
.details {
display: none;
margin-bottom: 30px;
font-size: 14px;
}
.window {
margin-bottom: 20px;
border: 1px solid $mediumGrey;
border-radius: 3px;
background: #fff;
@include box-shadow(0 1px 1px rgba(0, 0, 0, 0.1));
.window-contents {
padding: 20px;
}
.details {
margin-bottom: 30px;
font-size: 14px;
}
h4 {
padding: 6px 14px;
border-bottom: 1px solid #cbd1db;
border-radius: 3px 3px 0 0;
@include linear-gradient(top, rgba(255, 255, 255, .3), rgba(255, 255, 255, 0) 70%);
background-color: #edf1f5;
@include box-shadow(0 1px 0 rgba(255, 255, 255, .7) inset);
border-bottom: 1px solid $mediumGrey;
border-radius: 2px 2px 0 0;
@include linear-gradient(top, rgba(255, 255, 255, .4), rgba(255, 255, 255, 0));
background-color: $lightBluishGrey;
@include box-shadow(0 1px 0 rgba(255, 255, 255, 0.2) inset);
font-size: 14px;
font-weight: 600;
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.3);
}
label {
......@@ -345,8 +359,9 @@ body.show-wip {
}
.new-button {
@include grey-button;
padding: 20px 0;
@include green-button;
font-size: 13px;
padding: 8px 20px 10px;
text-align: center;
&.big {
......@@ -367,4 +382,39 @@ body.show-wip {
.delete-icon {
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,19 +47,34 @@
}
}
@mixin white-button {
@include button;
border: 1px solid $darkGrey;
border-radius: 3px;
@include linear-gradient(top, rgba(255, 255, 255, 0.6), rgba(255, 255, 255, 0));
background-color: #dfe5eb;
@include box-shadow(0 1px 0 rgba(255, 255, 255, .3) inset);
color: #778192;
@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;
}
}
&:hover {
background-color: #f2f6f9;
color: #778192;
}
@mixin white-button {
@include button;
border: 1px solid $mediumGrey;
border-radius: 3px;
@include linear-gradient(top, rgba(255, 255, 255, 0.6), rgba(255, 255, 255, 0));
background-color: #dfe5eb;
@include box-shadow(0 1px 0 rgba(255, 255, 255, .3) inset);
color: rgb(92, 103, 122);
text-shadow: 0 1px 0 rgba(255, 255, 255, .5);
&:hover {
background-color: rgb(222, 236, 247);
color: rgb(92, 103, 122);
}
}
@mixin orange-button {
......@@ -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 {
@include button;
border: 1px solid #1c1e20;
......@@ -109,18 +146,17 @@
@mixin edit-box {
padding: 15px 20px;
border-radius: 3px;
border: 1px solid #5597dd;
@include linear-gradient(top, rgba(255, 255, 255, 0.3), rgba(255, 255, 255, 0));
background-color: #5597dd;
background-color: $lightBluishGrey2;
color: #3c3c3c;
@include box-shadow(0 1px 0 rgba(255, 255, 255, .2) inset);
label {
color: #fff;
color: $baseFontColor;
}
input,
textarea {
border: 1px solid #3c3c3c;
border: 1px solid $darkGrey;
}
textarea {
......@@ -140,21 +176,19 @@
}
.save-button {
@include orange-button;
border-color: #3c3c3c;
@include blue-button;
margin-top: 0;
}
.cancel-button {
@include white-button;
border-color: #30649C;
margin-top: 0;
}
}
@mixin tree-view {
border: 1px solid #ced2db;
background: #edf1f5;
border: 1px solid $mediumGrey;
background: $lightGrey;
.branch {
margin-bottom: 10px;
......@@ -200,15 +234,10 @@
content: "- draft";
}
.public-item:after {
content: "- public";
}
.private-item:after {
content: "- private";
}
.public-item,
.private-item {
color: #a4aab7;
}
......@@ -219,7 +248,11 @@
}
a {
color: #2c2e33;
color: $baseFontColor;
&.new-unit-item {
color: #6d788b;
}
}
ol {
......@@ -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 {
float: left;
width: 260px;
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 {
position: relative;
background: #fff;
border: 1px solid $darkGrey;
border-radius: 3px;
margin: 10px 0;
border: 1px solid $mediumGrey;
margin-top: 15px;
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 {
margin-top: 0;
......@@ -49,7 +167,7 @@ input.courseware-unit-search-input {
margin-right: 15px;
strong {
font-weight: 700;
font-weight: bold;
}
}
......@@ -76,7 +194,7 @@ input.courseware-unit-search-input {
background: none;
@include box-shadow(none);
font-size: 13px;
font-weight: 700;
font-weight: bold;
color: $blue;
cursor: pointer;
}
......@@ -96,13 +214,200 @@ input.courseware-unit-search-input {
}
header {
height: 75px;
min-height: 75px;
@include clearfix();
.item-details, .section-published-date {
.item-details {
float: left;
padding: 21px 0 0;
}
.item-details {
display: inline-block;
padding: 20px 0 10px 0;
@include clearfix();
.section-name {
float: left;
margin-right: 10px;
width: 350px;
font-size: 19px;
font-weight: bold;
color: $blue;
}
.section-name-span {
cursor: pointer;
@include transition(color .15s);
&:hover {
color: $orange;
}
}
.section-name-edit {
position: relative;
width: 400px;
background: $white;
input {
font-size: 16px;
}
.save-button {
@include blue-button;
padding: 7px 20px 7px;
margin-right: 5px;
}
.cancel-button {
@include white-button;
padding: 7px 20px 7px;
}
}
.section-published-date {
float: right;
width: 265px;
margin-right: 220px;
@include border-radius(3px);
background: $lightGrey;
.published-status {
font-size: 12px;
margin-right: 15px;
strong {
font-weight: bold;
}
}
.schedule-button {
@include blue-button;
}
.edit-button {
@include blue-button;
}
.schedule-button,
.edit-button {
font-size: 11px;
padding: 3px 15px 5px;
}
}
.gradable-status {
position: absolute;
top: 20px;
right: 70px;
width: 145px;
.status-label {
position: absolute;
top: 0;
right: 2px;
display: none;
width: 100px;
padding: 10px 35px 10px 10px;
@include border-radius(3px);
background: $lightGrey;
color: $lightGrey;
text-align: right;
font-size: 12px;
font-weight: bold;
line-height: 16px;
}
.menu-toggle {
z-index: 10;
position: absolute;
top: 2px;
right: 5px;
padding: 5px;
color: $lightGrey;
&:hover, &.is-active {
color: $blue;
}
}
.menu {
z-index: 1;
display: none;
opacity: 0.0;
position: absolute;
top: -1px;
left: 2px;
margin: 0;
padding: 8px 12px;
background: $white;
border: 1px solid $mediumGrey;
font-size: 12px;
@include border-radius(4px);
@include box-shadow(0 1px 2px rgba(0, 0, 0, .2));
@include transition(opacity .15s);
@include transition(display .15s);
li {
width: 115px;
margin-bottom: 3px;
padding-bottom: 3px;
border-bottom: 1px solid $lightGrey;
&:last-child {
margin-bottom: 0;
padding-bottom: 0;
border: none;
a {
color: $darkGrey;
}
}
}
a {
&.is-selected {
font-weight: bold;
}
}
}
// dropdown state
&.is-active {
.menu {
z-index: 1000;
display: block;
opacity: 1.0;
}
.menu-toggle {
z-index: 10000;
}
}
// set state
&.is-set {
.menu-toggle {
color: $blue;
}
.status-label {
display: block;
color: $blue;
}
}
float: left;
padding: 21px 0 0;
}
}
.item-actions {
margin-top: 21px;
margin-right: 12px;
......@@ -139,6 +444,10 @@ input.courseware-unit-search-input {
}
}
.section-name-form {
margin-bottom: 15px;
}
.section-name-edit {
input {
font-size: 16px;
......@@ -146,13 +455,13 @@ input.courseware-unit-search-input {
.save-button {
@include blue-button;
padding: 10px 20px;
padding: 7px 20px 7px;
margin-right: 5px;
}
.cancel-button {
@include white-button;
padding: 10px 20px;
padding: 7px 20px 7px;
}
}
......@@ -161,7 +470,7 @@ input.courseware-unit-search-input {
color: #878e9d;
strong {
font-weight: 700;
font-weight: bold;
}
}
......@@ -183,7 +492,7 @@ input.courseware-unit-search-input {
&.new-section {
header {
height: auto;
@include clearfix;
@include clearfix();
}
.expand-collapse-icon {
......@@ -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-subsection-name-input {
width: 515px;
......@@ -200,7 +520,7 @@ input.courseware-unit-search-input {
.new-section-name-save,
.new-subsection-name-save {
@include blue-button;
padding: 6px 20px 8px;
padding: 4px 20px 7px;
margin: 0 5px;
color: #fff !important;
}
......@@ -208,7 +528,7 @@ input.courseware-unit-search-input {
.new-section-name-cancel,
.new-subsection-name-cancel {
@include white-button;
padding: 6px 20px 8px;
padding: 4px 20px 7px;
color: #8891a1 !important;
}
......@@ -291,4 +611,11 @@ input.courseware-unit-search-input {
.cancel-button {
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 @@
}
}
.new-course-button {
@include grey-button;
display: block;
padding: 20px;
text-align: center;
}
.new-course {
padding: 15px 25px;
margin-top: 20px;
......@@ -89,7 +82,6 @@
.new-course-save {
@include blue-button;
// padding: ;
}
.new-course-cancel {
......
......@@ -34,6 +34,14 @@
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 {
display: inline-block;
width: 14px;
......@@ -56,6 +64,27 @@
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 {
display: inline-block;
width: 12px;
......
......@@ -5,18 +5,15 @@ body.no-header {
}
@mixin active {
@include linear-gradient(top, rgba(0, 0, 0, 0.6), rgba(0, 0, 0, 0.3));
@include box-shadow(0 2px 8px rgba(0, 0, 0, .7) inset);
@include linear-gradient(top, rgba(255, 255, 255, .4), rgba(255, 255, 255, 0));
background-color: rgba(255, 255, 255, .3);
@include box-shadow(0 -1px 0 rgba(0, 0, 0, .2) inset, 0 1px 0 #fff inset);
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
}
.primary-header {
width: 100%;
height: 36px;
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);
margin-bottom: 30px;
&.active-tab-courseware #courseware-tab {
@include active;
......@@ -34,23 +31,16 @@ body.no-header {
@include active;
}
&.active-tab-import #import-tab {
&.active-tab-settings #settings-tab {
@include active;
}
#import-tab {
@include box-shadow(1px 0 0 #787981 inset, -1px 0 0 #3d3e44 inset, 1px 0 0 #787981, -1px 0 0 #3d3e44);
}
.left {
width: 750px;
&.active-tab-import #import-tab {
@include active;
}
.class-name {
max-width: 350px;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
&.active-tab-export #export-tab {
@include active;
}
.drop-icon {
......@@ -63,26 +53,57 @@ body.no-header {
line-height: 18px;
}
a,
.username {
float: left;
display: inline-block;
height: 29px;
padding: 7px 15px 0;
color: #e4e6ee;
.class-nav-bar {
clear: both;
@include linear-gradient(top, rgba(255, 255, 255, .4), rgba(255, 255, 255, 0));
background-color: $lightBluishGrey;
@include box-shadow(0 1px 0 rgba(255, 255, 255, 0.2) inset, 0 -1px 0 rgba(0, 0, 0, 0.2) inset);
}
.class-nav,
.class-nav li {
float: left;
.class-nav {
@include clearfix;
a {
float: left;
display: inline-block;
padding: 15px 25px 17px;
font-size: 15px;
color: #3c3c3c;
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.3);
&:hover {
@include linear-gradient(top, rgba(255, 255, 255, .2), rgba(255, 255, 255, 0));
}
}
li {
float: left;
}
}
a {
@include box-shadow(1px 0 0 #787981 inset, -1px 0 0 #3d3e44 inset, 1px 0 0 #787981, -1px 0 0 #3d3e44);
.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 {
display: inline-block;
height: 20px;
padding: 5px 10px 6px;
color: rgb(163, 171, 184);
}
&:hover {
background: rgba(255, 255, 255, .1);
.home {
position: relative;
top: 1px;
}
.log-out {
position: relative;
top: 3px;
}
}
}
\ No newline at end of file
......@@ -7,7 +7,20 @@
}
.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 {
......@@ -36,7 +49,7 @@
}
.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 @@
z-index: 11;
width: 35px;
border: none;
background: url(../img/drag-handles.png) center no-repeat $lightGrey;
background: url(../img/drag-handles.png) center no-repeat #fff;
&: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 @@
}
.component.editing {
border-left: 1px solid $mediumGrey;
border-right: 1px solid $mediumGrey;
.xmodule_display {
display: none;
}
}
.new .xmodule_display {
background: $yellow;
}
.xmodule_display {
padding: 20px 20px 22px;
font-size: 24px;
font-weight: 300;
background: $lightGrey;
background: #fff;
@include transition(background-color 3s);
}
.static-page-item {
......
......@@ -137,8 +137,7 @@
a {
font-size: 11px;
font-weight: 700;
line-height: 31px;
font-weight: bold;
text-transform: uppercase;
}
......@@ -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 @@
}
h2 {
margin: 30px 40px;
margin: 30px 40px 30px 0;
color: #646464;
font-size: 19px;
font-weight: 300;
......@@ -57,14 +57,10 @@
margin: 20px 40px;
&.new-component-item {
padding: 0;
padding: 20px;
border: none;
border-radius: 0;
&.adding {
background-color: $blue;
border-color: #437fbf;
}
border-radius: 3px;
background: $lightGrey;
.new-component-button {
display: block;
......@@ -85,12 +81,13 @@
border-radius: 3px 3px 0 0;
}
.new-component-type, .new-component-template {
@include clearfix;
.new-component-type {
a,
li {
display: inline-block;
}
a {
position: relative;
float: left;
width: 100px;
height: 100px;
margin-right: 10px;
......@@ -98,14 +95,8 @@
border-radius: 8px;
font-size: 13px;
line-height: 14px;
color: #fff;
text-align: center;
@include box-shadow(0 1px 1px rgba(0, 0, 0, .4), 0 1px 0 rgba(255, 255, 255, .4) inset);
@include transition(background-color .15s);
&:hover {
background-color: rgba(255, 255, 255, .2);
}
@include box-shadow(0 1px 1px rgba(0, 0, 0, .2), 0 1px 0 rgba(255, 255, 255, .4) inset);
.name {
position: absolute;
......@@ -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 {
margin-bottom: 20px;
li:first-child {
a {
border-radius: 3px 3px 0 0;
}
}
li:last-child {
a {
border-radius: 0 0 3px 3px;
}
}
a {
height: 60px;
display: block;
padding: 7px 20px;
border-bottom: none;
font-weight: 300;
}
}
.new-component,
.new-component {
text-align: center;
h5 {
color: $green;
}
}
.new-component-templates {
display: none;
position: absolute;
width: 100%;
padding: 20px;
@include clearfix;
.cancel-button {
@include blue-button;
border-color: #30649c;
@include white-button;
}
}
}
......@@ -142,7 +172,7 @@
}
.component {
border: 1px solid #d1ddec;
border: 1px solid $lightBluishGrey2;
border-radius: 3px;
background: #fff;
@include transition(none);
......@@ -157,7 +187,8 @@
}
&.editing {
border-color: #6696d7;
border: 1px solid $lightBluishGrey2;
z-index: 9999;
.drag-handle,
.component-actions {
......@@ -173,11 +204,6 @@
position: absolute;
top: 7px;
right: 9px;
@include transition(opacity .15s);
a {
color: $darkGrey;
}
}
.drag-handle {
......@@ -189,10 +215,10 @@
width: 15px;
height: 100%;
border-radius: 0 3px 3px 0;
border: 1px solid #d1ddec;
background: url(../img/white-drag-handles.png) center no-repeat #d1ddec;
border: 1px solid $lightBluishGrey2;
background: url(../img/white-drag-handles.png) center no-repeat $lightBluishGrey2;
cursor: move;
@include transition(all .15s);
@include transition(none);
}
}
......@@ -205,9 +231,6 @@
display: none;
padding: 20px;
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);
.metadata_edit {
......@@ -224,12 +247,6 @@
}
}
.CodeMirror {
border: 1px solid #3c3c3c;
background: #fff;
color: #3c3c3c;
}
h3 {
margin-bottom: 10px;
font-size: 18px;
......@@ -446,3 +463,26 @@
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 {
.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 {
display: none;
padding: 15px 20px;
background: $mediumGrey;
background-color: $lightBluishGrey2;
#result {
display: none;
......@@ -55,21 +32,22 @@
.add-button {
@include blue-button;
padding: 5px 20px 9px;
}
.cancel-button {
@include white-button;
padding: 5px 20px 9px;
}
}
.user-list {
border: 1px solid $mediumGrey;
border-top: none;
background: $lightGrey;
background: #fff;
li {
position: relative;
padding: 10px 20px;
padding: 20px;
border-bottom: 1px solid $mediumGrey;
&:last-child {
......@@ -81,12 +59,19 @@
}
.user-name {
width: 30%;
font-weight: 700;
margin-right: 10px;
font-size: 24px;
font-weight: 300;
}
.user-email {
font-size: 14px;
font-style: italic;
color: $mediumGrey;
}
.item-actions {
top: 9px;
top: 24px;
}
}
}
......
......@@ -10,13 +10,28 @@ $fg-min-width: 810px;
$sans-serif: 'Open Sans', $verdana;
$body-line-height: golden-ratio(.875em, 1);
$white: rgb(255,255,255);
$black: rgb(0,0,0);
$pink: rgb(182,37,104);
$error-red: rgb(253, 87, 87);
$baseFontColor: #3c3c3c;
$offBlack: #3c3c3c;
$black: rgb(0,0,0);
$white: rgb(255,255,255);
$blue: #5597dd;
$orange: #edbd3c;
$red: #b20610;
$green: #108614;
$lightGrey: #edf1f5;
$mediumGrey: #ced2db;
$mediumGrey: #b0b6c2;
$darkGrey: #8891a1;
$extraDarkGrey: #3d4043;
$paleYellow: #fffcf1;
\ No newline at end of file
$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 @@
@import "static-pages";
@import "users";
@import "import";
@import "settings";
@import "course-info";
@import "landing";
@import "graphics";
@import "modal";
@import "alerts";
@import "login";
@import "lms";
@import 'jquery-ui-calendar';
@import 'content-types';
......
......@@ -21,7 +21,7 @@
</div>
</td>
<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>
</td>
<td class="date-col">
......@@ -35,10 +35,11 @@
<div class="main-wrapper">
<div class="inner-wrapper">
<h1>Asset Library</h1>
<div class="page-actions">
<a href="#" class="upload-button">Upload New File</a>
<input type="text" class="asset-search-input search wip-box" placeholder="search assets" style="display:none"/>
<a href="#" class="upload-button new-button">
<span class="upload-icon"></span>Upload New Asset
</a>
<input type="text" class="asset-search-input search wip-box" placeholder="search assets" style="display:none"/>
</div>
<article class="asset-library">
<table>
......@@ -61,7 +62,7 @@
</div>
</td>
<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>
</td>
<td class="date-col">
......
......@@ -9,6 +9,9 @@
<%static:css group='base-style'/>
<link rel="stylesheet" type="text/css" href="${static.url('js/vendor/markitup/skins/simple/style.css')}" />
<link rel="stylesheet" type="text/css" href="${static.url('js/vendor/markitup/sets/wiki/style.css')}" />
<link rel="stylesheet" type="text/css" href="${static.url('css/vendor/symbolset.ss-standard.css')}" />
<link rel="stylesheet" type="text/css" href="${static.url('css/vendor/symbolset.ss-symbolicons-block.css')}" />
<title><%block name="title"></%block></title>
<meta name="viewport" content="width=device-width,initial-scale=1">
......@@ -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/markitup/jquery.markitup.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='module-js'/>
<script src="${static.url('js/vendor/jquery.inlineedit.js')}"></script>
......
......@@ -9,5 +9,5 @@
<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>
</div>
<a href="#" class="drag-handle"></a>
<a data-tooltip="Drag to reorder" href="#" class="drag-handle"></a>
${preview}
\ No newline at end of file
......@@ -17,13 +17,18 @@
<%block name="content">
<div class="main-wrapper">
<div class="inner-wrapper">
<div>
<h1>Static Tabs</h1>
</div>
<article class="unit-body window">
<article class="unit-body">
<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 class="tab-list">
<ol class='components'>
% for id in components:
......@@ -31,9 +36,7 @@
% endfor
<li class="new-component-item">
<a href="#" class="new-button big new-tab">
<span class="plus-icon"></span>New Tab
</a>
</li>
</ol>
</div>
......
......@@ -31,21 +31,6 @@
<label>Units:</label>
${units.enum_units(subsection, subsection_units=subsection_units)}
</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>
</div>
......@@ -62,7 +47,7 @@
</div>
<div class="sidebar">
<div class="unit-settings window">
<div class="unit-settings window id-holder" data-id="${subsection.location}">
<h4>Subsection Settings</h4>
<div class="window-contents">
<div class="scheduled-date-input row">
......@@ -84,6 +69,13 @@
<a href="#" class="sync-date no-spinner">Sync to ${parent_item.display_name}.</a></p>
% endif
</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">
<label>Due date:</label>
<a href="#" class="set-date">Set a due date</a>
......@@ -116,6 +108,10 @@
<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/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() {
// expand the due-date area if the values are set
......@@ -124,6 +120,23 @@
$('.set-date').hide();
$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>
</%block>
......@@ -4,7 +4,7 @@ ${content}
<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>
</div>
<a href="#" class="drag-handle"></a>
<a data-tooltip="Drag to reorder" href="#" class="drag-handle"></a>
<div class="component-editor">
<h5>Edit Video Component</h5>
<textarea class="component-source"><video youtube="1.50:q1xkuPsOY6Q,1.25:9WOY2dHz5i4,1.0:4rpg8Bq6hb4,0.75:KLim9Xkp7IY"/></textarea>
......
......@@ -8,7 +8,6 @@
<%block name="content">
<div class="main-wrapper">
<div class="inner-wrapper">
<h1>Import</h1>
<article class="import-overview">
<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>
......
......@@ -37,7 +37,7 @@
<h1>My Courses</h1>
<article class="my-classes">
% 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">
%for course, url in courses:
<li>
......
......@@ -5,28 +5,28 @@
<%block name="content">
<div class="main-wrapper">
<div class="inner-wrapper">
<h1>Users</h1>
<div class="page-actions">
%if allow_actions:
<a href="#" class="new-button new-user-button">
<span class="plus-icon white"></span>New User
</a>
%endif
</div>
<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:
<a href="#" class="new-user-button">
<span class="plus-icon"></span>New User
</a>
%endif
</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:
<div class="new-user-form">
<form class="new-user-form">
<div id="result"></div>
<div class="form-elements">
<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>
<a href="#" class="cancel-button">cancel</a>
<input type="submit" value="Add User" id="add_user" class="add-button" />
<input type="button" value="Cancel" class="cancel-button" />
</div>
</div>
</form>
%endif
<div>
<ol class="user-list">
......@@ -57,6 +57,7 @@
function showNewUserForm(e) {
e.preventDefault();
$newUserForm.slideDown(150);
$newUserForm.find('.email-input').focus();
}
function hideNewUserForm(e) {
......@@ -66,26 +67,37 @@
$('#email').val('');
}
function checkForCancel(e) {
if(e.which == 27) {
e.data.$cancelButton.click();
}
}
function addUser(e) {
e.preventDefault();
$.ajax({
url: '${add_user_postback_url}',
type: 'POST',
dataType: 'json',
contentType: 'application/json',
data:JSON.stringify({ 'email': $('#email').val()}),
}).done(function(data) {
if (data.ErrMsg != undefined)
$('#result').show().empty().append(data.ErrMsg);
else
location.reload();
});
}
$(document).ready(function() {
$newUserForm = $('.new-user-form');
$newUserForm.find('.cancel-button').bind('click', hideNewUserForm);
var $cancelButton = $newUserForm.find('.cancel-button');
$newUserForm.bind('submit', addUser);
$cancelButton.bind('click', hideNewUserForm);
$('.new-user-button').bind('click', showNewUserForm);
$('#add_user').click(function() {
$.ajax({
url: '${add_user_postback_url}',
type: 'POST',
dataType: 'json',
contentType: 'application/json',
data:JSON.stringify({ 'email': $('#email').val()}),
}).done(function(data) {
if (data.ErrMsg != undefined)
$('#result').show().empty().append(data.ErrMsg);
else
location.reload();
})
});
$body.bind('keyup', { $cancelButton: $cancelButton }, checkForCancel);
$('.remove-user').click(function() {
$.ajax({
......
......@@ -16,13 +16,37 @@
<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/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 name="header_extras">
<script type="text/template" id="new-section-template">
<section class="courseware-section branch new-section">
<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">
<h3 class="section-name">
<form class="section-name-form">
......@@ -38,7 +62,7 @@
<script type="text/template" id="blank-slate-template">
<section class="courseware-section branch new-section">
<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">
<h3 class="section-name">
<span class="section-name-span">Click here to set the section name</span>
......@@ -49,8 +73,8 @@
</form>
</div>
<div class="item-actions">
<a href="#" class="delete-button delete-section-button"><span class="delete-icon"></span></a>
<a href="#" class="drag-handle"></a>
<a href="#" data-tooltip="Delete this section" class="delete-button delete-section-button"><span class="delete-icon"></span></a>
<a href="#" data-tooltip="Drag to re-order" class="drag-handle"></a>
</div>
</header>
</section>
......@@ -77,8 +101,6 @@
</ol>
</li>
</script>
</%block>
<%block name="content">
......@@ -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-time time" type="text" name="start_time" value="" placeholder="HH:MM" class="time" size='10' autocomplete="off"/>
<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>
<a href="#" class="save-button">Save</a><a href="#" class="cancel-button">Cancel</a>
......@@ -98,17 +120,19 @@
<div class="main-wrapper">
<div class="inner-wrapper">
<h1>Courseware</h1>
<div class="page-actions"></div>
<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>
<div class="page-actions">
<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()}">
% for section in sections:
<section class="courseware-section branch" data-id="${section.location}">
<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}">
<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">
<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" />
......@@ -128,11 +152,12 @@
<span class="published-status"><strong>Will Release:</strong> ${start_date_str} at ${start_time_str}</span>
<a href="#" class="edit-button" data-date="${start_date_str}" data-time="${start_time_str}" data-id="${section.location}">Edit</a>
%endif
</div>
</div>
</div>
<div class="item-actions">
<a href="#" class="delete-button delete-section-button"><span class="delete-icon"></span></a>
<a href="#" class="drag-handle"></a>
<a href="#" data-tooltip="Delete this section" class="delete-button delete-section-button"><span class="delete-icon"></span></a>
<a href="#" data-tooltip="Drag to reorder" class="drag-handle"></a>
</div>
</header>
<div class="subsection-list">
......@@ -143,18 +168,22 @@
</div>
<ol data-section-id="${section.location.url()}">
% 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>
<a href="#" class="expand-collapse-icon expand"></a>
<div class="details">
<a href="#" data-tooltip="Expand/collapse this subsection" class="expand-collapse-icon expand"></a>
<a href="${reverse('edit_subsection', args=[subsection.location])}">
<span class="folder-icon"></span>
<span class="subsection-name"><span class="subsection-name-value">${subsection.display_name}</span></span>
</a>
</div>
<div class="gradable-status" data-initial-status="${subsection.metadata.get('format', 'Not Graded')}">
</div>
<div class="item-actions">
<a href="#" class="delete-button delete-subsection-button"><span class="delete-icon"></span></a>
<a href="#" class="drag-handle"></a>
<a href="#" data-tooltip="Delete this subsection" class="delete-button delete-subsection-button"><span class="delete-icon"></span></a>
<a href="#" data-tooltip="Drag to reorder" class="drag-handle"></a>
</div>
</div>
${units.enum_units(subsection)}
......
......@@ -32,12 +32,9 @@
% for id in components:
<li class="component" data-id="${id}"/>
% endfor
<li class="new-component-item">
<a href="#" class="new-component-button new-button big">
<span class="plus-icon"></span>New Component
</a>
<li class="new-component-item adding">
<div class="new-component">
<h5>Select Component Type</h5>
<h5>Add New Component</h5>
<ul class="new-component-type">
% for type in sorted(component_templates.keys()):
<li>
......@@ -48,7 +45,6 @@
</li>
% endfor
</ul>
<a href="#" class="cancel-button">Cancel</a>
</div>
% for type, templates in sorted(component_templates.items()):
<div class="new-component-templates new-component-${type}">
......@@ -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>
</div>
<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 class="row unit-actions">
<a href="#" class="delete-draft delete-button">Delete Draft</a>
......
<%! from django.core.urlresolvers import reverse %>
<% active_tab_class = 'active-tab-' + active_tab if active_tab else '' %>
<header class="primary-header ${active_tab_class}">
<nav class="inner-wrapper">
<div class="left">
<a href="/"><span class="home-icon"></span></a>
% if context_course:
<% 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>
<ul class="class-nav">
<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
</div>
<div class="right">
<span class="username">${ user.username }</span>
% if user.is_authenticated():
<a href="${reverse('logout')}">Log out</a>
% else:
<a href="${reverse('login')}">Log in</a>
% endif
<header class="primary-header ${active_tab_class}">
<div class="class">
<div class="inner-wrapper">
<div class="left">
% if context_course:
<% ctx_loc = context_course.location %>
<a href="/" class="home"><span class="small-home-icon"></span></a>
<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>
% endif
</div>
<div class="right">
<span class="username">${ user.username }</span>
% if user.is_authenticated():
<a href="${reverse('logout')}" class="log-out"><span class="log-out-icon"></span></a>
% else:
<a href="${reverse('login')}">Log in</a>
% endif
</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>
</header>
<%include file="metadata-edit.html" />
<section class="html-edit">
<textarea name="" class="edit-box" rows="8" cols="40">${data}</textarea>
<div name="" class="edit-box">${data}</div>
</section>
......@@ -4,7 +4,6 @@
hlskey = hashlib.md5(module.location.url()).hexdigest()
%>
<section class="metadata_edit">
<h3>Metadata</h3>
<ul>
% for keyname in editable_metadata_fields:
<li>
......
......@@ -26,8 +26,8 @@ This def will enumerate through a passed in subsection and list all of the units
</a>
% if actions:
<div class="item-actions">
<a href="#" class="delete-button" data-id="${unit.location}"><span class="delete-icon"></span></a>
<a href="#" class="drag-handle"></a>
<a href="#" data-tooltip="Delete this unit" class="delete-button" data-id="${unit.location}"><span class="delete-icon"></span></a>
<a href="#" data-tooltip="Drag to sort" class="drag-handle"></a>
</div>
% endif
</div>
......
<li class="video-box">
<div class="thumb"><img src="http://placehold.it/100x65" /></div>
<div class="thumb"></div>
<div class="meta">
<strong>video-name</strong> 236mb Uploaded 6 hours ago by <em>Anant Agrawal</em>
......
<li class="video-box">
<div class="thumb"><img src="http://placehold.it/155x90" /></div>
<div class="thumb"></div>
<div class="meta">
<strong>video-name</strong> 236mb
......
......@@ -35,9 +35,13 @@ urlpatterns = ('',
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<name>[^/]+)/remove_user$',
'contentstore.views.remove_user', name='remove_user'),
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/info/(?P<name>[^/]+)$', 'contentstore.views.course_info', name='course_info'),
# ??? 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>[^/]+)/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',
name='static_pages'),
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):
xml = self.xml
# TODO: XML can override external resource (grader/queue) URL
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]:
# Check if XML uses the ExternalResponse format or the generic CodeResponse format
......@@ -1230,6 +1236,13 @@ class CodeResponse(LoncapaResponse):
(err, self.answer_id, convert_files_to_filenames(student_answers)))
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
#------------------------------------------------------------
......
......@@ -12,6 +12,7 @@
<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/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">
......
......@@ -18,7 +18,7 @@ class StaticContent(object):
self.content_type = content_type
self.data = data
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
# cycles
self.import_path = import_path
......
......@@ -31,8 +31,7 @@ class MongoContentStore(ContentStore):
id = content.get_id()
# 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.fs.delete(id)
self.delete(id)
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:
......@@ -41,13 +40,16 @@ class MongoContentStore(ContentStore):
return content
def delete(self, id):
if self.fs.exists({"_id" : id}):
self.fs.delete(id)
def find(self, location):
id = StaticContent.get_id_from_location(location)
try:
with self.fs.get(id) as fp:
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)
except NoFile:
raise NotFoundError()
......
from fs.errors import ResourceNotFoundError
import logging
import json
from cStringIO import StringIO
from lxml import etree
from path import path # NOTE (THK): Only used for detecting presence of syllabus
import requests
import time
from cStringIO import StringIO
from xmodule.util.decorators import lazyproperty
from xmodule.graders import grader_from_conf
from xmodule.modulestore import Location
from xmodule.seq_module import SequenceDescriptor, SequenceModule
from xmodule.xml_module import XmlDescriptor
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__)
......@@ -92,10 +91,6 @@ class CourseDescriptor(SequenceDescriptor):
log.critical(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
# init. (Modulestore is in charge of figuring out where to load the policy from)
......@@ -105,19 +100,11 @@ class CourseDescriptor(SequenceDescriptor):
self.set_grading_policy(self.definition['data'].get('grading_policy', None))
def set_grading_policy(self, course_policy):
if course_policy is None:
course_policy = {}
def defaut_grading_policy(self):
"""
The JSON object can have the keys GRADER and GRADE_CUTOFFS. If either is
missing, it reverts to the default.
Return a dict which is a copy of the default grading policy
"""
default_policy_string = """
{
"GRADER" : [
default = {"GRADER" : [
{
"type" : "Homework",
"min_count" : 12,
......@@ -129,37 +116,44 @@ class CourseDescriptor(SequenceDescriptor):
"type" : "Lab",
"min_count" : 12,
"drop_count" : 2,
"category" : "Labs",
"weight" : 0.15
},
{
"type" : "Midterm",
"name" : "Midterm Exam",
"type" : "Midterm Exam",
"short_label" : "Midterm",
"min_count" : 1,
"drop_count" : 0,
"weight" : 0.3
},
{
"type" : "Final",
"name" : "Final Exam",
"type" : "Final Exam",
"short_label" : "Final",
"min_count" : 1,
"drop_count" : 0,
"weight" : 0.4
}
],
"GRADE_CUTOFFS" : {
"A" : 0.87,
"B" : 0.7,
"C" : 0.6
}
}
"Pass" : 0.5
}}
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
grading_policy = json.loads(default_policy_string)
grading_policy = self.defaut_grading_policy()
# Override any global settings with the course settings
grading_policy.update(course_policy)
# 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'])
self._grading_policy = grading_policy
......@@ -168,8 +162,8 @@ class CourseDescriptor(SequenceDescriptor):
@classmethod
def read_grading_policy(cls, paths, system):
"""Load a grading policy from the specified paths, in order, if it exists."""
# Default to a blank policy
policy_str = '""'
# Default to a blank policy dict
policy_str = '{}'
for policy_path in paths:
if not system.resources_fs.exists(policy_path):
......@@ -252,12 +246,52 @@ class CourseDescriptor(SequenceDescriptor):
return time.gmtime() > self.start
@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):
return self._grading_policy['GRADER']
@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):
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
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
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"
lineNumbers: true
lineWrapping: true
})
lineWrapping: true})
save: ->
data: @edit_box.getValue()
......@@ -51,6 +51,9 @@ class CachingDescriptorSystem(MakoDescriptorSystem):
self.modulestore = modulestore
self.module_data = module_data
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):
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:
display_name: Formula Repsonse
display_name: Formula Response
rerandomize: never
showanswer: always
data: |
......
......@@ -22,7 +22,8 @@ class VideoModule(XModule):
resource_string(__name__, 'js/src/video/display.coffee')] +
[resource_string(__name__, 'js/src/video/display/' + 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')]}
js_module_name = "Video"
......
......@@ -10,10 +10,11 @@ from collections import namedtuple
from pkg_resources import resource_listdir, resource_string, resource_isdir
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.modulestore.exceptions import ItemNotFoundError
import time
log = logging.getLogger('mitx.' + __name__)
......@@ -494,6 +495,11 @@ class XModuleDescriptor(Plugin, HTMLSnippet, ResourceTemplates):
return None
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
def own_metadata(self):
"""
......
......@@ -12,10 +12,10 @@ class @TooltipManager
'click': @hideTooltip
showTooltip: (e) =>
tooltipText = $(e.target).attr('data-tooltip')
$target = $(e.target).closest('[data-tooltip]')
tooltipText = $target.attr('data-tooltip')
@$tooltip.html(tooltipText)
@$body.append(@$tooltip)
$(e.target).children().css('pointer-events', 'none')
tooltipCoords =
x: e.pageX - (@$tooltip.outerWidth() / 2)
......@@ -26,8 +26,8 @@ class @TooltipManager
'top': tooltipCoords.y
@tooltipTimer = setTimeout ()=>
@$tooltip.show().css('opacity', 1)
@$tooltip.show().css('opacity', 1)
@tooltipTimer = setTimeout ()=>
@hideTooltip()
, 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
# Notes on using mongodb backed LMS and CMS
These are some random notes for developers, on how things are stored in mongodb, and how to debug mongodb data.
## Databases
......
......@@ -22,7 +22,8 @@ rake test_lms[false] || TESTS_FAILED=1
rake test_common/lib/capa || TESTS_FAILED=1
rake test_common/lib/xmodule || TESTS_FAILED=1
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
[ $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