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
This commit is contained in:
0
cms/djangoapps/__init__.py
Normal file
0
cms/djangoapps/__init__.py
Normal file
@@ -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))
|
||||
38
cms/djangoapps/contentstore/management/commands/clone.py
Normal file
38
cms/djangoapps/contentstore/management/commands/clone.py
Normal file
@@ -0,0 +1,38 @@
|
||||
###
|
||||
### 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)
|
||||
@@ -0,0 +1,40 @@
|
||||
###
|
||||
### 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)
|
||||
|
||||
|
||||
|
||||
33
cms/djangoapps/contentstore/management/commands/prompt.py
Normal file
33
cms/djangoapps/contentstore/management/commands/prompt.py
Normal file
@@ -0,0 +1,33 @@
|
||||
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")
|
||||
@@ -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
|
||||
|
||||
275
cms/djangoapps/contentstore/tests/test_course_settings.py
Normal file
275
cms/djangoapps/contentstore/tests/test_course_settings.py
Normal file
@@ -0,0 +1,275 @@
|
||||
from django.test.testcases import TestCase
|
||||
import datetime
|
||||
import time
|
||||
from django.contrib.auth.models import User
|
||||
import xmodule
|
||||
from django.test.client import Client
|
||||
from django.core.urlresolvers import reverse
|
||||
from xmodule.modulestore import Location
|
||||
from cms.djangoapps.models.settings.course_details import CourseDetails,\
|
||||
CourseSettingsEncoder
|
||||
import json
|
||||
from util import converters
|
||||
import calendar
|
||||
from util.converters import jsdate_to_time
|
||||
from django.utils.timezone import UTC
|
||||
from cms.djangoapps.models.settings.course_grading import CourseGradingModel
|
||||
from cms.djangoapps.contentstore.utils import get_modulestore
|
||||
import copy
|
||||
|
||||
# YYYY-MM-DDThh:mm:ss.s+/-HH:MM
|
||||
class ConvertersTestCase(TestCase):
|
||||
@staticmethod
|
||||
def struct_to_datetime(struct_time):
|
||||
return datetime.datetime(struct_time.tm_year, struct_time.tm_mon, struct_time.tm_mday, struct_time.tm_hour,
|
||||
struct_time.tm_min, struct_time.tm_sec, tzinfo = UTC())
|
||||
|
||||
def compare_dates(self, date1, date2, expected_delta):
|
||||
dt1 = ConvertersTestCase.struct_to_datetime(date1)
|
||||
dt2 = ConvertersTestCase.struct_to_datetime(date2)
|
||||
self.assertEqual(dt1 - dt2, expected_delta, str(date1) + "-" + str(date2) + "!=" + str(expected_delta))
|
||||
|
||||
def test_iso_to_struct(self):
|
||||
self.compare_dates(converters.jsdate_to_time("2013-01-01"), converters.jsdate_to_time("2012-12-31"), datetime.timedelta(days=1))
|
||||
self.compare_dates(converters.jsdate_to_time("2013-01-01T00"), converters.jsdate_to_time("2012-12-31T23"), datetime.timedelta(hours=1))
|
||||
self.compare_dates(converters.jsdate_to_time("2013-01-01T00:00"), converters.jsdate_to_time("2012-12-31T23:59"), datetime.timedelta(minutes=1))
|
||||
self.compare_dates(converters.jsdate_to_time("2013-01-01T00:00:00"), converters.jsdate_to_time("2012-12-31T23:59:59"), datetime.timedelta(seconds=1))
|
||||
|
||||
|
||||
class CourseTestCase(TestCase):
|
||||
def setUp(self):
|
||||
uname = 'testuser'
|
||||
email = 'test+courses@edx.org'
|
||||
password = 'foo'
|
||||
|
||||
# Create the use so we can log them in.
|
||||
self.user = User.objects.create_user(uname, email, password)
|
||||
|
||||
# Note that we do not actually need to do anything
|
||||
# for registration if we directly mark them active.
|
||||
self.user.is_active = True
|
||||
# Staff has access to view all courses
|
||||
self.user.is_staff = True
|
||||
self.user.save()
|
||||
|
||||
# Flush and initialize the module store
|
||||
# It needs the templates because it creates new records
|
||||
# by cloning from the template.
|
||||
# Note that if your test module gets in some weird state
|
||||
# (though it shouldn't), do this manually
|
||||
# from the bash shell to drop it:
|
||||
# $ mongo test_xmodule --eval "db.dropDatabase()"
|
||||
xmodule.modulestore.django._MODULESTORES = {}
|
||||
xmodule.modulestore.django.modulestore().collection.drop()
|
||||
xmodule.templates.update_templates()
|
||||
|
||||
self.client = Client()
|
||||
self.client.login(username=uname, password=password)
|
||||
|
||||
self.course_data = {
|
||||
'template': 'i4x://edx/templates/course/Empty',
|
||||
'org': 'MITx',
|
||||
'number': '999',
|
||||
'display_name': 'Robot Super Course',
|
||||
}
|
||||
self.course_location = Location('i4x', 'MITx', '999', 'course', 'Robot_Super_Course')
|
||||
self.create_course()
|
||||
|
||||
def tearDown(self):
|
||||
xmodule.modulestore.django._MODULESTORES = {}
|
||||
xmodule.modulestore.django.modulestore().collection.drop()
|
||||
|
||||
def create_course(self):
|
||||
"""Create new course"""
|
||||
self.client.post(reverse('create_new_course'), self.course_data)
|
||||
|
||||
class CourseDetailsTestCase(CourseTestCase):
|
||||
def test_virgin_fetch(self):
|
||||
details = CourseDetails.fetch(self.course_location)
|
||||
self.assertEqual(details.course_location, self.course_location, "Location not copied into")
|
||||
self.assertIsNone(details.end_date, "end date somehow initialized " + str(details.end_date))
|
||||
self.assertIsNone(details.enrollment_start, "enrollment_start date somehow initialized " + str(details.enrollment_start))
|
||||
self.assertIsNone(details.enrollment_end, "enrollment_end date somehow initialized " + str(details.enrollment_end))
|
||||
self.assertIsNone(details.syllabus, "syllabus somehow initialized" + str(details.syllabus))
|
||||
self.assertEqual(details.overview, "", "overview somehow initialized" + details.overview)
|
||||
self.assertIsNone(details.intro_video, "intro_video somehow initialized" + str(details.intro_video))
|
||||
self.assertIsNone(details.effort, "effort somehow initialized" + str(details.effort))
|
||||
|
||||
def test_encoder(self):
|
||||
details = CourseDetails.fetch(self.course_location)
|
||||
jsondetails = json.dumps(details, cls=CourseSettingsEncoder)
|
||||
jsondetails = json.loads(jsondetails)
|
||||
self.assertTupleEqual(Location(jsondetails['course_location']), self.course_location, "Location !=")
|
||||
# Note, start_date is being initialized someplace. I'm not sure why b/c the default will make no sense.
|
||||
self.assertIsNone(jsondetails['end_date'], "end date somehow initialized ")
|
||||
self.assertIsNone(jsondetails['enrollment_start'], "enrollment_start date somehow initialized ")
|
||||
self.assertIsNone(jsondetails['enrollment_end'], "enrollment_end date somehow initialized ")
|
||||
self.assertIsNone(jsondetails['syllabus'], "syllabus somehow initialized")
|
||||
self.assertEqual(jsondetails['overview'], "", "overview somehow initialized")
|
||||
self.assertIsNone(jsondetails['intro_video'], "intro_video somehow initialized")
|
||||
self.assertIsNone(jsondetails['effort'], "effort somehow initialized")
|
||||
|
||||
def test_update_and_fetch(self):
|
||||
## NOTE: I couldn't figure out how to validly test time setting w/ all the conversions
|
||||
jsondetails = CourseDetails.fetch(self.course_location)
|
||||
jsondetails.syllabus = "<a href='foo'>bar</a>"
|
||||
# encode - decode to convert date fields and other data which changes form
|
||||
self.assertEqual(CourseDetails.update_from_json(jsondetails.__dict__).syllabus,
|
||||
jsondetails.syllabus, "After set syllabus")
|
||||
jsondetails.overview = "Overview"
|
||||
self.assertEqual(CourseDetails.update_from_json(jsondetails.__dict__).overview,
|
||||
jsondetails.overview, "After set overview")
|
||||
jsondetails.intro_video = "intro_video"
|
||||
self.assertEqual(CourseDetails.update_from_json(jsondetails.__dict__).intro_video,
|
||||
jsondetails.intro_video, "After set intro_video")
|
||||
jsondetails.effort = "effort"
|
||||
self.assertEqual(CourseDetails.update_from_json(jsondetails.__dict__).effort,
|
||||
jsondetails.effort, "After set effort")
|
||||
|
||||
class CourseDetailsViewTest(CourseTestCase):
|
||||
def alter_field(self, url, details, field, val):
|
||||
setattr(details, field, val)
|
||||
# Need to partially serialize payload b/c the mock doesn't handle it correctly
|
||||
payload = copy.copy(details.__dict__)
|
||||
payload['course_location'] = details.course_location.url()
|
||||
payload['start_date'] = CourseDetailsViewTest.convert_datetime_to_iso(details.start_date)
|
||||
payload['end_date'] = CourseDetailsViewTest.convert_datetime_to_iso(details.end_date)
|
||||
payload['enrollment_start'] = CourseDetailsViewTest.convert_datetime_to_iso(details.enrollment_start)
|
||||
payload['enrollment_end'] = CourseDetailsViewTest.convert_datetime_to_iso(details.enrollment_end)
|
||||
resp = self.client.post(url, json.dumps(payload), "application/json")
|
||||
self.compare_details_with_encoding(json.loads(resp.content), details.__dict__, field + str(val))
|
||||
|
||||
@staticmethod
|
||||
def convert_datetime_to_iso(datetime):
|
||||
if datetime is not None:
|
||||
return datetime.isoformat("T")
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def test_update_and_fetch(self):
|
||||
details = CourseDetails.fetch(self.course_location)
|
||||
|
||||
resp = self.client.get(reverse('course_settings', kwargs={'org' : self.course_location.org, 'course' : self.course_location.course,
|
||||
'name' : self.course_location.name }))
|
||||
self.assertContains(resp, '<li><a href="#" class="is-shown" data-section="details">Course Details</a></li>', status_code=200, html=True)
|
||||
|
||||
# resp s/b json from here on
|
||||
url = reverse('course_settings', kwargs={'org' : self.course_location.org, 'course' : self.course_location.course,
|
||||
'name' : self.course_location.name, 'section' : 'details' })
|
||||
resp = self.client.get(url)
|
||||
self.compare_details_with_encoding(json.loads(resp.content), details.__dict__, "virgin get")
|
||||
|
||||
utc = UTC()
|
||||
self.alter_field(url, details, 'start_date', datetime.datetime(2012,11,12,1,30, tzinfo=utc))
|
||||
self.alter_field(url, details, 'start_date', datetime.datetime(2012,11,1,13,30, tzinfo=utc))
|
||||
self.alter_field(url, details, 'end_date', datetime.datetime(2013,2,12,1,30, tzinfo=utc))
|
||||
self.alter_field(url, details, 'enrollment_start', datetime.datetime(2012,10,12,1,30, tzinfo=utc))
|
||||
|
||||
self.alter_field(url, details, 'enrollment_end', datetime.datetime(2012,11,15,1,30, tzinfo=utc))
|
||||
self.alter_field(url, details, 'overview', "Overview")
|
||||
self.alter_field(url, details, 'intro_video', "intro_video")
|
||||
self.alter_field(url, details, 'effort', "effort")
|
||||
|
||||
def compare_details_with_encoding(self, encoded, details, context):
|
||||
self.compare_date_fields(details, encoded, context, 'start_date')
|
||||
self.compare_date_fields(details, encoded, context, 'end_date')
|
||||
self.compare_date_fields(details, encoded, context, 'enrollment_start')
|
||||
self.compare_date_fields(details, encoded, context, 'enrollment_end')
|
||||
self.assertEqual(details['overview'], encoded['overview'], context + " overviews not ==")
|
||||
self.assertEqual(details['intro_video'], encoded.get('intro_video', None), context + " intro_video not ==")
|
||||
self.assertEqual(details['effort'], encoded['effort'], context + " efforts not ==")
|
||||
|
||||
def compare_date_fields(self, details, encoded, context, field):
|
||||
if details[field] is not None:
|
||||
if field in encoded and encoded[field] is not None:
|
||||
encoded_encoded = jsdate_to_time(encoded[field])
|
||||
dt1 = ConvertersTestCase.struct_to_datetime(encoded_encoded)
|
||||
|
||||
if isinstance(details[field], datetime.datetime):
|
||||
dt2 = details[field]
|
||||
else:
|
||||
details_encoded = jsdate_to_time(details[field])
|
||||
dt2 = ConvertersTestCase.struct_to_datetime(details_encoded)
|
||||
|
||||
expected_delta = datetime.timedelta(0)
|
||||
self.assertEqual(dt1 - dt2, expected_delta, str(dt1) + "!=" + str(dt2) + " at " + context)
|
||||
else:
|
||||
self.fail(field + " missing from encoded but in details at " + context)
|
||||
elif field in encoded and encoded[field] is not None:
|
||||
self.fail(field + " included in encoding but missing from details at " + context)
|
||||
|
||||
class CourseGradingTest(CourseTestCase):
|
||||
def test_initial_grader(self):
|
||||
descriptor = get_modulestore(self.course_location).get_item(self.course_location)
|
||||
test_grader = CourseGradingModel(descriptor)
|
||||
# ??? How much should this test bake in expectations about defaults and thus fail if defaults change?
|
||||
self.assertEqual(self.course_location, test_grader.course_location, "Course locations")
|
||||
self.assertIsNotNone(test_grader.graders, "No graders")
|
||||
self.assertIsNotNone(test_grader.grade_cutoffs, "No cutoffs")
|
||||
|
||||
def test_fetch_grader(self):
|
||||
test_grader = CourseGradingModel.fetch(self.course_location.url())
|
||||
self.assertEqual(self.course_location, test_grader.course_location, "Course locations")
|
||||
self.assertIsNotNone(test_grader.graders, "No graders")
|
||||
self.assertIsNotNone(test_grader.grade_cutoffs, "No cutoffs")
|
||||
|
||||
test_grader = CourseGradingModel.fetch(self.course_location)
|
||||
self.assertEqual(self.course_location, test_grader.course_location, "Course locations")
|
||||
self.assertIsNotNone(test_grader.graders, "No graders")
|
||||
self.assertIsNotNone(test_grader.grade_cutoffs, "No cutoffs")
|
||||
|
||||
for i, grader in enumerate(test_grader.graders):
|
||||
subgrader = CourseGradingModel.fetch_grader(self.course_location, i)
|
||||
self.assertDictEqual(grader, subgrader, str(i) + "th graders not equal")
|
||||
|
||||
subgrader = CourseGradingModel.fetch_grader(self.course_location.list(), 0)
|
||||
self.assertDictEqual(test_grader.graders[0], subgrader, "failed with location as list")
|
||||
|
||||
def test_fetch_cutoffs(self):
|
||||
test_grader = CourseGradingModel.fetch_cutoffs(self.course_location)
|
||||
# ??? should this check that it's at least a dict? (expected is { "pass" : 0.5 } I think)
|
||||
self.assertIsNotNone(test_grader, "No cutoffs via fetch")
|
||||
|
||||
test_grader = CourseGradingModel.fetch_cutoffs(self.course_location.url())
|
||||
self.assertIsNotNone(test_grader, "No cutoffs via fetch with url")
|
||||
|
||||
def test_fetch_grace(self):
|
||||
test_grader = CourseGradingModel.fetch_grace_period(self.course_location)
|
||||
# almost a worthless test
|
||||
self.assertIn('grace_period', test_grader, "No grace via fetch")
|
||||
|
||||
test_grader = CourseGradingModel.fetch_grace_period(self.course_location.url())
|
||||
self.assertIn('grace_period', test_grader, "No cutoffs via fetch with url")
|
||||
|
||||
def test_update_from_json(self):
|
||||
test_grader = CourseGradingModel.fetch(self.course_location)
|
||||
altered_grader = CourseGradingModel.update_from_json(test_grader.__dict__)
|
||||
self.assertDictEqual(test_grader.__dict__, altered_grader.__dict__, "Noop update")
|
||||
|
||||
test_grader.graders[0]['weight'] = test_grader.graders[0].get('weight') * 2
|
||||
altered_grader = CourseGradingModel.update_from_json(test_grader.__dict__)
|
||||
self.assertDictEqual(test_grader.__dict__, altered_grader.__dict__, "Weight[0] * 2")
|
||||
|
||||
test_grader.grade_cutoffs['D'] = 0.3
|
||||
altered_grader = CourseGradingModel.update_from_json(test_grader.__dict__)
|
||||
self.assertDictEqual(test_grader.__dict__, altered_grader.__dict__, "cutoff add D")
|
||||
|
||||
test_grader.grace_period = {'hours' : '4'}
|
||||
altered_grader = CourseGradingModel.update_from_json(test_grader.__dict__)
|
||||
self.assertDictEqual(test_grader.__dict__, altered_grader.__dict__, "4 hour grace period")
|
||||
|
||||
def test_update_grader_from_json(self):
|
||||
test_grader = CourseGradingModel.fetch(self.course_location)
|
||||
altered_grader = CourseGradingModel.update_grader_from_json(test_grader.course_location, test_grader.graders[1])
|
||||
self.assertDictEqual(test_grader.graders[1], altered_grader, "Noop update")
|
||||
|
||||
test_grader.graders[1]['min_count'] = test_grader.graders[1].get('min_count') + 2
|
||||
altered_grader = CourseGradingModel.update_grader_from_json(test_grader.course_location, test_grader.graders[1])
|
||||
self.assertDictEqual(test_grader.graders[1], altered_grader, "min_count[1] + 2")
|
||||
|
||||
test_grader.graders[1]['drop_count'] = test_grader.graders[1].get('drop_count') + 1
|
||||
altered_grader = CourseGradingModel.update_grader_from_json(test_grader.course_location, test_grader.graders[1])
|
||||
self.assertDictEqual(test_grader.graders[1], altered_grader, "drop_count[1] + 2")
|
||||
|
||||
|
||||
18
cms/djangoapps/contentstore/tests/test_utils.py
Normal file
18
cms/djangoapps/contentstore/tests/test_utils.py
Normal file
@@ -0,0 +1,18 @@
|
||||
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)
|
||||
@@ -50,28 +50,23 @@ from contentstore.course_info_model import get_course_updates,\
|
||||
from cache_toolbox.core import del_cached_content
|
||||
from xmodule.timeparse import stringify_time
|
||||
from contentstore.module_info_model import get_module_info, set_module_info
|
||||
from cms.djangoapps.models.settings.course_details import CourseDetails,\
|
||||
CourseSettingsEncoder
|
||||
from cms.djangoapps.models.settings.course_grading import CourseGradingModel
|
||||
from cms.djangoapps.contentstore.utils import get_modulestore
|
||||
from lxml import etree
|
||||
|
||||
# to install PIL on MacOSX: 'easy_install http://dist.repoze.org/PIL-1.1.6.tar.gz'
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
COMPONENT_TYPES = ['customtag', 'discussion', 'html', 'problem', 'video']
|
||||
|
||||
DIRECT_ONLY_CATEGORIES = ['course', 'chapter', 'sequential', 'about', 'static_tab', 'course_info']
|
||||
|
||||
# cdodge: these are categories which should not be parented, they are detached from the hierarchy
|
||||
DETACHED_CATEGORIES = ['about', 'static_tab', 'course_info']
|
||||
|
||||
|
||||
def _modulestore(location):
|
||||
"""
|
||||
Returns the correct modulestore to use for modifying the specified location
|
||||
"""
|
||||
if location.category in DIRECT_ONLY_CATEGORIES:
|
||||
return modulestore('direct')
|
||||
else:
|
||||
return modulestore()
|
||||
|
||||
|
||||
# ==== Public views ==================================================
|
||||
|
||||
@ensure_csrf_cookie
|
||||
@@ -173,6 +168,7 @@ def course_index(request, org, course, name):
|
||||
'active_tab': 'courseware',
|
||||
'context_course': course,
|
||||
'sections': sections,
|
||||
'course_graders': json.dumps(CourseGradingModel.fetch(course.location).graders),
|
||||
'parent_location': course.location,
|
||||
'new_section_template': Location('i4x', 'edx', 'templates', 'chapter', 'Empty'),
|
||||
'new_subsection_template': Location('i4x', 'edx', 'templates', 'sequential', 'Empty'), # for now they are the same, but the could be different at some point...
|
||||
@@ -230,6 +226,8 @@ def edit_subsection(request, location):
|
||||
'create_new_unit_template': Location('i4x', 'edx', 'templates', 'vertical', 'Empty'),
|
||||
'lms_link': lms_link,
|
||||
'preview_link': preview_link,
|
||||
'course_graders': json.dumps(CourseGradingModel.fetch(course.location).graders),
|
||||
'parent_location': course.location,
|
||||
'parent_item': parent,
|
||||
'policy_metadata' : policy_metadata,
|
||||
'subsection_units' : subsection_units,
|
||||
@@ -324,7 +322,7 @@ def edit_unit(request, location):
|
||||
'draft_preview_link': preview_lms_link,
|
||||
'published_preview_link': lms_link,
|
||||
'subsection': containing_subsection,
|
||||
'release_date': get_date_display(datetime.fromtimestamp(time.mktime(containing_subsection.start))) if containing_subsection.start is not None else 'Unset',
|
||||
'release_date': get_date_display(datetime.fromtimestamp(time.mktime(containing_subsection.start))) if containing_subsection.start is not None else None,
|
||||
'section': containing_section,
|
||||
'create_new_unit_template': Location('i4x', 'edx', 'templates', 'vertical', 'Empty'),
|
||||
'unit_state': unit_state,
|
||||
@@ -345,6 +343,24 @@ def preview_component(request, location):
|
||||
'editor': wrap_xmodule(component.get_html, component, 'xmodule_edit.html')(),
|
||||
})
|
||||
|
||||
@expect_json
|
||||
@login_required
|
||||
@ensure_csrf_cookie
|
||||
def assignment_type_update(request, org, course, category, name):
|
||||
'''
|
||||
CRUD operations on assignment types for sections and subsections and anything else gradable.
|
||||
'''
|
||||
location = Location(['i4x', org, course, category, name])
|
||||
if not has_access(request.user, location):
|
||||
raise HttpResponseForbidden()
|
||||
|
||||
if request.method == 'GET':
|
||||
return HttpResponse(json.dumps(CourseGradingModel.get_section_grader_type(location)),
|
||||
mimetype="application/json")
|
||||
elif request.method == 'POST': # post or put, doesn't matter.
|
||||
return HttpResponse(json.dumps(CourseGradingModel.update_section_grader_type(location, request.POST)),
|
||||
mimetype="application/json")
|
||||
|
||||
|
||||
def user_author_string(user):
|
||||
'''Get an author string for commits by this user. Format:
|
||||
@@ -499,7 +515,7 @@ def load_preview_module(request, preview_id, descriptor, instance_state, shared_
|
||||
module,
|
||||
"xmodule_display.html",
|
||||
)
|
||||
|
||||
|
||||
module.get_html = replace_static_urls(
|
||||
module.get_html,
|
||||
module.metadata.get('data_dir', module.location.course),
|
||||
@@ -548,7 +564,7 @@ def delete_item(request):
|
||||
|
||||
item = modulestore().get_item(item_location)
|
||||
|
||||
store = _modulestore(item_loc)
|
||||
store = get_modulestore(item_loc)
|
||||
|
||||
|
||||
# @TODO: this probably leaves draft items dangling. My preferance would be for the semantic to be
|
||||
@@ -579,7 +595,7 @@ def save_item(request):
|
||||
if not has_access(request.user, item_location):
|
||||
raise PermissionDenied()
|
||||
|
||||
store = _modulestore(Location(item_location));
|
||||
store = get_modulestore(Location(item_location));
|
||||
|
||||
if request.POST.get('data') is not None:
|
||||
data = request.POST['data']
|
||||
@@ -669,8 +685,6 @@ def unpublish_unit(request):
|
||||
|
||||
return HttpResponse()
|
||||
|
||||
|
||||
|
||||
@login_required
|
||||
@expect_json
|
||||
def clone_item(request):
|
||||
@@ -682,10 +696,10 @@ def clone_item(request):
|
||||
if not has_access(request.user, parent_location):
|
||||
raise PermissionDenied()
|
||||
|
||||
parent = _modulestore(template).get_item(parent_location)
|
||||
parent = get_modulestore(template).get_item(parent_location)
|
||||
dest_location = parent_location._replace(category=template.category, name=uuid4().hex)
|
||||
|
||||
new_item = _modulestore(template).clone_item(template, dest_location)
|
||||
new_item = get_modulestore(template).clone_item(template, dest_location)
|
||||
|
||||
# TODO: This needs to be deleted when we have proper storage for static content
|
||||
new_item.metadata['data_dir'] = parent.metadata['data_dir']
|
||||
@@ -694,10 +708,10 @@ def clone_item(request):
|
||||
if display_name is not None:
|
||||
new_item.metadata['display_name'] = display_name
|
||||
|
||||
_modulestore(template).update_metadata(new_item.location.url(), new_item.own_metadata)
|
||||
get_modulestore(template).update_metadata(new_item.location.url(), new_item.own_metadata)
|
||||
|
||||
if new_item.location.category not in DETACHED_CATEGORIES:
|
||||
_modulestore(parent.location).update_children(parent_location, parent.definition.get('children', []) + [new_item.location.url()])
|
||||
get_modulestore(parent.location).update_children(parent_location, parent.definition.get('children', []) + [new_item.location.url()])
|
||||
|
||||
return HttpResponse(json.dumps({'id': dest_location.url()}))
|
||||
|
||||
@@ -857,7 +871,8 @@ def remove_user(request, location):
|
||||
def landing(request, org, course, coursename):
|
||||
return render_to_response('temp-course-landing.html', {})
|
||||
|
||||
|
||||
@login_required
|
||||
@ensure_csrf_cookie
|
||||
def static_pages(request, org, course, coursename):
|
||||
|
||||
location = ['i4x', org, course, 'course', coursename]
|
||||
@@ -877,14 +892,23 @@ def static_pages(request, org, course, coursename):
|
||||
def edit_static(request, org, course, coursename):
|
||||
return render_to_response('edit-static-page.html', {})
|
||||
|
||||
|
||||
@login_required
|
||||
@ensure_csrf_cookie
|
||||
def edit_tabs(request, org, course, coursename):
|
||||
location = ['i4x', org, course, 'course', coursename]
|
||||
course_item = modulestore().get_item(location)
|
||||
static_tabs_loc = Location('i4x', org, course, 'static_tab', None)
|
||||
|
||||
# check that logged in user has permissions to this item
|
||||
if not has_access(request.user, location):
|
||||
raise PermissionDenied()
|
||||
|
||||
static_tabs = modulestore('direct').get_items(static_tabs_loc)
|
||||
|
||||
# see tabs have been uninitialized (e.g. supporing courses created before tab support in studio)
|
||||
if course_item.tabs is None or len(course_item.tabs) == 0:
|
||||
initialize_course_tabs(course_item)
|
||||
|
||||
components = [
|
||||
static_tab.location.url()
|
||||
for static_tab
|
||||
@@ -945,6 +969,11 @@ def course_info_updates(request, org, course, provided_id=None):
|
||||
# ??? No way to check for access permission afaik
|
||||
# get current updates
|
||||
location = ['i4x', org, course, 'course_info', "updates"]
|
||||
|
||||
# check that logged in user has permissions to this item
|
||||
if not has_access(request.user, location):
|
||||
raise PermissionDenied()
|
||||
|
||||
# NB: we're setting Backbone.emulateHTTP to true on the client so everything comes as a post!!!
|
||||
if request.method == 'POST' and 'HTTP_X_HTTP_METHOD_OVERRIDE' in request.META:
|
||||
real_method = request.META['HTTP_X_HTTP_METHOD_OVERRIDE']
|
||||
@@ -953,13 +982,13 @@ def course_info_updates(request, org, course, provided_id=None):
|
||||
|
||||
if request.method == 'GET':
|
||||
return HttpResponse(json.dumps(get_course_updates(location)), mimetype="application/json")
|
||||
elif real_method == 'POST':
|
||||
# new instance (unless django makes PUT a POST): updates are coming as POST. Not sure why.
|
||||
return HttpResponse(json.dumps(update_course_updates(location, request.POST, provided_id)), mimetype="application/json")
|
||||
elif real_method == 'PUT':
|
||||
return HttpResponse(json.dumps(update_course_updates(location, request.POST, provided_id)), mimetype="application/json")
|
||||
elif real_method == 'DELETE': # coming as POST need to pull from Request Header X-HTTP-Method-Override DELETE
|
||||
return HttpResponse(json.dumps(delete_course_update(location, request.POST, provided_id)), mimetype="application/json")
|
||||
elif request.method == 'POST':
|
||||
try:
|
||||
return HttpResponse(json.dumps(update_course_updates(location, request.POST, provided_id)), mimetype="application/json")
|
||||
except etree.XMLSyntaxError:
|
||||
return HttpResponseBadRequest("Failed to save: malformed html", content_type="text/plain")
|
||||
|
||||
|
||||
@expect_json
|
||||
@@ -967,6 +996,10 @@ def course_info_updates(request, org, course, provided_id=None):
|
||||
@ensure_csrf_cookie
|
||||
def module_info(request, module_location):
|
||||
location = Location(module_location)
|
||||
|
||||
# check that logged in user has permissions to this item
|
||||
if not has_access(request.user, location):
|
||||
raise PermissionDenied()
|
||||
|
||||
# NB: we're setting Backbone.emulateHTTP to true on the client so everything comes as a post!!!
|
||||
if request.method == 'POST' and 'HTTP_X_HTTP_METHOD_OVERRIDE' in request.META:
|
||||
@@ -979,11 +1012,99 @@ def module_info(request, module_location):
|
||||
raise PermissionDenied()
|
||||
|
||||
if real_method == 'GET':
|
||||
return HttpResponse(json.dumps(get_module_info(_modulestore(location), location)), mimetype="application/json")
|
||||
return HttpResponse(json.dumps(get_module_info(get_modulestore(location), location)), mimetype="application/json")
|
||||
elif real_method == 'POST' or real_method == 'PUT':
|
||||
return HttpResponse(json.dumps(set_module_info(_modulestore(location), location, request.POST)), mimetype="application/json")
|
||||
return HttpResponse(json.dumps(set_module_info(get_modulestore(location), location, request.POST)), mimetype="application/json")
|
||||
else:
|
||||
raise Http400
|
||||
return HttpResponseBadRequest
|
||||
|
||||
@login_required
|
||||
@ensure_csrf_cookie
|
||||
def get_course_settings(request, org, course, name):
|
||||
"""
|
||||
Send models and views as well as html for editing the course settings to the client.
|
||||
|
||||
org, course, name: Attributes of the Location for the item to edit
|
||||
"""
|
||||
location = ['i4x', org, course, 'course', name]
|
||||
|
||||
# check that logged in user has permissions to this item
|
||||
if not has_access(request.user, location):
|
||||
raise PermissionDenied()
|
||||
|
||||
course_module = modulestore().get_item(location)
|
||||
course_details = CourseDetails.fetch(location)
|
||||
|
||||
return render_to_response('settings.html', {
|
||||
'active_tab': 'settings',
|
||||
'context_course': course_module,
|
||||
'course_details' : json.dumps(course_details, cls=CourseSettingsEncoder)
|
||||
})
|
||||
|
||||
@expect_json
|
||||
@login_required
|
||||
@ensure_csrf_cookie
|
||||
def course_settings_updates(request, org, course, name, section):
|
||||
"""
|
||||
restful CRUD operations on course settings. This differs from get_course_settings by communicating purely
|
||||
through json (not rendering any html) and handles section level operations rather than whole page.
|
||||
|
||||
org, course: Attributes of the Location for the item to edit
|
||||
section: one of details, faculty, grading, problems, discussions
|
||||
"""
|
||||
location = ['i4x', org, course, 'course', name]
|
||||
|
||||
# check that logged in user has permissions to this item
|
||||
if not has_access(request.user, location):
|
||||
raise PermissionDenied()
|
||||
|
||||
if section == 'details':
|
||||
manager = CourseDetails
|
||||
elif section == 'grading':
|
||||
manager = CourseGradingModel
|
||||
else: return
|
||||
|
||||
if request.method == 'GET':
|
||||
# Cannot just do a get w/o knowing the course name :-(
|
||||
return HttpResponse(json.dumps(manager.fetch(Location(['i4x', org, course, 'course',name])), cls=CourseSettingsEncoder),
|
||||
mimetype="application/json")
|
||||
elif request.method == 'POST': # post or put, doesn't matter.
|
||||
return HttpResponse(json.dumps(manager.update_from_json(request.POST), cls=CourseSettingsEncoder),
|
||||
mimetype="application/json")
|
||||
|
||||
@expect_json
|
||||
@login_required
|
||||
@ensure_csrf_cookie
|
||||
def course_grader_updates(request, org, course, name, grader_index=None):
|
||||
"""
|
||||
restful CRUD operations on course_info updates. This differs from get_course_settings by communicating purely
|
||||
through json (not rendering any html) and handles section level operations rather than whole page.
|
||||
|
||||
org, course: Attributes of the Location for the item to edit
|
||||
"""
|
||||
|
||||
location = ['i4x', org, course, 'course', name]
|
||||
|
||||
# check that logged in user has permissions to this item
|
||||
if not has_access(request.user, location):
|
||||
raise PermissionDenied()
|
||||
|
||||
if request.method == 'POST' and 'HTTP_X_HTTP_METHOD_OVERRIDE' in request.META:
|
||||
real_method = request.META['HTTP_X_HTTP_METHOD_OVERRIDE']
|
||||
else:
|
||||
real_method = request.method
|
||||
|
||||
if real_method == 'GET':
|
||||
# Cannot just do a get w/o knowing the course name :-(
|
||||
return HttpResponse(json.dumps(CourseGradingModel.fetch_grader(Location(['i4x', org, course, 'course',name]), grader_index)),
|
||||
mimetype="application/json")
|
||||
elif real_method == "DELETE":
|
||||
# ??? Shoudl this return anything? Perhaps success fail?
|
||||
CourseGradingModel.delete_grader(Location(['i4x', org, course, 'course',name]), grader_index)
|
||||
return HttpResponse()
|
||||
elif request.method == 'POST': # post or put, doesn't matter.
|
||||
return HttpResponse(json.dumps(CourseGradingModel.update_grader_from_json(Location(['i4x', org, course, 'course',name]), request.POST)),
|
||||
mimetype="application/json")
|
||||
|
||||
|
||||
@login_required
|
||||
@@ -1085,22 +1206,25 @@ def create_new_course(request):
|
||||
# set a default start date to now
|
||||
new_course.metadata['start'] = stringify_time(time.gmtime())
|
||||
|
||||
initialize_course_tabs(new_course)
|
||||
|
||||
create_all_course_groups(request.user, new_course.location)
|
||||
|
||||
return HttpResponse(json.dumps({'id': new_course.location.url()}))
|
||||
|
||||
def initialize_course_tabs(course):
|
||||
# set up the default tabs
|
||||
# I've added this because when we add static tabs, the LMS either expects a None for the tabs list or
|
||||
# at least a list populated with the minimal times
|
||||
# @TODO: I don't like the fact that the presentation tier is away of these data related constraints, let's find a better
|
||||
# place for this. Also rather than using a simple list of dictionaries a nice class model would be helpful here
|
||||
new_course.tabs = [{"type": "courseware"},
|
||||
course.tabs = [{"type": "courseware"},
|
||||
{"type": "course_info", "name": "Course Info"},
|
||||
{"type": "discussion", "name": "Discussion"},
|
||||
{"type": "wiki", "name": "Wiki"},
|
||||
{"type": "progress", "name": "Progress"}]
|
||||
|
||||
modulestore('direct').update_metadata(new_course.location.url(), new_course.own_metadata)
|
||||
|
||||
create_all_course_groups(request.user, new_course.location)
|
||||
|
||||
return HttpResponse(json.dumps({'id': new_course.location.url()}))
|
||||
modulestore('direct').update_metadata(course.location.url(), course.own_metadata)
|
||||
|
||||
@ensure_csrf_cookie
|
||||
@login_required
|
||||
|
||||
0
cms/djangoapps/models/__init__.py
Normal file
0
cms/djangoapps/models/__init__.py
Normal file
0
cms/djangoapps/models/settings/__init__.py
Normal file
0
cms/djangoapps/models/settings/__init__.py
Normal file
184
cms/djangoapps/models/settings/course_details.py
Normal file
184
cms/djangoapps/models/settings/course_details.py
Normal file
@@ -0,0 +1,184 @@
|
||||
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)
|
||||
265
cms/djangoapps/models/settings/course_grading.py
Normal file
265
cms/djangoapps/models/settings/course_grading.py
Normal file
@@ -0,0 +1,265 @@
|
||||
from xmodule.modulestore import Location
|
||||
from contentstore.utils import get_modulestore
|
||||
import re
|
||||
from util import converters
|
||||
|
||||
|
||||
class CourseGradingModel:
|
||||
"""
|
||||
Basically a DAO and Model combo for CRUD operations pertaining to grading policy.
|
||||
"""
|
||||
def __init__(self, course_descriptor):
|
||||
self.course_location = course_descriptor.location
|
||||
self.graders = [CourseGradingModel.jsonize_grader(i, grader) for i, grader in enumerate(course_descriptor.raw_grader)] # weights transformed to ints [0..100]
|
||||
self.grade_cutoffs = course_descriptor.grade_cutoffs
|
||||
self.grace_period = CourseGradingModel.convert_set_grace_period(course_descriptor)
|
||||
|
||||
@classmethod
|
||||
def fetch(cls, course_location):
|
||||
"""
|
||||
Fetch the course details for the given course from persistence and return a CourseDetails model.
|
||||
"""
|
||||
if not isinstance(course_location, Location):
|
||||
course_location = Location(course_location)
|
||||
|
||||
descriptor = get_modulestore(course_location).get_item(course_location)
|
||||
|
||||
model = cls(descriptor)
|
||||
return model
|
||||
|
||||
@staticmethod
|
||||
def fetch_grader(course_location, index):
|
||||
"""
|
||||
Fetch the course's nth grader
|
||||
Returns an empty dict if there's no such grader.
|
||||
"""
|
||||
if not isinstance(course_location, Location):
|
||||
course_location = Location(course_location)
|
||||
|
||||
descriptor = get_modulestore(course_location).get_item(course_location)
|
||||
# # ??? it would be good if these had the course_location in them so that they stand alone sufficiently
|
||||
# # but that would require not using CourseDescriptor's field directly. Opinions?
|
||||
|
||||
index = int(index)
|
||||
if len(descriptor.raw_grader) > index:
|
||||
return CourseGradingModel.jsonize_grader(index, descriptor.raw_grader[index])
|
||||
|
||||
# return empty model
|
||||
else:
|
||||
return {
|
||||
"id" : index,
|
||||
"type" : "",
|
||||
"min_count" : 0,
|
||||
"drop_count" : 0,
|
||||
"short_label" : None,
|
||||
"weight" : 0
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def fetch_cutoffs(course_location):
|
||||
"""
|
||||
Fetch the course's grade cutoffs.
|
||||
"""
|
||||
if not isinstance(course_location, Location):
|
||||
course_location = Location(course_location)
|
||||
|
||||
descriptor = get_modulestore(course_location).get_item(course_location)
|
||||
return descriptor.grade_cutoffs
|
||||
|
||||
@staticmethod
|
||||
def fetch_grace_period(course_location):
|
||||
"""
|
||||
Fetch the course's default grace period.
|
||||
"""
|
||||
if not isinstance(course_location, Location):
|
||||
course_location = Location(course_location)
|
||||
|
||||
descriptor = get_modulestore(course_location).get_item(course_location)
|
||||
return {'grace_period' : CourseGradingModel.convert_set_grace_period(descriptor) }
|
||||
|
||||
@staticmethod
|
||||
def update_from_json(jsondict):
|
||||
"""
|
||||
Decode the json into CourseGradingModel and save any changes. Returns the modified model.
|
||||
Probably not the usual path for updates as it's too coarse grained.
|
||||
"""
|
||||
course_location = jsondict['course_location']
|
||||
descriptor = get_modulestore(course_location).get_item(course_location)
|
||||
|
||||
graders_parsed = [CourseGradingModel.parse_grader(jsonele) for jsonele in jsondict['graders']]
|
||||
|
||||
descriptor.raw_grader = graders_parsed
|
||||
descriptor.grade_cutoffs = jsondict['grade_cutoffs']
|
||||
|
||||
get_modulestore(course_location).update_item(course_location, descriptor.definition['data'])
|
||||
CourseGradingModel.update_grace_period_from_json(course_location, jsondict['grace_period'])
|
||||
|
||||
return CourseGradingModel.fetch(course_location)
|
||||
|
||||
|
||||
@staticmethod
|
||||
def update_grader_from_json(course_location, grader):
|
||||
"""
|
||||
Create or update the grader of the given type (string key) for the given course. Returns the modified
|
||||
grader which is a full model on the client but not on the server (just a dict)
|
||||
"""
|
||||
if not isinstance(course_location, Location):
|
||||
course_location = Location(course_location)
|
||||
|
||||
descriptor = get_modulestore(course_location).get_item(course_location)
|
||||
# # ??? it would be good if these had the course_location in them so that they stand alone sufficiently
|
||||
# # but that would require not using CourseDescriptor's field directly. Opinions?
|
||||
|
||||
# parse removes the id; so, grab it before parse
|
||||
index = int(grader.get('id', len(descriptor.raw_grader)))
|
||||
grader = CourseGradingModel.parse_grader(grader)
|
||||
|
||||
if index < len(descriptor.raw_grader):
|
||||
descriptor.raw_grader[index] = grader
|
||||
else:
|
||||
descriptor.raw_grader.append(grader)
|
||||
|
||||
get_modulestore(course_location).update_item(course_location, descriptor.definition['data'])
|
||||
|
||||
return CourseGradingModel.jsonize_grader(index, descriptor.raw_grader[index])
|
||||
|
||||
@staticmethod
|
||||
def update_cutoffs_from_json(course_location, cutoffs):
|
||||
"""
|
||||
Create or update the grade cutoffs for the given course. Returns sent in cutoffs (ie., no extra
|
||||
db fetch).
|
||||
"""
|
||||
if not isinstance(course_location, Location):
|
||||
course_location = Location(course_location)
|
||||
|
||||
descriptor = get_modulestore(course_location).get_item(course_location)
|
||||
descriptor.grade_cutoffs = cutoffs
|
||||
get_modulestore(course_location).update_item(course_location, descriptor.definition['data'])
|
||||
|
||||
return cutoffs
|
||||
|
||||
|
||||
@staticmethod
|
||||
def update_grace_period_from_json(course_location, graceperiodjson):
|
||||
"""
|
||||
Update the course's default grace period. Incoming dict is {hours: h, minutes: m} possibly as a
|
||||
grace_period entry in an enclosing dict. It is also safe to call this method with a value of
|
||||
None for graceperiodjson.
|
||||
"""
|
||||
if not isinstance(course_location, Location):
|
||||
course_location = Location(course_location)
|
||||
|
||||
# Before a graceperiod has ever been created, it will be None (once it has been
|
||||
# created, it cannot be set back to None).
|
||||
if graceperiodjson is not None:
|
||||
if 'grace_period' in graceperiodjson:
|
||||
graceperiodjson = graceperiodjson['grace_period']
|
||||
|
||||
grace_rep = " ".join(["%s %s" % (value, key) for (key, value) in graceperiodjson.iteritems()])
|
||||
|
||||
descriptor = get_modulestore(course_location).get_item(course_location)
|
||||
descriptor.metadata['graceperiod'] = grace_rep
|
||||
get_modulestore(course_location).update_metadata(course_location, descriptor.metadata)
|
||||
|
||||
@staticmethod
|
||||
def delete_grader(course_location, index):
|
||||
"""
|
||||
Delete the grader of the given type from the given course.
|
||||
"""
|
||||
if not isinstance(course_location, Location):
|
||||
course_location = Location(course_location)
|
||||
|
||||
descriptor = get_modulestore(course_location).get_item(course_location)
|
||||
index = int(index)
|
||||
if index < len(descriptor.raw_grader):
|
||||
del descriptor.raw_grader[index]
|
||||
# force propagation to definition
|
||||
descriptor.raw_grader = descriptor.raw_grader
|
||||
get_modulestore(course_location).update_item(course_location, descriptor.definition['data'])
|
||||
|
||||
# NOTE cannot delete cutoffs. May be useful to reset
|
||||
@staticmethod
|
||||
def delete_cutoffs(course_location, cutoffs):
|
||||
"""
|
||||
Resets the cutoffs to the defaults
|
||||
"""
|
||||
if not isinstance(course_location, Location):
|
||||
course_location = Location(course_location)
|
||||
|
||||
descriptor = get_modulestore(course_location).get_item(course_location)
|
||||
descriptor.grade_cutoffs = descriptor.defaut_grading_policy['GRADE_CUTOFFS']
|
||||
get_modulestore(course_location).update_item(course_location, descriptor.definition['data'])
|
||||
|
||||
return descriptor.grade_cutoffs
|
||||
|
||||
@staticmethod
|
||||
def delete_grace_period(course_location):
|
||||
"""
|
||||
Delete the course's default grace period.
|
||||
"""
|
||||
if not isinstance(course_location, Location):
|
||||
course_location = Location(course_location)
|
||||
|
||||
descriptor = get_modulestore(course_location).get_item(course_location)
|
||||
if 'graceperiod' in descriptor.metadata: del descriptor.metadata['graceperiod']
|
||||
get_modulestore(course_location).update_metadata(course_location, descriptor.metadata)
|
||||
|
||||
@staticmethod
|
||||
def get_section_grader_type(location):
|
||||
if not isinstance(location, Location):
|
||||
location = Location(location)
|
||||
|
||||
descriptor = get_modulestore(location).get_item(location)
|
||||
return {
|
||||
"graderType" : descriptor.metadata.get('format', u"Not Graded"),
|
||||
"location" : location,
|
||||
"id" : 99 # just an arbitrary value to
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def update_section_grader_type(location, jsondict):
|
||||
if not isinstance(location, Location):
|
||||
location = Location(location)
|
||||
|
||||
descriptor = get_modulestore(location).get_item(location)
|
||||
if 'graderType' in jsondict and jsondict['graderType'] != u"Not Graded":
|
||||
descriptor.metadata['format'] = jsondict.get('graderType')
|
||||
descriptor.metadata['graded'] = True
|
||||
else:
|
||||
if 'format' in descriptor.metadata: del descriptor.metadata['format']
|
||||
if 'graded' in descriptor.metadata: del descriptor.metadata['graded']
|
||||
|
||||
get_modulestore(location).update_metadata(location, descriptor.metadata)
|
||||
|
||||
|
||||
@staticmethod
|
||||
def convert_set_grace_period(descriptor):
|
||||
# 5 hours 59 minutes 59 seconds => converted to iso format
|
||||
rawgrace = descriptor.metadata.get('graceperiod', None)
|
||||
if rawgrace:
|
||||
parsedgrace = {str(key): val for (val, key) in re.findall('\s*(\d+)\s*(\w+)', rawgrace)}
|
||||
return parsedgrace
|
||||
else: return None
|
||||
|
||||
@staticmethod
|
||||
def parse_grader(json_grader):
|
||||
# manual to clear out kruft
|
||||
result = {
|
||||
"type" : json_grader["type"],
|
||||
"min_count" : int(json_grader.get('min_count', 0)),
|
||||
"drop_count" : int(json_grader.get('drop_count', 0)),
|
||||
"short_label" : json_grader.get('short_label', None),
|
||||
"weight" : float(json_grader.get('weight', 0)) / 100.0
|
||||
}
|
||||
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def jsonize_grader(i, grader):
|
||||
grader['id'] = i
|
||||
if grader['weight']:
|
||||
grader['weight'] *= 100
|
||||
if not 'short_label' in grader:
|
||||
grader['short_label'] = ""
|
||||
|
||||
return grader
|
||||
@@ -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.
|
||||
|
||||
69
cms/static/client_templates/course_grade_policy.html
Normal file
69
cms/static/client_templates/course_grade_policy.html
Normal file
@@ -0,0 +1,69 @@
|
||||
<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()
|
||||
|
||||
BIN
cms/static/img/collapse-all-icon.png
Normal file
BIN
cms/static/img/collapse-all-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1018 B |
BIN
cms/static/img/home-icon-blue.png
Normal file
BIN
cms/static/img/home-icon-blue.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
BIN
cms/static/img/log-out-icon.png
Normal file
BIN
cms/static/img/log-out-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.0 KiB |
BIN
cms/static/img/small-home-icon.png
Normal file
BIN
cms/static/img/small-home-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.0 KiB |
BIN
cms/static/img/upload-icon.png
Normal file
BIN
cms/static/img/upload-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
@@ -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({
|
||||
|
||||
68
cms/static/js/models/course_relative.js
Normal file
68
cms/static/js/models/course_relative.js
Normal file
@@ -0,0 +1,68 @@
|
||||
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
|
||||
});
|
||||
83
cms/static/js/models/settings/course_details.js
Normal file
83
cms/static/js/models/settings/course_details.js
Normal file
@@ -0,0 +1,83 @@
|
||||
if (!CMS.Models['Settings']) CMS.Models.Settings = new Object();
|
||||
|
||||
CMS.Models.Settings.CourseDetails = Backbone.Model.extend({
|
||||
defaults: {
|
||||
location : null, // the course's Location model, required
|
||||
start_date: null, // maps to 'start'
|
||||
end_date: null, // maps to 'end'
|
||||
enrollment_start: null,
|
||||
enrollment_end: null,
|
||||
syllabus: null,
|
||||
overview: "",
|
||||
intro_video: null,
|
||||
effort: null // an int or null
|
||||
},
|
||||
|
||||
// When init'g from html script, ensure you pass {parse: true} as an option (2nd arg to reset)
|
||||
parse: function(attributes) {
|
||||
if (attributes['course_location']) {
|
||||
attributes.location = new CMS.Models.Location(attributes.course_location, {parse:true});
|
||||
}
|
||||
if (attributes['start_date']) {
|
||||
attributes.start_date = new Date(attributes.start_date);
|
||||
}
|
||||
if (attributes['end_date']) {
|
||||
attributes.end_date = new Date(attributes.end_date);
|
||||
}
|
||||
if (attributes['enrollment_start']) {
|
||||
attributes.enrollment_start = new Date(attributes.enrollment_start);
|
||||
}
|
||||
if (attributes['enrollment_end']) {
|
||||
attributes.enrollment_end = new Date(attributes.enrollment_end);
|
||||
}
|
||||
return attributes;
|
||||
},
|
||||
|
||||
validate: function(newattrs) {
|
||||
// Returns either nothing (no return call) so that validate works or an object of {field: errorstring} pairs
|
||||
// A bit funny in that the video key validation is asynchronous; so, it won't stop the validation.
|
||||
var errors = {};
|
||||
if (newattrs.start_date && newattrs.end_date && newattrs.start_date >= newattrs.end_date) {
|
||||
errors.end_date = "The course end date cannot be before the course start date.";
|
||||
}
|
||||
if (newattrs.start_date && newattrs.enrollment_start && newattrs.start_date < newattrs.enrollment_start) {
|
||||
errors.enrollment_start = "The course start date cannot be before the enrollment start date.";
|
||||
}
|
||||
if (newattrs.enrollment_start && newattrs.enrollment_end && newattrs.enrollment_start >= newattrs.enrollment_end) {
|
||||
errors.enrollment_end = "The enrollment start date cannot be after the enrollment end date.";
|
||||
}
|
||||
if (newattrs.end_date && newattrs.enrollment_end && newattrs.end_date < newattrs.enrollment_end) {
|
||||
errors.enrollment_end = "The enrollment end date cannot be after the course end date.";
|
||||
}
|
||||
if (newattrs.intro_video && newattrs.intro_video !== this.get('intro_video')) {
|
||||
if (this._videokey_illegal_chars.exec(newattrs.intro_video)) {
|
||||
errors.intro_video = "Key should only contain letters, numbers, _, or -";
|
||||
}
|
||||
// TODO check if key points to a real video using google's youtube api
|
||||
}
|
||||
if (!_.isEmpty(errors)) return errors;
|
||||
// NOTE don't return empty errors as that will be interpreted as an error state
|
||||
},
|
||||
|
||||
url: function() {
|
||||
var location = this.get('location');
|
||||
return '/' + location.get('org') + "/" + location.get('course') + '/settings/' + location.get('name') + '/section/details';
|
||||
},
|
||||
|
||||
_videokey_illegal_chars : /[^a-zA-Z0-9_-]/g,
|
||||
save_videosource: function(newsource) {
|
||||
// newsource either is <video youtube="speed:key, *"/> or just the "speed:key, *" string
|
||||
// returns the videosource for the preview which iss the key whose speed is closest to 1
|
||||
if (_.isEmpty(newsource) && !_.isEmpty(this.get('intro_video'))) this.save({'intro_video': null});
|
||||
// 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 "";
|
||||
}
|
||||
});
|
||||
127
cms/static/js/models/settings/course_grading_policy.js
Normal file
127
cms/static/js/models/settings/course_grading_policy.js
Normal file
@@ -0,0 +1,127 @@
|
||||
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);
|
||||
}
|
||||
});
|
||||
43
cms/static/js/models/settings/course_settings.js
Normal file
43
cms/static/js/models/settings/course_settings.js
Normal file
@@ -0,0 +1,43 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
90
cms/static/js/views/grader-select-view.js
Normal file
90
cms/static/js/views/grader-select-view.js
Normal file
@@ -0,0 +1,90 @@
|
||||
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">✓</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();
|
||||
}
|
||||
})
|
||||
673
cms/static/js/views/settings/main_settings_view.js
Normal file
673
cms/static/js/views/settings/main_settings_view.js
Normal file
@@ -0,0 +1,673 @@
|
||||
if (!CMS.Views['Settings']) CMS.Views.Settings = {};
|
||||
|
||||
// TODO move to common place
|
||||
CMS.Views.ValidatingView = Backbone.View.extend({
|
||||
// Intended as an abstract class which catches validation errors on the model and
|
||||
// decorates the fields. Needs wiring per class, but this initialization shows how
|
||||
// either have your init call this one or copy the contents
|
||||
initialize : function() {
|
||||
this.model.on('error', this.handleValidationError, this);
|
||||
this.selectorToField = _.invert(this.fieldToSelectorMap);
|
||||
},
|
||||
|
||||
errorTemplate : _.template('<span class="message-error"><%= message %></span>'),
|
||||
|
||||
events : {
|
||||
"blur input" : "clearValidationErrors",
|
||||
"blur textarea" : "clearValidationErrors"
|
||||
},
|
||||
fieldToSelectorMap : {
|
||||
// Your subclass must populate this w/ all of the model keys and dom selectors
|
||||
// which may be the subjects of validation errors
|
||||
},
|
||||
_cacheValidationErrors : [],
|
||||
handleValidationError : function(model, error) {
|
||||
// error is object w/ fields and error strings
|
||||
for (var field in error) {
|
||||
var ele = this.$el.find('#' + this.fieldToSelectorMap[field]);
|
||||
this._cacheValidationErrors.push(ele);
|
||||
if ($(ele).is('div')) {
|
||||
// put error on the contained inputs
|
||||
$(ele).find('input, textarea').addClass('error');
|
||||
}
|
||||
else $(ele).addClass('error');
|
||||
$(ele).parent().append(this.errorTemplate({message : error[field]}));
|
||||
}
|
||||
},
|
||||
|
||||
clearValidationErrors : function() {
|
||||
// error is object w/ fields and error strings
|
||||
while (this._cacheValidationErrors.length > 0) {
|
||||
var ele = this._cacheValidationErrors.pop();
|
||||
if ($(ele).is('div')) {
|
||||
// put error on the contained inputs
|
||||
$(ele).find('input, textarea').removeClass('error');
|
||||
}
|
||||
else $(ele).removeClass('error');
|
||||
$(ele).nextAll('.message-error').remove();
|
||||
}
|
||||
},
|
||||
|
||||
saveIfChanged : function(event) {
|
||||
// returns true if the value changed and was thus sent to server
|
||||
var field = this.selectorToField[event.currentTarget.id];
|
||||
var currentVal = this.model.get(field);
|
||||
var newVal = $(event.currentTarget).val();
|
||||
if (currentVal != newVal) {
|
||||
this.clearValidationErrors();
|
||||
this.model.save(field, newVal);
|
||||
return true;
|
||||
}
|
||||
else return false;
|
||||
}
|
||||
});
|
||||
|
||||
CMS.Views.Settings.Main = Backbone.View.extend({
|
||||
// Model class is CMS.Models.Settings.CourseSettings
|
||||
// allow navigation between the tabs
|
||||
events: {
|
||||
'click .settings-page-menu a': "showSettingsTab",
|
||||
'mouseover #timezone' : "updateTime"
|
||||
},
|
||||
|
||||
currentTab: null,
|
||||
subviews: {}, // indexed by tab name
|
||||
|
||||
initialize: function() {
|
||||
// load templates
|
||||
this.currentTab = this.$el.find('.settings-page-menu .is-shown').attr('data-section');
|
||||
// create the initial subview
|
||||
this.subviews[this.currentTab] = this.createSubview();
|
||||
|
||||
// fill in fields
|
||||
this.$el.find("#course-name").val(this.model.get('courseLocation').get('name'));
|
||||
this.$el.find("#course-organization").val(this.model.get('courseLocation').get('org'));
|
||||
this.$el.find("#course-number").val(this.model.get('courseLocation').get('course'));
|
||||
this.$el.find('.set-date').datepicker({ 'dateFormat': 'm/d/yy' });
|
||||
this.$el.find(":input, textarea").focus(function() {
|
||||
$("label[for='" + this.id + "']").addClass("is-focused");
|
||||
}).blur(function() {
|
||||
$("label").removeClass("is-focused");
|
||||
});
|
||||
this.render();
|
||||
},
|
||||
|
||||
render: function() {
|
||||
|
||||
// create any necessary subviews and put them onto the page
|
||||
if (!this.model.has(this.currentTab)) {
|
||||
// TODO disable screen until fetch completes?
|
||||
var cachethis = this;
|
||||
this.model.retrieve(this.currentTab, function() {
|
||||
cachethis.subviews[cachethis.currentTab] = cachethis.createSubview();
|
||||
cachethis.subviews[cachethis.currentTab].render();
|
||||
});
|
||||
}
|
||||
else this.subviews[this.currentTab].render();
|
||||
|
||||
var dateIntrospect = new Date();
|
||||
this.$el.find('#timezone').html("(" + dateIntrospect.getTimezone() + ")");
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
createSubview: function() {
|
||||
switch (this.currentTab) {
|
||||
case 'details':
|
||||
return new CMS.Views.Settings.Details({
|
||||
el: this.$el.find('.settings-' + this.currentTab),
|
||||
model: this.model.get(this.currentTab)
|
||||
});
|
||||
case 'faculty':
|
||||
break;
|
||||
case 'grading':
|
||||
return new CMS.Views.Settings.Grading({
|
||||
el: this.$el.find('.settings-' + this.currentTab),
|
||||
model: this.model.get(this.currentTab)
|
||||
});
|
||||
case 'problems':
|
||||
break;
|
||||
case 'discussions':
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
updateTime : function(e) {
|
||||
var now = new Date();
|
||||
var hours = now.getHours();
|
||||
var minutes = now.getMinutes();
|
||||
$(e.currentTarget).attr('title', (hours % 12 === 0 ? 12 : hours % 12) + ":" + (minutes < 10 ? "0" : "") +
|
||||
now.getMinutes() + (hours < 12 ? "am" : "pm") + " (current local time)");
|
||||
},
|
||||
|
||||
showSettingsTab: function(e) {
|
||||
this.currentTab = $(e.target).attr('data-section');
|
||||
$('.settings-page-section > section').hide();
|
||||
$('.settings-' + this.currentTab).show();
|
||||
$('.settings-page-menu .is-shown').removeClass('is-shown');
|
||||
$(e.target).addClass('is-shown');
|
||||
// fetch model for the tab if not loaded already
|
||||
this.render();
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
CMS.Views.Settings.Details = CMS.Views.ValidatingView.extend({
|
||||
// Model class is CMS.Models.Settings.CourseDetails
|
||||
events : {
|
||||
"blur input" : "updateModel",
|
||||
"blur textarea" : "updateModel",
|
||||
'click .remove-course-syllabus' : "removeSyllabus",
|
||||
'click .new-course-syllabus' : 'assetSyllabus',
|
||||
'click .remove-course-introduction-video' : "removeVideo",
|
||||
'focus #course-overview' : "codeMirrorize"
|
||||
},
|
||||
initialize : function() {
|
||||
// TODO move the html frag to a loaded asset
|
||||
this.fileAnchorTemplate = _.template('<a href="<%= fullpath %>"> <i class="ss-icon ss-standard">📄</i><%= filename %></a>');
|
||||
this.model.on('error', this.handleValidationError, this);
|
||||
this.selectorToField = _.invert(this.fieldToSelectorMap);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
this.setupDatePicker('start_date');
|
||||
this.setupDatePicker('end_date');
|
||||
this.setupDatePicker('enrollment_start');
|
||||
this.setupDatePicker('enrollment_end');
|
||||
|
||||
if (this.model.has('syllabus')) {
|
||||
this.$el.find(this.fieldToSelectorMap['syllabus']).html(
|
||||
this.fileAnchorTemplate({
|
||||
fullpath : this.model.get('syllabus'),
|
||||
filename: 'syllabus'}));
|
||||
this.$el.find('.remove-course-syllabus').show();
|
||||
}
|
||||
else {
|
||||
this.$el.find('#' + this.fieldToSelectorMap['syllabus']).html("");
|
||||
this.$el.find('.remove-course-syllabus').hide();
|
||||
}
|
||||
|
||||
this.$el.find('#' + this.fieldToSelectorMap['overview']).val(this.model.get('overview'));
|
||||
this.codeMirrorize(null, $('#course-overview')[0]);
|
||||
|
||||
this.$el.find('.current-course-introduction-video iframe').attr('src', this.model.videosourceSample());
|
||||
if (this.model.has('intro_video')) {
|
||||
this.$el.find('.remove-course-introduction-video').show();
|
||||
this.$el.find('#' + this.fieldToSelectorMap['intro_video']).val(this.model.get('intro_video'));
|
||||
}
|
||||
else this.$el.find('.remove-course-introduction-video').hide();
|
||||
|
||||
this.$el.find('#' + this.fieldToSelectorMap['effort']).val(this.model.get('effort'));
|
||||
|
||||
return this;
|
||||
},
|
||||
fieldToSelectorMap : {
|
||||
'start_date' : "course-start",
|
||||
'end_date' : 'course-end',
|
||||
'enrollment_start' : 'enrollment-start',
|
||||
'enrollment_end' : 'enrollment-end',
|
||||
'syllabus' : '.current-course-syllabus .doc-filename',
|
||||
'overview' : 'course-overview',
|
||||
'intro_video' : 'course-introduction-video',
|
||||
'effort' : "course-effort"
|
||||
},
|
||||
|
||||
setupDatePicker : function(fieldName) {
|
||||
var cacheModel = this.model;
|
||||
var div = this.$el.find('#' + this.fieldToSelectorMap[fieldName]);
|
||||
var datefield = $(div).find(".date");
|
||||
var timefield = $(div).find(".time");
|
||||
var cachethis = this;
|
||||
var savefield = function() {
|
||||
cachethis.clearValidationErrors();
|
||||
var date = datefield.datepicker('getDate');
|
||||
if (date) {
|
||||
var time = timefield.timepicker("getSecondsFromMidnight");
|
||||
if (!time) {
|
||||
time = 0;
|
||||
}
|
||||
var newVal = new Date(date.getTime() + time * 1000);
|
||||
if (cacheModel.get(fieldName) != newVal) cacheModel.save(fieldName, newVal);
|
||||
}
|
||||
};
|
||||
|
||||
// instrument as date and time pickers
|
||||
timefield.timepicker();
|
||||
|
||||
// FIXME being called 2x on each change. Was trapping datepicker onSelect b4 but change to datepair broke that
|
||||
datefield.datepicker({ onSelect : savefield });
|
||||
timefield.on('changeTime', savefield);
|
||||
|
||||
datefield.datepicker('setDate', this.model.get(fieldName));
|
||||
if (this.model.has(fieldName)) timefield.timepicker('setTime', this.model.get(fieldName));
|
||||
},
|
||||
|
||||
updateModel: function(event) {
|
||||
switch (event.currentTarget.id) {
|
||||
case 'course-start-date': // handled via onSelect method
|
||||
case 'course-end-date':
|
||||
case 'course-enrollment-start-date':
|
||||
case 'course-enrollment-end-date':
|
||||
break;
|
||||
|
||||
case 'course-overview':
|
||||
// handled via code mirror
|
||||
break;
|
||||
|
||||
case 'course-effort':
|
||||
this.saveIfChanged(event);
|
||||
break;
|
||||
case 'course-introduction-video':
|
||||
this.clearValidationErrors();
|
||||
var previewsource = this.model.save_videosource($(event.currentTarget).val());
|
||||
this.$el.find(".current-course-introduction-video iframe").attr("src", previewsource);
|
||||
if (this.model.has('intro_video')) {
|
||||
this.$el.find('.remove-course-introduction-video').show();
|
||||
}
|
||||
else {
|
||||
this.$el.find('.remove-course-introduction-video').hide();
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
removeSyllabus: function() {
|
||||
if (this.model.has('syllabus')) this.model.save({'syllabus': null});
|
||||
},
|
||||
|
||||
assetSyllabus : function() {
|
||||
// TODO implement
|
||||
},
|
||||
|
||||
removeVideo: function() {
|
||||
if (this.model.has('intro_video')) {
|
||||
this.model.save_videosource(null);
|
||||
this.$el.find(".current-course-introduction-video iframe").attr("src", "");
|
||||
this.$el.find('#' + this.fieldToSelectorMap['intro_video']).val("");
|
||||
this.$el.find('.remove-course-introduction-video').hide();
|
||||
}
|
||||
},
|
||||
codeMirrors : {},
|
||||
codeMirrorize : function(e, forcedTarget) {
|
||||
if (forcedTarget) {
|
||||
thisTarget = forcedTarget;
|
||||
thisTarget.id = $(thisTarget).attr('id');
|
||||
} else {
|
||||
thisTarget = e.currentTarget;
|
||||
}
|
||||
|
||||
if (!this.codeMirrors[thisTarget.id]) {
|
||||
var cachethis = this;
|
||||
var field = this.selectorToField[thisTarget.id];
|
||||
this.codeMirrors[thisTarget.id] = CodeMirror.fromTextArea(thisTarget, {
|
||||
mode: "text/html", lineNumbers: true, lineWrapping: true,
|
||||
onBlur : function(mirror) {
|
||||
mirror.save();
|
||||
cachethis.clearValidationErrors();
|
||||
var newVal = mirror.getValue();
|
||||
if (cachethis.model.get(field) != newVal) cachethis.model.save(field, newVal);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
CMS.Views.Settings.Grading = CMS.Views.ValidatingView.extend({
|
||||
// Model class is CMS.Models.Settings.CourseGradingPolicy
|
||||
events : {
|
||||
"blur input" : "updateModel",
|
||||
"blur textarea" : "updateModel",
|
||||
"blur span[contenteditable=true]" : "updateDesignation",
|
||||
"click .settings-extra header" : "showSettingsExtras",
|
||||
"click .new-grade-button" : "addNewGrade",
|
||||
"click .remove-button" : "removeGrade",
|
||||
"click .add-grading-data" : "addAssignmentType"
|
||||
},
|
||||
initialize : function() {
|
||||
// load template for grading view
|
||||
var self = this;
|
||||
this.gradeCutoffTemplate = _.template('<li class="grade-specific-bar" style="width:<%= width %>%"><span class="letter-grade" contenteditable>' +
|
||||
'<%= descriptor %>' +
|
||||
'</span><span class="range"></span>' +
|
||||
'<% if (removable) {%><a href="#" class="remove-button">remove</a><% ;} %>' +
|
||||
'</li>');
|
||||
|
||||
// Instrument grading scale
|
||||
// convert cutoffs to inversely ordered list
|
||||
var modelCutoffs = this.model.get('grade_cutoffs');
|
||||
for (var cutoff in modelCutoffs) {
|
||||
this.descendingCutoffs.push({designation: cutoff, cutoff: Math.round(modelCutoffs[cutoff] * 100)});
|
||||
}
|
||||
this.descendingCutoffs = _.sortBy(this.descendingCutoffs,
|
||||
function (gradeEle) { return -gradeEle['cutoff']; });
|
||||
|
||||
// Instrument grace period
|
||||
this.$el.find('#course-grading-graceperiod').timepicker();
|
||||
|
||||
// instantiates an editor template for each update in the collection
|
||||
// Because this calls render, put it after everything which render may depend upon to prevent race condition.
|
||||
window.templateLoader.loadRemoteTemplate("course_grade_policy",
|
||||
"/static/client_templates/course_grade_policy.html",
|
||||
function (raw_template) {
|
||||
self.template = _.template(raw_template);
|
||||
self.render();
|
||||
}
|
||||
);
|
||||
this.model.on('error', this.handleValidationError, this);
|
||||
this.model.get('graders').on('remove', this.render, this);
|
||||
this.model.get('graders').on('reset', this.render, this);
|
||||
this.model.get('graders').on('add', this.render, this);
|
||||
this.selectorToField = _.invert(this.fieldToSelectorMap);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
// prevent bootstrap race condition by event dispatch
|
||||
if (!this.template) return;
|
||||
|
||||
// Create and render the grading type subs
|
||||
var self = this;
|
||||
var gradelist = this.$el.find('.course-grading-assignment-list');
|
||||
// Undo the double invocation error. At some point, fix the double invocation
|
||||
$(gradelist).empty();
|
||||
var gradeCollection = this.model.get('graders');
|
||||
gradeCollection.each(function(gradeModel) {
|
||||
$(gradelist).append(self.template({model : gradeModel }));
|
||||
var newEle = gradelist.children().last();
|
||||
var newView = new CMS.Views.Settings.GraderView({el: newEle,
|
||||
model : gradeModel, collection : gradeCollection });
|
||||
});
|
||||
|
||||
// render the grade cutoffs
|
||||
this.renderCutoffBar();
|
||||
|
||||
var graceEle = this.$el.find('#course-grading-graceperiod');
|
||||
graceEle.timepicker({'timeFormat' : 'H:i'}); // init doesn't take setTime
|
||||
if (this.model.has('grace_period')) graceEle.timepicker('setTime', this.model.gracePeriodToDate());
|
||||
|
||||
return this;
|
||||
},
|
||||
addAssignmentType : function(e) {
|
||||
e.preventDefault();
|
||||
this.model.get('graders').push({});
|
||||
},
|
||||
fieldToSelectorMap : {
|
||||
'grace_period' : 'course-grading-graceperiod'
|
||||
},
|
||||
updateModel : function(event) {
|
||||
if (!this.selectorToField[event.currentTarget.id]) return;
|
||||
|
||||
switch (this.selectorToField[event.currentTarget.id]) {
|
||||
case 'grace_period':
|
||||
this.clearValidationErrors();
|
||||
var newVal = this.model.dateToGracePeriod($(event.currentTarget).timepicker('getTime'));
|
||||
if (this.model.get('grace_period') != newVal) this.model.save('grace_period', newVal);
|
||||
break;
|
||||
|
||||
default:
|
||||
this.saveIfChanged(event);
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
// Grade sliders attributes and methods
|
||||
// Grade bars are li's ordered A -> F with A taking whole width, B overlaying it with its paint, ...
|
||||
// The actual cutoff for each grade is the width % of the next lower grade; so, the hack here
|
||||
// is to lay down a whole width bar claiming it's A and then lay down bars for each actual grade
|
||||
// starting w/ A but posting the label in the preceding li and setting the label of the last to "Fail" or "F"
|
||||
|
||||
// A does not have a drag bar (cannot change its upper limit)
|
||||
// Need to insert new bars in right place.
|
||||
GRADES : ['A', 'B', 'C', 'D'], // defaults for new grade designators
|
||||
descendingCutoffs : [], // array of { designation : , cutoff : }
|
||||
gradeBarWidth : null, // cache of value since it won't change (more certain)
|
||||
|
||||
renderCutoffBar: function() {
|
||||
var gradeBar =this.$el.find('.grade-bar');
|
||||
this.gradeBarWidth = gradeBar.width();
|
||||
var gradelist = gradeBar.children('.grades');
|
||||
// HACK fixing a duplicate call issue by undoing previous call effect. Need to figure out why called 2x
|
||||
gradelist.empty();
|
||||
var nextWidth = 100; // first width is 100%
|
||||
// Can probably be simplified to one variable now.
|
||||
var removable = false;
|
||||
var draggable = false; // first and last are not removable, first is not draggable
|
||||
_.each(this.descendingCutoffs,
|
||||
function(cutoff, index) {
|
||||
var newBar = this.gradeCutoffTemplate({
|
||||
descriptor : cutoff['designation'] ,
|
||||
width : nextWidth,
|
||||
removable : removable });
|
||||
gradelist.append(newBar);
|
||||
if (draggable) {
|
||||
newBar = gradelist.children().last(); // get the dom object not the unparsed string
|
||||
newBar.resizable({
|
||||
handles: "e",
|
||||
containment : "parent",
|
||||
start : this.startMoveClosure(),
|
||||
resize : this.moveBarClosure(),
|
||||
stop : this.stopDragClosure()
|
||||
});
|
||||
}
|
||||
// prepare for next
|
||||
nextWidth = cutoff['cutoff'];
|
||||
removable = true; // first is not removable, all others are
|
||||
draggable = true;
|
||||
},
|
||||
this);
|
||||
// add fail which is not in data
|
||||
var failBar = this.gradeCutoffTemplate({ descriptor : this.failLabel(),
|
||||
width : nextWidth, removable : false});
|
||||
$(failBar).find("span[contenteditable=true]").attr("contenteditable", false);
|
||||
gradelist.append(failBar);
|
||||
gradelist.children().last().resizable({
|
||||
handles: "e",
|
||||
containment : "parent",
|
||||
start : this.startMoveClosure(),
|
||||
resize : this.moveBarClosure(),
|
||||
stop : this.stopDragClosure()
|
||||
});
|
||||
|
||||
this.renderGradeRanges();
|
||||
},
|
||||
|
||||
showSettingsExtras : function(event) {
|
||||
$(event.currentTarget).toggleClass('active');
|
||||
$(event.currentTarget).siblings.toggleClass('is-shown');
|
||||
},
|
||||
|
||||
|
||||
startMoveClosure : function() {
|
||||
// set min/max widths
|
||||
var cachethis = this;
|
||||
var widthPerPoint = cachethis.gradeBarWidth / 100;
|
||||
return function(event, ui) {
|
||||
var barIndex = ui.element.index();
|
||||
// min and max represent limits not labels (note, can's make smaller than 3 points wide)
|
||||
var min = (barIndex < cachethis.descendingCutoffs.length ? cachethis.descendingCutoffs[barIndex]['cutoff'] + 3 : 3);
|
||||
// minus 2 b/c minus 1 is the element we're effecting. It's max is just shy of the next one above it
|
||||
var max = (barIndex >= 2 ? cachethis.descendingCutoffs[barIndex - 2]['cutoff'] - 3 : 97);
|
||||
ui.element.resizable("option",{minWidth : min * widthPerPoint, maxWidth : max * widthPerPoint});
|
||||
};
|
||||
},
|
||||
|
||||
moveBarClosure : function() {
|
||||
// 0th ele doesn't have a bar; so, will never invoke this
|
||||
var cachethis = this;
|
||||
return function(event, ui) {
|
||||
var barIndex = ui.element.index();
|
||||
// min and max represent limits not labels (note, can's make smaller than 3 points wide)
|
||||
var min = (barIndex < cachethis.descendingCutoffs.length ? cachethis.descendingCutoffs[barIndex]['cutoff'] + 3 : 3);
|
||||
// minus 2 b/c minus 1 is the element we're effecting. It's max is just shy of the next one above it
|
||||
var max = (barIndex >= 2 ? cachethis.descendingCutoffs[barIndex - 2]['cutoff'] - 3 : 100);
|
||||
var percentage = Math.min(Math.max(ui.size.width / cachethis.gradeBarWidth * 100, min), max);
|
||||
cachethis.descendingCutoffs[barIndex - 1]['cutoff'] = Math.round(percentage);
|
||||
cachethis.renderGradeRanges();
|
||||
};
|
||||
},
|
||||
|
||||
renderGradeRanges: function() {
|
||||
// the labels showing the range e.g., 71-80
|
||||
var cutoffs = this.descendingCutoffs;
|
||||
this.$el.find('.range').each(function(i) {
|
||||
var min = (i < cutoffs.length ? cutoffs[i]['cutoff'] : 0);
|
||||
var max = (i > 0 ? cutoffs[i - 1]['cutoff'] : 100);
|
||||
$(this).text(min + '-' + max);
|
||||
});
|
||||
},
|
||||
|
||||
stopDragClosure: function() {
|
||||
var cachethis = this;
|
||||
return function(event, ui) {
|
||||
// for some reason the resize is setting height to 0
|
||||
cachethis.saveCutoffs();
|
||||
};
|
||||
},
|
||||
|
||||
saveCutoffs: function() {
|
||||
this.model.save('grade_cutoffs',
|
||||
_.reduce(this.descendingCutoffs,
|
||||
function(object, cutoff) {
|
||||
object[cutoff['designation']] = cutoff['cutoff'] / 100.0;
|
||||
return object;
|
||||
},
|
||||
{}));
|
||||
},
|
||||
|
||||
addNewGrade: function(e) {
|
||||
e.preventDefault();
|
||||
var gradeLength = this.descendingCutoffs.length; // cutoffs doesn't include fail/f so this is only the passing grades
|
||||
if(gradeLength > 3) {
|
||||
// TODO shouldn't we disable the button
|
||||
return;
|
||||
}
|
||||
var failBarWidth = this.descendingCutoffs[gradeLength - 1]['cutoff'];
|
||||
// going to split the grade above the insertion point in half leaving fail in same place
|
||||
var nextGradeTop = (gradeLength > 1 ? this.descendingCutoffs[gradeLength - 2]['cutoff'] : 100);
|
||||
var targetWidth = failBarWidth + ((nextGradeTop - failBarWidth) / 2);
|
||||
this.descendingCutoffs.push({designation: this.GRADES[gradeLength], cutoff: failBarWidth});
|
||||
this.descendingCutoffs[gradeLength - 1]['cutoff'] = Math.round(targetWidth);
|
||||
|
||||
var $newGradeBar = this.gradeCutoffTemplate({ descriptor : this.GRADES[gradeLength],
|
||||
width : targetWidth, removable : true });
|
||||
var gradeDom = this.$el.find('.grades');
|
||||
gradeDom.children().last().before($newGradeBar);
|
||||
var newEle = gradeDom.children()[gradeLength];
|
||||
$(newEle).resizable({
|
||||
handles: "e",
|
||||
containment : "parent",
|
||||
start : this.startMoveClosure(),
|
||||
resize : this.moveBarClosure(),
|
||||
stop : this.stopDragClosure()
|
||||
});
|
||||
|
||||
// Munge existing grade labels?
|
||||
// If going from Pass/Fail to 3 levels, change to Pass to A
|
||||
if (gradeLength === 1 && this.descendingCutoffs[0]['designation'] === 'Pass') {
|
||||
this.descendingCutoffs[0]['designation'] = this.GRADES[0];
|
||||
this.setTopGradeLabel();
|
||||
}
|
||||
this.setFailLabel();
|
||||
|
||||
this.renderGradeRanges();
|
||||
this.saveCutoffs();
|
||||
},
|
||||
|
||||
removeGrade: function(e) {
|
||||
e.preventDefault();
|
||||
var domElement = $(e.currentTarget).closest('li');
|
||||
var index = domElement.index();
|
||||
// copy the boundary up to the next higher grade then remove
|
||||
this.descendingCutoffs[index - 1]['cutoff'] = this.descendingCutoffs[index]['cutoff'];
|
||||
this.descendingCutoffs.splice(index, 1);
|
||||
domElement.remove();
|
||||
|
||||
if (this.descendingCutoffs.length === 1 && this.descendingCutoffs[0]['designation'] === this.GRADES[0]) {
|
||||
this.descendingCutoffs[0]['designation'] = 'Pass';
|
||||
this.setTopGradeLabel();
|
||||
}
|
||||
this.setFailLabel();
|
||||
this.renderGradeRanges();
|
||||
this.saveCutoffs();
|
||||
},
|
||||
|
||||
updateDesignation: function(e) {
|
||||
var index = $(e.currentTarget).closest('li').index();
|
||||
this.descendingCutoffs[index]['designation'] = $(e.currentTarget).html();
|
||||
this.saveCutoffs();
|
||||
},
|
||||
|
||||
failLabel: function() {
|
||||
if (this.descendingCutoffs.length === 1) return 'Fail';
|
||||
else return 'F';
|
||||
},
|
||||
setFailLabel: function() {
|
||||
this.$el.find('.grades .letter-grade').last().html(this.failLabel());
|
||||
},
|
||||
setTopGradeLabel: function() {
|
||||
this.$el.find('.grades .letter-grade').first().html(this.descendingCutoffs[0]['designation']);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
CMS.Views.Settings.GraderView = CMS.Views.ValidatingView.extend({
|
||||
// Model class is CMS.Models.Settings.CourseGrader
|
||||
events : {
|
||||
"blur input" : "updateModel",
|
||||
"blur textarea" : "updateModel",
|
||||
"click .remove-grading-data" : "deleteModel"
|
||||
},
|
||||
initialize : function() {
|
||||
this.model.on('error', this.handleValidationError, this);
|
||||
this.selectorToField = _.invert(this.fieldToSelectorMap);
|
||||
this.render();
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return this;
|
||||
},
|
||||
fieldToSelectorMap : {
|
||||
'type' : 'course-grading-assignment-name',
|
||||
'short_label' : 'course-grading-assignment-shortname',
|
||||
'min_count' : 'course-grading-assignment-totalassignments',
|
||||
'drop_count' : 'course-grading-assignment-droppable',
|
||||
'weight' : 'course-grading-assignment-gradeweight'
|
||||
},
|
||||
updateModel : function(event) {
|
||||
// HACK to fix model sometimes losing its pointer to the collection [I think I fixed this but leaving
|
||||
// this in out of paranoia. If this error ever happens, the user will get a warning that they cannot
|
||||
// give 2 assignments the same name.]
|
||||
if (!this.model.collection) {
|
||||
this.model.collection = this.collection;
|
||||
}
|
||||
|
||||
switch (event.currentTarget.id) {
|
||||
case 'course-grading-assignment-totalassignments':
|
||||
this.$el.find('#course-grading-assignment-droppable').attr('max', $(event.currentTarget).val());
|
||||
this.saveIfChanged(event);
|
||||
break;
|
||||
case 'course-grading-assignment-name':
|
||||
var oldName = this.model.get('type');
|
||||
if (this.saveIfChanged(event) && !_.isEmpty(oldName)) {
|
||||
// overload the error display logic
|
||||
this._cacheValidationErrors.push(event.currentTarget);
|
||||
$(event.currentTarget).parent().append(
|
||||
this.errorTemplate({message : 'For grading to work, you must change all "' + oldName +
|
||||
'" subsections to "' + this.model.get('type') + '".'}));
|
||||
};
|
||||
break;
|
||||
default:
|
||||
this.saveIfChanged(event);
|
||||
break;
|
||||
}
|
||||
},
|
||||
deleteModel : function(e) {
|
||||
this.model.destroy();
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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: #f2f6f9;
|
||||
color: #778192;
|
||||
}
|
||||
&:hover {
|
||||
background-color: #129416;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
@@ -1,21 +1,139 @@
|
||||
|
||||
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,12 +214,199 @@ 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;
|
||||
}
|
||||
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;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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-settings #settings-tab {
|
||||
@include active;
|
||||
}
|
||||
|
||||
&.active-tab-import #import-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;
|
||||
}
|
||||
|
||||
.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 {
|
||||
@include box-shadow(1px 0 0 #787981 inset, -1px 0 0 #3d3e44 inset, 1px 0 0 #787981, -1px 0 0 #3d3e44);
|
||||
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 {
|
||||
background: rgba(255, 255, 255, .1);
|
||||
&:hover {
|
||||
@include linear-gradient(top, rgba(255, 255, 255, .2), rgba(255, 255, 255, 0));
|
||||
}
|
||||
}
|
||||
|
||||
li {
|
||||
float: left;
|
||||
}
|
||||
}
|
||||
|
||||
.class {
|
||||
@include clearfix;
|
||||
height: 100%;
|
||||
font-size: 12px;
|
||||
color: rgb(163, 171, 184);
|
||||
@include linear-gradient(top, rgba(255, 255, 255, 0), rgba(255, 255, 255, .1));
|
||||
background-color: rgb(47, 53, 63);
|
||||
|
||||
a {
|
||||
display: inline-block;
|
||||
height: 20px;
|
||||
padding: 5px 10px 6px;
|
||||
color: rgb(163, 171, 184);
|
||||
}
|
||||
|
||||
.home {
|
||||
position: relative;
|
||||
top: 1px;
|
||||
}
|
||||
|
||||
.log-out {
|
||||
position: relative;
|
||||
top: 3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
800
cms/static/sass/_settings.scss
Normal file
800
cms/static/sass/_settings.scss
Normal file
@@ -0,0 +1,800 @@
|
||||
.settings {
|
||||
.settings-overview {
|
||||
@extend .window;
|
||||
@include clearfix;
|
||||
display: table;
|
||||
width: 100%;
|
||||
|
||||
// layout
|
||||
.sidebar {
|
||||
display: table-cell;
|
||||
float: none;
|
||||
width: 20%;
|
||||
padding: 30px 0 30px 20px;
|
||||
@include border-radius(3px 0 0 3px);
|
||||
background: $lightGrey;
|
||||
}
|
||||
|
||||
.main-column {
|
||||
display: table-cell;
|
||||
float: none;
|
||||
width: 80%;
|
||||
padding: 30px 40px 30px 60px;
|
||||
}
|
||||
|
||||
.settings-page-menu {
|
||||
a {
|
||||
display: block;
|
||||
padding-left: 20px;
|
||||
line-height: 52px;
|
||||
|
||||
&.is-shown {
|
||||
background: #fff;
|
||||
@include border-radius(5px 0 0 5px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.settings-page-section {
|
||||
> .alert {
|
||||
display: none;
|
||||
|
||||
&.is-shown {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
> section {
|
||||
display: none;
|
||||
margin-bottom: 40px;
|
||||
|
||||
&.is-shown {
|
||||
display: block;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
> .title {
|
||||
margin-bottom: 30px;
|
||||
font-size: 28px;
|
||||
font-weight: 300;
|
||||
color: $blue;
|
||||
}
|
||||
|
||||
> section {
|
||||
margin-bottom: 100px;
|
||||
@include clearfix;
|
||||
|
||||
header {
|
||||
@include clearfix;
|
||||
border-bottom: 1px solid $mediumGrey;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 10px;
|
||||
|
||||
h3 {
|
||||
color: $darkGrey;
|
||||
float: left;
|
||||
|
||||
margin: 0 40px 0 0;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.detail {
|
||||
float: right;
|
||||
margin-top: 3px;
|
||||
color: $mediumGrey;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
padding-bottom: 0;
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// form basics
|
||||
label, .label {
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: none;
|
||||
font-size: 15px;
|
||||
font-weight: 400;
|
||||
|
||||
&.check-label {
|
||||
display: inline;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
&.ranges {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
input, textarea {
|
||||
@include transition(all 1s ease-in-out);
|
||||
@include box-sizing(border-box);
|
||||
font-size: 15px;
|
||||
|
||||
&.long {
|
||||
width: 100%;
|
||||
min-width: 400px;
|
||||
}
|
||||
|
||||
&.tall {
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
&.short {
|
||||
min-width: 100px;
|
||||
width: 25%;
|
||||
}
|
||||
|
||||
&.date {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
&.time {
|
||||
width: 85px !important;
|
||||
min-width: 85px !important;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
border: none;
|
||||
@include box-shadow(none);
|
||||
padding: 0;
|
||||
color: $darkGrey !important;
|
||||
font-weight: bold;
|
||||
background: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
textarea.tinymce {
|
||||
border: 1px solid $darkGrey;
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
input[type="checkbox"], input[type="radio"] {
|
||||
|
||||
}
|
||||
|
||||
input:disabled + .copy > label, input:disabled + .label {
|
||||
color: $mediumGrey;
|
||||
}
|
||||
|
||||
|
||||
.input-default input, .input-default textarea {
|
||||
color: $mediumGrey;
|
||||
background: $lightGrey;
|
||||
}
|
||||
|
||||
::-webkit-input-placeholder {
|
||||
color: $mediumGrey;
|
||||
font-size: 13px;
|
||||
}
|
||||
:-moz-placeholder {
|
||||
color: $mediumGrey;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.tip {
|
||||
color: $mediumGrey;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
|
||||
// form layouts
|
||||
.row {
|
||||
margin-bottom: 30px;
|
||||
padding-bottom: 30px;
|
||||
border-bottom: 1px solid $lightGrey;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
// structural labels, not semantic labels per se
|
||||
> label, .label {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
// tips
|
||||
.tip-inline {
|
||||
display: inline-block;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.tip-stacked {
|
||||
display: block;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
// structural field, not semantic fields per se
|
||||
.field {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
|
||||
> input, > textarea, .input {
|
||||
display: inline-block;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.group {
|
||||
input, textarea {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.label, label {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
// multi-field
|
||||
&.multi {
|
||||
display: block;
|
||||
background: tint($lightGrey, 50%);
|
||||
padding: 20px;
|
||||
@include border-radius(4px);
|
||||
@include box-sizing(border-box);
|
||||
|
||||
.group {
|
||||
margin-bottom: 10px;
|
||||
max-width: 175px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
input, .input, textarea {
|
||||
|
||||
}
|
||||
|
||||
.tip-stacked {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// multi stacked
|
||||
&.multi-stacked {
|
||||
|
||||
.group {
|
||||
input, .input, textarea {
|
||||
min-width: 370px;
|
||||
width: 370px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// multi-field inline
|
||||
&.multi-inline {
|
||||
@include clearfix;
|
||||
|
||||
.group {
|
||||
float: left;
|
||||
margin-right: 20px;
|
||||
|
||||
&:nth-child(2) {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.input, input, textarea {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// input-list
|
||||
.input-list {
|
||||
|
||||
.input {
|
||||
margin-bottom: 15px;
|
||||
padding-bottom: 15px;
|
||||
border-bottom: 1px dotted $lightGrey;
|
||||
@include clearfix();
|
||||
|
||||
&:last-child {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.row {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//radio buttons and checkboxes
|
||||
.input-radio {
|
||||
@include clearfix();
|
||||
|
||||
input {
|
||||
display: block;
|
||||
float: left;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.copy {
|
||||
position: relative;
|
||||
top: -5px;
|
||||
float: left;
|
||||
width: 350px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.tip {
|
||||
display: block;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.message-error {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.input-checkbox {
|
||||
|
||||
}
|
||||
|
||||
// enumerated inputs
|
||||
&.enum {
|
||||
}
|
||||
}
|
||||
|
||||
// layout - aligned label/field pairs
|
||||
&.row-col2 {
|
||||
|
||||
> label, .label {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.field {
|
||||
width: 400px ! important;
|
||||
}
|
||||
|
||||
&.multi-inline {
|
||||
@include clearfix();
|
||||
|
||||
.group {
|
||||
width: 170px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.field-additional {
|
||||
margin-left: 204px;
|
||||
}
|
||||
}
|
||||
|
||||
// editing controls - adding
|
||||
.new-item, .replace-item {
|
||||
clear: both;
|
||||
display: block;
|
||||
margin-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
@include grey-button;
|
||||
@include box-sizing(border-box);
|
||||
}
|
||||
|
||||
|
||||
// editing controls - removing
|
||||
.delete-button {
|
||||
float: right;
|
||||
}
|
||||
|
||||
// editing controls - preview
|
||||
.input-existing {
|
||||
display: block !important;
|
||||
|
||||
.current {
|
||||
width: 100%;
|
||||
margin: 10px 0;
|
||||
padding: 10px;
|
||||
@include box-sizing(border-box);
|
||||
@include border-radius(5px);
|
||||
font-size: 14px;
|
||||
background: tint($lightGrey, 50%);
|
||||
@include clearfix();
|
||||
|
||||
.doc-filename {
|
||||
display: inline-block;
|
||||
width: 220px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.remove-doc-data {
|
||||
display: inline-block;
|
||||
margin-top: 0;
|
||||
width: 150px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// specific sections
|
||||
.settings-details {
|
||||
|
||||
}
|
||||
|
||||
.settings-faculty {
|
||||
|
||||
.settings-faculty-members {
|
||||
|
||||
> header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.field .multi {
|
||||
display: block;
|
||||
margin-bottom: 40px;
|
||||
padding: 20px;
|
||||
background: tint($lightGrey, 50%);
|
||||
@include border-radius(4px);
|
||||
@include box-sizing(border-box);
|
||||
}
|
||||
|
||||
.course-faculty-list-item {
|
||||
|
||||
.row {
|
||||
|
||||
&:nth-child(4) {
|
||||
padding-bottom: 0;
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.remove-faculty-photo {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
#course-faculty-bio-input {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.new-course-faculty-item {
|
||||
}
|
||||
|
||||
.current-faculty-photo {
|
||||
padding: 0;
|
||||
|
||||
img {
|
||||
display: block;
|
||||
@include box-shadow(0 1px 3px rgba(0,0,0,0.1));
|
||||
padding: 10px;
|
||||
border: 2px solid $mediumGrey;
|
||||
background: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.settings-grading {
|
||||
|
||||
.setting-grading-assignment-types {
|
||||
|
||||
.row .field.enum {
|
||||
width: 684px;
|
||||
}
|
||||
}
|
||||
|
||||
.course-grading-assignment-list-item {
|
||||
|
||||
}
|
||||
|
||||
.input-list {
|
||||
.row {
|
||||
|
||||
.input {
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.settings-handouts {
|
||||
|
||||
}
|
||||
|
||||
.settings-problems {
|
||||
|
||||
> section {
|
||||
|
||||
&.is-shown {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.settings-discussions {
|
||||
|
||||
.course-discussions-categories-list-item {
|
||||
|
||||
label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.group {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.remove-item {
|
||||
display: inline-block !important;
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// states
|
||||
label.is-focused {
|
||||
color: $blue;
|
||||
@include transition(color 1s ease-in-out);
|
||||
}
|
||||
|
||||
// extras/abbreviations
|
||||
// .settings-extras {
|
||||
|
||||
// > header {
|
||||
// cursor: pointer;
|
||||
|
||||
// &.active {
|
||||
|
||||
// }
|
||||
// }
|
||||
|
||||
// > div {
|
||||
// display: none;
|
||||
// @include transition(display 0.25s ease-in-out);
|
||||
|
||||
// &.is-shown {
|
||||
// display: block;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
input.error, textarea.error {
|
||||
border-color: $red;
|
||||
}
|
||||
|
||||
.message-error {
|
||||
display: block;
|
||||
margin-top: 5px;
|
||||
color: $red;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
// misc
|
||||
.divide {
|
||||
display: none;
|
||||
}
|
||||
|
||||
i.ss-icon {
|
||||
position: relative;
|
||||
top: 1px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.well {
|
||||
padding: 20px;
|
||||
background: $lightGrey;
|
||||
border: 1px solid $mediumGrey;
|
||||
@include border-radius(4px);
|
||||
@include box-shadow(0 1px 1px rgba(0,0,0,0.05) inset)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
h3 {
|
||||
margin-bottom: 30px;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
color: $blue;
|
||||
}
|
||||
|
||||
.grade-controls {
|
||||
@include clearfix;
|
||||
width: 642px;
|
||||
}
|
||||
|
||||
.new-grade-button {
|
||||
position: relative;
|
||||
float: left;
|
||||
display: block;
|
||||
width: 29px;
|
||||
height: 29px;
|
||||
margin: 10px 20px 0 0;
|
||||
border-radius: 20px;
|
||||
border: 1px solid $darkGrey;
|
||||
@include linear-gradient(top, rgba(255, 255, 255, 0.8), rgba(255, 255, 255, 0));
|
||||
background-color: #d1dae3;
|
||||
@include box-shadow(0 1px 0 rgba(255, 255, 255, .3) inset);
|
||||
color: #6d788b;
|
||||
|
||||
.plus-icon {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
margin-left: -6px;
|
||||
margin-top: -6px;
|
||||
}
|
||||
}
|
||||
|
||||
.grade-slider {
|
||||
float: left;
|
||||
width: 580px;
|
||||
margin-bottom: 10px;
|
||||
|
||||
.grade-bar {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
background: $lightGrey;
|
||||
|
||||
.increments {
|
||||
position: relative;
|
||||
|
||||
li {
|
||||
position: absolute;
|
||||
top: 52px;
|
||||
width: 30px;
|
||||
margin-left: -15px;
|
||||
font-size: 9px;
|
||||
text-align: center;
|
||||
|
||||
&.increment-0 {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
&.increment-10 {
|
||||
left: 10%;
|
||||
}
|
||||
|
||||
&.increment-20 {
|
||||
left: 20%;
|
||||
}
|
||||
|
||||
&.increment-30 {
|
||||
left: 30%;
|
||||
}
|
||||
|
||||
&.increment-40 {
|
||||
left: 40%;
|
||||
}
|
||||
|
||||
&.increment-50 {
|
||||
left: 50%;
|
||||
}
|
||||
|
||||
&.increment-60 {
|
||||
left: 60%;
|
||||
}
|
||||
|
||||
&.increment-70 {
|
||||
left: 70%;
|
||||
}
|
||||
|
||||
&.increment-80 {
|
||||
left: 80%;
|
||||
}
|
||||
|
||||
&.increment-90 {
|
||||
left: 90%;
|
||||
}
|
||||
|
||||
&.increment-100 {
|
||||
left: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.grade-specific-bar {
|
||||
height: 50px !important;
|
||||
}
|
||||
|
||||
.grades {
|
||||
position: relative;
|
||||
|
||||
li {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
height: 50px;
|
||||
text-align: right;
|
||||
@include border-radius(2px);
|
||||
|
||||
&:hover,
|
||||
&.is-dragging {
|
||||
.remove-button {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-dragging {
|
||||
|
||||
|
||||
}
|
||||
|
||||
.remove-button {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: -17px;
|
||||
right: 1px;
|
||||
height: 17px;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
&:nth-child(1) {
|
||||
background: #4fe696;
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
background: #ffdf7e;
|
||||
}
|
||||
|
||||
&:nth-child(3) {
|
||||
background: #ffb657;
|
||||
}
|
||||
|
||||
&:nth-child(4) {
|
||||
background: #ef54a1;
|
||||
}
|
||||
|
||||
&:nth-child(5),
|
||||
&.bar-fail {
|
||||
background: #fb336c;
|
||||
}
|
||||
|
||||
.letter-grade {
|
||||
display: block;
|
||||
margin: 10px 15px 0 0;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
line-height: 14px;
|
||||
}
|
||||
|
||||
.range {
|
||||
display: block;
|
||||
margin-right: 15px;
|
||||
font-size: 10px;
|
||||
line-height: 12px;
|
||||
}
|
||||
|
||||
.drag-bar {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: -1px;
|
||||
height: 50px;
|
||||
width: 2px;
|
||||
background-color: #fff;
|
||||
@include box-shadow(-1px 0 3px rgba(0,0,0,0.1));
|
||||
|
||||
cursor: ew-resize;
|
||||
@include transition(none);
|
||||
|
||||
&:hover {
|
||||
width: 6px;
|
||||
right: -2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
height: 60px;
|
||||
position: relative;
|
||||
border: 1px solid $darkGreen;
|
||||
background: $green;
|
||||
color: #fff;
|
||||
@include transition(background-color .15s);
|
||||
|
||||
&:hover {
|
||||
background: $brightGreen;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.new-component,
|
||||
.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 {
|
||||
display: block;
|
||||
padding: 7px 20px;
|
||||
border-bottom: none;
|
||||
font-weight: 300;
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,31 +1,8 @@
|
||||
.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;
|
||||
$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}
|
||||
@@ -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"/>: <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>
|
||||
|
||||
<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="page-actions">
|
||||
%if allow_actions:
|
||||
<div class="new-user-form">
|
||||
<a href="#" class="new-button new-user-button">
|
||||
<span class="plus-icon white"></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:
|
||||
<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)}
|
||||
|
||||
729
cms/templates/settings.html
Normal file
729
cms/templates/settings.html
Normal file
@@ -0,0 +1,729 @@
|
||||
<%inherit file="base.html" />
|
||||
<%block name="bodyclass">settings</%block>
|
||||
<%block name="title">Settings</%block>
|
||||
|
||||
<%namespace name='static' file='static_content.html'/>
|
||||
<%!
|
||||
from contentstore import utils
|
||||
%>
|
||||
|
||||
|
||||
<%block name="jsextra">
|
||||
<link rel="stylesheet" type="text/css" href="${static.url('js/vendor/timepicker/jquery.timepicker.css')}" />
|
||||
<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/template_loader.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/models/course_relative.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/models/settings/course_details.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/models/settings/course_settings.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/models/settings/course_grading_policy.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/views/settings/main_settings_view.js')}"></script>
|
||||
|
||||
<script type="text/javascript">
|
||||
$(document).ready(function(){
|
||||
|
||||
var settingsModel = new CMS.Models.Settings.CourseSettings({
|
||||
courseLocation: new CMS.Models.Location('${context_course.location}',{parse:true}),
|
||||
details: new CMS.Models.Settings.CourseDetails(${course_details|n},{parse:true})
|
||||
});
|
||||
|
||||
var editor = new CMS.Views.Settings.Main({
|
||||
el: $('.main-wrapper'),
|
||||
model : settingsModel
|
||||
});
|
||||
|
||||
editor.render();
|
||||
});
|
||||
|
||||
</script>
|
||||
</%block>
|
||||
|
||||
<%block name="content">
|
||||
<!-- -->
|
||||
<div class="main-wrapper">
|
||||
<div class="inner-wrapper">
|
||||
<h1>Settings</h1>
|
||||
<article class="settings-overview">
|
||||
<div class="sidebar">
|
||||
<nav class="settings-page-menu">
|
||||
<ul>
|
||||
<li><a href="#" class="is-shown" data-section="details">Course Details</a></li>
|
||||
<!-- <li><a href="#" data-section="faculty">Faculty</a></li> -->
|
||||
<li><a href="#" data-section="grading">Grading</a></li>
|
||||
<!-- <li><a href="#" data-section="problems">Problems</a></li> -->
|
||||
<!-- <li><a href="#" data-section="discussions">Discussions</a></li> -->
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
<div class="settings-page-section main-column">
|
||||
|
||||
<section class="settings-details is-shown">
|
||||
<h2 class="title">Course Details</h2>
|
||||
|
||||
<section class="settings-details-basic">
|
||||
<header>
|
||||
<h3>Basic Information</h3>
|
||||
<span class="detail">The nuts and bolts of your course</span>
|
||||
</header>
|
||||
|
||||
<div class="row row-col2">
|
||||
<label for="course-name">Course Name:</label>
|
||||
<div class="field">
|
||||
<div class="input">
|
||||
<input type="text" class="long" id="course-name" value="[Course Name]" disabled="disabled">
|
||||
<span class="tip tip-stacked">This is used in <a href="${utils.get_lms_link_for_about_page(context_course.location)}">your course URL</a>, and cannot be changed</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row row-col2">
|
||||
<label for="course-organization">Organization:</label>
|
||||
<div class="field">
|
||||
<div class="input">
|
||||
<input type="text" class="long" id="course-organization" value="[Course Organization]" disabled="disabled">
|
||||
<span class="tip tip-stacked">This is used in <a href="${utils.get_lms_link_for_about_page(context_course.location)}">your course URL</a>, and cannot be changed</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row row-col2">
|
||||
<label for="course-number">Course Number:</label>
|
||||
<div class="field">
|
||||
<div class="input">
|
||||
<input type="text" class="short" id="course-number" value="[Course No.]" disabled="disabled">
|
||||
<span class="tip tip-stacked">This is used in <a href="${utils.get_lms_link_for_about_page(context_course.location)}">your course URL</a>, and cannot be changed</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section><!-- .settings-details-basic -->
|
||||
|
||||
<hr class="divide" />
|
||||
|
||||
<section class="settings-details-schedule">
|
||||
<header>
|
||||
<h3>Course Schedule</h3>
|
||||
<span class="detail">Important steps and segments of your course</span>
|
||||
</header>
|
||||
|
||||
<div class="row row-col2">
|
||||
<h4 class="label">Course Dates:</h4>
|
||||
|
||||
<div class="field">
|
||||
<div class="input multi multi-inline" id="course-start">
|
||||
<div class="group">
|
||||
<label for="course-start-date">Start Date</label>
|
||||
<input type="text" class="start-date date start datepicker" id="course-start-date" placeholder="MM/DD/YYYY" autocomplete="off">
|
||||
<span class="tip tip-stacked">First day the course begins</span>
|
||||
</div>
|
||||
|
||||
<div class="group">
|
||||
<label for="course-start-time">Start Time</label>
|
||||
<input type="text" class="time start timepicker" id="course-start-time" value="" placeholder="HH:MM" autocomplete="off">
|
||||
<span class="tip tip-stacked" id="timezone"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field field-additional">
|
||||
<div class="input multi multi-inline" id="course-end">
|
||||
<div class="group">
|
||||
<label for="course-end-date">End Date</label>
|
||||
<input type="text" class="end-date date end" id="course-end-date" placeholder="MM/DD/YYYY" autocomplete="off">
|
||||
<span class="tip tip-stacked">Last day the course is active</span>
|
||||
</div>
|
||||
|
||||
<div class="group">
|
||||
<label for="course-end-time">End Time</label>
|
||||
<input type="text" class="time end" id="course-end-time" value="" placeholder="HH:MM" autocomplete="off">
|
||||
<span class="tip tip-stacked" id="timezone"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row row-col2">
|
||||
<h4 class="label">Enrollment Dates:</h4>
|
||||
|
||||
<div class="field">
|
||||
<div class="input multi multi-inline" id="enrollment-start">
|
||||
<div class="group">
|
||||
<label for="course-enrollment-start-date">Start Date</label>
|
||||
<input type="text" class="start-date date start" id="course-enrollment-start-date" placeholder="MM/DD/YYYY" autocomplete="off">
|
||||
<span class="tip tip-stacked">First day students can enroll</span>
|
||||
</div>
|
||||
|
||||
<div class="group">
|
||||
<label for="course-enrollment-start-time">Start Time</label>
|
||||
<input type="text" class="time start" id="course-enrollment-start-time" value="" placeholder="HH:MM" autocomplete="off">
|
||||
<span class="tip tip-stacked" id="timezone"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field field-additional">
|
||||
<div class="input multi multi-inline" id="enrollment-end">
|
||||
<div class="group">
|
||||
<label for="course-enrollment-end-date">End Date</label>
|
||||
<input type="text" class="end-date date end" id="course-enrollment-end-date" placeholder="MM/DD/YYYY" autocomplete="off">
|
||||
<span class="tip tip-stacked">Last day students can enroll</span>
|
||||
</div>
|
||||
|
||||
<div class="group">
|
||||
<label for="course-enrollment-end-time">End Time</label>
|
||||
<input type="text" class="time end" id="course-enrollment-end-time" value="" placeholder="HH:MM" autocomplete="off">
|
||||
<span class="tip tip-stacked" id="timezone"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- <div class="row row-col2">
|
||||
<label for="course-syllabus">Course Syllabus</label>
|
||||
<div class="field">
|
||||
<div class="input input-existing">
|
||||
<div class="current current-course-syllabus">
|
||||
<span class="doc-filename"></span>
|
||||
|
||||
<a href="#" class="remove-item remove-course-syllabus remove-doc-data" id="course-syllabus"><span class="delete-icon"></span> Delete Syllabus</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input">
|
||||
<a href="#" class="new-item new-course-syllabus add-syllabus-data" id="course-syllabus">
|
||||
<span class="upload-icon"></span>Upload Syllabus
|
||||
</a>
|
||||
<span class="tip tip-inline">PDF formatting preferred</span>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
</section><!-- .settings-details-schedule -->
|
||||
|
||||
<hr class="divide" />
|
||||
|
||||
<section class="setting-details-marketing">
|
||||
<header>
|
||||
<h3>Introducing Your Course</h3>
|
||||
<span class="detail">Information for perspective students</span>
|
||||
</header>
|
||||
|
||||
<div class="row row-col2">
|
||||
<label for="course-overview">Course Overview:</label>
|
||||
<div class="field">
|
||||
<div class="input">
|
||||
<textarea class="long tall tinymce text-editor" id="course-overview"></textarea>
|
||||
<span class="tip tip-stacked">Introductions, prerequisites, FAQs that are used on <a href="${utils.get_lms_link_for_about_page(context_course.location)}">your course summary page</a></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row row-col2">
|
||||
<label for="course-introduction-video">Introduction Video:</label>
|
||||
<div class="field">
|
||||
<div class="input input-existing">
|
||||
<div class="current current-course-introduction-video">
|
||||
<iframe width="380" height="215" src="" frameborder="0" allowfullscreen></iframe>
|
||||
|
||||
<a href="#" class="remove-item remove-course-introduction-video remove-video-data"><span class="delete-icon"></span> Delete Video</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input">
|
||||
<input type="text" class="long new-course-introduction-video add-video-data" id="course-introduction-video" value="" placeholder="id" autocomplete="off">
|
||||
<span class="tip tip-stacked">Video restrictions go here</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section><!-- .settings-details-marketing -->
|
||||
|
||||
<hr class="divide" />
|
||||
|
||||
<section class="settings-details-requirements">
|
||||
<header>
|
||||
<h3>Requirements</h3>
|
||||
<span class="detail">Expectations of the students taking this course</span>
|
||||
</header>
|
||||
|
||||
<div class="row row-col2">
|
||||
<label for="course-effort">Hours of Effort per Week:</label>
|
||||
<div class="field">
|
||||
<div class="input">
|
||||
<input type="text" class="short time" id="course-effort" placeholder="HH:MM">
|
||||
<span class="tip tip-inline">Time spent on all course work</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</section><!-- .settings-details -->
|
||||
|
||||
<section class="settings-faculty">
|
||||
<h2 class="title">Faculty</h2>
|
||||
|
||||
<section class="settings-faculty-members">
|
||||
<header>
|
||||
<h3>Faculty Members</h3>
|
||||
<span class="detail">Individuals instructing and help with this course</span>
|
||||
</header>
|
||||
|
||||
<div class="row">
|
||||
<div class="field enum">
|
||||
<ul class="input-list course-faculty-list">
|
||||
<li class="input input-existing multi course-faculty-list-item">
|
||||
<div class="row row-col2">
|
||||
<label for="course-faculty-1-firstname">Faculty First Name:</label>
|
||||
<div class="field">
|
||||
<input type="text" class="long" id="course-faculty-1-firstname">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row row-col2">
|
||||
<label for="course-faculty-1-lastname">Faculty Last Name:</label>
|
||||
<div class="field">
|
||||
<input type="text" class="long" id="course-faculty-1-lastname">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row row-col2">
|
||||
<label for="course-faculty-1-photo">Faculty Photo</label>
|
||||
<div class="field">
|
||||
<div class="input input-existing">
|
||||
<div class="current current-faculty-1-photo">
|
||||
<a href="#" class="remove-item remove-faculty-photo remove-video-data"><span class="delete-icon"></span> Delete Faculty Photo</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<label for="course-faculty-1-bio">Faculty Bio:</label>
|
||||
<div class="field">
|
||||
<textarea class="long tall edit-box tinymce" id="course-faculty-1-bio"></textarea>
|
||||
<span class="tip tip-stacked">A brief description of your education, experience, and expertise</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a href="#" class="remove-item remove-faculty-data"><span class="delete-icon"></span> Delete Faculty Member</a>
|
||||
</li>
|
||||
|
||||
<li class="input multi course-faculty-list-item">
|
||||
<div class="row row-col2">
|
||||
<label for="course-faculty-2-firstname">Faculty First Name:</label>
|
||||
<div class="field">
|
||||
<input type="text" class="long" id="course-faculty-2-firstname">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row row-col2">
|
||||
<label for="course-faculty-2-lastname">Faculty Last Name:</label>
|
||||
<div class="field">
|
||||
<input type="text" class="long" id="course-faculty-2-lastname">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row row-col2">
|
||||
<label for="course-faculty-2-photo">Faculty Photo</label>
|
||||
<div class="field">
|
||||
<div class="input">
|
||||
<a href="#" class="new-item new-faculty-photo add-faculty-photo-data" id="course-faculty-2-photo">
|
||||
<span class="upload-icon"></span>Upload Faculty Photo
|
||||
</a>
|
||||
<span class="tip tip-inline">Max size: 30KB</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<label for="course-faculty-2-bio">Faculty Bio:</label>
|
||||
<div class="field">
|
||||
<div clas="input">
|
||||
<textarea class="long tall edit-box tinymce" id="course-faculty-2-bio"></textarea>
|
||||
<span class="tip tip-stacked">A brief description of your education, experience, and expertise</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<a href="#" class="new-item new-course-faculty-item add-faculty-data">
|
||||
<span class="plus-icon"></span>New Faculty Member
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</section><!-- .settings-staff -->
|
||||
|
||||
<section class="settings-grading">
|
||||
<h2 class="title">Grading</h2>
|
||||
|
||||
<section class="settings-grading-range">
|
||||
<header>
|
||||
<h3>Overall Grade Range</h3>
|
||||
<span class="detail">Course grade ranges and their values</span>
|
||||
</header>
|
||||
|
||||
<div class="row">
|
||||
|
||||
<div class="grade-controls course-grading-range well">
|
||||
<a href="#" class="new-grade-button"><span class="plus-icon"></span></a>
|
||||
<div class="grade-slider">
|
||||
<div class="grade-bar">
|
||||
<ol class="increments">
|
||||
<li class="increment-0">0</li>
|
||||
<li class="increment-10">10</li>
|
||||
<li class="increment-20">20</li>
|
||||
<li class="increment-30">30</li>
|
||||
<li class="increment-40">40</li>
|
||||
<li class="increment-50">50</li>
|
||||
<li class="increment-60">60</li>
|
||||
<li class="increment-70">70</li>
|
||||
<li class="increment-80">80</li>
|
||||
<li class="increment-90">90</li>
|
||||
<li class="increment-100">100</li>
|
||||
</ol>
|
||||
<ol class="grades">
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="settings-grading-general">
|
||||
<header>
|
||||
<h3>General Grading</h3>
|
||||
<span class="detail">Deadlines and Requirements</span>
|
||||
</header>
|
||||
|
||||
<div class="row row-col2">
|
||||
<label for="course-grading-graceperiod">Grace Period on Deadline:</label>
|
||||
|
||||
<div class="field">
|
||||
<div class="input">
|
||||
<input type="text" class="short time" id="course-grading-graceperiod" value="0:00" placeholder="e.g. 10 minutes">
|
||||
<span class="tip tip-inline">leeway on due dates</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="setting-grading-assignment-types">
|
||||
<header>
|
||||
<h3>Assignment Types</h3>
|
||||
</header>
|
||||
|
||||
<div class="row">
|
||||
<div class="field enum">
|
||||
<ul class="input-list course-grading-assignment-list">
|
||||
</ul>
|
||||
|
||||
<a href="#" class="new-button new-course-grading-item add-grading-data">
|
||||
<span class="plus-icon white"></span>New Assignment Type
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</section><!-- .settings-grading -->
|
||||
|
||||
<section class="settings-problems">
|
||||
<h2 class="title">Problems</h2>
|
||||
|
||||
<section class="settings-problems-general">
|
||||
<header>
|
||||
<h3>General Settings</h3>
|
||||
<span class="detail">Course-wide settings for all problems</span>
|
||||
</header>
|
||||
|
||||
<div class="row row-col2">
|
||||
<h4 class="label">Problem Randomization:</h4>
|
||||
|
||||
<div class="field">
|
||||
<div class="input input-radio">
|
||||
<input checked="checked" type="radio" name="course-problems-general-randomization" id="course-problems-general-randomization-always" value="Always">
|
||||
|
||||
<div class="copy">
|
||||
<label for="course-problems-general-randomization-always">Always</label>
|
||||
<span class="tip tip-stacked"><strong>randomize all</strong> problems</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input input-radio">
|
||||
<input type="radio" name="course-problems-general-randomization" id="course-problems-general-randomization-never" value="Never">
|
||||
|
||||
<div class="copy">
|
||||
<label for="course-problems-general-randomization-never">Never</label>
|
||||
<span class="tip tip-stacked"><strong>do not randomize</strong> problems</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input input-radio">
|
||||
<input type="radio" name="course-problems-general-randomization" id="course-problems-general-randomization-perstudent" value="Per Student">
|
||||
|
||||
<div class="copy">
|
||||
<label for="course-problems-general-randomization-perstudent">Per Student</label>
|
||||
<span class="tip tip-stacked">randomize problems <strong>per student</strong></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row row-col2">
|
||||
<h4 class="label">Show Answers:</h4>
|
||||
|
||||
<div class="field">
|
||||
<div class="input input-radio">
|
||||
<input checked="checked" type="radio" name="course-problems-general-showanswer" id="course-problems-general-showanswer-always" value="Always">
|
||||
|
||||
<div class="copy">
|
||||
<label for="course-problems-general-showanswer-always">Always</label>
|
||||
<span class="tip tip-stacked">Answers will be shown after the number of attempts has been met</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input input-radio">
|
||||
<input type="radio" name="course-problems-general-showanswer" id="course-problems-general-showanswer-never" value="Never">
|
||||
|
||||
<div class="copy">
|
||||
<label for="course-problems-general-showanswer-never">Never</label>
|
||||
<span class="tip tip-stacked">Answers will never be shown, regardless of attempts</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row row-col2">
|
||||
<label for="pcourse-roblems-general-attempts">Number of Attempts <br /> Allowed on Problems: </label>
|
||||
|
||||
<div class="field">
|
||||
<div class="input">
|
||||
<input type="text" class="short" id="course-problems-general-attempts" placeholder="0 or higher" value="0">
|
||||
<span class="tip tip-stacked">Students will this have this number of chances to answer a problem. To set infinite atttempts, use "0"</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section><!-- .settings-problems-general -->
|
||||
|
||||
<section class="settings-problems-assignment-1 settings-extras">
|
||||
<header>
|
||||
<h3>[Assignment Type Name]</h3>
|
||||
</header>
|
||||
|
||||
<div class="row row-col2">
|
||||
<h4 class="label">Problem Randomization:</h4>
|
||||
|
||||
<div class="field">
|
||||
<div class="input input-radio">
|
||||
<input checked="checked" type="radio" name="course-problems-assignment-1-randomization" id="course-problems-assignment-1-randomization-always" value="Always">
|
||||
|
||||
<div class="copy">
|
||||
<label for="course-problems-assignment-1-randomization-always">Always</label>
|
||||
<span class="tip tip-stacked"><strong>randomize all</strong> problems</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input input-radio">
|
||||
<input type="radio" name="course-problems-assignment-1-randomization" id="course-problems-assignment-1-randomization-never" value="Never">
|
||||
|
||||
<div class="copy">
|
||||
<label for="course-problems-assignment-1-randomization-never">Never</label>
|
||||
<span class="tip tip-stacked"><strong>do not randomize</strong> problems</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input input-radio">
|
||||
<input type="radio" name="course-problems-assignment-1-randomization" id="course-problems-assignment-1-randomization-perstudent" value="Per Student">
|
||||
|
||||
<div class="copy">
|
||||
<label for="course-problems-assignment-1-randomization-perstudent">Per Student</label>
|
||||
<span class="tip tip-stacked">randomize problems <strong>per student</strong></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row row-col2">
|
||||
<h4 class="label">Show Answers:</h4>
|
||||
|
||||
<div class="field">
|
||||
<div class="input input-radio">
|
||||
<input checked="checked" type="radio" name="course-problems-assignment-1-showanswer" id="course-problems-assignment-1-showanswer-always" value="Always">
|
||||
|
||||
<div class="copy">
|
||||
<label for="course-problems-assignment-1-showanswer-always">Always</label>
|
||||
<span class="tip tip-stacked">Answers will be shown after the number of attempts has been met</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input input-radio">
|
||||
<input type="radio" name="course-problems-assignment-1-showanswer" id="course-problems-assignment-1-showanswer-never" value="Never">
|
||||
|
||||
<div class="copy">
|
||||
<label for="pcourse-roblems-assignment-1-showanswer-never">Never</label>
|
||||
<span class="tip tip-stacked">Answers will never be shown, regardless of attempts</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row row-col2">
|
||||
<label for="course-problems-assignment-1-attempts">Number of Attempts <br /> Allowed on Problems: </label>
|
||||
|
||||
<div class="field">
|
||||
<div class="input">
|
||||
<input type="text" class="short" id="course-problems-assignment-1-attempts" placeholder="0 or higher" value="0">
|
||||
<span class="tip tip-stacked">Students will this have this number of chances to answer a problem. To set infinite atttempts, use "0"</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section><!-- .settings-problems-assignment-1 -->
|
||||
</section><!-- .settings-problems -->
|
||||
|
||||
<section class="settings-discussions">
|
||||
<h2 class="title">Discussions</h2>
|
||||
|
||||
<section class="settings-discussions-general">
|
||||
<header>
|
||||
<h3>General Settings</h3>
|
||||
<span class="detail">Course-wide settings for online discussion</span>
|
||||
</header>
|
||||
|
||||
<div class="row row-col2">
|
||||
<h4 class="label">Anonymous Discussions:</h4>
|
||||
|
||||
<div class="field">
|
||||
<div class="input input-radio">
|
||||
<input type="radio" name="course-discussions-anonymous" id="course-discussions-anonymous-allow" value="Allow">
|
||||
|
||||
<div class="copy">
|
||||
<label for="course-discussions-anonymous-allow">Allow</label>
|
||||
<span class="tip tip-stacked">Students and faculty <strong>will be able to post anonymously</strong></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input input-radio">
|
||||
<input checked="checked" type="radio" name="course-discussions-anonymous" id="course-discussions-anonymous-dontallow" value="Do Not Allow">
|
||||
|
||||
<div class="copy">
|
||||
<label for="course-discussions-anonymous-dontallow">Do not allow</label>
|
||||
<span class="tip tip-stacked"><strong>Posting anonymously is not allowed</strong>. Any previous anonymous posts <strong>will be reverted to non-anonymous</strong></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row row-col2">
|
||||
<h4 class="label">Anonymous Discussions:</h4>
|
||||
|
||||
<div class="field">
|
||||
<div class="input input-radio">
|
||||
<input checked="checked" type="radio" name="course-discussions-anonymous" id="course-discussions-anonymous-allow" value="Allow">
|
||||
|
||||
<div class="copy">
|
||||
<label for="course-discussions-anonymous-allow">Allow</label>
|
||||
<span class="tip tip-stacked">Students and faculty <strong>will be able to post anonymously</strong></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input input-radio">
|
||||
<input disabled="disabled" type="radio" name="course-discussions-anonymous" id="course-discussions-anonymous-dontallow" value="Do Not Allow">
|
||||
|
||||
<div class="copy">
|
||||
<label for="course-discussions-anonymous-dontallow">Do not allow</label>
|
||||
<span class="tip tip-stacked">This option is disabled since there are previous discussions that are anonymous.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row row-col2">
|
||||
<h4 class="label">Discussion Categories</h4>
|
||||
|
||||
<div class="field enum">
|
||||
<ul class="input-list course-discussions-categories-list sortable">
|
||||
<li class="input input-existing input-default course-discussions-categories-list-item sortable-item">
|
||||
<div class="group">
|
||||
<label for="course-discussions-categories-1-name">Category Name: </label>
|
||||
<input type="text" class="course-discussions-categories-name" id="course-discussions-categories-1-name" placeholder="" value="General" disabled="disabled">
|
||||
</div>
|
||||
|
||||
<a href="#" class="drag-handle"></a>
|
||||
</li>
|
||||
|
||||
<li class="input input-existing input-default course-discussions-categories-list-item sortable-item">
|
||||
<div class="group">
|
||||
<label for="course-discussions-categories-2-name">Category Name: </label>
|
||||
<input type="text" class="course-discussions-categories-name" id="course-discussions-categories-2-name" placeholder="" value="Feedback" disabled="disabled">
|
||||
</div>
|
||||
|
||||
<a href="#" class="drag-handle"></a>
|
||||
</li>
|
||||
|
||||
<li class="input input-existing input-default course-discussions-categories-list-item sortable-item">
|
||||
<div class="group">
|
||||
<label for="course-discussions-categories-3-name">Category Name: </label>
|
||||
<input type="text" class="course-discussions-categories-name" id="course-discussions-categories-3-name" placeholder="" value="Troubleshooting" disabled="disabled">
|
||||
</div>
|
||||
|
||||
<a href="#" class="drag-handle"></a>
|
||||
</li>
|
||||
|
||||
<li class="input input-existing course-discussions-categories-list-item sortable-item">
|
||||
<div class="group">
|
||||
<label for="course-discussions-categories-4-name">Category Name: </label>
|
||||
<input type="text" class="course-discussions-categories-name" id="course-discussions-categories-4-name" placeholder="" value="Study Groups">
|
||||
|
||||
<a href="#" class="remove-item remove-course-discussions-categories-data"><span class="delete-icon"></span> Delete Category</a>
|
||||
</div>
|
||||
|
||||
<a href="#" class="drag-handle"></a>
|
||||
</li>
|
||||
|
||||
<li class="input input-existing course-discussions-categories-list-item sortable-item">
|
||||
<div class="group">
|
||||
<label for="course-discussions-categories-5-name">Category Name: </label>
|
||||
<input type="text" class="course-discussions-categories-name" id="course-discussions-categories-5-name" placeholder="" value="Lectures">
|
||||
</div>
|
||||
|
||||
<a href="#" class="remove-item remove-course-discussions-categories-data"><span class="delete-icon"></span> Delete Category</a>
|
||||
|
||||
<a href="#" class="drag-handle"></a>
|
||||
</li>
|
||||
|
||||
<li class="input input-existing course-discussions-categories-list-item sortable-item">
|
||||
<div class="group">
|
||||
<label for="course-discussions-categories-6-name">Category Name: </label>
|
||||
<input type="text" class="course-discussions-categories-name" id="course-discussions-categories-6-name" placeholder="" value="Labs">
|
||||
</div>
|
||||
|
||||
<a href="#" class="remove-item remove-course-discussions-categories-data"><span class="delete-icon"></span> Delete Category</a>
|
||||
|
||||
<a href="#" class="drag-handle"></a>
|
||||
</li>
|
||||
|
||||
<li class="input input-existing course-discussions-categories-list-item sortable-item">
|
||||
<div class="group">
|
||||
<label for="course-discussions-categories-6-name">Category Name: </label>
|
||||
<input type="text" class="course-discussions-categories-name" id="course-discussions-categories-6-name" placeholder="" value="">
|
||||
</div>
|
||||
|
||||
<a href="#" class="remove-item remove-course-discussions-categories-data"><span class="delete-icon"></span> Delete Category</a>
|
||||
|
||||
<a href="#" class="drag-handle"></a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<a href="#" class="new-item new-course-discussions-categories-item add-categories-data">
|
||||
<span class="plus-icon"></span>New Discussion Category
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section><!-- .settings-discussions-general -->
|
||||
</section><!-- .settings-discussions -->
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
<footer></footer>
|
||||
</%block>
|
||||
@@ -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>
|
||||
|
||||
@@ -1,30 +1,39 @@
|
||||
<%! 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>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<%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>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<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>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<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'),
|
||||
|
||||
0
common/djangoapps/__init__.py
Normal file
0
common/djangoapps/__init__.py
Normal file
0
common/djangoapps/models/__init__.py
Normal file
0
common/djangoapps/models/__init__.py
Normal file
25
common/djangoapps/models/course_relative.py
Normal file
25
common/djangoapps/models/course_relative.py
Normal file
@@ -0,0 +1,25 @@
|
||||
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 = ""
|
||||
|
||||
24
common/djangoapps/util/converters.py
Normal file
24
common/djangoapps/util/converters.py
Normal file
@@ -0,0 +1,24 @@
|
||||
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
|
||||
@@ -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()
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
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):
|
||||
@@ -251,13 +245,53 @@ class CourseDescriptor(SequenceDescriptor):
|
||||
def has_started(self):
|
||||
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):
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
<section class="html-edit">
|
||||
<div name="" class="edit-box" rows="8" cols="40"><problem>
|
||||
<p></p>
|
||||
<multiplechoiceresponse>
|
||||
<pre><problem>
|
||||
<p></p></pre>
|
||||
<div><foo>bar</foo></div></div>
|
||||
</section>
|
||||
2
common/lib/xmodule/xmodule/js/spec/.gitignore
vendored
Normal file
2
common/lib/xmodule/xmodule/js/spec/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
*.js
|
||||
|
||||
17
common/lib/xmodule/xmodule/js/spec/html/edit_spec.coffee
Normal file
17
common/lib/xmodule/xmodule/js/spec/html/edit_spec.coffee
Normal file
@@ -0,0 +1,17 @@
|
||||
describe 'HTMLEditingDescriptor', ->
|
||||
describe 'Read data from server, create Editor, and get data back out', ->
|
||||
it 'Does not munge <', ->
|
||||
# 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("""<problem>
|
||||
<p></p>
|
||||
<multiplechoiceresponse>
|
||||
<pre><problem>
|
||||
<p></p></pre>
|
||||
<div><foo>bar</foo></div>""")
|
||||
2
common/lib/xmodule/xmodule/js/src/.gitignore
vendored
Normal file
2
common/lib/xmodule/xmodule/js/src/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
*.js
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
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)
|
||||
|
||||
130
common/lib/xmodule/xmodule/modulestore/store_utilities.py
Normal file
130
common/lib/xmodule/xmodule/modulestore/store_utilities.py
Normal file
@@ -0,0 +1,130 @@
|
||||
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
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
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
|
||||
|
||||
33
common/static/js/vendor/underscore-min.js
vendored
33
common/static/js/vendor/underscore-min.js
vendored
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
roots/2012_Fall.xml
|
||||
1
common/test/data/graded/course.xml
Normal file
1
common/test/data/graded/course.xml
Normal file
@@ -0,0 +1 @@
|
||||
<course org="edX" course="graded" url_name="2012_Fall"/>
|
||||
@@ -1 +0,0 @@
|
||||
roots/2012_Fall.xml
|
||||
1
common/test/data/self_assessment/course.xml
Normal file
1
common/test/data/self_assessment/course.xml
Normal file
@@ -0,0 +1 @@
|
||||
<course org="edX" course="sa_test" url_name="2012_Fall"/>
|
||||
@@ -1 +0,0 @@
|
||||
roots/2012_Fall.xml
|
||||
1
common/test/data/test_start_date/course.xml
Normal file
1
common/test/data/test_start_date/course.xml
Normal file
@@ -0,0 +1 @@
|
||||
<course org="edX" course="test_start_date" url_name="2012_Fall"/>
|
||||
@@ -1 +0,0 @@
|
||||
roots/2012_Fall.xml
|
||||
1
common/test/data/toy/course.xml
Normal file
1
common/test/data/toy/course.xml
Normal file
@@ -0,0 +1 @@
|
||||
<course org="edX" course="toy" url_name="2012_Fall"/>
|
||||
@@ -1,5 +1,5 @@
|
||||
# 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' ]
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user