Merge branch 'master' into ux/marco/textbook
This commit is contained in:
@@ -41,7 +41,8 @@ disable=
|
||||
# R0902: Too many instance attributes
|
||||
# R0903: Too few public methods (1/2)
|
||||
# R0904: Too many public methods
|
||||
W0141,W0142,R0201,R0901,R0902,R0903,R0904
|
||||
# R0913: Too many arguments
|
||||
W0141,W0142,R0201,R0901,R0902,R0903,R0904,R0913
|
||||
|
||||
|
||||
[REPORTS]
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from lxml import html, etree
|
||||
from lxml import html
|
||||
import re
|
||||
from django.http import HttpResponseBadRequest
|
||||
import logging
|
||||
import django.utils
|
||||
|
||||
## TODO store as array of { date, content } and override course_info_module.definition_from_xml
|
||||
## This should be in a class which inherits from XmlDescriptor
|
||||
# # TODO store as array of { date, content } and override course_info_module.definition_from_xml
|
||||
# # This should be in a class which inherits from XmlDescriptor
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_course_updates(location):
|
||||
@@ -26,9 +28,11 @@ def get_course_updates(location):
|
||||
|
||||
# purely to handle free formed updates not done via editor. Actually kills them, but at least doesn't break.
|
||||
try:
|
||||
course_html_parsed = etree.fromstring(course_updates.data)
|
||||
except etree.XMLSyntaxError:
|
||||
course_html_parsed = etree.fromstring("<ol></ol>")
|
||||
course_html_parsed = html.fromstring(course_updates.data)
|
||||
except:
|
||||
log.error("Cannot parse: " + course_updates.data)
|
||||
escaped = django.utils.html.escape(course_updates.data)
|
||||
course_html_parsed = html.fromstring("<ol><li>" + escaped + "</li></ol>")
|
||||
|
||||
# Confirm that root is <ol>, iterate over <li>, pull out <h2> subs and then rest of val
|
||||
course_upd_collection = []
|
||||
@@ -64,9 +68,11 @@ def update_course_updates(location, update, passed_id=None):
|
||||
|
||||
# purely to handle free formed updates not done via editor. Actually kills them, but at least doesn't break.
|
||||
try:
|
||||
course_html_parsed = etree.fromstring(course_updates.data)
|
||||
except etree.XMLSyntaxError:
|
||||
course_html_parsed = etree.fromstring("<ol></ol>")
|
||||
course_html_parsed = html.fromstring(course_updates.data)
|
||||
except:
|
||||
log.error("Cannot parse: " + course_updates.data)
|
||||
escaped = django.utils.html.escape(course_updates.data)
|
||||
course_html_parsed = html.fromstring("<ol><li>" + escaped + "</li></ol>")
|
||||
|
||||
# No try/catch b/c failure generates an error back to client
|
||||
new_html_parsed = html.fromstring('<li><h2>' + update['date'] + '</h2>' + update['content'] + '</li>')
|
||||
@@ -85,12 +91,19 @@ def update_course_updates(location, update, passed_id=None):
|
||||
passed_id = course_updates.location.url() + "/" + str(idx)
|
||||
|
||||
# update db record
|
||||
course_updates.data = etree.tostring(course_html_parsed)
|
||||
course_updates.data = html.tostring(course_html_parsed)
|
||||
modulestore('direct').update_item(location, course_updates.data)
|
||||
|
||||
return {"id" : passed_id,
|
||||
"date" : update['date'],
|
||||
"content" :update['content']}
|
||||
if (len(new_html_parsed) == 1):
|
||||
content = new_html_parsed[0].tail
|
||||
else:
|
||||
content = "\n".join([html.tostring(ele)
|
||||
for ele in new_html_parsed[1:]])
|
||||
|
||||
return {"id": passed_id,
|
||||
"date": update['date'],
|
||||
"content": content}
|
||||
|
||||
|
||||
def delete_course_update(location, update, passed_id):
|
||||
"""
|
||||
@@ -108,9 +121,11 @@ def delete_course_update(location, update, passed_id):
|
||||
# TODO use delete_blank_text parser throughout and cache as a static var in a class
|
||||
# purely to handle free formed updates not done via editor. Actually kills them, but at least doesn't break.
|
||||
try:
|
||||
course_html_parsed = etree.fromstring(course_updates.data)
|
||||
except etree.XMLSyntaxError:
|
||||
course_html_parsed = etree.fromstring("<ol></ol>")
|
||||
course_html_parsed = html.fromstring(course_updates.data)
|
||||
except:
|
||||
log.error("Cannot parse: " + course_updates.data)
|
||||
escaped = django.utils.html.escape(course_updates.data)
|
||||
course_html_parsed = html.fromstring("<ol><li>" + escaped + "</li></ol>")
|
||||
|
||||
if course_html_parsed.tag == 'ol':
|
||||
# ??? Should this use the id in the json or in the url or does it matter?
|
||||
@@ -121,7 +136,7 @@ def delete_course_update(location, update, passed_id):
|
||||
course_html_parsed.remove(element_to_delete)
|
||||
|
||||
# update db record
|
||||
course_updates.data = etree.tostring(course_html_parsed)
|
||||
course_updates.data = html.tostring(course_html_parsed)
|
||||
store = modulestore('direct')
|
||||
store.update_item(location, course_updates.data)
|
||||
|
||||
@@ -132,7 +147,6 @@ def get_idx(passed_id):
|
||||
"""
|
||||
From the url w/ idx appended, get the idx.
|
||||
"""
|
||||
# TODO compile this regex into a class static and reuse for each call
|
||||
idx_matcher = re.search(r'.*/(\d+)$', passed_id)
|
||||
idx_matcher = re.search(r'.*?/?(\d+)$', passed_id)
|
||||
if idx_matcher:
|
||||
return int(idx_matcher.group(1))
|
||||
|
||||
@@ -7,8 +7,6 @@ from selenium.common.exceptions import WebDriverException, StaleElementReference
|
||||
from selenium.webdriver.support import expected_conditions as EC
|
||||
from selenium.webdriver.common.by import By
|
||||
|
||||
from terrain.factories import UserFactory, RegistrationFactory, UserProfileFactory
|
||||
from terrain.factories import CourseFactory, GroupFactory
|
||||
from xmodule.modulestore.django import _MODULESTORES, modulestore
|
||||
from xmodule.templates import update_templates
|
||||
from auth.authz import get_user_by_email
|
||||
@@ -61,7 +59,7 @@ def create_studio_user(
|
||||
email='robot+studio@edx.org',
|
||||
password='test',
|
||||
is_staff=False):
|
||||
studio_user = UserFactory.build(
|
||||
studio_user = world.UserFactory.build(
|
||||
username=uname,
|
||||
email=email,
|
||||
password=password,
|
||||
@@ -69,11 +67,11 @@ def create_studio_user(
|
||||
studio_user.set_password(password)
|
||||
studio_user.save()
|
||||
|
||||
registration = RegistrationFactory(user=studio_user)
|
||||
registration = world.RegistrationFactory(user=studio_user)
|
||||
registration.register(studio_user)
|
||||
registration.activate()
|
||||
|
||||
user_profile = UserProfileFactory(user=studio_user)
|
||||
user_profile = world.UserProfileFactory(user=studio_user)
|
||||
|
||||
|
||||
def flush_xmodule_store():
|
||||
@@ -175,11 +173,11 @@ def log_into_studio(
|
||||
|
||||
|
||||
def create_a_course():
|
||||
c = CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course')
|
||||
c = world.CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course')
|
||||
|
||||
# Add the user to the instructor group of the course
|
||||
# so they will have the permissions to see it in studio
|
||||
g = GroupFactory.create(name='instructor_MITx/999/Robot_Super_Course')
|
||||
g = world.GroupFactory.create(name='instructor_MITx/999/Robot_Super_Course')
|
||||
u = get_user_by_email('robot+studio@edx.org')
|
||||
u.groups.add(g)
|
||||
u.save()
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import factory
|
||||
from student.models import User, UserProfile, Registration
|
||||
from datetime import datetime
|
||||
import uuid
|
||||
|
||||
|
||||
class UserProfileFactory(factory.Factory):
|
||||
FACTORY_FOR = UserProfile
|
||||
|
||||
user = None
|
||||
name = 'Robot Studio'
|
||||
courseware = 'course.xml'
|
||||
|
||||
|
||||
class RegistrationFactory(factory.Factory):
|
||||
FACTORY_FOR = Registration
|
||||
|
||||
user = None
|
||||
activation_key = uuid.uuid4().hex
|
||||
|
||||
|
||||
class UserFactory(factory.Factory):
|
||||
FACTORY_FOR = User
|
||||
|
||||
username = 'robot-studio'
|
||||
email = 'robot+studio@edx.org'
|
||||
password = 'test'
|
||||
first_name = 'Robot'
|
||||
last_name = 'Studio'
|
||||
is_staff = False
|
||||
is_active = True
|
||||
is_superuser = False
|
||||
last_login = datetime.now()
|
||||
date_joined = datetime.now()
|
||||
@@ -1,5 +1,4 @@
|
||||
from lettuce import world, step
|
||||
from terrain.factories import *
|
||||
from common import *
|
||||
from nose.tools import assert_true, assert_false, assert_equal
|
||||
|
||||
@@ -10,15 +9,15 @@ logger = getLogger(__name__)
|
||||
@step(u'I have a course with no sections$')
|
||||
def have_a_course(step):
|
||||
clear_courses()
|
||||
course = CourseFactory.create()
|
||||
course = world.CourseFactory.create()
|
||||
|
||||
|
||||
@step(u'I have a course with 1 section$')
|
||||
def have_a_course_with_1_section(step):
|
||||
clear_courses()
|
||||
course = CourseFactory.create()
|
||||
section = ItemFactory.create(parent_location=course.location)
|
||||
subsection1 = ItemFactory.create(
|
||||
course = world.CourseFactory.create()
|
||||
section = world.ItemFactory.create(parent_location=course.location)
|
||||
subsection1 = world.ItemFactory.create(
|
||||
parent_location=section.location,
|
||||
template='i4x://edx/templates/sequential/Empty',
|
||||
display_name='Subsection One',)
|
||||
@@ -27,20 +26,20 @@ def have_a_course_with_1_section(step):
|
||||
@step(u'I have a course with multiple sections$')
|
||||
def have_a_course_with_two_sections(step):
|
||||
clear_courses()
|
||||
course = CourseFactory.create()
|
||||
section = ItemFactory.create(parent_location=course.location)
|
||||
subsection1 = ItemFactory.create(
|
||||
course = world.CourseFactory.create()
|
||||
section = world.ItemFactory.create(parent_location=course.location)
|
||||
subsection1 = world.ItemFactory.create(
|
||||
parent_location=section.location,
|
||||
template='i4x://edx/templates/sequential/Empty',
|
||||
display_name='Subsection One',)
|
||||
section2 = ItemFactory.create(
|
||||
section2 = world.ItemFactory.create(
|
||||
parent_location=course.location,
|
||||
display_name='Section Two',)
|
||||
subsection2 = ItemFactory.create(
|
||||
subsection2 = world.ItemFactory.create(
|
||||
parent_location=section2.location,
|
||||
template='i4x://edx/templates/sequential/Empty',
|
||||
display_name='Subsection Alpha',)
|
||||
subsection3 = ItemFactory.create(
|
||||
subsection3 = world.ItemFactory.create(
|
||||
parent_location=section2.location,
|
||||
template='i4x://edx/templates/sequential/Empty',
|
||||
display_name='Subsection Beta',)
|
||||
|
||||
@@ -101,6 +101,20 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
|
||||
self.assertEqual(reverse_tabs, course_tabs)
|
||||
|
||||
def test_import_polls(self):
|
||||
import_from_xml(modulestore(), 'common/test/data/', ['full'])
|
||||
|
||||
module_store = modulestore('direct')
|
||||
found = False
|
||||
|
||||
item = None
|
||||
items = module_store.get_items(['i4x', 'edX', 'full', 'poll_question', None, None])
|
||||
found = len(items) > 0
|
||||
|
||||
self.assertTrue(found)
|
||||
# check that there's actually content in the 'question' field
|
||||
self.assertGreater(len(items[0].question),0)
|
||||
|
||||
def test_delete(self):
|
||||
import_from_xml(modulestore(), 'common/test/data/', ['full'])
|
||||
|
||||
|
||||
@@ -1,31 +1,135 @@
|
||||
from contentstore.tests.test_course_settings import CourseTestCase
|
||||
from django.core.urlresolvers import reverse
|
||||
import json
|
||||
from webob.exc import HTTPServerError
|
||||
from django.http import HttpResponseBadRequest
|
||||
|
||||
|
||||
class CourseUpdateTest(CourseTestCase):
|
||||
def test_course_update(self):
|
||||
# first get the update to force the creation
|
||||
url = reverse('course_info', kwargs={'org': self.course_location.org, 'course': self.course_location.course,
|
||||
'name': self.course_location.name})
|
||||
url = reverse('course_info',
|
||||
kwargs={'org': self.course_location.org,
|
||||
'course': self.course_location.course,
|
||||
'name': self.course_location.name})
|
||||
self.client.get(url)
|
||||
|
||||
content = '<iframe width="560" height="315" src="http://www.youtube.com/embed/RocY-Jd93XU" frameborder="0"></iframe>'
|
||||
init_content = '<iframe width="560" height="315" src="http://www.youtube.com/embed/RocY-Jd93XU" frameborder="0">'
|
||||
content = init_content + '</iframe>'
|
||||
payload = {'content': content,
|
||||
'date': 'January 8, 2013'}
|
||||
url = reverse('course_info', kwargs={'org': self.course_location.org, 'course': self.course_location.course,
|
||||
'provided_id': ''})
|
||||
url = reverse('course_info_json', kwargs={'org': self.course_location.org,
|
||||
'course': self.course_location.course,
|
||||
'provided_id': ''})
|
||||
|
||||
resp = self.client.post(url, json.dumps(payload), "application/json")
|
||||
|
||||
payload = json.loads(resp.content)
|
||||
|
||||
self.assertHTMLEqual(content, payload['content'], "single iframe")
|
||||
self.assertHTMLEqual(payload['content'], content)
|
||||
|
||||
url = reverse('course_info', kwargs={'org': self.course_location.org, 'course': self.course_location.course,
|
||||
'provided_id': payload['id']})
|
||||
content += '<div>div <p>p</p></div>'
|
||||
first_update_url = reverse('course_info_json',
|
||||
kwargs={'org': self.course_location.org,
|
||||
'course': self.course_location.course,
|
||||
'provided_id': payload['id']})
|
||||
content += '<div>div <p>p<br/></p></div>'
|
||||
payload['content'] = content
|
||||
resp = self.client.post(first_update_url, json.dumps(payload),
|
||||
"application/json")
|
||||
|
||||
self.assertHTMLEqual(content, json.loads(resp.content)['content'],
|
||||
"iframe w/ div")
|
||||
|
||||
# now put in an evil update
|
||||
content = '<ol/>'
|
||||
payload = {'content': content,
|
||||
'date': 'January 11, 2013'}
|
||||
url = reverse('course_info_json',
|
||||
kwargs={'org': self.course_location.org,
|
||||
'course': self.course_location.course,
|
||||
'provided_id': ''})
|
||||
|
||||
resp = self.client.post(url, json.dumps(payload), "application/json")
|
||||
|
||||
self.assertHTMLEqual(content, json.loads(resp.content)['content'], "iframe w/ div")
|
||||
payload = json.loads(resp.content)
|
||||
|
||||
self.assertHTMLEqual(content, payload['content'], "self closing ol")
|
||||
|
||||
url = reverse('course_info_json',
|
||||
kwargs={'org': self.course_location.org,
|
||||
'course': self.course_location.course,
|
||||
'provided_id': ''})
|
||||
resp = self.client.get(url)
|
||||
payload = json.loads(resp.content)
|
||||
self.assertTrue(len(payload) == 2)
|
||||
|
||||
# can't test non-json paylod b/c expect_json throws error
|
||||
# try json w/o required fields
|
||||
self.assertContains(
|
||||
self.client.post(url, json.dumps({'garbage': 1}),
|
||||
"application/json"),
|
||||
'Failed to save', status_code=400)
|
||||
|
||||
# now try to update a non-existent update
|
||||
url = reverse('course_info_json',
|
||||
kwargs={'org': self.course_location.org,
|
||||
'course': self.course_location.course,
|
||||
'provided_id': '9'})
|
||||
content = 'blah blah'
|
||||
payload = {'content': content,
|
||||
'date': 'January 21, 2013'}
|
||||
self.assertContains(
|
||||
self.client.post(url, json.dumps(payload), "application/json"),
|
||||
'Failed to save', status_code=400)
|
||||
|
||||
# update w/ malformed html
|
||||
content = '<garbage tag No closing brace to force <span>error</span>'
|
||||
payload = {'content': content,
|
||||
'date': 'January 11, 2013'}
|
||||
url = reverse('course_info_json', kwargs={'org': self.course_location.org,
|
||||
'course': self.course_location.course,
|
||||
'provided_id': ''})
|
||||
|
||||
resp = self.client.post(url, json.dumps(payload), "application/json")
|
||||
|
||||
payload = json.loads(resp.content)
|
||||
|
||||
self.assertContains(
|
||||
self.client.post(url, json.dumps(payload), "application/json"),
|
||||
'<garbage')
|
||||
|
||||
# now try to delete a non-existent update
|
||||
url = reverse('course_info_json', kwargs={'org': self.course_location.org,
|
||||
'course': self.course_location.course,
|
||||
'provided_id': '19'})
|
||||
payload = {'content': content,
|
||||
'date': 'January 21, 2013'}
|
||||
self.assertContains(self.client.delete(url), "delete", status_code=400)
|
||||
|
||||
# now delete a real update
|
||||
content = 'blah blah'
|
||||
payload = {'content': content,
|
||||
'date': 'January 28, 2013'}
|
||||
url = reverse('course_info_json', kwargs={'org': self.course_location.org,
|
||||
'course': self.course_location.course,
|
||||
'provided_id': ''})
|
||||
resp = self.client.post(url, json.dumps(payload), "application/json")
|
||||
payload = json.loads(resp.content)
|
||||
this_id = payload['id']
|
||||
self.assertHTMLEqual(content, payload['content'], "single iframe")
|
||||
# first count the entries
|
||||
url = reverse('course_info_json',
|
||||
kwargs={'org': self.course_location.org,
|
||||
'course': self.course_location.course,
|
||||
'provided_id': ''})
|
||||
resp = self.client.get(url)
|
||||
payload = json.loads(resp.content)
|
||||
before_delete = len(payload)
|
||||
|
||||
url = reverse('course_info_json',
|
||||
kwargs={'org': self.course_location.org,
|
||||
'course': self.course_location.course,
|
||||
'provided_id': this_id})
|
||||
resp = self.client.delete(url)
|
||||
payload = json.loads(resp.content)
|
||||
self.assertTrue(len(payload) == before_delete - 1)
|
||||
|
||||
@@ -18,7 +18,8 @@ from django.core.files.temp import NamedTemporaryFile
|
||||
# to install PIL on MacOSX: 'easy_install http://dist.repoze.org/PIL-1.1.6.tar.gz'
|
||||
from PIL import Image
|
||||
|
||||
from django.http import HttpResponse, Http404, HttpResponseBadRequest, HttpResponseForbidden
|
||||
from django.http import HttpResponse, Http404, HttpResponseBadRequest, HttpResponseForbidden, HttpResponseServerError
|
||||
from django.http import HttpResponseNotFound
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.core.context_processors import csrf
|
||||
@@ -53,12 +54,12 @@ from auth.authz import INSTRUCTOR_ROLE_NAME, STAFF_ROLE_NAME, create_all_course_
|
||||
from .utils import get_course_location_for_item, get_lms_link_for_item, compute_unit_state, get_date_display, UnitState, get_course_for_item
|
||||
|
||||
from xmodule.modulestore.xml_importer import import_from_xml
|
||||
from contentstore.course_info_model import get_course_updates,\
|
||||
from contentstore.course_info_model import get_course_updates, \
|
||||
update_course_updates, delete_course_update
|
||||
from cache_toolbox.core import del_cached_content
|
||||
from xmodule.timeparse import stringify_time
|
||||
from contentstore.module_info_model import get_module_info, set_module_info
|
||||
from models.settings.course_details import CourseDetails,\
|
||||
from models.settings.course_details import CourseDetails, \
|
||||
CourseSettingsEncoder
|
||||
from models.settings.course_grading import CourseGradingModel
|
||||
from contentstore.utils import get_modulestore
|
||||
@@ -72,7 +73,7 @@ log = logging.getLogger(__name__)
|
||||
|
||||
COMPONENT_TYPES = ['customtag', 'discussion', 'html', 'problem', 'video']
|
||||
|
||||
ADVANCED_COMPONENT_TYPES = ['annotatable','combinedopenended', 'peergrading']
|
||||
ADVANCED_COMPONENT_TYPES = ['annotatable', 'combinedopenended', 'peergrading']
|
||||
ADVANCED_COMPONENT_CATEGORY = 'advanced'
|
||||
ADVANCED_COMPONENT_POLICY_KEY = 'advanced_modules'
|
||||
|
||||
@@ -321,7 +322,7 @@ def edit_unit(request, location):
|
||||
category = ADVANCED_COMPONENT_CATEGORY
|
||||
|
||||
if category in component_types:
|
||||
#This is a hack to create categories for different xmodules
|
||||
# This is a hack to create categories for different xmodules
|
||||
component_templates[category].append((
|
||||
template.display_name_with_default,
|
||||
template.location.url(),
|
||||
@@ -416,7 +417,7 @@ def assignment_type_update(request, org, course, category, name):
|
||||
if request.method == 'GET':
|
||||
return HttpResponse(json.dumps(CourseGradingModel.get_section_grader_type(location)),
|
||||
mimetype="application/json")
|
||||
elif request.method == 'POST': # post or put, doesn't matter.
|
||||
elif request.method == 'POST': # post or put, doesn't matter.
|
||||
return HttpResponse(json.dumps(CourseGradingModel.update_section_grader_type(location, request.POST)),
|
||||
mimetype="application/json")
|
||||
|
||||
@@ -830,7 +831,7 @@ def upload_asset(request, org, course, coursename):
|
||||
if thumbnail_content is not None:
|
||||
content.thumbnail_location = thumbnail_location
|
||||
|
||||
#then commit the content
|
||||
# then commit the content
|
||||
contentstore().save(content)
|
||||
del_cached_content(content.location)
|
||||
|
||||
@@ -873,7 +874,7 @@ def manage_users(request, location):
|
||||
})
|
||||
|
||||
|
||||
def create_json_response(errmsg = None):
|
||||
def create_json_response(errmsg=None):
|
||||
if errmsg is not None:
|
||||
resp = HttpResponse(json.dumps({'Status': 'Failed', 'ErrMsg': errmsg}))
|
||||
else:
|
||||
@@ -1117,14 +1118,22 @@ def course_info_updates(request, org, course, provided_id=None):
|
||||
real_method = request.method
|
||||
|
||||
if request.method == 'GET':
|
||||
return HttpResponse(json.dumps(get_course_updates(location)), mimetype="application/json")
|
||||
elif real_method == 'DELETE': # coming as POST need to pull from Request Header X-HTTP-Method-Override DELETE
|
||||
return HttpResponse(json.dumps(delete_course_update(location, request.POST, provided_id)), mimetype="application/json")
|
||||
return HttpResponse(json.dumps(get_course_updates(location)),
|
||||
mimetype="application/json")
|
||||
elif real_method == 'DELETE':
|
||||
try:
|
||||
return HttpResponse(json.dumps(delete_course_update(location,
|
||||
request.POST, provided_id)), mimetype="application/json")
|
||||
except:
|
||||
return HttpResponseBadRequest("Failed to delete",
|
||||
content_type="text/plain")
|
||||
elif request.method == 'POST':
|
||||
try:
|
||||
return HttpResponse(json.dumps(update_course_updates(location, request.POST, provided_id)), mimetype="application/json")
|
||||
return HttpResponse(json.dumps(update_course_updates(location,
|
||||
request.POST, provided_id)), mimetype="application/json")
|
||||
except:
|
||||
return HttpResponseBadRequest("Failed to save: malformed html", content_type="text/plain")
|
||||
return HttpResponseBadRequest("Failed to save",
|
||||
content_type="text/plain")
|
||||
|
||||
|
||||
@expect_json
|
||||
@@ -1259,7 +1268,7 @@ def course_settings_updates(request, org, course, name, section):
|
||||
# Cannot just do a get w/o knowing the course name :-(
|
||||
return HttpResponse(json.dumps(manager.fetch(Location(['i4x', org, course, 'course', name])), cls=CourseSettingsEncoder),
|
||||
mimetype="application/json")
|
||||
elif request.method == 'POST': # post or put, doesn't matter.
|
||||
elif request.method == 'POST': # post or put, doesn't matter.
|
||||
return HttpResponse(json.dumps(manager.update_from_json(request.POST), cls=CourseSettingsEncoder),
|
||||
mimetype="application/json")
|
||||
|
||||
@@ -1294,12 +1303,12 @@ def course_grader_updates(request, org, course, name, grader_index=None):
|
||||
# ??? 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.
|
||||
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")
|
||||
|
||||
|
||||
## NB: expect_json failed on ["key", "key2"] and json payload
|
||||
# # NB: expect_json failed on ["key", "key2"] and json payload
|
||||
@login_required
|
||||
@ensure_csrf_cookie
|
||||
def course_advanced_updates(request, org, course, name):
|
||||
@@ -1557,7 +1566,7 @@ def generate_export_course(request, org, course, name):
|
||||
logging.debug('root = {0}'.format(root_dir))
|
||||
|
||||
export_to_xml(modulestore('direct'), contentstore(), loc, root_dir, name)
|
||||
#filename = root_dir / name + '.tar.gz'
|
||||
# filename = root_dir / name + '.tar.gz'
|
||||
|
||||
logging.debug('tar file being generated at {0}'.format(export_file.name))
|
||||
tf = tarfile.open(name=export_file.name, mode='w:gz')
|
||||
@@ -1597,3 +1606,11 @@ def event(request):
|
||||
console logs don't get distracted :-)
|
||||
'''
|
||||
return HttpResponse(True)
|
||||
|
||||
|
||||
def render_404(request):
|
||||
return HttpResponseNotFound(render_to_string('404.html', {}))
|
||||
|
||||
|
||||
def render_500(request):
|
||||
return HttpResponseServerError(render_to_string('500.html', {}))
|
||||
|
||||
@@ -127,8 +127,7 @@ DEBUG_TOOLBAR_PANELS = (
|
||||
'debug_toolbar.panels.sql.SQLDebugPanel',
|
||||
'debug_toolbar.panels.signals.SignalDebugPanel',
|
||||
'debug_toolbar.panels.logger.LoggingPanel',
|
||||
# This is breaking Mongo updates-- Christina is investigating.
|
||||
# 'debug_toolbar_mongo.panel.MongoDebugPanel',
|
||||
'debug_toolbar_mongo.panel.MongoDebugPanel',
|
||||
|
||||
# Enabling the profiler has a weird bug as of django-debug-toolbar==0.9.4 and
|
||||
# Django=1.3.1/1.4 where requests to views get duplicated (your method gets
|
||||
@@ -143,4 +142,4 @@ DEBUG_TOOLBAR_CONFIG = {
|
||||
|
||||
# To see stacktraces for MongoDB queries, set this to True.
|
||||
# Stacktraces slow down page loads drastically (for pages with lots of queries).
|
||||
# DEBUG_TOOLBAR_MONGO_STACKTRACES = False
|
||||
DEBUG_TOOLBAR_MONGO_STACKTRACES = False
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"js_files": [
|
||||
"/static/js/vendor/RequireJS.js",
|
||||
"/static/js/vendor/jquery.min.js",
|
||||
"/static/js/vendor/jquery-ui.min.js",
|
||||
"/static/js/vendor/jquery.ui.draggable.js",
|
||||
"/static/js/vendor/jquery.cookie.js",
|
||||
"/static/js/vendor/json2.js",
|
||||
"/static/js/vendor/underscore-min.js",
|
||||
"/static/js/vendor/backbone-min.js"
|
||||
"static_files": [
|
||||
"js/vendor/RequireJS.js",
|
||||
"js/vendor/jquery.min.js",
|
||||
"js/vendor/jquery-ui.min.js",
|
||||
"js/vendor/jquery.ui.draggable.js",
|
||||
"js/vendor/jquery.cookie.js",
|
||||
"js/vendor/json2.js",
|
||||
"js/vendor/underscore-min.js",
|
||||
"js/vendor/backbone-min.js"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -34,7 +34,10 @@ class CMS.Views.UnitEdit extends Backbone.View
|
||||
|
||||
@$('.components').sortable(
|
||||
handle: '.drag-handle'
|
||||
update: (event, ui) => @model.save(children: @components())
|
||||
update: (event, ui) =>
|
||||
payload = children : @components()
|
||||
options = success : => @model.unset('children')
|
||||
@model.save(payload, options)
|
||||
helper: 'clone'
|
||||
opacity: '0.5'
|
||||
placeholder: 'component-placeholder'
|
||||
@@ -109,7 +112,14 @@ class CMS.Views.UnitEdit extends Backbone.View
|
||||
id: $component.data('id')
|
||||
}, =>
|
||||
$component.remove()
|
||||
@model.save(children: @components())
|
||||
# b/c we don't vigilantly keep children up to date
|
||||
# get rid of it before it hurts someone
|
||||
# sorry for the js, i couldn't figure out the coffee equivalent
|
||||
`_this.model.save({children: _this.components()},
|
||||
{success: function(model) {
|
||||
model.unset('children');
|
||||
}}
|
||||
);`
|
||||
)
|
||||
|
||||
deleteDraft: (event) ->
|
||||
|
||||
@@ -142,8 +142,11 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
|
||||
|
||||
onDelete: function(event) {
|
||||
event.preventDefault();
|
||||
// TODO ask for confirmation
|
||||
// remove the dom element and delete the model
|
||||
|
||||
if (!confirm('Are you sure you want to delete this update? This action cannot be undone.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
var targetModel = this.eventModel(event);
|
||||
this.modelDom(event).remove();
|
||||
var cacheThis = this;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// studio base styling
|
||||
// studio - base styling
|
||||
// ====================
|
||||
|
||||
// basic reset
|
||||
// basic setup
|
||||
html {
|
||||
font-size: 62.5%;
|
||||
overflow-y: scroll;
|
||||
@@ -9,7 +9,7 @@ html {
|
||||
|
||||
body {
|
||||
@include font-size(16);
|
||||
min-width: 980px;
|
||||
min-width: $fg-min-width;
|
||||
background: $gray-l5;
|
||||
line-height: 1.6;
|
||||
color: $baseFontColor;
|
||||
@@ -350,10 +350,11 @@ h1 {
|
||||
// layout - grandfathered
|
||||
.main-wrapper {
|
||||
position: relative;
|
||||
margin: 0 40px;
|
||||
margin: 40px;
|
||||
}
|
||||
|
||||
.inner-wrapper {
|
||||
@include clearfix();
|
||||
position: relative;
|
||||
max-width: 1280px;
|
||||
margin: auto;
|
||||
@@ -363,6 +364,12 @@ h1 {
|
||||
}
|
||||
}
|
||||
|
||||
.main-column {
|
||||
clear: both;
|
||||
float: left;
|
||||
width: 70%;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
float: right;
|
||||
width: 28%;
|
||||
@@ -378,109 +385,6 @@ h1 {
|
||||
|
||||
// ====================
|
||||
|
||||
// forms
|
||||
input[type="text"],
|
||||
input[type="email"],
|
||||
input[type="password"],
|
||||
textarea.text {
|
||||
padding: 6px 8px 8px;
|
||||
@include box-sizing(border-box);
|
||||
border: 1px solid $mediumGrey;
|
||||
border-radius: 2px;
|
||||
@include linear-gradient($lightGrey, tint($lightGrey, 90%));
|
||||
background-color: $lightGrey;
|
||||
@include box-shadow(0 1px 2px rgba(0, 0, 0, .1) inset);
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
font-size: 11px;
|
||||
color: $baseFontColor;
|
||||
outline: 0;
|
||||
|
||||
&::-webkit-input-placeholder,
|
||||
&:-moz-placeholder,
|
||||
&:-ms-input-placeholder {
|
||||
color: #979faf;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
@include linear-gradient($paleYellow, tint($paleYellow, 90%));
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
&[disabled] {
|
||||
border-color: $gray-l4;
|
||||
color: $gray-l2;
|
||||
}
|
||||
|
||||
&[readonly] {
|
||||
border-color: $gray-l4;
|
||||
color: $gray-l1;
|
||||
|
||||
&:focus {
|
||||
@include linear-gradient($lightGrey, tint($lightGrey, 90%));
|
||||
outline: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// forms - specific
|
||||
input.search {
|
||||
padding: 6px 15px 8px 30px;
|
||||
@include box-sizing(border-box);
|
||||
border: 1px solid $darkGrey;
|
||||
border-radius: 20px;
|
||||
background: url(../img/search-icon.png) no-repeat 8px 7px #edf1f5;
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
color: $baseFontColor;
|
||||
outline: 0;
|
||||
|
||||
&::-webkit-input-placeholder {
|
||||
color: #979faf;
|
||||
}
|
||||
}
|
||||
|
||||
label {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
code {
|
||||
padding: 0 4px;
|
||||
border-radius: 3px;
|
||||
background: #eee;
|
||||
font-family: Monaco, monospace;
|
||||
}
|
||||
|
||||
.CodeMirror {
|
||||
font-size: 13px;
|
||||
border: 1px solid $darkGrey;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.text-editor {
|
||||
width: 100%;
|
||||
min-height: 80px;
|
||||
padding: 10px;
|
||||
@include box-sizing(border-box);
|
||||
border: 1px solid $mediumGrey;
|
||||
@include linear-gradient(top, rgba(255, 255, 255, 0), rgba(255, 255, 255, 0.3));
|
||||
background-color: #edf1f5;
|
||||
@include box-shadow(0 1px 2px rgba(0, 0, 0, 0.1) inset);
|
||||
font-family: Monaco, monospace;
|
||||
}
|
||||
|
||||
// ====================
|
||||
|
||||
// UI - chrome
|
||||
.window {
|
||||
@include clearfix();
|
||||
@include border-radius(3px);
|
||||
@include box-shadow(0 1px 1px $shadow-l1);
|
||||
margin-bottom: $baseline;
|
||||
border: 1px solid $gray-l2;
|
||||
background: $white;
|
||||
}
|
||||
|
||||
// ====================
|
||||
|
||||
// UI - actions
|
||||
.new-unit-item,
|
||||
.new-subsection-item,
|
||||
@@ -861,14 +765,4 @@ body.hide-wip {
|
||||
.wip-box {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
// ====================
|
||||
|
||||
// needed fudges for now
|
||||
body.dashboard {
|
||||
|
||||
.my-classes {
|
||||
margin-top: $baseline;
|
||||
}
|
||||
}
|
||||
@@ -1,367 +0,0 @@
|
||||
section.cal {
|
||||
@include box-sizing(border-box);
|
||||
@include clearfix;
|
||||
padding: 20px;
|
||||
|
||||
> header {
|
||||
display: none;
|
||||
@include clearfix;
|
||||
margin-bottom: 10px;
|
||||
opacity: .4;
|
||||
@include transition;
|
||||
text-shadow: 0 1px 0 #fff;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
h2 {
|
||||
@include inline-block();
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
font-size: 14px;
|
||||
padding: 6px 6px 6px 0;
|
||||
font-size: 12px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
ul {
|
||||
@include inline-block;
|
||||
float: right;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
&.actions {
|
||||
float: left;
|
||||
}
|
||||
|
||||
li {
|
||||
@include inline-block;
|
||||
margin-right: 6px;
|
||||
border-right: 1px solid #ddd;
|
||||
padding: 0 6px 0 0;
|
||||
|
||||
&:last-child {
|
||||
border-right: 0;
|
||||
margin-right: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
a {
|
||||
@include inline-block();
|
||||
font-size: 12px;
|
||||
@include inline-block;
|
||||
margin: 0 6px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
ul {
|
||||
@include inline-block();
|
||||
margin: 0;
|
||||
|
||||
li {
|
||||
@include inline-block();
|
||||
padding: 0;
|
||||
border-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ol {
|
||||
list-style: none;
|
||||
@include clearfix;
|
||||
border: 1px solid lighten( $dark-blue , 30% );
|
||||
background: #FFF;
|
||||
width: 100%;
|
||||
@include box-sizing(border-box);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
@include box-shadow(0 0 5px lighten($dark-blue, 45%));
|
||||
@include border-radius(3px);
|
||||
overflow: hidden;
|
||||
|
||||
> li {
|
||||
border-right: 1px solid lighten($dark-blue, 40%);
|
||||
border-bottom: 1px solid lighten($dark-blue, 40%);
|
||||
@include box-sizing(border-box);
|
||||
float: left;
|
||||
width: flex-grid(3) + ((flex-gutter() * 3) / 4);
|
||||
background-color: $light-blue;
|
||||
@include box-shadow(inset 0 0 0 1px lighten($light-blue, 8%));
|
||||
|
||||
&:hover {
|
||||
li.create-module {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&:nth-child(4n) {
|
||||
border-right: 0;
|
||||
}
|
||||
|
||||
header {
|
||||
border-bottom: 1px solid lighten($dark-blue, 40%);
|
||||
@include box-shadow(0 2px 2px $light-blue);
|
||||
display: block;
|
||||
margin-bottom: 2px;
|
||||
background: #FFF;
|
||||
|
||||
h1 {
|
||||
font-size: 14px;
|
||||
text-transform: uppercase;
|
||||
border-bottom: 1px solid lighten($dark-blue, 60%);
|
||||
padding: 6px;
|
||||
color: $bright-blue;
|
||||
margin: 0;
|
||||
|
||||
a {
|
||||
color: $bright-blue;
|
||||
display: block;
|
||||
padding: 6px;
|
||||
margin: -6px;
|
||||
|
||||
&:hover {
|
||||
color: darken($bright-blue, 10%);
|
||||
background: lighten($yellow, 10%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
li {
|
||||
background: #fff;
|
||||
color: #888;
|
||||
border-bottom: 0;
|
||||
font-size: 12px;
|
||||
@include box-shadow(none);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
margin: 0 0 1px 0;
|
||||
padding: 0;
|
||||
|
||||
li {
|
||||
border-bottom: 1px solid darken($light-blue, 6%);
|
||||
// @include box-shadow(0 1px 0 lighten($light-blue, 4%));
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
text-shadow: 0 1px 0 #fff;
|
||||
|
||||
&:hover {
|
||||
background-color: lighten($yellow, 14%);
|
||||
|
||||
a.draggable {
|
||||
background-color: lighten($yellow, 14%);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&.editable {
|
||||
padding: 3px 6px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: lighten($dark-blue, 10%);
|
||||
display: block;
|
||||
padding: 6px 35px 6px 6px;
|
||||
|
||||
&:hover {
|
||||
background-color: lighten($yellow, 10%);
|
||||
}
|
||||
|
||||
&.draggable {
|
||||
background-color: $light-blue;
|
||||
opacity: .3;
|
||||
padding: 0;
|
||||
|
||||
&:hover {
|
||||
background-color: lighten($yellow, 10%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.create-module {
|
||||
position: relative;
|
||||
opacity: 0;
|
||||
@include transition(all 3s ease-in-out);
|
||||
background: darken($light-blue, 2%);
|
||||
|
||||
> div {
|
||||
background: $dark-blue;
|
||||
@include box-shadow(0 0 5px darken($light-blue, 60%));
|
||||
@include box-sizing(border-box);
|
||||
display: none;
|
||||
margin-left: 3%;
|
||||
padding: 10px;
|
||||
@include position(absolute, 30px 0 0 0);
|
||||
width: 90%;
|
||||
z-index: 99;
|
||||
|
||||
ul {
|
||||
li {
|
||||
border-bottom: 0;
|
||||
background: none;
|
||||
|
||||
input {
|
||||
@include box-sizing(border-box);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
select {
|
||||
@include box-sizing(border-box);
|
||||
width: 100%;
|
||||
|
||||
option {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
div {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: $light-blue;
|
||||
float: right;
|
||||
|
||||
&:first-child {
|
||||
float: left;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
section.new-section {
|
||||
margin: 10px 0 40px;
|
||||
@include inline-block();
|
||||
position: relative;
|
||||
|
||||
> a {
|
||||
@extend .button;
|
||||
display: block;
|
||||
}
|
||||
|
||||
section {
|
||||
display: none;
|
||||
@include position(absolute, 30px 0 0 0);
|
||||
background: rgba(#000, .8);
|
||||
min-width: 300px;
|
||||
padding: 10px;
|
||||
@include box-sizing(border-box);
|
||||
@include border-radius(3px);
|
||||
z-index: 99;
|
||||
|
||||
&:before {
|
||||
content: " ";
|
||||
display: block;
|
||||
background: rgba(#000, .8);
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
@include position(absolute, -5px 0 0 20%);
|
||||
@include transform(rotate(45deg));
|
||||
}
|
||||
|
||||
form {
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
|
||||
li {
|
||||
border-bottom: 0;
|
||||
background: none;
|
||||
margin-bottom: 6px;
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
@include box-sizing(border-box);
|
||||
border-color: #000;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
select {
|
||||
width: 100%;
|
||||
@include box-sizing(border-box);
|
||||
|
||||
option {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
float: right;
|
||||
|
||||
&:first-child {
|
||||
float: left;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
body.content
|
||||
section.cal {
|
||||
width: flex-grid(3);
|
||||
float: left;
|
||||
overflow: scroll;
|
||||
@include box-sizing(border-box);
|
||||
opacity: .4;
|
||||
@include transition();
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
> header {
|
||||
@include transition;
|
||||
overflow: hidden;
|
||||
|
||||
> a {
|
||||
display: none;
|
||||
}
|
||||
|
||||
ul {
|
||||
float: none;
|
||||
display: block;
|
||||
|
||||
li {
|
||||
|
||||
ul {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ol {
|
||||
li {
|
||||
@include box-sizing(border-box);
|
||||
width: 100%;
|
||||
border-right: 0;
|
||||
|
||||
&.create-module {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
// studio - utilities - mixins and extends
|
||||
// ====================
|
||||
|
||||
@mixin clearfix {
|
||||
&:after {
|
||||
content: '';
|
||||
|
||||
@@ -1,689 +0,0 @@
|
||||
|
||||
input.courseware-unit-search-input {
|
||||
float: left;
|
||||
width: 260px;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.branch {
|
||||
|
||||
.section-item {
|
||||
@include clearfix();
|
||||
|
||||
.details {
|
||||
display: block;
|
||||
float: left;
|
||||
margin-bottom: 0;
|
||||
width: 650px;
|
||||
}
|
||||
|
||||
.gradable-status {
|
||||
float: right;
|
||||
position: relative;
|
||||
top: -4px;
|
||||
right: 50px;
|
||||
width: 145px;
|
||||
|
||||
.status-label {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: -5px;
|
||||
display: none;
|
||||
width: 110px;
|
||||
padding: 5px 40px 5px 10px;
|
||||
@include border-radius(3px);
|
||||
color: $lightGrey;
|
||||
text-align: right;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
.menu-toggle {
|
||||
z-index: 10;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 5px;
|
||||
padding: 5px;
|
||||
color: $mediumGrey;
|
||||
|
||||
&:hover, &.is-active {
|
||||
color: $blue;
|
||||
}
|
||||
}
|
||||
|
||||
.menu {
|
||||
z-index: 1;
|
||||
display: none;
|
||||
opacity: 0.0;
|
||||
position: absolute;
|
||||
top: -1px;
|
||||
left: 5px;
|
||||
margin: 0;
|
||||
padding: 8px 12px;
|
||||
background: $white;
|
||||
border: 1px solid $mediumGrey;
|
||||
font-size: 12px;
|
||||
@include border-radius(4px);
|
||||
@include box-shadow(0 1px 2px rgba(0, 0, 0, .2));
|
||||
@include transition(opacity .15s);
|
||||
|
||||
|
||||
li {
|
||||
width: 115px;
|
||||
margin-bottom: 3px;
|
||||
padding-bottom: 3px;
|
||||
border-bottom: 1px solid $lightGrey;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
border: none;
|
||||
|
||||
a {
|
||||
color: $darkGrey;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
color: $blue;
|
||||
|
||||
&.is-selected {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// dropdown state
|
||||
&.is-active {
|
||||
|
||||
.menu {
|
||||
z-index: 1000;
|
||||
display: block;
|
||||
opacity: 1.0;
|
||||
}
|
||||
|
||||
.menu-toggle {
|
||||
z-index: 10000;
|
||||
}
|
||||
}
|
||||
|
||||
// set state
|
||||
&.is-set {
|
||||
|
||||
.menu-toggle {
|
||||
color: $blue;
|
||||
}
|
||||
|
||||
.status-label {
|
||||
display: block;
|
||||
color: $blue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.courseware-section {
|
||||
position: relative;
|
||||
background: #fff;
|
||||
border-radius: 3px;
|
||||
border: 1px solid $mediumGrey;
|
||||
margin-top: 15px;
|
||||
padding-bottom: 12px;
|
||||
@include box-shadow(0 1px 1px rgba(0, 0, 0, 0.1));
|
||||
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
&.collapsed {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
label {
|
||||
float: left;
|
||||
line-height: 29px;
|
||||
}
|
||||
|
||||
.datepair {
|
||||
float: left;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.section-published-date {
|
||||
position: absolute;
|
||||
top: 19px;
|
||||
right: 90px;
|
||||
padding: 4px 10px;
|
||||
border-radius: 3px;
|
||||
background: $lightGrey;
|
||||
text-align: right;
|
||||
|
||||
.published-status {
|
||||
font-size: 12px;
|
||||
margin-right: 15px;
|
||||
|
||||
strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.schedule-button {
|
||||
@include blue-button;
|
||||
}
|
||||
|
||||
.edit-button {
|
||||
@include blue-button;
|
||||
}
|
||||
|
||||
.schedule-button,
|
||||
.edit-button {
|
||||
font-size: 11px;
|
||||
padding: 3px 15px 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.datepair .date,
|
||||
.datepair .time {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
border: none;
|
||||
background: none;
|
||||
@include box-shadow(none);
|
||||
font-size: 13px;
|
||||
font-weight: bold;
|
||||
color: $blue;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.datepair .date {
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.datepair .time {
|
||||
width: 65px;
|
||||
}
|
||||
|
||||
&.collapsed .subsection-list,
|
||||
.collapsed .subsection-list,
|
||||
.collapsed > ol {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
header {
|
||||
min-height: 75px;
|
||||
@include clearfix();
|
||||
|
||||
.item-details, .section-published-date {
|
||||
|
||||
}
|
||||
|
||||
.item-details {
|
||||
display: inline-block;
|
||||
padding: 20px 0 10px 0;
|
||||
@include clearfix();
|
||||
|
||||
.section-name {
|
||||
float: left;
|
||||
margin-right: 10px;
|
||||
width: 350px;
|
||||
font-size: 19px;
|
||||
font-weight: bold;
|
||||
color: $blue;
|
||||
}
|
||||
|
||||
.section-name-span {
|
||||
cursor: pointer;
|
||||
@include transition(color .15s);
|
||||
|
||||
&:hover {
|
||||
color: $orange;
|
||||
}
|
||||
}
|
||||
|
||||
.section-name-edit {
|
||||
position: relative;
|
||||
width: 400px;
|
||||
background: $white;
|
||||
|
||||
input {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.save-button {
|
||||
@include blue-button;
|
||||
padding: 7px 20px 7px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.cancel-button {
|
||||
@include white-button;
|
||||
padding: 7px 20px 7px;
|
||||
}
|
||||
}
|
||||
|
||||
.section-published-date {
|
||||
float: right;
|
||||
width: 265px;
|
||||
margin-right: 220px;
|
||||
@include border-radius(3px);
|
||||
background: $lightGrey;
|
||||
|
||||
.published-status {
|
||||
font-size: 12px;
|
||||
margin-right: 15px;
|
||||
|
||||
strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.schedule-button {
|
||||
@include blue-button;
|
||||
}
|
||||
|
||||
.edit-button {
|
||||
@include blue-button;
|
||||
}
|
||||
|
||||
.schedule-button,
|
||||
.edit-button {
|
||||
font-size: 11px;
|
||||
padding: 3px 15px 5px;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.gradable-status {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 70px;
|
||||
width: 145px;
|
||||
|
||||
.status-label {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 2px;
|
||||
display: none;
|
||||
width: 100px;
|
||||
padding: 10px 35px 10px 10px;
|
||||
@include border-radius(3px);
|
||||
background: $lightGrey;
|
||||
color: $lightGrey;
|
||||
text-align: right;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
.menu-toggle {
|
||||
z-index: 10;
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 5px;
|
||||
padding: 5px;
|
||||
color: $lightGrey;
|
||||
|
||||
&:hover, &.is-active {
|
||||
color: $blue;
|
||||
}
|
||||
}
|
||||
|
||||
.menu {
|
||||
z-index: 1;
|
||||
display: none;
|
||||
opacity: 0.0;
|
||||
position: absolute;
|
||||
top: -1px;
|
||||
left: 2px;
|
||||
margin: 0;
|
||||
padding: 8px 12px;
|
||||
background: $white;
|
||||
border: 1px solid $mediumGrey;
|
||||
font-size: 12px;
|
||||
@include border-radius(4px);
|
||||
@include box-shadow(0 1px 2px rgba(0, 0, 0, .2));
|
||||
@include transition(opacity .15s);
|
||||
@include transition(display .15s);
|
||||
|
||||
|
||||
li {
|
||||
width: 115px;
|
||||
margin-bottom: 3px;
|
||||
padding-bottom: 3px;
|
||||
border-bottom: 1px solid $lightGrey;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
border: none;
|
||||
|
||||
a {
|
||||
color: $darkGrey;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
|
||||
&.is-selected {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// dropdown state
|
||||
&.is-active {
|
||||
|
||||
.menu {
|
||||
z-index: 1000;
|
||||
display: block;
|
||||
opacity: 1.0;
|
||||
}
|
||||
|
||||
|
||||
.menu-toggle {
|
||||
z-index: 10000;
|
||||
}
|
||||
}
|
||||
|
||||
// set state
|
||||
&.is-set {
|
||||
|
||||
.menu-toggle {
|
||||
color: $blue;
|
||||
}
|
||||
|
||||
.status-label {
|
||||
display: block;
|
||||
color: $blue;
|
||||
}
|
||||
}
|
||||
|
||||
float: left;
|
||||
padding: 21px 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
.item-actions {
|
||||
margin-top: 21px;
|
||||
margin-right: 12px;
|
||||
|
||||
.edit-button,
|
||||
.delete-button {
|
||||
margin-top: -3px;
|
||||
}
|
||||
}
|
||||
|
||||
.expand-collapse-icon {
|
||||
float: left;
|
||||
margin: 29px 6px 16px 16px;
|
||||
@include transition(none);
|
||||
|
||||
&.expand {
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
&.collapsed {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.drag-handle {
|
||||
margin-left: 11px;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 19px;
|
||||
font-weight: 700;
|
||||
color: $blue;
|
||||
}
|
||||
|
||||
.section-name-span {
|
||||
cursor: pointer;
|
||||
@include transition(color .15s);
|
||||
|
||||
&:hover {
|
||||
color: $orange;
|
||||
}
|
||||
}
|
||||
|
||||
.section-name-form {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.section-name-edit {
|
||||
input {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.save-button {
|
||||
@include blue-button;
|
||||
padding: 7px 20px 7px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.cancel-button {
|
||||
@include white-button;
|
||||
padding: 7px 20px 7px;
|
||||
}
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 12px;
|
||||
color: #878e9d;
|
||||
|
||||
strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.list-header {
|
||||
@include linear-gradient(top, transparent, rgba(0, 0, 0, .1));
|
||||
background-color: #ced2db;
|
||||
border-radius: 3px 3px 0 0;
|
||||
}
|
||||
|
||||
.subsection-list {
|
||||
margin: 0 12px;
|
||||
|
||||
> ol {
|
||||
@include tree-view;
|
||||
border-top-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.new-section {
|
||||
|
||||
header {
|
||||
height: auto;
|
||||
@include clearfix();
|
||||
}
|
||||
|
||||
.expand-collapse-icon {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.item-details {
|
||||
padding: 25px 0 0 0;
|
||||
|
||||
.section-name {
|
||||
float: none;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.toggle-button-sections {
|
||||
display: none;
|
||||
position: relative;
|
||||
float: right;
|
||||
margin-top: 10px;
|
||||
|
||||
font-size: 13px;
|
||||
color: $darkGrey;
|
||||
|
||||
&.is-shown {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.ss-icon {
|
||||
@include border-radius(20px);
|
||||
position: relative;
|
||||
top: -1px;
|
||||
display: inline-block;
|
||||
margin-right: 2px;
|
||||
line-height: 5px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.label {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
.new-section-name,
|
||||
.new-subsection-name-input {
|
||||
width: 515px;
|
||||
}
|
||||
|
||||
.new-section-name-save,
|
||||
.new-subsection-name-save {
|
||||
@include blue-button;
|
||||
padding: 4px 20px 7px;
|
||||
margin: 0 5px;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
.new-section-name-cancel,
|
||||
.new-subsection-name-cancel {
|
||||
@include white-button;
|
||||
padding: 4px 20px 7px;
|
||||
color: #8891a1 !important;
|
||||
}
|
||||
|
||||
.dummy-calendar {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 55px;
|
||||
left: 110px;
|
||||
z-index: 9999;
|
||||
border: 1px solid #3C3C3C;
|
||||
@include box-shadow(0 1px 15px rgba(0, 0, 0, .2));
|
||||
}
|
||||
|
||||
.unit-name-input {
|
||||
padding: 20px 40px;
|
||||
|
||||
label {
|
||||
display: block;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.preview {
|
||||
background: url(../img/preview.jpg) center top no-repeat;
|
||||
}
|
||||
|
||||
.edit-subsection-publish-settings {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 100px;
|
||||
left: 50%;
|
||||
z-index: 99999;
|
||||
width: 600px;
|
||||
margin-left: -300px;
|
||||
background: #fff;
|
||||
text-align: center;
|
||||
|
||||
.settings {
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 34px;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.picker {
|
||||
margin: 30px 0 65px;
|
||||
}
|
||||
|
||||
.description {
|
||||
margin-top: 30px;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
strong {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.start-date,
|
||||
.start-time {
|
||||
font-size: 19px;
|
||||
}
|
||||
|
||||
.save-button {
|
||||
@include blue-button;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.cancel-button {
|
||||
@include white-button;
|
||||
}
|
||||
|
||||
.save-button,
|
||||
.cancel-button {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.collapse-all-button {
|
||||
float: right;
|
||||
margin-top: 10px;
|
||||
font-size: 13px;
|
||||
color: $darkGrey;
|
||||
}
|
||||
|
||||
// sort/drag and drop
|
||||
.ui-droppable {
|
||||
@include transition (padding 0.5s ease-in-out 0s);
|
||||
min-height: 20px;
|
||||
padding: 0;
|
||||
|
||||
&.dropover {
|
||||
padding: 15px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.ui-draggable-dragging {
|
||||
@include box-shadow(0 1px 2px rgba(0, 0, 0, .3));
|
||||
border: 1px solid $darkGrey;
|
||||
opacity : 0.2;
|
||||
&:hover {
|
||||
opacity : 1.0;
|
||||
.section-item {
|
||||
background: $yellow !important;
|
||||
}
|
||||
}
|
||||
|
||||
// hiding unit button - temporary fix until this semantically corrected
|
||||
.new-unit-item {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
ol.ui-droppable .branch:first-child .section-item {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
.class-list {
|
||||
margin-top: 20px;
|
||||
border-radius: 3px;
|
||||
border: 1px solid $darkGrey;
|
||||
background: #fff;
|
||||
@include box-shadow(0 1px 2px rgba(0, 0, 0, .1));
|
||||
|
||||
li {
|
||||
position: relative;
|
||||
border-bottom: 1px solid $mediumGrey;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.class-link {
|
||||
z-index: 100;
|
||||
display: block;
|
||||
padding: 20px 25px;
|
||||
line-height: 1.3;
|
||||
|
||||
&:hover {
|
||||
background: $paleYellow;
|
||||
|
||||
+ .view-live-button {
|
||||
opacity: 1.0;
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.class-name {
|
||||
display: block;
|
||||
font-size: 19px;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.detail {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
margin-right: 20px;
|
||||
color: #3c3c3c;
|
||||
}
|
||||
|
||||
// view live button
|
||||
.view-live-button {
|
||||
z-index: 10000;
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
right: $baseline;
|
||||
padding: ($baseline/4) ($baseline/2);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
|
||||
&:hover {
|
||||
opacity: 1.0;
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.new-course {
|
||||
padding: 15px 25px;
|
||||
margin-top: 20px;
|
||||
border-radius: 3px;
|
||||
border: 1px solid $darkGrey;
|
||||
background: #fff;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, .1);
|
||||
@include clearfix;
|
||||
|
||||
.row {
|
||||
margin-bottom: 15px;
|
||||
@include clearfix;
|
||||
}
|
||||
|
||||
.column {
|
||||
float: left;
|
||||
width: 48%;
|
||||
}
|
||||
|
||||
.column:first-child {
|
||||
margin-right: 4%;
|
||||
}
|
||||
|
||||
.course-info {
|
||||
width: 600px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.new-course-org,
|
||||
.new-course-number,
|
||||
.new-course-name {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.new-course-name {
|
||||
font-size: 19px;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.new-course-save {
|
||||
@include blue-button;
|
||||
}
|
||||
|
||||
.new-course-cancel {
|
||||
@include white-button;
|
||||
}
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
.faded-hr-divider {
|
||||
@include background-image(linear-gradient(180deg, rgba(200,200,200, 0) 0%,
|
||||
rgba(200,200,200, 1) 50%,
|
||||
rgba(200,200,200, 0)));
|
||||
height: 1px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.faded-hr-divider-medium {
|
||||
@include background-image(linear-gradient(180deg, rgba(240,240,240, 0) 0%,
|
||||
rgba(240,240,240, 1) 50%,
|
||||
rgba(240,240,240, 0)));
|
||||
height: 1px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.faded-hr-divider-light {
|
||||
@include background-image(linear-gradient(180deg, rgba(255,255,255, 0) 0%,
|
||||
rgba(255,255,255, 0.8) 50%,
|
||||
rgba(255,255,255, 0)));
|
||||
height: 1px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.faded-vertical-divider {
|
||||
@include background-image(linear-gradient(90deg, rgba(200,200,200, 0) 0%,
|
||||
rgba(200,200,200, 1) 50%,
|
||||
rgba(200,200,200, 0)));
|
||||
height: 100%;
|
||||
width: 1px;
|
||||
}
|
||||
|
||||
.faded-vertical-divider-light {
|
||||
@include background-image(linear-gradient(90deg, rgba(255,255,255, 0) 0%,
|
||||
rgba(255,255,255, 0.6) 50%,
|
||||
rgba(255,255,255, 0)));
|
||||
height: 100%;
|
||||
width: 1px;
|
||||
}
|
||||
|
||||
.vertical-divider {
|
||||
@extend .faded-vertical-divider;
|
||||
position: relative;
|
||||
|
||||
&::after {
|
||||
@extend .faded-vertical-divider-light;
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
left: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
.horizontal-divider {
|
||||
border: none;
|
||||
@extend .faded-hr-divider;
|
||||
position: relative;
|
||||
|
||||
&::after {
|
||||
@extend .faded-hr-divider-light;
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
.fade-right-hr-divider {
|
||||
@include background-image(linear-gradient(180deg, rgba(200,200,200, 0) 0%,
|
||||
rgba(200,200,200, 1)));
|
||||
border: none;
|
||||
}
|
||||
|
||||
.fade-left-hr-divider {
|
||||
@include background-image(linear-gradient(180deg, rgba(200,200,200, 1) 0%,
|
||||
rgba(200,200,200, 0)));
|
||||
border: none;
|
||||
}
|
||||
@@ -1,126 +0,0 @@
|
||||
// This is a temporary page, which will be replaced once we have a more extensive course catalog and marketing site for edX labs.
|
||||
|
||||
.class-landing {
|
||||
|
||||
.main-wrapper {
|
||||
width: 700px !important;
|
||||
margin: 100px auto;
|
||||
}
|
||||
|
||||
.class-info {
|
||||
padding: 30px 40px 40px;
|
||||
@extend .window;
|
||||
|
||||
hgroup {
|
||||
padding-bottom: 26px;
|
||||
border-bottom: 1px solid $mediumGrey;
|
||||
}
|
||||
|
||||
h1 {
|
||||
float: none;
|
||||
font-size: 30px;
|
||||
font-weight: 300;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: #5d6779;
|
||||
}
|
||||
|
||||
.class-actions {
|
||||
@include clearfix;
|
||||
padding: 15px 0;
|
||||
margin-bottom: 18px;
|
||||
border-bottom: 1px solid $mediumGrey;
|
||||
}
|
||||
|
||||
.log-in-form {
|
||||
@include clearfix;
|
||||
padding: 15px 0 20px;
|
||||
margin-bottom: 18px;
|
||||
border-bottom: 1px solid $mediumGrey;
|
||||
|
||||
.log-in-submit-button {
|
||||
@include blue-button;
|
||||
padding: 6px 20px 8px;
|
||||
margin: 24px 0 0;
|
||||
}
|
||||
|
||||
.column {
|
||||
float: left;
|
||||
width: 41%;
|
||||
margin-right: 1%;
|
||||
|
||||
&.submit {
|
||||
width: 16%;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
label {
|
||||
float: left;
|
||||
}
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
font-family: $sans-serif;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.forgot-button {
|
||||
float: right;
|
||||
margin-bottom: 6px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.sign-up-button {
|
||||
@include blue-button;
|
||||
display: block;
|
||||
width: 250px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.log-in-button {
|
||||
@include white-button;
|
||||
float: right;
|
||||
}
|
||||
|
||||
.sign-up-button,
|
||||
.log-in-button {
|
||||
padding: 8px 0 12px;
|
||||
font-size: 18px;
|
||||
font-weight: 300;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.class-description {
|
||||
margin-top: 30px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
p + p {
|
||||
margin-top: 22px;
|
||||
}
|
||||
}
|
||||
|
||||
.edx-labs-logo-small {
|
||||
display: block;
|
||||
width: 124px;
|
||||
height: 30px;
|
||||
margin: auto;
|
||||
background: url(../img/edx-labs-logo-small.png) no-repeat;
|
||||
text-indent: -9999px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.edge-logo {
|
||||
display: block;
|
||||
width: 143px;
|
||||
height: 39px;
|
||||
margin: auto;
|
||||
background: url(../images/edge-logo-small.png) no-repeat;
|
||||
text-indent: -9999px;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
body {
|
||||
@include clearfix();
|
||||
height: 100%;
|
||||
font: 14px $body-font-family;
|
||||
background-color: lighten($dark-blue, 62%);
|
||||
background-image: url('/static/img/noise.png');
|
||||
|
||||
> section {
|
||||
display: table;
|
||||
table-layout: fixed;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
> header {
|
||||
background: $dark-blue;
|
||||
@include background-image(url('/static/img/noise.png'), linear-gradient(lighten($dark-blue, 10%), $dark-blue));
|
||||
border-bottom: 1px solid darken($dark-blue, 15%);
|
||||
@include box-shadow(inset 0 -1px 0 lighten($dark-blue, 10%));
|
||||
@include box-sizing(border-box);
|
||||
color: #fff;
|
||||
display: block;
|
||||
float: none;
|
||||
padding: 0 20px;
|
||||
text-shadow: 0 -1px 0 darken($dark-blue, 15%);
|
||||
width: 100%;
|
||||
|
||||
nav {
|
||||
@include clearfix;
|
||||
|
||||
> a {
|
||||
@include hide-text;
|
||||
background: url('/static/img/menu.png') 0 center no-repeat;
|
||||
border-right: 1px solid darken($dark-blue, 10%);
|
||||
@include box-shadow(1px 0 0 lighten($dark-blue, 10%));
|
||||
display: block;
|
||||
float: left;
|
||||
height: 19px;
|
||||
padding: 8px 10px 8px 0;
|
||||
width: 14px;
|
||||
|
||||
&:hover, &:focus {
|
||||
opacity: .7;
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
border-right: 1px solid darken($dark-blue, 10%);
|
||||
@include box-shadow(1px 0 0 lighten($dark-blue, 10%));
|
||||
float: left;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
text-transform: uppercase;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
|
||||
a {
|
||||
color: #fff;
|
||||
padding: 8px 20px;
|
||||
display: block;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(darken($dark-blue, 15%), .5);
|
||||
color: $yellow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
color: rgba(#fff, .8);
|
||||
|
||||
&:hover {
|
||||
color: rgba(#fff, .6);
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
float: left;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
@include clearfix;
|
||||
|
||||
&.user-nav {
|
||||
float: right;
|
||||
border-left: 1px solid darken($dark-blue, 10%);
|
||||
}
|
||||
|
||||
li {
|
||||
border-right: 1px solid darken($dark-blue, 10%);
|
||||
float: left;
|
||||
@include box-shadow(1px 0 0 lighten($dark-blue, 10%));
|
||||
|
||||
a {
|
||||
padding: 8px 20px;
|
||||
display: block;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(darken($dark-blue, 15%), .5);
|
||||
color: $yellow;
|
||||
}
|
||||
|
||||
&.new-module {
|
||||
&:before {
|
||||
@include inline-block;
|
||||
content: "+";
|
||||
font-weight: bold;
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.content {
|
||||
section.main-content {
|
||||
border-left: 2px solid $dark-blue;
|
||||
@include box-sizing(border-box);
|
||||
width: flex-grid(9) + flex-gutter();
|
||||
float: left;
|
||||
@include box-shadow( -2px 0 0 lighten($dark-blue, 55%));
|
||||
@include transition();
|
||||
background: #FFF;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
.component {
|
||||
font-family: 'Open Sans', Verdana, Arial, Helvetica, sans-serif;
|
||||
font-size: 16px;
|
||||
line-height: 1.6;
|
||||
color: #3c3c3c;
|
||||
|
||||
a {
|
||||
color: #1d9dd9;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 16px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
h1 {
|
||||
float: none;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: #646464;
|
||||
font-size: 19px;
|
||||
font-weight: 300;
|
||||
letter-spacing: 1px;
|
||||
margin-bottom: 15px;
|
||||
margin-left: 0;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 19px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
h4 {
|
||||
background: none;
|
||||
padding: 0;
|
||||
border: none;
|
||||
@include box-shadow(none);
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
code {
|
||||
margin: 0 2px;
|
||||
padding: 0px 5px;
|
||||
border-radius: 3px;
|
||||
border: 1px solid #eaeaea;
|
||||
white-space: nowrap;
|
||||
font-family: Monaco, monospace;
|
||||
font-size: 14px;
|
||||
background-color: #f8f8f8;
|
||||
}
|
||||
|
||||
p + h2, ul + h2, ol + h2, p + h3 {
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
p + p, ul + p, ol + p {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #3c3c3c;
|
||||
font: normal 1em/1.6em;
|
||||
margin: 0px;
|
||||
}
|
||||
}
|
||||
@@ -1,139 +0,0 @@
|
||||
.edx-studio-logo-large {
|
||||
display: block;
|
||||
width: 224px;
|
||||
height: 45px;
|
||||
margin: 100px auto 30px;
|
||||
background: url(../img/edx-studio-large.png) no-repeat;
|
||||
}
|
||||
|
||||
.sign-up-box,
|
||||
.log-in-box {
|
||||
width: 500px;
|
||||
margin: auto;
|
||||
border-radius: 3px;
|
||||
|
||||
header {
|
||||
height: 36px;
|
||||
border-radius: 3px 3px 0 0;
|
||||
border: 1px solid #2c2e33;
|
||||
@include linear-gradient(top, #686b76, #54565e);
|
||||
color: #fff;
|
||||
@include box-shadow(0 1px 1px rgba(0, 0, 0, 0.2), 0 -1px 0px rgba(255, 255, 255, 0.05) inset, 0 1px 0 rgba(255, 255, 255, .25) inset);
|
||||
|
||||
h1 {
|
||||
float: none;
|
||||
margin: 5px 0;
|
||||
font-size: 15px;
|
||||
font-weight: 300;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
form {
|
||||
padding: 40px;
|
||||
border: 1px solid $darkGrey;
|
||||
border-top-width: 0;
|
||||
border-radius: 0 0 3px 3px;
|
||||
background: #fff;
|
||||
@include box-shadow(0 1px 2px rgba(0, 0, 0, .1));
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
input[type="text"],
|
||||
input[type="email"],
|
||||
input[type="password"] {
|
||||
width: 100%;
|
||||
font-size: 20px;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.row {
|
||||
@include clearfix;
|
||||
margin-bottom: 24px;
|
||||
|
||||
.split {
|
||||
float: left;
|
||||
width: 48%;
|
||||
|
||||
&:first-child {
|
||||
margin-right: 4%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
@include clearfix;
|
||||
margin-top: 32px;
|
||||
margin-bottom: 5px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.log-in-button,
|
||||
.create-account-button {
|
||||
@include blue-button;
|
||||
padding: 8px 0 10px;
|
||||
font-family: $sans-serif;
|
||||
@include transition(all .15s);
|
||||
}
|
||||
|
||||
.create-account-button {
|
||||
padding: 10px 40px 12px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.enrolled {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.sign-up-button {
|
||||
@include white-button;
|
||||
padding: 7px 0 9px;
|
||||
}
|
||||
|
||||
.log-in-button,
|
||||
.sign-up-button {
|
||||
@include box-sizing(border-box);
|
||||
float: left;
|
||||
width: 45%;
|
||||
}
|
||||
|
||||
.or {
|
||||
float: left;
|
||||
display: inline-block;
|
||||
width: 10%;
|
||||
font-size: 15px;
|
||||
line-height: 36px;
|
||||
color: $darkGrey;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.forgot-button {
|
||||
float: right;
|
||||
font-size: 11px;
|
||||
font-weight: 400;
|
||||
line-height: 21px;
|
||||
}
|
||||
|
||||
.log-in-extra {
|
||||
margin-top: 10px;
|
||||
text-align: right;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
#login_error,
|
||||
#register_error {
|
||||
display: none;
|
||||
margin-bottom: 30px;
|
||||
padding: 5px 10px;
|
||||
border-radius: 3px;
|
||||
background: $error-red;
|
||||
font-size: 14px;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
@@ -1,128 +0,0 @@
|
||||
section.video-new, section.video-edit, section.problem-new, section.problem-edit {
|
||||
position: absolute;
|
||||
top: 72px;
|
||||
right: 0;
|
||||
background: #fff;
|
||||
width: flex-grid(6);
|
||||
@include box-shadow(0 0 6px #666);
|
||||
border: 1px solid #333;
|
||||
border-right: 0;
|
||||
z-index: 4;
|
||||
|
||||
> header {
|
||||
background: #666;
|
||||
@include clearfix;
|
||||
color: #fff;
|
||||
padding: 6px;
|
||||
border-bottom: 1px solid #333;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
|
||||
h2 {
|
||||
float: left;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #fff;
|
||||
|
||||
&.save-update {
|
||||
float: right;
|
||||
}
|
||||
|
||||
&.cancel {
|
||||
float: left;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
> section {
|
||||
padding: 20px;
|
||||
|
||||
> header {
|
||||
h1 {
|
||||
font-size: 24px;
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
||||
section {
|
||||
&.status-settings {
|
||||
ul {
|
||||
list-style: none;
|
||||
@include border-radius(2px);
|
||||
border: 1px solid #999;
|
||||
@include inline-block();
|
||||
|
||||
li {
|
||||
@include inline-block();
|
||||
border-right: 1px solid #999;
|
||||
padding: 6px;
|
||||
|
||||
&:last-child {
|
||||
border-right: 0;
|
||||
}
|
||||
|
||||
&.current {
|
||||
background: #eee;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a.settings {
|
||||
@include inline-block();
|
||||
margin: 0 20px;
|
||||
border: 1px solid #999;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
select {
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
|
||||
&.meta {
|
||||
background: #eee;
|
||||
padding: 10px;
|
||||
margin: 20px 0;
|
||||
@include clearfix();
|
||||
|
||||
div {
|
||||
float: left;
|
||||
margin-right: 20px;
|
||||
|
||||
h2 {
|
||||
font-size: 14px;
|
||||
@include inline-block();
|
||||
}
|
||||
|
||||
p {
|
||||
@include inline-block();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
section.notes {
|
||||
margin-top: 20px;
|
||||
padding: 6px;
|
||||
background: #eee;
|
||||
border: 1px solid #ccc;
|
||||
|
||||
textarea {
|
||||
@include box-sizing(border-box);
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 14px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
input[type="submit"]{
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
section.problem-new, section.problem-edit {
|
||||
> section {
|
||||
textarea {
|
||||
@include box-sizing(border-box);
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
div.preview {
|
||||
background: #eee;
|
||||
@include box-sizing(border-box);
|
||||
height: 40px;
|
||||
padding: 10px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
a.save {
|
||||
@extend .button;
|
||||
@include inline-block();
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
// studio - utilities - reset
|
||||
// ====================
|
||||
|
||||
// * {
|
||||
// @include box-sizing(border-box);
|
||||
// }
|
||||
|
||||
html, body, div, span, applet, object, iframe,
|
||||
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
|
||||
a, abbr, acronym, address, big, cite, code,
|
||||
@@ -18,7 +25,7 @@ time, mark, audio, video {
|
||||
font: inherit;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
/* HTML5 display-role reset for older browsers */
|
||||
|
||||
article, aside, details, figcaption, figure,
|
||||
footer, header, hgroup, menu, nav, section {
|
||||
display: block;
|
||||
@@ -38,12 +45,6 @@ q:before, q:after {
|
||||
content: none;
|
||||
}
|
||||
|
||||
/* remember to define visible focus styles!
|
||||
:focus {
|
||||
outline: ?????;
|
||||
} */
|
||||
|
||||
/* remember to highlight inserts somehow! */
|
||||
ins {
|
||||
text-decoration: none;
|
||||
}
|
||||
@@ -56,10 +57,11 @@ table {
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
/* Reset styles to remove ui-lightness jquery ui theme
|
||||
from the tabs component (used in the add component problem tab menu)
|
||||
*/
|
||||
// ====================
|
||||
|
||||
// grandfathered styles
|
||||
|
||||
// reset styles to remove ui-lightness jquery ui theme from the tabs component (used in the add component problem tab menu)
|
||||
.ui-tabs {
|
||||
padding: 0;
|
||||
white-space: normal;
|
||||
@@ -118,10 +120,7 @@ from the tabs component (used in the add component problem tab menu)
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* reapplying the tab styles from unit.scss after
|
||||
removing jquery ui ui-lightness styling
|
||||
*/
|
||||
|
||||
// reapplying the tab styles from unit.scss after removing jquery ui ui-lightness styling
|
||||
.problem-type-tabs {
|
||||
border:none;
|
||||
list-style-type: none;
|
||||
@@ -146,26 +145,4 @@ removing jquery ui ui-lightness styling
|
||||
border: 0px;
|
||||
}
|
||||
}
|
||||
/*
|
||||
li {
|
||||
float:left;
|
||||
display:inline-block;
|
||||
text-align:center;
|
||||
width: auto;
|
||||
//@include linear-gradient(top, rgba(255, 255, 255, .4), rgba(255, 255, 255, 0));
|
||||
//background-color: tint($lightBluishGrey, 20%);
|
||||
//@include box-shadow(0 1px 0 rgba(255, 255, 255, 0.2) inset, 0 -1px 0 rgba(0, 0, 0, 0.2) inset);
|
||||
opacity:.8;
|
||||
|
||||
&:hover {
|
||||
opacity:1;
|
||||
}
|
||||
|
||||
&.current {
|
||||
border: 0px;
|
||||
//@include active;
|
||||
opacity:1;
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
@@ -1,239 +0,0 @@
|
||||
section#unit-wrapper {
|
||||
section.filters {
|
||||
@include clearfix;
|
||||
display: none;
|
||||
opacity: .4;
|
||||
margin-bottom: 10px;
|
||||
@include transition;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
h2 {
|
||||
@include inline-block();
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
font-size: 14px;
|
||||
padding: 6px 6px 6px 0;
|
||||
font-size: 12px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
ul {
|
||||
@include clearfix();
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
li {
|
||||
@include inline-block;
|
||||
margin-right: 6px;
|
||||
border-right: 1px solid #ddd;
|
||||
padding-right: 6px;
|
||||
|
||||
&.search {
|
||||
float: right;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
a {
|
||||
&.more {
|
||||
font-size: 12px;
|
||||
@include inline-block;
|
||||
margin: 0 6px;
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
div.content {
|
||||
display: table;
|
||||
border: 1px solid lighten($dark-blue, 40%);
|
||||
width: 100%;
|
||||
@include border-radius(3px);
|
||||
@include box-shadow(0 0 4px lighten($dark-blue, 50%));
|
||||
|
||||
section {
|
||||
header {
|
||||
background: #fff;
|
||||
padding: 6px;
|
||||
border-bottom: 1px solid lighten($dark-blue, 60%);
|
||||
@include clearfix;
|
||||
|
||||
h2 {
|
||||
color: $bright-blue;
|
||||
// float: left;
|
||||
font-size: 14px;
|
||||
letter-spacing: 1px;
|
||||
// line-height: 20px;
|
||||
text-transform: uppercase;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.modules {
|
||||
@include box-sizing(border-box);
|
||||
display: table-cell;
|
||||
width: flex-grid(6, 9);
|
||||
border-right: 1px solid lighten($dark-blue, 40%);
|
||||
|
||||
&.empty {
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
|
||||
a {
|
||||
@extend .button;
|
||||
@include inline-block();
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
ol {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
li {
|
||||
border-bottom: 1px solid lighten($dark-blue, 60%);
|
||||
|
||||
a {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
ol {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
li {
|
||||
padding: 6px;
|
||||
position: relative;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: lighten($yellow, 10%);
|
||||
|
||||
a.draggable {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
a.draggable {
|
||||
float: right;
|
||||
opacity: .4;
|
||||
}
|
||||
|
||||
&.group {
|
||||
padding: 0;
|
||||
|
||||
header {
|
||||
padding: 6px;
|
||||
background: none;
|
||||
|
||||
h3 {
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
ol {
|
||||
border-left: 4px solid #999;
|
||||
border-bottom: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
li {
|
||||
&:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.scratch-pad {
|
||||
@include box-sizing(border-box);
|
||||
display: table-cell;
|
||||
width: flex-grid(3, 9) + flex-gutter(9);
|
||||
vertical-align: top;
|
||||
|
||||
ol {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
li {
|
||||
background: $light-blue;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
&.new-module a {
|
||||
background-color: darken($light-blue, 2%);
|
||||
border-bottom: 1px solid darken($light-blue, 8%);
|
||||
|
||||
&:hover {
|
||||
background-color: lighten($yellow, 10%);
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
color: $dark-blue;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
li {
|
||||
padding: 6px;
|
||||
border-collapse: collapse;
|
||||
border-bottom: 1px solid darken($light-blue, 8%);
|
||||
position: relative;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: 1px solid darken($light-blue, 8%);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: lighten($yellow, 10%);
|
||||
|
||||
a.draggable {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
&.empty {
|
||||
padding: 12px;
|
||||
|
||||
a {
|
||||
@extend .button;
|
||||
display: block;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
a.draggable {
|
||||
opacity: .3;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,295 +0,0 @@
|
||||
.subsection .main-wrapper {
|
||||
margin: 40px;
|
||||
}
|
||||
|
||||
.subsection .inner-wrapper {
|
||||
@include clearfix();
|
||||
}
|
||||
|
||||
.subsection-body {
|
||||
padding: 32px 40px;
|
||||
@include clearfix;
|
||||
|
||||
> div {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
input {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.unit-subtitle {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sortable-unit-list {
|
||||
ol {
|
||||
@include tree-view;
|
||||
}
|
||||
}
|
||||
|
||||
.policy-list {
|
||||
input[disabled] {
|
||||
border: none;
|
||||
@include box-shadow(none);
|
||||
}
|
||||
|
||||
.policy-list-name {
|
||||
margin-right: 5px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.policy-list-value {
|
||||
width: 320px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.policy-list-element {
|
||||
.save-button,
|
||||
.cancel-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.edit-icon {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
&.editing,
|
||||
&.new-policy-list-element {
|
||||
.policy-list-name,
|
||||
.policy-list-value {
|
||||
border: 1px solid #b0b6c2;
|
||||
@include linear-gradient(top, rgba(255, 255, 255, 0), rgba(255, 255, 255, .3));
|
||||
background-color: #edf1f5;
|
||||
@include box-shadow(0 1px 2px rgba(0, 0, 0, .1) inset);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.new-policy-list-element {
|
||||
padding: 10px 10px 0;
|
||||
margin: 0 -10px 10px;
|
||||
border-radius: 3px;
|
||||
background: $mediumGrey;
|
||||
|
||||
.save-button {
|
||||
@include blue-button;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.cancel-button {
|
||||
@include white-button;
|
||||
}
|
||||
|
||||
.edit-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.delete-icon {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.new-policy-item {
|
||||
margin: 10px 0;
|
||||
|
||||
.plus-icon-small {
|
||||
position: relative;
|
||||
top: -1px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.subsection-name-input {
|
||||
label {
|
||||
display: block;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.scheduled-date-input,
|
||||
.due-date-input {
|
||||
@include clearfix;
|
||||
|
||||
.date-input,
|
||||
.time-input {
|
||||
display: inline-block;
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.inherits-check {
|
||||
label {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
.notice {
|
||||
margin-top: 6px;
|
||||
font-size: 11px;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.due-date-input {
|
||||
label {
|
||||
display: inline-block !important;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
a {
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.date-setter {
|
||||
@include clearfix;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.remove-date {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.row.visibility {
|
||||
label {
|
||||
display: inline-block !important;
|
||||
margin-right: 10px;
|
||||
line-height: 21px;
|
||||
}
|
||||
|
||||
a {
|
||||
display: inline-block;
|
||||
height: 31px;
|
||||
margin-right: 8px;
|
||||
vertical-align: middle;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
line-height: 31px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.large-toggle {
|
||||
width: 41px;
|
||||
background: url(../img/large-toggles.png) no-repeat;
|
||||
background-position: 0 -50px;
|
||||
|
||||
.hidden {
|
||||
background-position: 0 -5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.gradable {
|
||||
|
||||
label {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.gradable-status {
|
||||
position: relative;
|
||||
top: -4px;
|
||||
display: inline-block;
|
||||
margin-left: 10px;
|
||||
width: 65%;
|
||||
|
||||
.status-label {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
color: $blue;
|
||||
border: none;
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.menu-toggle {
|
||||
z-index: 100;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
background: transparent;
|
||||
|
||||
&:hover, &.is-active {
|
||||
color: $blue;
|
||||
}
|
||||
}
|
||||
|
||||
.menu {
|
||||
z-index: 1;
|
||||
position: absolute;
|
||||
top: -12px;
|
||||
left: -7px;
|
||||
display: none;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 8px 12px;
|
||||
opacity: 0.0;
|
||||
background: $white;
|
||||
border: 1px solid $mediumGrey;
|
||||
font-size: 12px;
|
||||
@include border-radius(4px);
|
||||
@include box-shadow(0 1px 2px rgba(0, 0, 0, .2));
|
||||
@include transition(opacity .15s);
|
||||
|
||||
|
||||
li {
|
||||
margin-bottom: 3px;
|
||||
padding-bottom: 3px;
|
||||
border-bottom: 1px solid $lightGrey;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
|
||||
&.is-selected {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// dropdown state
|
||||
&.is-active {
|
||||
|
||||
.menu {
|
||||
z-index: 10000;
|
||||
display: block;
|
||||
opacity: 1.0;
|
||||
}
|
||||
|
||||
.menu-toggle {
|
||||
z-index: 1000;
|
||||
}
|
||||
}
|
||||
|
||||
// set state
|
||||
&.is-set {
|
||||
|
||||
.menu-toggle {
|
||||
color: $blue;
|
||||
}
|
||||
|
||||
.status-label {
|
||||
display: block;
|
||||
color: $blue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,667 +0,0 @@
|
||||
.unit .main-wrapper {
|
||||
@include clearfix();
|
||||
margin: 40px;
|
||||
}
|
||||
|
||||
//Problem Selector tab menu requirements
|
||||
.js .tabs .tab {
|
||||
display: none;
|
||||
}
|
||||
//end problem selector reqs
|
||||
|
||||
.main-column {
|
||||
clear: both;
|
||||
float: left;
|
||||
width: 70%;
|
||||
}
|
||||
|
||||
.unit-body.published {
|
||||
.components > li {
|
||||
border: none;
|
||||
|
||||
.rendered-component {
|
||||
padding: 0 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.unit-body {
|
||||
.breadcrumbs {
|
||||
border-radius: 3px 3px 0 0;
|
||||
border-bottom: 1px solid #cbd1db;
|
||||
@include linear-gradient(top, rgba(255, 255, 255, .3), rgba(255, 255, 255, 0) 70%);
|
||||
background-color: #edf1f5;
|
||||
@include box-shadow(0 1px 0 rgba(255, 255, 255, .7) inset);
|
||||
@include clearfix;
|
||||
|
||||
li {
|
||||
float: left;
|
||||
}
|
||||
|
||||
a,
|
||||
.current-page {
|
||||
display: block;
|
||||
padding: 15px 35px 15px 30px;
|
||||
font-size: 14px;
|
||||
background: url(../img/breadcrumb-arrow.png) no-repeat right center;
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 30px 40px 30px 0;
|
||||
color: #646464;
|
||||
font-size: 19px;
|
||||
font-weight: 300;
|
||||
letter-spacing: 1px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.components {
|
||||
|
||||
> li {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
margin: 20px 40px;
|
||||
|
||||
|
||||
|
||||
.title {
|
||||
margin: 0 0 15px 0;
|
||||
color: $mediumGrey;
|
||||
|
||||
.value {
|
||||
}
|
||||
}
|
||||
|
||||
&.new-component-item {
|
||||
margin: 20px 0px;
|
||||
border-top: 1px solid $mediumGrey;
|
||||
box-shadow: 0 2px 1px rgba(182, 182, 182, 0.75) inset;
|
||||
background-color: $lightGrey;
|
||||
margin-bottom: 0px;
|
||||
padding-bottom: 20px;
|
||||
|
||||
.new-component-button {
|
||||
display: block;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
color: #edf1f5;
|
||||
}
|
||||
|
||||
h5 {
|
||||
margin: 20px 0px;
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.rendered-component {
|
||||
display: none;
|
||||
background: #fff;
|
||||
border-radius: 3px 3px 0 0;
|
||||
}
|
||||
|
||||
.new-component-type {
|
||||
|
||||
a,
|
||||
li {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
a {
|
||||
border: 1px solid $mediumGrey;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
color: #fff;
|
||||
margin-right: 15px;
|
||||
margin-bottom: 20px;
|
||||
border-radius: 8px;
|
||||
font-size: 15px;
|
||||
line-height: 14px;
|
||||
text-align: center;
|
||||
@include box-shadow(0 1px 1px rgba(0, 0, 0, .2), 0 1px 0 rgba(255, 255, 255, .4) inset);
|
||||
|
||||
.name {
|
||||
position: absolute;
|
||||
bottom: 5px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
@include box-sizing(border-box);
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.new-component-templates {
|
||||
display: none;
|
||||
margin: 20px 40px 20px 40px;
|
||||
border-radius: 3px;
|
||||
border: 1px solid $mediumGrey;
|
||||
background-color: #fff;
|
||||
@include box-shadow(0 1px 1px rgba(0, 0, 0, .2), 0 1px 0 rgba(255, 255, 255, .4) inset);
|
||||
@include clearfix;
|
||||
|
||||
.cancel-button {
|
||||
margin: 20px 0px 10px 10px;
|
||||
@include white-button;
|
||||
}
|
||||
|
||||
.problem-type-tabs {
|
||||
display: none;
|
||||
}
|
||||
|
||||
// specific menu types
|
||||
&.new-component-problem {
|
||||
padding-bottom:10px;
|
||||
|
||||
.ss-icon, .editor-indicator {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.problem-type-tabs {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.new-component-type,
|
||||
.new-component-template {
|
||||
@include clearfix;
|
||||
|
||||
a {
|
||||
position: relative;
|
||||
border: 1px solid $darkGreen;
|
||||
background: tint($green,20%);
|
||||
color: #fff;
|
||||
|
||||
&:hover {
|
||||
background: $brightGreen;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.problem-type-tabs {
|
||||
list-style-type: none;
|
||||
border-radius: 0;
|
||||
width: 100%;
|
||||
@include linear-gradient(top, rgba(255, 255, 255, .4), rgba(255, 255, 255, 0));
|
||||
background-color: $lightBluishGrey;
|
||||
@include box-shadow(0 1px 0 rgba(255, 255, 255, 0.2) inset, 0 -1px 0 rgba(0, 0, 0, 0.2) inset);
|
||||
|
||||
li:first-child {
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
li {
|
||||
float:left;
|
||||
display:inline-block;
|
||||
text-align:center;
|
||||
width: auto;
|
||||
@include linear-gradient(top, rgba(255, 255, 255, .4), rgba(255, 255, 255, 0));
|
||||
background-color: tint($lightBluishGrey, 10%);
|
||||
@include box-shadow(0 1px 0 rgba(255, 255, 255, 0.2) inset, 0 -1px 0 rgba(0, 0, 0, 0.2) inset);
|
||||
opacity:.8;
|
||||
|
||||
&:hover {
|
||||
opacity:1;
|
||||
background-color: tint($lightBluishGrey, 20%);
|
||||
}
|
||||
|
||||
&.ui-state-active {
|
||||
border: 0px;
|
||||
@include active;
|
||||
opacity:1;
|
||||
}
|
||||
}
|
||||
|
||||
a{
|
||||
display: block;
|
||||
padding: 15px 25px;
|
||||
font-size: 15px;
|
||||
line-height: 16px;
|
||||
text-align: center;
|
||||
color: #3c3c3c;
|
||||
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
.new-component-template {
|
||||
|
||||
a {
|
||||
background: #fff;
|
||||
border: 0px;
|
||||
color: #3c3c3c;
|
||||
@include transition (none);
|
||||
|
||||
&:hover {
|
||||
background: tint($green,30%);
|
||||
color: #fff;
|
||||
@include transition(background-color .15s);
|
||||
}
|
||||
}
|
||||
|
||||
li {
|
||||
border:none;
|
||||
border-bottom: 1px dashed $lightGrey;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
li:first-child {
|
||||
a {
|
||||
border-top: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
li:nth-child(2) {
|
||||
a {
|
||||
border-radius: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
@include clearfix();
|
||||
display: block;
|
||||
padding: 7px 20px;
|
||||
border-bottom: none;
|
||||
font-weight: 500;
|
||||
|
||||
.name {
|
||||
float: left;
|
||||
|
||||
.ss-icon {
|
||||
@include transition(opacity .15s);
|
||||
display: inline-block;
|
||||
top: 1px;
|
||||
margin-right: 5px;
|
||||
opacity: 0.5;
|
||||
width: 17;
|
||||
height: 21px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
.editor-indicator {
|
||||
@include transition(opacity .15s);
|
||||
float: right;
|
||||
position: relative;
|
||||
top: 3px;
|
||||
font-size: 12px;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.ss-icon, .editor-indicator {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: #fff;
|
||||
|
||||
.ss-icon {
|
||||
opacity: 1.0;
|
||||
}
|
||||
|
||||
.editor-indicator {
|
||||
opacity: 1.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// specific editor types
|
||||
.empty {
|
||||
|
||||
a {
|
||||
line-height: 1.4;
|
||||
font-weight: 400;
|
||||
background: #fff;
|
||||
color: #3c3c3c;
|
||||
|
||||
|
||||
&:hover {
|
||||
background: tint($green,30%);
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.new-component {
|
||||
text-align: center;
|
||||
|
||||
h5 {
|
||||
color: $darkGreen;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.component {
|
||||
border: 1px solid $lightBluishGrey2;
|
||||
border-radius: 3px;
|
||||
background: #fff;
|
||||
@include transition(none);
|
||||
|
||||
&:hover {
|
||||
border-color: #6696d7;
|
||||
|
||||
.drag-handle {
|
||||
background-color: $blue;
|
||||
border-color: $blue;
|
||||
}
|
||||
}
|
||||
|
||||
&.editing {
|
||||
border: 1px solid $lightBluishGrey2;
|
||||
z-index: auto;
|
||||
|
||||
.drag-handle,
|
||||
.component-actions {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.component-placeholder {
|
||||
border-color: #6696d7;
|
||||
}
|
||||
|
||||
.component-actions {
|
||||
position: absolute;
|
||||
top: 7px;
|
||||
right: 9px;
|
||||
}
|
||||
|
||||
.drag-handle {
|
||||
position: absolute;
|
||||
display: block;
|
||||
top: -1px;
|
||||
right: -16px;
|
||||
z-index: 10;
|
||||
width: 15px;
|
||||
height: 100%;
|
||||
border-radius: 0 3px 3px 0;
|
||||
border: 1px solid $lightBluishGrey2;
|
||||
background: url(../img/white-drag-handles.png) center no-repeat $lightBluishGrey2;
|
||||
cursor: move;
|
||||
@include transition(none);
|
||||
}
|
||||
}
|
||||
|
||||
.xmodule_display {
|
||||
padding: 40px 20px 20px;
|
||||
overflow-x: auto;
|
||||
|
||||
h1 {
|
||||
float: none;
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.wrapper-component-editor {
|
||||
z-index: 9999;
|
||||
position: relative;
|
||||
background: $lightBluishGrey2;
|
||||
}
|
||||
|
||||
.component-editor {
|
||||
@include edit-box;
|
||||
@include box-shadow(none);
|
||||
display: none;
|
||||
padding: 20px;
|
||||
border-radius: 2px 2px 0 0;
|
||||
|
||||
.metadata_edit {
|
||||
margin-bottom: 20px;
|
||||
font-size: 13px;
|
||||
|
||||
li {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: inline-block;
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin-bottom: 10px;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
h5 {
|
||||
margin-bottom: 8px;
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.save-button {
|
||||
margin-top: 10px;
|
||||
margin: 15px 8px 0 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.unit-settings {
|
||||
.window-contents {
|
||||
padding: 10px 20px;
|
||||
}
|
||||
|
||||
.unit-actions {
|
||||
border-bottom: none;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.published-alert {
|
||||
display: none;
|
||||
padding: 10px;
|
||||
border: 1px solid #edbd3c;
|
||||
border-radius: 3px;
|
||||
background: #fbf6e1;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
|
||||
div {
|
||||
margin-top: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
input[type="radio"] {
|
||||
margin-right: 7px;
|
||||
}
|
||||
|
||||
.status {
|
||||
font-size: 12px;
|
||||
|
||||
strong {
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
.preview-button, .view-button {
|
||||
@include white-button;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.publish-button {
|
||||
@include orange-button;
|
||||
}
|
||||
|
||||
.delete-button {
|
||||
@include blue-button;
|
||||
}
|
||||
|
||||
.delete-draft {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.delete-button,
|
||||
.preview-button,
|
||||
.publish-button,
|
||||
.view-button {
|
||||
font-size: 11px;
|
||||
margin-top: 10px;
|
||||
padding: 6px 15px 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.unit-history {
|
||||
&.collapsed {
|
||||
h4 {
|
||||
border-bottom: none;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.window-contents {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
ol {
|
||||
border: 1px solid #ced2db;
|
||||
|
||||
li {
|
||||
display: block;
|
||||
padding: 6px 8px 8px 10px;
|
||||
background: #edf1f5;
|
||||
font-size: 12px;
|
||||
|
||||
&:hover {
|
||||
background: #fffcf1;
|
||||
|
||||
.item-actions {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
&.checked {
|
||||
background: #d1dae3;
|
||||
}
|
||||
|
||||
.item-actions {
|
||||
display: none;
|
||||
}
|
||||
|
||||
input[type="radio"] {
|
||||
margin-right: 7px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.unit-location {
|
||||
.url {
|
||||
width: 100%;
|
||||
margin-bottom: 10px;
|
||||
@include box-shadow(none);
|
||||
}
|
||||
|
||||
.draft-tag,
|
||||
.hidden-tag,
|
||||
.private-tag,
|
||||
.has-new-draft-tag {
|
||||
font-size: 8px;
|
||||
}
|
||||
|
||||
.window-contents > ol {
|
||||
@include tree-view;
|
||||
|
||||
.section-item {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
font-size: 11px;
|
||||
padding: 2px 8px 4px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
@include box-sizing(border-box);
|
||||
}
|
||||
|
||||
ol {
|
||||
.section-item {
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.new-unit-item {
|
||||
margin-left: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
ol ol {
|
||||
.section-item {
|
||||
padding-left: 34px;
|
||||
}
|
||||
|
||||
.new-unit-item {
|
||||
margin: 0 0 10px 41px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.edit-state-draft {
|
||||
.visibility,
|
||||
|
||||
.edit-draft-message,
|
||||
.view-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.published-alert {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.edit-state-public {
|
||||
.delete-draft,
|
||||
.component-actions,
|
||||
.new-component-item,
|
||||
.editing-draft-alert,
|
||||
.publish-draft-message,
|
||||
.preview-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.published-alert {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.drag-handle {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.edit-state-private {
|
||||
.delete-draft,
|
||||
.publish-draft,
|
||||
.editing-draft-alert,
|
||||
.create-draft,
|
||||
.view-button {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
// editing units from courseware
|
||||
body.unit {
|
||||
|
||||
.component {
|
||||
padding-top: 30px;
|
||||
|
||||
.component-actions {
|
||||
@include box-sizing(border-box);
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
padding: 15px;
|
||||
top: 0;
|
||||
left: 0;
|
||||
border-bottom: 1px solid $lightBluishGrey2;
|
||||
background: $lightGrey;
|
||||
}
|
||||
|
||||
&.editing {
|
||||
padding-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
// studio - utilities - variables
|
||||
// ====================
|
||||
|
||||
$baseline: 20px;
|
||||
|
||||
// grid
|
||||
@@ -12,11 +15,18 @@ $fg-min-width: 900px;
|
||||
// type
|
||||
$sans-serif: 'Open Sans', $verdana;
|
||||
$body-line-height: golden-ratio(.875em, 1);
|
||||
$error-red: rgb(253, 87, 87);
|
||||
|
||||
// colors - new for re-org
|
||||
$black: rgb(0,0,0);
|
||||
$black-t0: rgba(0,0,0,0.125);
|
||||
$black-t1: rgba(0,0,0,0.25);
|
||||
$black-t2: rgba(0,0,0,0.50);
|
||||
$black-t3: rgba(0,0,0,0.75);
|
||||
$white: rgb(255,255,255);
|
||||
$white-t0: rgba(255,255,255,0.125);
|
||||
$white-t1: rgba(255,255,255,0.25);
|
||||
$white-t2: rgba(255,255,255,0.50);
|
||||
$white-t3: rgba(255,255,255,0.75);
|
||||
|
||||
$gray: rgb(127,127,127);
|
||||
$gray-l1: tint($gray,20%);
|
||||
@@ -39,6 +49,12 @@ $blue-d1: shade($blue,20%);
|
||||
$blue-d2: shade($blue,40%);
|
||||
$blue-d3: shade($blue,60%);
|
||||
$blue-d4: shade($blue,80%);
|
||||
$blue-s1: saturate($blue,15%);
|
||||
$blue-s2: saturate($blue,30%);
|
||||
$blue-s3: saturate($blue,45%);
|
||||
$blue-u1: desaturate($blue,15%);
|
||||
$blue-u2: desaturate($blue,30%);
|
||||
$blue-u3: desaturate($blue,45%);
|
||||
|
||||
$pink: rgb(183, 37, 103);
|
||||
$pink-l1: tint($pink,20%);
|
||||
@@ -50,6 +66,29 @@ $pink-d1: shade($pink,20%);
|
||||
$pink-d2: shade($pink,40%);
|
||||
$pink-d3: shade($pink,60%);
|
||||
$pink-d4: shade($pink,80%);
|
||||
$pink-s1: saturate($pink,15%);
|
||||
$pink-s2: saturate($pink,30%);
|
||||
$pink-s3: saturate($pink,45%);
|
||||
$pink-u1: desaturate($pink,15%);
|
||||
$pink-u2: desaturate($pink,30%);
|
||||
$pink-u3: desaturate($pink,45%);
|
||||
|
||||
$red: rgb(178, 6, 16);
|
||||
$red-l1: tint($red,20%);
|
||||
$red-l2: tint($red,40%);
|
||||
$red-l3: tint($red,60%);
|
||||
$red-l4: tint($red,80%);
|
||||
$red-l5: tint($red,90%);
|
||||
$red-d1: shade($red,20%);
|
||||
$red-d2: shade($red,40%);
|
||||
$red-d3: shade($red,60%);
|
||||
$red-d4: shade($red,80%);
|
||||
$red-s1: saturate($red,15%);
|
||||
$red-s2: saturate($red,30%);
|
||||
$red-s3: saturate($red,45%);
|
||||
$red-u1: desaturate($red,15%);
|
||||
$red-u2: desaturate($red,30%);
|
||||
$red-u3: desaturate($red,45%);
|
||||
|
||||
$green: rgb(37, 184, 90);
|
||||
$green-l1: tint($green,20%);
|
||||
@@ -61,6 +100,12 @@ $green-d1: shade($green,20%);
|
||||
$green-d2: shade($green,40%);
|
||||
$green-d3: shade($green,60%);
|
||||
$green-d4: shade($green,80%);
|
||||
$green-s1: saturate($green,15%);
|
||||
$green-s2: saturate($green,30%);
|
||||
$green-s3: saturate($green,45%);
|
||||
$green-u1: desaturate($green,15%);
|
||||
$green-u2: desaturate($green,30%);
|
||||
$green-u3: desaturate($green,45%);
|
||||
|
||||
$yellow: rgb(231, 214, 143);
|
||||
$yellow-l1: tint($yellow,20%);
|
||||
@@ -72,6 +117,29 @@ $yellow-d1: shade($yellow,20%);
|
||||
$yellow-d2: shade($yellow,40%);
|
||||
$yellow-d3: shade($yellow,60%);
|
||||
$yellow-d4: shade($yellow,80%);
|
||||
$yellow-s1: saturate($yellow,15%);
|
||||
$yellow-s2: saturate($yellow,30%);
|
||||
$yellow-s3: saturate($yellow,45%);
|
||||
$yellow-u1: desaturate($yellow,15%);
|
||||
$yellow-u2: desaturate($yellow,30%);
|
||||
$yellow-u3: desaturate($yellow,45%);
|
||||
|
||||
$orange: rgb(237, 189, 60);
|
||||
$orange-l1: tint($orange,20%);
|
||||
$orange-l2: tint($orange,40%);
|
||||
$orange-l3: tint($orange,60%);
|
||||
$orange-l4: tint($orange,80%);
|
||||
$orange-l5: tint($orange,90%);
|
||||
$orange-d1: shade($orange,20%);
|
||||
$orange-d2: shade($orange,40%);
|
||||
$orange-d3: shade($orange,60%);
|
||||
$orange-d4: shade($orange,80%);
|
||||
$orange-s1: saturate($orange,15%);
|
||||
$orange-s2: saturate($orange,30%);
|
||||
$orange-s3: saturate($orange,45%);
|
||||
$orange-u1: desaturate($orange,15%);
|
||||
$orange-u2: desaturate($orange,30%);
|
||||
$orange-u3: desaturate($orange,45%);
|
||||
|
||||
$shadow: rgba(0,0,0,0.2);
|
||||
$shadow-l1: rgba(0,0,0,0.1);
|
||||
@@ -80,8 +148,6 @@ $shadow-d1: rgba(0,0,0,0.4);
|
||||
// colors - inherited
|
||||
$baseFontColor: #3c3c3c;
|
||||
$offBlack: #3c3c3c;
|
||||
$orange: #edbd3c;
|
||||
$red: #b20610;
|
||||
$green: #108614;
|
||||
$lightGrey: #edf1f5;
|
||||
$mediumGrey: #b0b6c2;
|
||||
@@ -94,4 +160,5 @@ $brightGreen: rgb(22, 202, 87);
|
||||
$disabledGreen: rgb(124, 206, 153);
|
||||
$darkGreen: rgb(52, 133, 76);
|
||||
$lightBluishGrey: rgb(197, 207, 223);
|
||||
$lightBluishGrey2: rgb(213, 220, 228);
|
||||
$lightBluishGrey2: rgb(213, 220, 228);
|
||||
$error-red: rgb(253, 87, 87);
|
||||
@@ -1,33 +0,0 @@
|
||||
section.video-new, section.video-edit {
|
||||
> section {
|
||||
|
||||
section.upload {
|
||||
padding: 6px;
|
||||
margin-bottom: 10px;
|
||||
border: 1px solid #ddd;
|
||||
|
||||
a.upload-button {
|
||||
@extend .button;
|
||||
@include inline-block();
|
||||
}
|
||||
}
|
||||
|
||||
section.in-use {
|
||||
h2 {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
div {
|
||||
background: #eee;
|
||||
text-align: center;
|
||||
padding: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
a.save-update {
|
||||
@extend .button;
|
||||
@include inline-block();
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,256 +0,0 @@
|
||||
section.week-edit,
|
||||
section.week-new,
|
||||
section.sequence-edit {
|
||||
|
||||
> header {
|
||||
border-bottom: 2px solid #333;
|
||||
@include clearfix();
|
||||
|
||||
div {
|
||||
@include clearfix();
|
||||
padding: 6px 20px;
|
||||
|
||||
h1 {
|
||||
font-size: 18px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
float: left;
|
||||
}
|
||||
|
||||
p {
|
||||
float: right;
|
||||
}
|
||||
|
||||
&.week {
|
||||
background: #eee;
|
||||
font-size: 12px;
|
||||
border-bottom: 1px solid #ccc;
|
||||
|
||||
h2 {
|
||||
font-size: 12px;
|
||||
@include inline-block();
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
@include inline-block();
|
||||
|
||||
li {
|
||||
@include inline-block();
|
||||
margin-right: 10px;
|
||||
|
||||
p {
|
||||
float: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
section.goals {
|
||||
background: #eee;
|
||||
padding: 6px 20px;
|
||||
border-top: 1px solid #ccc;
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
color: #999;
|
||||
|
||||
li {
|
||||
margin-bottom: 6px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> section.content {
|
||||
@include box-sizing(border-box);
|
||||
padding: 20px;
|
||||
|
||||
section.filters {
|
||||
@include clearfix;
|
||||
margin-bottom: 10px;
|
||||
background: #efefef;
|
||||
border: 1px solid #ddd;
|
||||
|
||||
ul {
|
||||
@include clearfix();
|
||||
list-style: none;
|
||||
padding: 6px;
|
||||
|
||||
li {
|
||||
@include inline-block();
|
||||
|
||||
&.advanced {
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> div {
|
||||
display: table;
|
||||
border: 1px solid;
|
||||
width: 100%;
|
||||
|
||||
section {
|
||||
header {
|
||||
background: #eee;
|
||||
padding: 6px;
|
||||
border-bottom: 1px solid #ccc;
|
||||
@include clearfix;
|
||||
|
||||
h2 {
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
font-size: 12px;
|
||||
float: left;
|
||||
}
|
||||
}
|
||||
|
||||
&.modules {
|
||||
@include box-sizing(border-box);
|
||||
display: table-cell;
|
||||
width: flex-grid(6, 9);
|
||||
border-right: 1px solid #333;
|
||||
|
||||
&.empty {
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
|
||||
a {
|
||||
@extend .button;
|
||||
@include inline-block();
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
ol {
|
||||
list-style: none;
|
||||
border-bottom: 1px solid #333;
|
||||
|
||||
li {
|
||||
border-bottom: 1px solid #333;
|
||||
|
||||
&:last-child{
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
ol {
|
||||
list-style: none;
|
||||
|
||||
li {
|
||||
padding: 6px;
|
||||
|
||||
&:hover {
|
||||
a.draggable {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
a.draggable {
|
||||
float: right;
|
||||
opacity: .5;
|
||||
}
|
||||
|
||||
&.group {
|
||||
padding: 0;
|
||||
|
||||
header {
|
||||
padding: 6px;
|
||||
background: none;
|
||||
|
||||
h3 {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
ol {
|
||||
border-left: 4px solid #999;
|
||||
border-bottom: 0;
|
||||
|
||||
li {
|
||||
&:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.scratch-pad {
|
||||
@include box-sizing(border-box);
|
||||
display: table-cell;
|
||||
width: flex-grid(3, 9) + flex-gutter(9);
|
||||
vertical-align: top;
|
||||
|
||||
ol {
|
||||
list-style: none;
|
||||
border-bottom: 1px solid #999;
|
||||
|
||||
li {
|
||||
border-bottom: 1px solid #999;
|
||||
background: #f9f9f9;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
|
||||
li {
|
||||
padding: 6px;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
a.draggable {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&.empty {
|
||||
padding: 12px;
|
||||
|
||||
a {
|
||||
@extend .button;
|
||||
display: block;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
a.draggable {
|
||||
float: right;
|
||||
opacity: .3;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #000;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,39 +1,51 @@
|
||||
// studio - css architecture
|
||||
// ====================
|
||||
|
||||
// bourbon libs and resets
|
||||
@import 'bourbon/bourbon';
|
||||
@import 'bourbon/addons/button';
|
||||
@import 'vendor/normalize';
|
||||
@import 'keyframes';
|
||||
|
||||
@import 'reset';
|
||||
|
||||
// utilities
|
||||
@import 'variables';
|
||||
@import 'mixins';
|
||||
@import 'cms_mixins';
|
||||
|
||||
@import "fonts";
|
||||
@import "variables";
|
||||
@import "cms_mixins";
|
||||
@import "extends";
|
||||
@import "base";
|
||||
@import "header";
|
||||
@import "footer";
|
||||
@import "dashboard";
|
||||
@import "courseware";
|
||||
@import "subsection";
|
||||
@import "unit";
|
||||
@import "assets";
|
||||
@import "static-pages";
|
||||
@import "users";
|
||||
@import "import";
|
||||
@import "export";
|
||||
@import "settings";
|
||||
@import "course-info";
|
||||
@import "landing";
|
||||
@import "graphics";
|
||||
@import "modal";
|
||||
@import "alerts";
|
||||
@import "login";
|
||||
@import "account";
|
||||
@import "index";
|
||||
@import 'jquery-ui-calendar';
|
||||
// assets
|
||||
@import 'assets/fonts';
|
||||
@import 'assets/graphics';
|
||||
@import 'assets/keyframes';
|
||||
|
||||
@import 'content-types';
|
||||
// base
|
||||
@import 'base';
|
||||
|
||||
// elements
|
||||
@import 'elements/header';
|
||||
@import 'elements/footer';
|
||||
@import 'elements/navigation';
|
||||
@import 'elements/forms';
|
||||
@import 'elements/modal';
|
||||
@import 'elements/alerts';
|
||||
@import 'elements/jquery-ui-calendar';
|
||||
|
||||
// specific views
|
||||
@import 'views/account';
|
||||
@import 'views/assets';
|
||||
@import 'views/updates';
|
||||
@import 'views/dashboard';
|
||||
@import 'views/export';
|
||||
@import 'views/index';
|
||||
@import 'views/import';
|
||||
@import 'views/outline';
|
||||
@import 'views/settings';
|
||||
@import 'views/static-pages';
|
||||
@import 'views/subsection';
|
||||
@import 'views/unit';
|
||||
@import 'views/users';
|
||||
|
||||
@import 'assets/content-types';
|
||||
|
||||
// xblock-related
|
||||
@import 'module/module-styles.scss';
|
||||
@import 'descriptor/module-styles.scss';
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// studio - elements - alerts, notifications, prompts
|
||||
// ====================
|
||||
|
||||
// notifications
|
||||
.wrapper-notification {
|
||||
@include clearfix();
|
||||
@@ -1,4 +1,6 @@
|
||||
//studio global footer
|
||||
// studio - elements - global footer
|
||||
// ====================
|
||||
|
||||
.wrapper-footer {
|
||||
margin: ($baseline*1.5) 0 $baseline 0;
|
||||
padding: $baseline;
|
||||
76
cms/static/sass/elements/_forms.scss
Normal file
76
cms/static/sass/elements/_forms.scss
Normal file
@@ -0,0 +1,76 @@
|
||||
// studio - elements - forms
|
||||
// ====================
|
||||
|
||||
// forms - general
|
||||
input[type="text"],
|
||||
input[type="email"],
|
||||
input[type="password"],
|
||||
textarea.text {
|
||||
padding: 6px 8px 8px;
|
||||
@include box-sizing(border-box);
|
||||
border: 1px solid $mediumGrey;
|
||||
border-radius: 2px;
|
||||
@include linear-gradient($lightGrey, tint($lightGrey, 90%));
|
||||
background-color: $lightGrey;
|
||||
@include box-shadow(0 1px 2px rgba(0, 0, 0, .1) inset);
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
font-size: 11px;
|
||||
color: $baseFontColor;
|
||||
outline: 0;
|
||||
|
||||
&::-webkit-input-placeholder,
|
||||
&:-moz-placeholder,
|
||||
&:-ms-input-placeholder {
|
||||
color: #979faf;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
@include linear-gradient($paleYellow, tint($paleYellow, 90%));
|
||||
outline: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// forms - specific
|
||||
input.search {
|
||||
padding: 6px 15px 8px 30px;
|
||||
@include box-sizing(border-box);
|
||||
border: 1px solid $darkGrey;
|
||||
border-radius: 20px;
|
||||
background: url(../img/search-icon.png) no-repeat 8px 7px #edf1f5;
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
color: $baseFontColor;
|
||||
outline: 0;
|
||||
|
||||
&::-webkit-input-placeholder {
|
||||
color: #979faf;
|
||||
}
|
||||
}
|
||||
|
||||
label {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
code {
|
||||
padding: 0 4px;
|
||||
border-radius: 3px;
|
||||
background: #eee;
|
||||
font-family: Monaco, monospace;
|
||||
}
|
||||
|
||||
.CodeMirror {
|
||||
font-size: 13px;
|
||||
border: 1px solid $darkGrey;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.text-editor {
|
||||
width: 100%;
|
||||
min-height: 80px;
|
||||
padding: 10px;
|
||||
@include box-sizing(border-box);
|
||||
border: 1px solid $mediumGrey;
|
||||
@include linear-gradient(top, rgba(255, 255, 255, 0), rgba(255, 255, 255, 0.3));
|
||||
background-color: #edf1f5;
|
||||
@include box-shadow(0 1px 2px rgba(0, 0, 0, 0.1) inset);
|
||||
font-family: Monaco, monospace;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// studio global header and navigation
|
||||
// studio - elements - global header
|
||||
// ====================
|
||||
|
||||
.wrapper-header {
|
||||
@@ -1,3 +1,6 @@
|
||||
// studio - elements - JQUI calendar
|
||||
// ====================
|
||||
|
||||
.ui-datepicker {
|
||||
border-color: $darkGrey;
|
||||
border-radius: 2px;
|
||||
@@ -1,3 +1,6 @@
|
||||
// studio - elements - modal windows
|
||||
// ====================
|
||||
|
||||
.modal-cover {
|
||||
display: none;
|
||||
position: fixed;
|
||||
24
cms/static/sass/elements/_navigation.scss
Normal file
24
cms/static/sass/elements/_navigation.scss
Normal file
@@ -0,0 +1,24 @@
|
||||
// studio - elements - navigation
|
||||
// ====================
|
||||
|
||||
// common
|
||||
|
||||
// ====================
|
||||
|
||||
// primary
|
||||
|
||||
// ====================
|
||||
|
||||
// right hand side
|
||||
|
||||
// ====================
|
||||
|
||||
// tabs
|
||||
|
||||
// ====================
|
||||
|
||||
// dropdown
|
||||
|
||||
// ====================
|
||||
|
||||
//
|
||||
@@ -1,5 +1,6 @@
|
||||
// Studio - Sign In/Up
|
||||
// studio - views - sign up/in
|
||||
// ====================
|
||||
|
||||
body.signup, body.signin {
|
||||
|
||||
.wrapper-content {
|
||||
@@ -1,4 +1,8 @@
|
||||
.uploads {
|
||||
// studio - views - assets
|
||||
// ====================
|
||||
|
||||
body.course.uploads {
|
||||
|
||||
input.asset-search-input {
|
||||
float: left;
|
||||
width: 260px;
|
||||
124
cms/static/sass/views/_dashboard.scss
Normal file
124
cms/static/sass/views/_dashboard.scss
Normal file
@@ -0,0 +1,124 @@
|
||||
// studio - views - user dashboard
|
||||
// ====================
|
||||
|
||||
body.dashboard {
|
||||
|
||||
.my-classes {
|
||||
margin-top: $baseline;
|
||||
}
|
||||
|
||||
.class-list {
|
||||
margin-top: 20px;
|
||||
border-radius: 3px;
|
||||
border: 1px solid $darkGrey;
|
||||
background: #fff;
|
||||
@include box-shadow(0 1px 2px rgba(0, 0, 0, .1));
|
||||
|
||||
li {
|
||||
position: relative;
|
||||
border-bottom: 1px solid $mediumGrey;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.class-link {
|
||||
z-index: 100;
|
||||
display: block;
|
||||
padding: 20px 25px;
|
||||
line-height: 1.3;
|
||||
|
||||
&:hover {
|
||||
background: $paleYellow;
|
||||
|
||||
+ .view-live-button {
|
||||
opacity: 1.0;
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.class-name {
|
||||
display: block;
|
||||
font-size: 19px;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.detail {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
margin-right: 20px;
|
||||
color: #3c3c3c;
|
||||
}
|
||||
|
||||
// view live button
|
||||
.view-live-button {
|
||||
z-index: 10000;
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
right: $baseline;
|
||||
padding: ($baseline/4) ($baseline/2);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
|
||||
&:hover {
|
||||
opacity: 1.0;
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.new-course {
|
||||
padding: 15px 25px;
|
||||
margin-top: 20px;
|
||||
border-radius: 3px;
|
||||
border: 1px solid $darkGrey;
|
||||
background: #fff;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, .1);
|
||||
@include clearfix;
|
||||
|
||||
.row {
|
||||
margin-bottom: 15px;
|
||||
@include clearfix;
|
||||
}
|
||||
|
||||
.column {
|
||||
float: left;
|
||||
width: 48%;
|
||||
}
|
||||
|
||||
.column:first-child {
|
||||
margin-right: 4%;
|
||||
}
|
||||
|
||||
.course-info {
|
||||
width: 600px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.new-course-org,
|
||||
.new-course-number,
|
||||
.new-course-name {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.new-course-name {
|
||||
font-size: 19px;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.new-course-save {
|
||||
@include blue-button;
|
||||
}
|
||||
|
||||
.new-course-cancel {
|
||||
@include white-button;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,8 @@
|
||||
.export {
|
||||
// studio - views - course export
|
||||
// ====================
|
||||
|
||||
body.course.export {
|
||||
|
||||
.export-overview {
|
||||
@extend .window;
|
||||
@include clearfix;
|
||||
@@ -118,6 +122,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,4 +1,8 @@
|
||||
.import {
|
||||
// studio - views - course import
|
||||
// ====================
|
||||
|
||||
body.course.import {
|
||||
|
||||
.import-overview {
|
||||
@extend .window;
|
||||
@include clearfix;
|
||||
@@ -1,5 +1,7 @@
|
||||
// how it works/not signed in index
|
||||
.index {
|
||||
// studio - views - how it works
|
||||
// ====================
|
||||
|
||||
body.index {
|
||||
|
||||
&.not-signedin {
|
||||
|
||||
680
cms/static/sass/views/_outline.scss
Normal file
680
cms/static/sass/views/_outline.scss
Normal file
@@ -0,0 +1,680 @@
|
||||
// studio - views - course outline
|
||||
// ====================
|
||||
|
||||
body.course.outline {
|
||||
|
||||
input.courseware-unit-search-input {
|
||||
float: left;
|
||||
width: 260px;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.branch {
|
||||
|
||||
.section-item {
|
||||
@include clearfix();
|
||||
|
||||
.details {
|
||||
display: block;
|
||||
float: left;
|
||||
margin-bottom: 0;
|
||||
width: 650px;
|
||||
}
|
||||
|
||||
.gradable-status {
|
||||
float: right;
|
||||
position: relative;
|
||||
top: -4px;
|
||||
right: 50px;
|
||||
width: 145px;
|
||||
|
||||
.status-label {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: -5px;
|
||||
display: none;
|
||||
width: 110px;
|
||||
padding: 5px 40px 5px 10px;
|
||||
@include border-radius(3px);
|
||||
color: $lightGrey;
|
||||
text-align: right;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
.menu-toggle {
|
||||
z-index: 10;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 5px;
|
||||
padding: 5px;
|
||||
color: $mediumGrey;
|
||||
|
||||
&:hover, &.is-active {
|
||||
color: $blue;
|
||||
}
|
||||
}
|
||||
|
||||
.menu {
|
||||
z-index: 1;
|
||||
display: none;
|
||||
opacity: 0.0;
|
||||
position: absolute;
|
||||
top: -1px;
|
||||
left: 5px;
|
||||
margin: 0;
|
||||
padding: 8px 12px;
|
||||
background: $white;
|
||||
border: 1px solid $mediumGrey;
|
||||
font-size: 12px;
|
||||
@include border-radius(4px);
|
||||
@include box-shadow(0 1px 2px rgba(0, 0, 0, .2));
|
||||
@include transition(opacity .15s);
|
||||
|
||||
|
||||
li {
|
||||
width: 115px;
|
||||
margin-bottom: 3px;
|
||||
padding-bottom: 3px;
|
||||
border-bottom: 1px solid $lightGrey;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
border: none;
|
||||
|
||||
a {
|
||||
color: $darkGrey;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
color: $blue;
|
||||
|
||||
&.is-selected {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// dropdown state
|
||||
&.is-active {
|
||||
|
||||
.menu {
|
||||
z-index: 1000;
|
||||
display: block;
|
||||
opacity: 1.0;
|
||||
}
|
||||
|
||||
.menu-toggle {
|
||||
z-index: 10000;
|
||||
}
|
||||
}
|
||||
|
||||
// set state
|
||||
&.is-set {
|
||||
|
||||
.menu-toggle {
|
||||
color: $blue;
|
||||
}
|
||||
|
||||
.status-label {
|
||||
display: block;
|
||||
color: $blue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.courseware-section {
|
||||
position: relative;
|
||||
background: #fff;
|
||||
border-radius: 3px;
|
||||
border: 1px solid $mediumGrey;
|
||||
margin-top: 15px;
|
||||
padding-bottom: 12px;
|
||||
@include box-shadow(0 1px 1px rgba(0, 0, 0, 0.1));
|
||||
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
&.collapsed {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
label {
|
||||
float: left;
|
||||
line-height: 29px;
|
||||
}
|
||||
|
||||
.datepair {
|
||||
float: left;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.section-published-date {
|
||||
position: absolute;
|
||||
top: 19px;
|
||||
right: 90px;
|
||||
padding: 4px 10px;
|
||||
border-radius: 3px;
|
||||
background: $lightGrey;
|
||||
text-align: right;
|
||||
|
||||
.published-status {
|
||||
font-size: 12px;
|
||||
margin-right: 15px;
|
||||
|
||||
strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.schedule-button {
|
||||
@include blue-button;
|
||||
}
|
||||
|
||||
.edit-button {
|
||||
@include blue-button;
|
||||
}
|
||||
|
||||
.schedule-button,
|
||||
.edit-button {
|
||||
font-size: 11px;
|
||||
padding: 3px 15px 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.datepair .date,
|
||||
.datepair .time {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
border: none;
|
||||
background: none;
|
||||
@include box-shadow(none);
|
||||
font-size: 13px;
|
||||
font-weight: bold;
|
||||
color: $blue;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.datepair .date {
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.datepair .time {
|
||||
width: 65px;
|
||||
}
|
||||
|
||||
&.collapsed .subsection-list,
|
||||
.collapsed .subsection-list,
|
||||
.collapsed > ol {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
header {
|
||||
min-height: 75px;
|
||||
@include clearfix();
|
||||
|
||||
.item-details, .section-published-date {
|
||||
|
||||
}
|
||||
|
||||
.item-details {
|
||||
display: inline-block;
|
||||
padding: 20px 0 10px 0;
|
||||
@include clearfix();
|
||||
|
||||
.section-name {
|
||||
float: left;
|
||||
margin-right: 10px;
|
||||
width: 350px;
|
||||
font-size: 19px;
|
||||
font-weight: bold;
|
||||
color: $blue;
|
||||
}
|
||||
|
||||
.section-name-span {
|
||||
cursor: pointer;
|
||||
@include transition(color .15s);
|
||||
|
||||
&:hover {
|
||||
color: $orange;
|
||||
}
|
||||
}
|
||||
|
||||
.section-name-edit {
|
||||
position: relative;
|
||||
width: 400px;
|
||||
background: $white;
|
||||
|
||||
input {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.save-button {
|
||||
@include blue-button;
|
||||
padding: 7px 20px 7px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.cancel-button {
|
||||
@include white-button;
|
||||
padding: 7px 20px 7px;
|
||||
}
|
||||
}
|
||||
|
||||
.section-published-date {
|
||||
float: right;
|
||||
width: 265px;
|
||||
margin-right: 220px;
|
||||
@include border-radius(3px);
|
||||
background: $lightGrey;
|
||||
|
||||
.published-status {
|
||||
font-size: 12px;
|
||||
margin-right: 15px;
|
||||
|
||||
strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.schedule-button {
|
||||
@include blue-button;
|
||||
}
|
||||
|
||||
.edit-button {
|
||||
@include blue-button;
|
||||
}
|
||||
|
||||
.schedule-button,
|
||||
.edit-button {
|
||||
font-size: 11px;
|
||||
padding: 3px 15px 5px;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.gradable-status {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 70px;
|
||||
width: 145px;
|
||||
|
||||
.status-label {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 2px;
|
||||
display: none;
|
||||
width: 100px;
|
||||
padding: 10px 35px 10px 10px;
|
||||
@include border-radius(3px);
|
||||
background: $lightGrey;
|
||||
color: $lightGrey;
|
||||
text-align: right;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
.menu-toggle {
|
||||
z-index: 10;
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 5px;
|
||||
padding: 5px;
|
||||
color: $lightGrey;
|
||||
|
||||
&:hover, &.is-active {
|
||||
color: $blue;
|
||||
}
|
||||
}
|
||||
|
||||
.menu {
|
||||
z-index: 1;
|
||||
display: none;
|
||||
opacity: 0.0;
|
||||
position: absolute;
|
||||
top: -1px;
|
||||
left: 2px;
|
||||
margin: 0;
|
||||
padding: 8px 12px;
|
||||
background: $white;
|
||||
border: 1px solid $mediumGrey;
|
||||
font-size: 12px;
|
||||
@include border-radius(4px);
|
||||
@include box-shadow(0 1px 2px rgba(0, 0, 0, .2));
|
||||
@include transition(opacity .15s);
|
||||
@include transition(display .15s);
|
||||
|
||||
|
||||
li {
|
||||
width: 115px;
|
||||
margin-bottom: 3px;
|
||||
padding-bottom: 3px;
|
||||
border-bottom: 1px solid $lightGrey;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
border: none;
|
||||
|
||||
a {
|
||||
color: $darkGrey;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
|
||||
&.is-selected {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// dropdown state
|
||||
&.is-active {
|
||||
|
||||
.menu {
|
||||
z-index: 1000;
|
||||
display: block;
|
||||
opacity: 1.0;
|
||||
}
|
||||
|
||||
|
||||
.menu-toggle {
|
||||
z-index: 10000;
|
||||
}
|
||||
}
|
||||
|
||||
// set state
|
||||
&.is-set {
|
||||
|
||||
.menu-toggle {
|
||||
color: $blue;
|
||||
}
|
||||
|
||||
.status-label {
|
||||
display: block;
|
||||
color: $blue;
|
||||
}
|
||||
}
|
||||
|
||||
float: left;
|
||||
padding: 21px 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
.item-actions {
|
||||
margin-top: 21px;
|
||||
margin-right: 12px;
|
||||
|
||||
.edit-button,
|
||||
.delete-button {
|
||||
margin-top: -3px;
|
||||
}
|
||||
}
|
||||
|
||||
.expand-collapse-icon {
|
||||
float: left;
|
||||
margin: 29px 6px 16px 16px;
|
||||
@include transition(none);
|
||||
|
||||
&.expand {
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
&.collapsed {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.drag-handle {
|
||||
margin-left: 11px;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 19px;
|
||||
font-weight: 700;
|
||||
color: $blue;
|
||||
}
|
||||
|
||||
.section-name-span {
|
||||
cursor: pointer;
|
||||
@include transition(color .15s);
|
||||
|
||||
&:hover {
|
||||
color: $orange;
|
||||
}
|
||||
}
|
||||
|
||||
.section-name-form {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.section-name-edit {
|
||||
input {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.save-button {
|
||||
@include blue-button;
|
||||
padding: 7px 20px 7px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.cancel-button {
|
||||
@include white-button;
|
||||
padding: 7px 20px 7px;
|
||||
}
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 12px;
|
||||
color: #878e9d;
|
||||
|
||||
strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.list-header {
|
||||
@include linear-gradient(top, transparent, rgba(0, 0, 0, .1));
|
||||
background-color: #ced2db;
|
||||
border-radius: 3px 3px 0 0;
|
||||
}
|
||||
|
||||
.subsection-list {
|
||||
margin: 0 12px;
|
||||
|
||||
> ol {
|
||||
@include tree-view;
|
||||
border-top-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.new-section {
|
||||
|
||||
header {
|
||||
height: auto;
|
||||
@include clearfix();
|
||||
}
|
||||
|
||||
.expand-collapse-icon {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.item-details {
|
||||
padding: 25px 0 0 0;
|
||||
|
||||
.section-name {
|
||||
float: none;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.toggle-button-sections {
|
||||
display: none;
|
||||
position: relative;
|
||||
float: right;
|
||||
margin-top: 10px;
|
||||
|
||||
font-size: 13px;
|
||||
color: $darkGrey;
|
||||
|
||||
&.is-shown {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.ss-icon {
|
||||
@include border-radius(20px);
|
||||
position: relative;
|
||||
top: -1px;
|
||||
display: inline-block;
|
||||
margin-right: 2px;
|
||||
line-height: 5px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.label {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
.new-section-name,
|
||||
.new-subsection-name-input {
|
||||
width: 515px;
|
||||
}
|
||||
|
||||
.new-section-name-save,
|
||||
.new-subsection-name-save {
|
||||
@include blue-button;
|
||||
padding: 4px 20px 7px;
|
||||
margin: 0 5px;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
.new-section-name-cancel,
|
||||
.new-subsection-name-cancel {
|
||||
@include white-button;
|
||||
padding: 4px 20px 7px;
|
||||
color: #8891a1 !important;
|
||||
}
|
||||
|
||||
.dummy-calendar {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 55px;
|
||||
left: 110px;
|
||||
z-index: 9999;
|
||||
border: 1px solid #3C3C3C;
|
||||
@include box-shadow(0 1px 15px rgba(0, 0, 0, .2));
|
||||
}
|
||||
|
||||
.preview {
|
||||
background: url(../img/preview.jpg) center top no-repeat;
|
||||
}
|
||||
|
||||
.edit-subsection-publish-settings {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 100px;
|
||||
left: 50%;
|
||||
z-index: 99999;
|
||||
width: 600px;
|
||||
margin-left: -300px;
|
||||
background: #fff;
|
||||
text-align: center;
|
||||
|
||||
.settings {
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 34px;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.picker {
|
||||
margin: 30px 0 65px;
|
||||
}
|
||||
|
||||
.description {
|
||||
margin-top: 30px;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
strong {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.start-date,
|
||||
.start-time {
|
||||
font-size: 19px;
|
||||
}
|
||||
|
||||
.save-button {
|
||||
@include blue-button;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.cancel-button {
|
||||
@include white-button;
|
||||
}
|
||||
|
||||
.save-button,
|
||||
.cancel-button {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.collapse-all-button {
|
||||
float: right;
|
||||
margin-top: 10px;
|
||||
font-size: 13px;
|
||||
color: $darkGrey;
|
||||
}
|
||||
|
||||
// sort/drag and drop
|
||||
.ui-droppable {
|
||||
@include transition (padding 0.5s ease-in-out 0s);
|
||||
min-height: 20px;
|
||||
padding: 0;
|
||||
|
||||
&.dropover {
|
||||
padding: 15px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.ui-draggable-dragging {
|
||||
@include box-shadow(0 1px 2px rgba(0, 0, 0, .3));
|
||||
border: 1px solid $darkGrey;
|
||||
opacity : 0.2;
|
||||
&:hover {
|
||||
opacity : 1.0;
|
||||
.section-item {
|
||||
background: $yellow !important;
|
||||
}
|
||||
}
|
||||
|
||||
// hiding unit button - temporary fix until this semantically corrected
|
||||
.new-unit-item {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
ol.ui-droppable .branch:first-child .section-item {
|
||||
border-top: none;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
// Studio - Course Settings
|
||||
// studio - views - course settings
|
||||
// ====================
|
||||
|
||||
body.course.settings {
|
||||
|
||||
.content-primary, .content-supplementary {
|
||||
@@ -1,4 +1,8 @@
|
||||
.static-pages {
|
||||
// studio - views - course static pages
|
||||
// ====================
|
||||
|
||||
body.course.static-pages {
|
||||
|
||||
.new-static-page-button {
|
||||
@include grey-button;
|
||||
display: block;
|
||||
@@ -16,6 +20,51 @@
|
||||
margin: 0 0 5px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.wrapper-component-editor {
|
||||
z-index: 9999;
|
||||
position: relative;
|
||||
background: $lightBluishGrey2;
|
||||
}
|
||||
|
||||
.component-editor {
|
||||
@include edit-box;
|
||||
@include box-shadow(none);
|
||||
display: none;
|
||||
padding: 20px;
|
||||
border-radius: 2px 2px 0 0;
|
||||
|
||||
.metadata_edit {
|
||||
margin-bottom: 20px;
|
||||
font-size: 13px;
|
||||
|
||||
li {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: inline-block;
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin-bottom: 10px;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
h5 {
|
||||
margin-bottom: 8px;
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.save-button {
|
||||
margin-top: 10px;
|
||||
margin: 15px 8px 0 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.component-editor {
|
||||
@@ -35,6 +84,7 @@
|
||||
}
|
||||
|
||||
.component {
|
||||
position: relative;
|
||||
border: 1px solid $mediumGrey;
|
||||
border-top: none;
|
||||
|
||||
@@ -56,10 +106,13 @@
|
||||
}
|
||||
|
||||
.drag-handle {
|
||||
position: absolute;
|
||||
display: block;
|
||||
top: 0;
|
||||
right: 0;
|
||||
z-index: 11;
|
||||
width: 35px;
|
||||
height: 100%;
|
||||
border: none;
|
||||
background: url(../img/drag-handles.png) center no-repeat #fff;
|
||||
|
||||
@@ -69,6 +122,7 @@
|
||||
}
|
||||
|
||||
.component-actions {
|
||||
position: absolute;
|
||||
top: 26px;
|
||||
right: 44px;
|
||||
}
|
||||
450
cms/static/sass/views/_subsection.scss
Normal file
450
cms/static/sass/views/_subsection.scss
Normal file
@@ -0,0 +1,450 @@
|
||||
// studio - views - course subsection
|
||||
// ====================
|
||||
|
||||
body.course.subsection {
|
||||
|
||||
.unit-settings {
|
||||
.window-contents {
|
||||
padding: 10px 20px;
|
||||
}
|
||||
|
||||
.unit-actions {
|
||||
border-bottom: none;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.published-alert {
|
||||
display: none;
|
||||
padding: 10px;
|
||||
border: 1px solid #edbd3c;
|
||||
border-radius: 3px;
|
||||
background: #fbf6e1;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
|
||||
div {
|
||||
margin-top: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
input[type="radio"] {
|
||||
margin-right: 7px;
|
||||
}
|
||||
|
||||
.status {
|
||||
font-size: 12px;
|
||||
|
||||
strong {
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
.preview-button, .view-button {
|
||||
@include white-button;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.publish-button {
|
||||
@include orange-button;
|
||||
}
|
||||
|
||||
.delete-button {
|
||||
@include blue-button;
|
||||
}
|
||||
|
||||
.delete-draft {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.delete-button,
|
||||
.preview-button,
|
||||
.publish-button,
|
||||
.view-button {
|
||||
font-size: 11px;
|
||||
margin-top: 10px;
|
||||
padding: 6px 15px 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.unit-history {
|
||||
&.collapsed {
|
||||
h4 {
|
||||
border-bottom: none;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.window-contents {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
ol {
|
||||
border: 1px solid #ced2db;
|
||||
|
||||
li {
|
||||
display: block;
|
||||
padding: 6px 8px 8px 10px;
|
||||
background: #edf1f5;
|
||||
font-size: 12px;
|
||||
|
||||
&:hover {
|
||||
background: #fffcf1;
|
||||
|
||||
.item-actions {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
&.checked {
|
||||
background: #d1dae3;
|
||||
}
|
||||
|
||||
.item-actions {
|
||||
display: none;
|
||||
}
|
||||
|
||||
input[type="radio"] {
|
||||
margin-right: 7px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.unit-location {
|
||||
.url {
|
||||
width: 100%;
|
||||
margin-bottom: 10px;
|
||||
@include box-shadow(none);
|
||||
}
|
||||
|
||||
.draft-tag,
|
||||
.hidden-tag,
|
||||
.private-tag,
|
||||
.has-new-draft-tag {
|
||||
font-size: 8px;
|
||||
}
|
||||
|
||||
.window-contents > ol {
|
||||
@include tree-view;
|
||||
|
||||
.section-item {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
font-size: 11px;
|
||||
padding: 2px 8px 4px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
@include box-sizing(border-box);
|
||||
}
|
||||
|
||||
ol {
|
||||
.section-item {
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.new-unit-item {
|
||||
margin-left: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
ol ol {
|
||||
.section-item {
|
||||
padding-left: 34px;
|
||||
}
|
||||
|
||||
.new-unit-item {
|
||||
margin: 0 0 10px 41px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.subsection-body {
|
||||
padding: 32px 40px;
|
||||
@include clearfix;
|
||||
|
||||
> div {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
input {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.unit-subtitle {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sortable-unit-list {
|
||||
ol {
|
||||
@include tree-view;
|
||||
}
|
||||
}
|
||||
|
||||
.policy-list {
|
||||
input[disabled] {
|
||||
border: none;
|
||||
@include box-shadow(none);
|
||||
}
|
||||
|
||||
.policy-list-name {
|
||||
margin-right: 5px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.policy-list-value {
|
||||
width: 320px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.policy-list-element {
|
||||
.save-button,
|
||||
.cancel-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.edit-icon {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
&.editing,
|
||||
&.new-policy-list-element {
|
||||
.policy-list-name,
|
||||
.policy-list-value {
|
||||
border: 1px solid #b0b6c2;
|
||||
@include linear-gradient(top, rgba(255, 255, 255, 0), rgba(255, 255, 255, .3));
|
||||
background-color: #edf1f5;
|
||||
@include box-shadow(0 1px 2px rgba(0, 0, 0, .1) inset);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.new-policy-list-element {
|
||||
padding: 10px 10px 0;
|
||||
margin: 0 -10px 10px;
|
||||
border-radius: 3px;
|
||||
background: $mediumGrey;
|
||||
|
||||
.save-button {
|
||||
@include blue-button;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.cancel-button {
|
||||
@include white-button;
|
||||
}
|
||||
|
||||
.edit-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.delete-icon {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.new-policy-item {
|
||||
margin: 10px 0;
|
||||
|
||||
.plus-icon-small {
|
||||
position: relative;
|
||||
top: -1px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.subsection-name-input {
|
||||
label {
|
||||
display: block;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.scheduled-date-input,
|
||||
.due-date-input {
|
||||
@include clearfix;
|
||||
|
||||
.date-input,
|
||||
.time-input {
|
||||
display: inline-block;
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.inherits-check {
|
||||
label {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
.notice {
|
||||
margin-top: 6px;
|
||||
font-size: 11px;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.due-date-input {
|
||||
label {
|
||||
display: inline-block !important;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
a {
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.date-setter {
|
||||
@include clearfix;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.remove-date {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.row.visibility {
|
||||
label {
|
||||
display: inline-block !important;
|
||||
margin-right: 10px;
|
||||
line-height: 21px;
|
||||
}
|
||||
|
||||
a {
|
||||
display: inline-block;
|
||||
height: 31px;
|
||||
margin-right: 8px;
|
||||
vertical-align: middle;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
line-height: 31px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.large-toggle {
|
||||
width: 41px;
|
||||
background: url(../img/large-toggles.png) no-repeat;
|
||||
background-position: 0 -50px;
|
||||
|
||||
.hidden {
|
||||
background-position: 0 -5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.gradable {
|
||||
|
||||
label {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.gradable-status {
|
||||
position: relative;
|
||||
top: -4px;
|
||||
display: inline-block;
|
||||
margin-left: 10px;
|
||||
width: 65%;
|
||||
|
||||
.status-label {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
color: $blue;
|
||||
border: none;
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.menu-toggle {
|
||||
z-index: 100;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
background: transparent;
|
||||
|
||||
&:hover, &.is-active {
|
||||
color: $blue;
|
||||
}
|
||||
}
|
||||
|
||||
.menu {
|
||||
z-index: 1;
|
||||
position: absolute;
|
||||
top: -12px;
|
||||
left: -7px;
|
||||
display: none;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 8px 12px;
|
||||
opacity: 0.0;
|
||||
background: $white;
|
||||
border: 1px solid $mediumGrey;
|
||||
font-size: 12px;
|
||||
@include border-radius(4px);
|
||||
@include box-shadow(0 1px 2px rgba(0, 0, 0, .2));
|
||||
@include transition(opacity .15s);
|
||||
|
||||
|
||||
li {
|
||||
margin-bottom: 3px;
|
||||
padding-bottom: 3px;
|
||||
border-bottom: 1px solid $lightGrey;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
|
||||
&.is-selected {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// dropdown state
|
||||
&.is-active {
|
||||
|
||||
.menu {
|
||||
z-index: 10000;
|
||||
display: block;
|
||||
opacity: 1.0;
|
||||
}
|
||||
|
||||
.menu-toggle {
|
||||
z-index: 1000;
|
||||
}
|
||||
}
|
||||
|
||||
// set state
|
||||
&.is-set {
|
||||
|
||||
.menu-toggle {
|
||||
color: $blue;
|
||||
}
|
||||
|
||||
.status-label {
|
||||
display: block;
|
||||
color: $blue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
681
cms/static/sass/views/_unit.scss
Normal file
681
cms/static/sass/views/_unit.scss
Normal file
@@ -0,0 +1,681 @@
|
||||
// studio - views - unit
|
||||
// ====================
|
||||
|
||||
body.course.unit {
|
||||
|
||||
.unit .main-wrapper {
|
||||
@include clearfix();
|
||||
margin: 40px;
|
||||
}
|
||||
|
||||
//Problem Selector tab menu requirements
|
||||
.js .tabs .tab {
|
||||
display: none;
|
||||
}
|
||||
//end problem selector reqs
|
||||
|
||||
.main-column {
|
||||
clear: both;
|
||||
float: left;
|
||||
width: 70%;
|
||||
}
|
||||
|
||||
.unit-body.published {
|
||||
.components > li {
|
||||
border: none;
|
||||
|
||||
.rendered-component {
|
||||
padding: 0 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.unit-body {
|
||||
|
||||
.unit-name-input {
|
||||
padding: 20px 40px;
|
||||
|
||||
label {
|
||||
display: block;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.breadcrumbs {
|
||||
border-radius: 3px 3px 0 0;
|
||||
border-bottom: 1px solid #cbd1db;
|
||||
@include linear-gradient(top, rgba(255, 255, 255, .3), rgba(255, 255, 255, 0) 70%);
|
||||
background-color: #edf1f5;
|
||||
@include box-shadow(0 1px 0 rgba(255, 255, 255, .7) inset);
|
||||
@include clearfix;
|
||||
|
||||
li {
|
||||
float: left;
|
||||
}
|
||||
|
||||
a,
|
||||
.current-page {
|
||||
display: block;
|
||||
padding: 15px 35px 15px 30px;
|
||||
font-size: 14px;
|
||||
background: url(../img/breadcrumb-arrow.png) no-repeat right center;
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 30px 40px 30px 0;
|
||||
color: #646464;
|
||||
font-size: 19px;
|
||||
font-weight: 300;
|
||||
letter-spacing: 1px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.components {
|
||||
|
||||
> li {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
margin: 20px 40px;
|
||||
|
||||
|
||||
|
||||
.title {
|
||||
margin: 0 0 15px 0;
|
||||
color: $mediumGrey;
|
||||
|
||||
.value {
|
||||
}
|
||||
}
|
||||
|
||||
&.new-component-item {
|
||||
margin: 20px 0px;
|
||||
border-top: 1px solid $mediumGrey;
|
||||
box-shadow: 0 2px 1px rgba(182, 182, 182, 0.75) inset;
|
||||
background-color: $lightGrey;
|
||||
margin-bottom: 0px;
|
||||
padding-bottom: 20px;
|
||||
|
||||
.new-component-button {
|
||||
display: block;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
color: #edf1f5;
|
||||
}
|
||||
|
||||
h5 {
|
||||
margin: 20px 0px;
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.rendered-component {
|
||||
display: none;
|
||||
background: #fff;
|
||||
border-radius: 3px 3px 0 0;
|
||||
}
|
||||
|
||||
.new-component-type {
|
||||
|
||||
a,
|
||||
li {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
a {
|
||||
border: 1px solid $mediumGrey;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
color: #fff;
|
||||
margin-right: 15px;
|
||||
margin-bottom: 20px;
|
||||
border-radius: 8px;
|
||||
font-size: 15px;
|
||||
line-height: 14px;
|
||||
text-align: center;
|
||||
@include box-shadow(0 1px 1px rgba(0, 0, 0, .2), 0 1px 0 rgba(255, 255, 255, .4) inset);
|
||||
|
||||
.name {
|
||||
position: absolute;
|
||||
bottom: 5px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
@include box-sizing(border-box);
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.new-component-templates {
|
||||
display: none;
|
||||
margin: 20px 40px 20px 40px;
|
||||
border-radius: 3px;
|
||||
border: 1px solid $mediumGrey;
|
||||
background-color: #fff;
|
||||
@include box-shadow(0 1px 1px rgba(0, 0, 0, .2), 0 1px 0 rgba(255, 255, 255, .4) inset);
|
||||
@include clearfix;
|
||||
|
||||
.cancel-button {
|
||||
margin: 20px 0px 10px 10px;
|
||||
@include white-button;
|
||||
}
|
||||
|
||||
.problem-type-tabs {
|
||||
display: none;
|
||||
}
|
||||
|
||||
// specific menu types
|
||||
&.new-component-problem {
|
||||
padding-bottom:10px;
|
||||
|
||||
.ss-icon, .editor-indicator {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.problem-type-tabs {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.new-component-type,
|
||||
.new-component-template {
|
||||
@include clearfix;
|
||||
|
||||
a {
|
||||
position: relative;
|
||||
border: 1px solid $darkGreen;
|
||||
background: tint($green,20%);
|
||||
color: #fff;
|
||||
|
||||
&:hover {
|
||||
background: $brightGreen;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.problem-type-tabs {
|
||||
list-style-type: none;
|
||||
border-radius: 0;
|
||||
width: 100%;
|
||||
@include linear-gradient(top, rgba(255, 255, 255, .4), rgba(255, 255, 255, 0));
|
||||
background-color: $lightBluishGrey;
|
||||
@include box-shadow(0 1px 0 rgba(255, 255, 255, 0.2) inset, 0 -1px 0 rgba(0, 0, 0, 0.2) inset);
|
||||
|
||||
li:first-child {
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
li {
|
||||
float:left;
|
||||
display:inline-block;
|
||||
text-align:center;
|
||||
width: auto;
|
||||
@include linear-gradient(top, rgba(255, 255, 255, .4), rgba(255, 255, 255, 0));
|
||||
background-color: tint($lightBluishGrey, 10%);
|
||||
@include box-shadow(0 1px 0 rgba(255, 255, 255, 0.2) inset, 0 -1px 0 rgba(0, 0, 0, 0.2) inset);
|
||||
opacity:.8;
|
||||
|
||||
&:hover {
|
||||
opacity:1;
|
||||
background-color: tint($lightBluishGrey, 20%);
|
||||
}
|
||||
|
||||
&.ui-state-active {
|
||||
border: 0px;
|
||||
@include active;
|
||||
opacity:1;
|
||||
}
|
||||
}
|
||||
|
||||
a{
|
||||
display: block;
|
||||
padding: 15px 25px;
|
||||
font-size: 15px;
|
||||
line-height: 16px;
|
||||
text-align: center;
|
||||
color: #3c3c3c;
|
||||
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
.new-component-template {
|
||||
|
||||
a {
|
||||
background: #fff;
|
||||
border: 0px;
|
||||
color: #3c3c3c;
|
||||
@include transition (none);
|
||||
|
||||
&:hover {
|
||||
background: tint($green,30%);
|
||||
color: #fff;
|
||||
@include transition(background-color .15s);
|
||||
}
|
||||
}
|
||||
|
||||
li {
|
||||
border:none;
|
||||
border-bottom: 1px dashed $lightGrey;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
li:first-child {
|
||||
a {
|
||||
border-top: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
li:nth-child(2) {
|
||||
a {
|
||||
border-radius: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
@include clearfix();
|
||||
display: block;
|
||||
padding: 7px 20px;
|
||||
border-bottom: none;
|
||||
font-weight: 500;
|
||||
|
||||
.name {
|
||||
float: left;
|
||||
|
||||
.ss-icon {
|
||||
@include transition(opacity .15s);
|
||||
display: inline-block;
|
||||
top: 1px;
|
||||
margin-right: 5px;
|
||||
opacity: 0.5;
|
||||
width: 17;
|
||||
height: 21px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
.editor-indicator {
|
||||
@include transition(opacity .15s);
|
||||
float: right;
|
||||
position: relative;
|
||||
top: 3px;
|
||||
font-size: 12px;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.ss-icon, .editor-indicator {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: #fff;
|
||||
|
||||
.ss-icon {
|
||||
opacity: 1.0;
|
||||
}
|
||||
|
||||
.editor-indicator {
|
||||
opacity: 1.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// specific editor types
|
||||
.empty {
|
||||
|
||||
a {
|
||||
line-height: 1.4;
|
||||
font-weight: 400;
|
||||
background: #fff;
|
||||
color: #3c3c3c;
|
||||
|
||||
|
||||
&:hover {
|
||||
background: tint($green,30%);
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.new-component {
|
||||
text-align: center;
|
||||
|
||||
h5 {
|
||||
color: $darkGreen;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.component {
|
||||
border: 1px solid $lightBluishGrey2;
|
||||
border-radius: 3px;
|
||||
background: #fff;
|
||||
@include transition(none);
|
||||
|
||||
&:hover {
|
||||
border-color: #6696d7;
|
||||
|
||||
.drag-handle {
|
||||
background-color: $blue;
|
||||
border-color: $blue;
|
||||
}
|
||||
}
|
||||
|
||||
&.editing {
|
||||
border: 1px solid $lightBluishGrey2;
|
||||
z-index: auto;
|
||||
|
||||
.drag-handle,
|
||||
.component-actions {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.component-placeholder {
|
||||
border-color: #6696d7;
|
||||
}
|
||||
|
||||
.drag-handle {
|
||||
position: absolute;
|
||||
display: block;
|
||||
top: -1px;
|
||||
right: -16px;
|
||||
z-index: 10;
|
||||
width: 15px;
|
||||
height: 100%;
|
||||
border-radius: 0 3px 3px 0;
|
||||
border: 1px solid $lightBluishGrey2;
|
||||
background: url(../img/white-drag-handles.png) center no-repeat $lightBluishGrey2;
|
||||
cursor: move;
|
||||
@include transition(none);
|
||||
}
|
||||
}
|
||||
|
||||
.xmodule_display {
|
||||
padding: 40px 20px 20px;
|
||||
overflow-x: auto;
|
||||
|
||||
h1 {
|
||||
float: none;
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.wrapper-component-editor {
|
||||
z-index: 9999;
|
||||
position: relative;
|
||||
background: $lightBluishGrey2;
|
||||
}
|
||||
|
||||
.component-editor {
|
||||
@include edit-box;
|
||||
@include box-shadow(none);
|
||||
display: none;
|
||||
padding: 20px;
|
||||
border-radius: 2px 2px 0 0;
|
||||
|
||||
.metadata_edit {
|
||||
margin-bottom: 20px;
|
||||
font-size: 13px;
|
||||
|
||||
li {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: inline-block;
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin-bottom: 10px;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
h5 {
|
||||
margin-bottom: 8px;
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.save-button {
|
||||
margin-top: 10px;
|
||||
margin: 15px 8px 0 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.unit-settings {
|
||||
.window-contents {
|
||||
padding: 10px 20px;
|
||||
}
|
||||
|
||||
.unit-actions {
|
||||
border-bottom: none;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.published-alert {
|
||||
display: none;
|
||||
padding: 10px;
|
||||
border: 1px solid #edbd3c;
|
||||
border-radius: 3px;
|
||||
background: #fbf6e1;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
|
||||
div {
|
||||
margin-top: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
input[type="radio"] {
|
||||
margin-right: 7px;
|
||||
}
|
||||
|
||||
.status {
|
||||
font-size: 12px;
|
||||
|
||||
strong {
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
.preview-button, .view-button {
|
||||
@include white-button;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.publish-button {
|
||||
@include orange-button;
|
||||
}
|
||||
|
||||
.delete-button {
|
||||
@include blue-button;
|
||||
}
|
||||
|
||||
.delete-draft {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.delete-button,
|
||||
.preview-button,
|
||||
.publish-button,
|
||||
.view-button {
|
||||
font-size: 11px;
|
||||
margin-top: 10px;
|
||||
padding: 6px 15px 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.unit-history {
|
||||
&.collapsed {
|
||||
h4 {
|
||||
border-bottom: none;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.window-contents {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
ol {
|
||||
border: 1px solid #ced2db;
|
||||
|
||||
li {
|
||||
display: block;
|
||||
padding: 6px 8px 8px 10px;
|
||||
background: #edf1f5;
|
||||
font-size: 12px;
|
||||
|
||||
&:hover {
|
||||
background: #fffcf1;
|
||||
|
||||
.item-actions {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
&.checked {
|
||||
background: #d1dae3;
|
||||
}
|
||||
|
||||
.item-actions {
|
||||
display: none;
|
||||
}
|
||||
|
||||
input[type="radio"] {
|
||||
margin-right: 7px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.unit-location {
|
||||
.url {
|
||||
width: 100%;
|
||||
margin-bottom: 10px;
|
||||
@include box-shadow(none);
|
||||
}
|
||||
|
||||
.draft-tag,
|
||||
.hidden-tag,
|
||||
.private-tag,
|
||||
.has-new-draft-tag {
|
||||
font-size: 8px;
|
||||
}
|
||||
|
||||
.window-contents > ol {
|
||||
@include tree-view;
|
||||
|
||||
.section-item {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
font-size: 11px;
|
||||
padding: 2px 8px 4px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
@include box-sizing(border-box);
|
||||
}
|
||||
|
||||
ol {
|
||||
.section-item {
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.new-unit-item {
|
||||
margin-left: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
ol ol {
|
||||
.section-item {
|
||||
padding-left: 34px;
|
||||
}
|
||||
|
||||
.new-unit-item {
|
||||
margin: 0 0 10px 41px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.edit-state-draft {
|
||||
.visibility,
|
||||
|
||||
.edit-draft-message,
|
||||
.view-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.published-alert {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.edit-state-public {
|
||||
.delete-draft,
|
||||
.component-actions,
|
||||
.new-component-item,
|
||||
.editing-draft-alert,
|
||||
.publish-draft-message,
|
||||
.preview-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.published-alert {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.drag-handle {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.edit-state-private {
|
||||
.delete-draft,
|
||||
.publish-draft,
|
||||
.editing-draft-alert,
|
||||
.create-draft,
|
||||
.view-button {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// editing units from courseware
|
||||
body.unit {
|
||||
|
||||
.component {
|
||||
padding-top: 30px;
|
||||
|
||||
.component-actions {
|
||||
@include box-sizing(border-box);
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
padding: 15px;
|
||||
top: 0;
|
||||
left: 0;
|
||||
border-bottom: 1px solid $lightBluishGrey2;
|
||||
background: $lightGrey;
|
||||
}
|
||||
|
||||
&.editing {
|
||||
padding-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,8 @@
|
||||
.course-info {
|
||||
// studio - views - course updates
|
||||
// ====================
|
||||
|
||||
body.course.updates {
|
||||
|
||||
h2 {
|
||||
margin-bottom: 24px;
|
||||
font-size: 22px;
|
||||
@@ -1,4 +1,8 @@
|
||||
.users {
|
||||
// studio - views - course users
|
||||
// ====================
|
||||
|
||||
body.course.users {
|
||||
|
||||
.new-user-form {
|
||||
display: none;
|
||||
padding: 15px 20px;
|
||||
14
cms/templates/404.html
Normal file
14
cms/templates/404.html
Normal file
@@ -0,0 +1,14 @@
|
||||
<%inherit file="base.html" />
|
||||
<%block name="title">Page Not Found</%block>
|
||||
|
||||
<%block name="content">
|
||||
|
||||
<div class="wrapper-content wrapper">
|
||||
<section class="content">
|
||||
|
||||
<h1>Page not found</h1>
|
||||
<p>The page that you were looking for was not found. Go back to the <a href="/">homepage</a> or let us know about any pages that may have been moved at <a href="mailto:technical@edx.org">technical@edx.org</a>.</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
</%block>
|
||||
13
cms/templates/500.html
Normal file
13
cms/templates/500.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<%inherit file="base.html" />
|
||||
<%block name="title">Server Error</%block>
|
||||
|
||||
<%block name="content">
|
||||
|
||||
<div class="wrapper-content wrapper">
|
||||
<section class="content">
|
||||
<h1>Currently the <em>edX</em> servers are down</h1>
|
||||
<p>Our staff is currently working to get the site back up as soon as possible. Please email us at <a href="mailto:technical@edx.org">technical@edx.org</a> to report any problems or downtime.</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
</%block>
|
||||
10
cms/urls.py
10
cms/urls.py
@@ -43,7 +43,7 @@ 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'),
|
||||
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/course_info/updates/(?P<provided_id>.*)$', 'contentstore.views.course_info_updates', name='course_info'),
|
||||
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/course_info/updates/(?P<provided_id>.*)$', 'contentstore.views.course_info_updates', name='course_info_json'),
|
||||
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/settings-details/(?P<name>[^/]+)$', 'contentstore.views.get_course_settings', name='course_settings'),
|
||||
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/settings-grading/(?P<name>[^/]+)$', 'contentstore.views.course_config_graders_page', name='course_settings'),
|
||||
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/settings-details/(?P<name>[^/]+)/section/(?P<section>[^/]+).*$', 'contentstore.views.course_settings_updates', name='course_settings'),
|
||||
@@ -100,7 +100,13 @@ urlpatterns += (
|
||||
)
|
||||
|
||||
if settings.ENABLE_JASMINE:
|
||||
## Jasmine
|
||||
# # Jasmine
|
||||
urlpatterns = urlpatterns + (url(r'^_jasmine/', include('django_jasmine.urls')),)
|
||||
|
||||
urlpatterns = patterns(*urlpatterns)
|
||||
|
||||
# Custom error pages
|
||||
handler404 = 'contentstore.views.render_404'
|
||||
handler500 = 'contentstore.views.render_500'
|
||||
|
||||
|
||||
|
||||
@@ -15,6 +15,24 @@ from .models import CourseUserGroup
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# tl;dr: global state is bad. capa reseeds random every time a problem is loaded. Even
|
||||
# if and when that's fixed, it's a good idea to have a local generator to avoid any other
|
||||
# code that messes with the global random module.
|
||||
_local_random = None
|
||||
|
||||
def local_random():
|
||||
"""
|
||||
Get the local random number generator. In a function so that we don't run
|
||||
random.Random() at import time.
|
||||
"""
|
||||
# ironic, isn't it?
|
||||
global _local_random
|
||||
|
||||
if _local_random is None:
|
||||
_local_random = random.Random()
|
||||
|
||||
return _local_random
|
||||
|
||||
def is_course_cohorted(course_id):
|
||||
"""
|
||||
Given a course id, return a boolean for whether or not the course is
|
||||
@@ -129,13 +147,7 @@ def get_cohort(user, course_id):
|
||||
return None
|
||||
|
||||
# Put user in a random group, creating it if needed
|
||||
choice = random.randrange(0, n)
|
||||
group_name = choices[choice]
|
||||
|
||||
# Victor: we are seeing very strange behavior on prod, where almost all users
|
||||
# end up in the same group. Log at INFO to try to figure out what's going on.
|
||||
log.info("DEBUG: adding user {0} to cohort {1}. choice={2}".format(
|
||||
user, group_name,choice))
|
||||
group_name = local_random().choice(choices)
|
||||
|
||||
group, created = CourseUserGroup.objects.get_or_create(
|
||||
course_id=course_id,
|
||||
|
||||
@@ -75,10 +75,15 @@ class UserProfile(models.Model):
|
||||
GENDER_CHOICES = (('m', 'Male'), ('f', 'Female'), ('o', 'Other'))
|
||||
gender = models.CharField(blank=True, null=True, max_length=6, db_index=True,
|
||||
choices=GENDER_CHOICES)
|
||||
LEVEL_OF_EDUCATION_CHOICES = (('p_se', 'Doctorate in science or engineering'),
|
||||
('p_oth', 'Doctorate in another field'),
|
||||
|
||||
# [03/21/2013] removed these, but leaving comment since there'll still be
|
||||
# p_se and p_oth in the existing data in db.
|
||||
# ('p_se', 'Doctorate in science or engineering'),
|
||||
# ('p_oth', 'Doctorate in another field'),
|
||||
LEVEL_OF_EDUCATION_CHOICES = (('p', 'Doctorate'),
|
||||
('m', "Master's or professional degree"),
|
||||
('b', "Bachelor's degree"),
|
||||
('a', "Associate's degree"),
|
||||
('hs', "Secondary/high school"),
|
||||
('jhs', "Junior secondary/junior high/middle school"),
|
||||
('el', "Elementary/primary school"),
|
||||
|
||||
0
common/djangoapps/student/tests/__init__.py
Normal file
0
common/djangoapps/student/tests/__init__.py
Normal file
59
common/djangoapps/student/tests/factories.py
Normal file
59
common/djangoapps/student/tests/factories.py
Normal file
@@ -0,0 +1,59 @@
|
||||
from student.models import (User, UserProfile, Registration,
|
||||
CourseEnrollmentAllowed, CourseEnrollment)
|
||||
from django.contrib.auth.models import Group
|
||||
from datetime import datetime
|
||||
from factory import Factory, SubFactory
|
||||
from uuid import uuid4
|
||||
|
||||
|
||||
class GroupFactory(Factory):
|
||||
FACTORY_FOR = Group
|
||||
|
||||
name = 'staff_MITx/999/Robot_Super_Course'
|
||||
|
||||
|
||||
class UserProfileFactory(Factory):
|
||||
FACTORY_FOR = UserProfile
|
||||
|
||||
user = None
|
||||
name = 'Robot Test'
|
||||
level_of_education = None
|
||||
gender = 'm'
|
||||
mailing_address = None
|
||||
goals = 'World domination'
|
||||
|
||||
|
||||
class RegistrationFactory(Factory):
|
||||
FACTORY_FOR = Registration
|
||||
|
||||
user = None
|
||||
activation_key = uuid4().hex
|
||||
|
||||
|
||||
class UserFactory(Factory):
|
||||
FACTORY_FOR = User
|
||||
|
||||
username = 'robot'
|
||||
email = 'robot+test@edx.org'
|
||||
password = 'test'
|
||||
first_name = 'Robot'
|
||||
last_name = 'Test'
|
||||
is_staff = False
|
||||
is_active = True
|
||||
is_superuser = False
|
||||
last_login = datetime(2012, 1, 1)
|
||||
date_joined = datetime(2011, 1, 1)
|
||||
|
||||
|
||||
class CourseEnrollmentFactory(Factory):
|
||||
FACTORY_FOR = CourseEnrollment
|
||||
|
||||
user = SubFactory(UserFactory)
|
||||
course_id = 'edX/toy/2012_Fall'
|
||||
|
||||
|
||||
class CourseEnrollmentAllowedFactory(Factory):
|
||||
FACTORY_FOR = CourseEnrollmentAllowed
|
||||
|
||||
email = 'test@edx.org'
|
||||
course_id = 'edX/test/2012_Fall'
|
||||
@@ -9,8 +9,8 @@ import logging
|
||||
from django.test import TestCase
|
||||
from mock import Mock
|
||||
|
||||
from .models import unique_id_for_user
|
||||
from .views import process_survey_link, _cert_info
|
||||
from student.models import unique_id_for_user
|
||||
from student.views import process_survey_link, _cert_info
|
||||
|
||||
COURSE_1 = 'edX/toy/2012_Fall'
|
||||
COURSE_2 = 'edx/full/6.002_Spring_2012'
|
||||
@@ -311,7 +311,7 @@ def change_enrollment(request):
|
||||
course = course_from_id(course_id)
|
||||
except ItemNotFoundError:
|
||||
log.warning("User {0} tried to enroll in non-existent course {1}"
|
||||
.format(user.username, enrollment.course_id))
|
||||
.format(user.username, course_id))
|
||||
return {'success': False, 'error': 'The course requested does not exist.'}
|
||||
|
||||
if not has_access(user, course, 'enroll'):
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
from lettuce import before, after, world
|
||||
from splinter.browser import Browser
|
||||
from logging import getLogger
|
||||
import time
|
||||
|
||||
# Let the LMS and CMS do their one-time setup
|
||||
# For example, setting up mongo caches
|
||||
from lms import one_time_startup
|
||||
from cms import one_time_startup
|
||||
|
||||
logger = getLogger(__name__)
|
||||
logger.info("Loading the lettuce acceptance testing terrain file...")
|
||||
@@ -11,6 +15,9 @@ from django.core.management import call_command
|
||||
|
||||
@before.harvest
|
||||
def initial_setup(server):
|
||||
'''
|
||||
Launch the browser once before executing the tests
|
||||
'''
|
||||
# Launch the browser app (choose one of these below)
|
||||
world.browser = Browser('chrome')
|
||||
# world.browser = Browser('phantomjs')
|
||||
@@ -19,14 +26,18 @@ def initial_setup(server):
|
||||
|
||||
@before.each_scenario
|
||||
def reset_data(scenario):
|
||||
# Clean out the django test database defined in the
|
||||
# envs/acceptance.py file: mitx_all/db/test_mitx.db
|
||||
'''
|
||||
Clean out the django test database defined in the
|
||||
envs/acceptance.py file: mitx_all/db/test_mitx.db
|
||||
'''
|
||||
logger.debug("Flushing the test database...")
|
||||
call_command('flush', interactive=False)
|
||||
|
||||
|
||||
@after.all
|
||||
def teardown_browser(total):
|
||||
# Quit firefox
|
||||
'''
|
||||
Quit the browser after executing the tests
|
||||
'''
|
||||
world.browser.quit()
|
||||
pass
|
||||
|
||||
@@ -1,163 +1,64 @@
|
||||
from student.models import User, UserProfile, Registration
|
||||
from django.contrib.auth.models import Group
|
||||
from datetime import datetime
|
||||
from factory import Factory
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from time import gmtime
|
||||
from uuid import uuid4
|
||||
from xmodule.timeparse import stringify_time
|
||||
from xmodule.modulestore.inheritance import own_metadata
|
||||
'''
|
||||
Factories are defined in other modules and absorbed here into the
|
||||
lettuce world so that they can be used by both unit tests
|
||||
and integration / BDD tests.
|
||||
'''
|
||||
import student.tests.factories as sf
|
||||
import xmodule.modulestore.tests.factories as xf
|
||||
from lettuce import world
|
||||
|
||||
|
||||
class GroupFactory(Factory):
|
||||
FACTORY_FOR = Group
|
||||
|
||||
name = 'staff_MITx/999/Robot_Super_Course'
|
||||
|
||||
|
||||
class UserProfileFactory(Factory):
|
||||
FACTORY_FOR = UserProfile
|
||||
|
||||
user = None
|
||||
name = 'Robot Test'
|
||||
level_of_education = None
|
||||
gender = 'm'
|
||||
mailing_address = None
|
||||
goals = 'World domination'
|
||||
|
||||
|
||||
class RegistrationFactory(Factory):
|
||||
FACTORY_FOR = Registration
|
||||
|
||||
user = None
|
||||
activation_key = uuid4().hex
|
||||
|
||||
|
||||
class UserFactory(Factory):
|
||||
FACTORY_FOR = User
|
||||
|
||||
username = 'robot'
|
||||
email = 'robot+test@edx.org'
|
||||
password = 'test'
|
||||
first_name = 'Robot'
|
||||
last_name = 'Test'
|
||||
is_staff = False
|
||||
is_active = True
|
||||
is_superuser = False
|
||||
last_login = datetime(2012, 1, 1)
|
||||
date_joined = datetime(2011, 1, 1)
|
||||
|
||||
|
||||
def XMODULE_COURSE_CREATION(class_to_create, **kwargs):
|
||||
return XModuleCourseFactory._create(class_to_create, **kwargs)
|
||||
|
||||
|
||||
def XMODULE_ITEM_CREATION(class_to_create, **kwargs):
|
||||
return XModuleItemFactory._create(class_to_create, **kwargs)
|
||||
|
||||
|
||||
class XModuleCourseFactory(Factory):
|
||||
@world.absorb
|
||||
class UserFactory(sf.UserFactory):
|
||||
"""
|
||||
Factory for XModule courses.
|
||||
User account for lms / cms
|
||||
"""
|
||||
|
||||
ABSTRACT_FACTORY = True
|
||||
_creation_function = (XMODULE_COURSE_CREATION,)
|
||||
|
||||
@classmethod
|
||||
def _create(cls, target_class, *args, **kwargs):
|
||||
|
||||
template = Location('i4x', 'edx', 'templates', 'course', 'Empty')
|
||||
org = kwargs.get('org')
|
||||
number = kwargs.get('number')
|
||||
display_name = kwargs.get('display_name')
|
||||
location = Location('i4x', org, number,
|
||||
'course', Location.clean(display_name))
|
||||
|
||||
store = modulestore('direct')
|
||||
|
||||
# Write the data to the mongo datastore
|
||||
new_course = store.clone_item(template, location)
|
||||
|
||||
# This metadata code was copied from cms/djangoapps/contentstore/views.py
|
||||
if display_name is not None:
|
||||
new_course.display_name = display_name
|
||||
|
||||
new_course.lms.start = gmtime()
|
||||
new_course.tabs = [{"type": "courseware"},
|
||||
{"type": "course_info", "name": "Course Info"},
|
||||
{"type": "discussion", "name": "Discussion"},
|
||||
{"type": "wiki", "name": "Wiki"},
|
||||
{"type": "progress", "name": "Progress"}]
|
||||
|
||||
# Update the data in the mongo datastore
|
||||
store.update_metadata(new_course.location.url(), own_metadata(new_course))
|
||||
|
||||
return new_course
|
||||
|
||||
|
||||
class Course:
|
||||
pass
|
||||
|
||||
|
||||
class CourseFactory(XModuleCourseFactory):
|
||||
FACTORY_FOR = Course
|
||||
|
||||
template = 'i4x://edx/templates/course/Empty'
|
||||
org = 'MITx'
|
||||
number = '999'
|
||||
display_name = 'Robot Super Course'
|
||||
|
||||
|
||||
class XModuleItemFactory(Factory):
|
||||
@world.absorb
|
||||
class UserProfileFactory(sf.UserProfileFactory):
|
||||
"""
|
||||
Factory for XModule items.
|
||||
Demographics etc for the User
|
||||
"""
|
||||
|
||||
ABSTRACT_FACTORY = True
|
||||
_creation_function = (XMODULE_ITEM_CREATION,)
|
||||
|
||||
@classmethod
|
||||
def _create(cls, target_class, *args, **kwargs):
|
||||
"""
|
||||
kwargs must include parent_location, template. Can contain display_name
|
||||
target_class is ignored
|
||||
"""
|
||||
|
||||
DETACHED_CATEGORIES = ['about', 'static_tab', 'course_info']
|
||||
|
||||
parent_location = Location(kwargs.get('parent_location'))
|
||||
template = Location(kwargs.get('template'))
|
||||
display_name = kwargs.get('display_name')
|
||||
|
||||
store = modulestore('direct')
|
||||
|
||||
# This code was based off that in cms/djangoapps/contentstore/views.py
|
||||
parent = store.get_item(parent_location)
|
||||
dest_location = parent_location._replace(category=template.category, name=uuid4().hex)
|
||||
|
||||
new_item = store.clone_item(template, dest_location)
|
||||
|
||||
# replace the display name with an optional parameter passed in from the caller
|
||||
if display_name is not None:
|
||||
new_item.display_name = display_name
|
||||
|
||||
store.update_metadata(new_item.location.url(), own_metadata(new_item))
|
||||
|
||||
if new_item.location.category not in DETACHED_CATEGORIES:
|
||||
store.update_children(parent_location, parent.children + [new_item.location.url()])
|
||||
|
||||
return new_item
|
||||
|
||||
|
||||
class Item:
|
||||
pass
|
||||
|
||||
|
||||
class ItemFactory(XModuleItemFactory):
|
||||
FACTORY_FOR = Item
|
||||
@world.absorb
|
||||
class RegistrationFactory(sf.RegistrationFactory):
|
||||
"""
|
||||
Activation key for registering the user account
|
||||
"""
|
||||
pass
|
||||
|
||||
parent_location = 'i4x://MITx/999/course/Robot_Super_Course'
|
||||
template = 'i4x://edx/templates/chapter/Empty'
|
||||
display_name = 'Section One'
|
||||
|
||||
@world.absorb
|
||||
class GroupFactory(sf.GroupFactory):
|
||||
"""
|
||||
Groups for user permissions for courses
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
@world.absorb
|
||||
class CourseEnrollmentAllowedFactory(sf.CourseEnrollmentAllowed):
|
||||
"""
|
||||
Users allowed to enroll in the course outside of the usual window
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
@world.absorb
|
||||
class CourseFactory(xf.CourseFactory):
|
||||
"""
|
||||
Courseware courses
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
@world.absorb
|
||||
class ItemFactory(xf.ItemFactory):
|
||||
"""
|
||||
Everything included inside a course
|
||||
"""
|
||||
pass
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
from lettuce import world, step
|
||||
from .factories import *
|
||||
from lettuce.django import django_url
|
||||
from django.conf import settings
|
||||
from django.http import HttpRequest
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.auth import authenticate, login
|
||||
from django.contrib.auth.middleware import AuthenticationMiddleware
|
||||
from django.contrib.sessions.middleware import SessionMiddleware
|
||||
from student.models import CourseEnrollment
|
||||
from urllib import quote_plus
|
||||
from nose.tools import assert_equals
|
||||
@@ -9,6 +14,7 @@ from bs4 import BeautifulSoup
|
||||
import time
|
||||
import re
|
||||
import os.path
|
||||
from selenium.common.exceptions import WebDriverException
|
||||
|
||||
from logging import getLogger
|
||||
logger = getLogger(__name__)
|
||||
@@ -69,10 +75,15 @@ def the_page_title_should_be(step, title):
|
||||
assert_equals(world.browser.title, title)
|
||||
|
||||
|
||||
@step(u'the page title should contain "([^"]*)"$')
|
||||
def the_page_title_should_contain(step, title):
|
||||
assert(title in world.browser.title)
|
||||
|
||||
|
||||
@step('I am a logged in user$')
|
||||
def i_am_logged_in_user(step):
|
||||
create_user('robot')
|
||||
log_in('robot@edx.org', 'test')
|
||||
log_in('robot', 'test')
|
||||
|
||||
|
||||
@step('I am not logged in$')
|
||||
@@ -80,18 +91,6 @@ def i_am_not_logged_in(step):
|
||||
world.browser.cookies.delete()
|
||||
|
||||
|
||||
@step('I am registered for a course$')
|
||||
def i_am_registered_for_a_course(step):
|
||||
create_user('robot')
|
||||
u = User.objects.get(username='robot')
|
||||
CourseEnrollment.objects.get_or_create(user=u, course_id='MITx/6.002x/2012_Fall')
|
||||
|
||||
|
||||
@step('I am registered for course "([^"]*)"$')
|
||||
def i_am_registered_for_course_by_id(step, course_id):
|
||||
register_by_course_id(course_id)
|
||||
|
||||
|
||||
@step('I am staff for course "([^"]*)"$')
|
||||
def i_am_staff_for_course_by_id(step, course_id):
|
||||
register_by_course_id(course_id, True)
|
||||
@@ -99,7 +98,7 @@ def i_am_staff_for_course_by_id(step, course_id):
|
||||
|
||||
@step('I log in$')
|
||||
def i_log_in(step):
|
||||
log_in('robot@edx.org', 'test')
|
||||
log_in('robot', 'test')
|
||||
|
||||
|
||||
@step(u'I am an edX user$')
|
||||
@@ -108,6 +107,7 @@ def i_am_an_edx_user(step):
|
||||
|
||||
#### helper functions
|
||||
|
||||
|
||||
@world.absorb
|
||||
def scroll_to_bottom():
|
||||
# Maximize the browser
|
||||
@@ -116,30 +116,55 @@ def scroll_to_bottom():
|
||||
|
||||
@world.absorb
|
||||
def create_user(uname):
|
||||
|
||||
# If the user already exists, don't try to create it again
|
||||
if len(User.objects.filter(username=uname)) > 0:
|
||||
return
|
||||
|
||||
portal_user = UserFactory.build(username=uname, email=uname + '@edx.org')
|
||||
portal_user.set_password('test')
|
||||
portal_user.save()
|
||||
|
||||
registration = RegistrationFactory(user=portal_user)
|
||||
registration = world.RegistrationFactory(user=portal_user)
|
||||
registration.register(portal_user)
|
||||
registration.activate()
|
||||
|
||||
user_profile = UserProfileFactory(user=portal_user)
|
||||
user_profile = world.UserProfileFactory(user=portal_user)
|
||||
|
||||
|
||||
@world.absorb
|
||||
def log_in(email, password):
|
||||
world.browser.cookies.delete()
|
||||
world.browser.visit(django_url('/'))
|
||||
world.browser.is_element_present_by_css('header.global', 10)
|
||||
world.browser.click_link_by_href('#login-modal')
|
||||
login_form = world.browser.find_by_css('form#login_form')
|
||||
login_form.find_by_name('email').fill(email)
|
||||
login_form.find_by_name('password').fill(password)
|
||||
login_form.find_by_name('submit').click()
|
||||
def log_in(username, password):
|
||||
'''
|
||||
Log the user in programatically
|
||||
'''
|
||||
|
||||
# wait for the page to redraw
|
||||
assert world.browser.is_element_present_by_css('.content-wrapper', 10)
|
||||
# Authenticate the user
|
||||
user = authenticate(username=username, password=password)
|
||||
assert(user is not None and user.is_active)
|
||||
|
||||
# Send a fake HttpRequest to log the user in
|
||||
# We need to process the request using
|
||||
# Session middleware and Authentication middleware
|
||||
# to ensure that session state can be stored
|
||||
request = HttpRequest()
|
||||
SessionMiddleware().process_request(request)
|
||||
AuthenticationMiddleware().process_request(request)
|
||||
login(request, user)
|
||||
|
||||
# Save the session
|
||||
request.session.save()
|
||||
|
||||
# Retrieve the sessionid and add it to the browser's cookies
|
||||
cookie_dict = {settings.SESSION_COOKIE_NAME: request.session.session_key}
|
||||
try:
|
||||
world.browser.cookies.add(cookie_dict)
|
||||
|
||||
# WebDriver has an issue where we cannot set cookies
|
||||
# before we make a GET request, so if we get an error,
|
||||
# we load the '/' page and try again
|
||||
except:
|
||||
world.browser.visit(django_url('/'))
|
||||
world.browser.cookies.add(cookie_dict)
|
||||
|
||||
|
||||
@world.absorb
|
||||
@@ -196,6 +221,7 @@ def save_the_course_content(path='/tmp'):
|
||||
u = world.browser.url
|
||||
section_url = u[u.find('courseware/') + 11:]
|
||||
|
||||
|
||||
if not os.path.exists(path):
|
||||
os.makedirs(path)
|
||||
|
||||
@@ -203,3 +229,15 @@ def save_the_course_content(path='/tmp'):
|
||||
f = open('%s/%s' % (path, filename), 'w')
|
||||
f.write(output)
|
||||
f.close
|
||||
|
||||
@world.absorb
|
||||
def css_click(css_selector):
|
||||
try:
|
||||
world.browser.find_by_css(css_selector).click()
|
||||
|
||||
except WebDriverException:
|
||||
# Occassionally, MathJax or other JavaScript can cover up
|
||||
# an element temporarily.
|
||||
# If this happens, wait a second, then try again
|
||||
time.sleep(1)
|
||||
world.browser.find_by_css(css_selector).click()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from lxml import etree
|
||||
from abc import ABCMeta, abstractmethod
|
||||
|
||||
|
||||
class ResponseXMLFactory(object):
|
||||
""" Abstract base class for capa response XML factories.
|
||||
Subclasses override create_response_element and
|
||||
@@ -13,7 +14,7 @@ class ResponseXMLFactory(object):
|
||||
""" Subclasses override to return an etree element
|
||||
representing the capa response XML
|
||||
(e.g. <numericalresponse>).
|
||||
|
||||
|
||||
The tree should NOT contain any input elements
|
||||
(such as <textline />) as these will be added later."""
|
||||
return None
|
||||
@@ -25,7 +26,7 @@ class ResponseXMLFactory(object):
|
||||
return None
|
||||
|
||||
def build_xml(self, **kwargs):
|
||||
""" Construct an XML string for a capa response
|
||||
""" Construct an XML string for a capa response
|
||||
based on **kwargs.
|
||||
|
||||
**kwargs is a dictionary that will be passed
|
||||
@@ -37,7 +38,7 @@ class ResponseXMLFactory(object):
|
||||
|
||||
*question_text*: The text of the question to display,
|
||||
wrapped in <p> tags.
|
||||
|
||||
|
||||
*explanation_text*: The detailed explanation that will
|
||||
be shown if the user answers incorrectly.
|
||||
|
||||
@@ -75,7 +76,7 @@ class ResponseXMLFactory(object):
|
||||
for i in range(0, int(num_responses)):
|
||||
response_element = self.create_response_element(**kwargs)
|
||||
root.append(response_element)
|
||||
|
||||
|
||||
# Add input elements
|
||||
for j in range(0, int(num_inputs)):
|
||||
input_element = self.create_input_element(**kwargs)
|
||||
@@ -135,7 +136,7 @@ class ResponseXMLFactory(object):
|
||||
# Names of group elements
|
||||
group_element_names = {'checkbox': 'checkboxgroup',
|
||||
'radio': 'radiogroup',
|
||||
'multiple': 'choicegroup' }
|
||||
'multiple': 'choicegroup'}
|
||||
|
||||
# Retrieve **kwargs
|
||||
choices = kwargs.get('choices', [True])
|
||||
@@ -151,13 +152,11 @@ class ResponseXMLFactory(object):
|
||||
choice_element = etree.SubElement(group_element, "choice")
|
||||
choice_element.set("correct", "true" if correct_val else "false")
|
||||
|
||||
# Add some text describing the choice
|
||||
etree.SubElement(choice_element, "startouttext")
|
||||
etree.text = "Choice description"
|
||||
etree.SubElement(choice_element, "endouttext")
|
||||
|
||||
# Add a name identifying the choice, if one exists
|
||||
# For simplicity, we use the same string as both the
|
||||
# name attribute and the text of the element
|
||||
if name:
|
||||
choice_element.text = str(name)
|
||||
choice_element.set("name", str(name))
|
||||
|
||||
return group_element
|
||||
@@ -217,7 +216,7 @@ class CustomResponseXMLFactory(ResponseXMLFactory):
|
||||
|
||||
*answer*: Inline script that calculates the answer
|
||||
"""
|
||||
|
||||
|
||||
# Retrieve **kwargs
|
||||
cfn = kwargs.get('cfn', None)
|
||||
expect = kwargs.get('expect', None)
|
||||
@@ -247,7 +246,7 @@ class SchematicResponseXMLFactory(ResponseXMLFactory):
|
||||
|
||||
def create_response_element(self, **kwargs):
|
||||
""" Create the <schematicresponse> XML element.
|
||||
|
||||
|
||||
Uses *kwargs*:
|
||||
|
||||
*answer*: The Python script used to evaluate the answer.
|
||||
@@ -274,6 +273,7 @@ class SchematicResponseXMLFactory(ResponseXMLFactory):
|
||||
For testing, we create a bare-bones version of <schematic>."""
|
||||
return etree.Element("schematic")
|
||||
|
||||
|
||||
class CodeResponseXMLFactory(ResponseXMLFactory):
|
||||
""" Factory for creating <coderesponse> XML trees """
|
||||
|
||||
@@ -286,9 +286,9 @@ class CodeResponseXMLFactory(ResponseXMLFactory):
|
||||
|
||||
def create_response_element(self, **kwargs):
|
||||
""" Create a <coderesponse> XML element:
|
||||
|
||||
|
||||
Uses **kwargs:
|
||||
|
||||
|
||||
*initial_display*: The code that initially appears in the textbox
|
||||
[DEFAULT: "Enter code here"]
|
||||
*answer_display*: The answer to display to the student
|
||||
@@ -328,6 +328,7 @@ class CodeResponseXMLFactory(ResponseXMLFactory):
|
||||
# return None here
|
||||
return None
|
||||
|
||||
|
||||
class ChoiceResponseXMLFactory(ResponseXMLFactory):
|
||||
""" Factory for creating <choiceresponse> XML trees """
|
||||
|
||||
@@ -356,13 +357,13 @@ class FormulaResponseXMLFactory(ResponseXMLFactory):
|
||||
|
||||
*num_samples*: The number of times to sample the student's answer
|
||||
to numerically compare it to the correct answer.
|
||||
|
||||
|
||||
*tolerance*: The tolerance within which answers will be accepted
|
||||
[DEFAULT: 0.01]
|
||||
[DEFAULT: 0.01]
|
||||
|
||||
*answer*: The answer to the problem. Can be a formula string
|
||||
or a Python variable defined in a script
|
||||
(e.g. "$calculated_answer" for a Python variable
|
||||
or a Python variable defined in a script
|
||||
(e.g. "$calculated_answer" for a Python variable
|
||||
called calculated_answer)
|
||||
[REQUIRED]
|
||||
|
||||
@@ -387,7 +388,7 @@ class FormulaResponseXMLFactory(ResponseXMLFactory):
|
||||
# Set the sample information
|
||||
sample_str = self._sample_str(sample_dict, num_samples, tolerance)
|
||||
response_element.set("samples", sample_str)
|
||||
|
||||
|
||||
|
||||
# Set the tolerance
|
||||
responseparam_element = etree.SubElement(response_element, "responseparam")
|
||||
@@ -408,7 +409,7 @@ class FormulaResponseXMLFactory(ResponseXMLFactory):
|
||||
|
||||
# We could sample a different range, but for simplicity,
|
||||
# we use the same sample string for the hints
|
||||
# that we used previously.
|
||||
# that we used previously.
|
||||
formulahint_element.set("samples", sample_str)
|
||||
|
||||
formulahint_element.set("answer", str(hint_prompt))
|
||||
@@ -436,10 +437,11 @@ class FormulaResponseXMLFactory(ResponseXMLFactory):
|
||||
high_range_vals = [str(f[1]) for f in sample_dict.values()]
|
||||
sample_str = (",".join(sample_dict.keys()) + "@" +
|
||||
",".join(low_range_vals) + ":" +
|
||||
",".join(high_range_vals) +
|
||||
",".join(high_range_vals) +
|
||||
"#" + str(num_samples))
|
||||
return sample_str
|
||||
|
||||
|
||||
class ImageResponseXMLFactory(ResponseXMLFactory):
|
||||
""" Factory for producing <imageresponse> XML """
|
||||
|
||||
@@ -450,9 +452,9 @@ class ImageResponseXMLFactory(ResponseXMLFactory):
|
||||
|
||||
def create_input_element(self, **kwargs):
|
||||
""" Create the <imageinput> element.
|
||||
|
||||
|
||||
Uses **kwargs:
|
||||
|
||||
|
||||
*src*: URL for the image file [DEFAULT: "/static/image.jpg"]
|
||||
|
||||
*width*: Width of the image [DEFAULT: 100]
|
||||
@@ -490,7 +492,7 @@ class ImageResponseXMLFactory(ResponseXMLFactory):
|
||||
input_element.set("src", str(src))
|
||||
input_element.set("width", str(width))
|
||||
input_element.set("height", str(height))
|
||||
|
||||
|
||||
if rectangle:
|
||||
input_element.set("rectangle", rectangle)
|
||||
|
||||
@@ -499,6 +501,7 @@ class ImageResponseXMLFactory(ResponseXMLFactory):
|
||||
|
||||
return input_element
|
||||
|
||||
|
||||
class JavascriptResponseXMLFactory(ResponseXMLFactory):
|
||||
""" Factory for producing <javascriptresponse> XML """
|
||||
|
||||
@@ -522,7 +525,7 @@ class JavascriptResponseXMLFactory(ResponseXMLFactory):
|
||||
|
||||
# Both display_src and display_class given,
|
||||
# or neither given
|
||||
assert((display_src and display_class) or
|
||||
assert((display_src and display_class) or
|
||||
(not display_src and not display_class))
|
||||
|
||||
# Create the <javascriptresponse> element
|
||||
@@ -552,6 +555,7 @@ class JavascriptResponseXMLFactory(ResponseXMLFactory):
|
||||
""" Create the <javascriptinput> element """
|
||||
return etree.Element("javascriptinput")
|
||||
|
||||
|
||||
class MultipleChoiceResponseXMLFactory(ResponseXMLFactory):
|
||||
""" Factory for producing <multiplechoiceresponse> XML """
|
||||
|
||||
@@ -564,6 +568,7 @@ class MultipleChoiceResponseXMLFactory(ResponseXMLFactory):
|
||||
kwargs['choice_type'] = 'multiple'
|
||||
return ResponseXMLFactory.choicegroup_input_xml(**kwargs)
|
||||
|
||||
|
||||
class TrueFalseResponseXMLFactory(ResponseXMLFactory):
|
||||
""" Factory for producing <truefalseresponse> XML """
|
||||
|
||||
@@ -576,6 +581,7 @@ class TrueFalseResponseXMLFactory(ResponseXMLFactory):
|
||||
kwargs['choice_type'] = 'multiple'
|
||||
return ResponseXMLFactory.choicegroup_input_xml(**kwargs)
|
||||
|
||||
|
||||
class OptionResponseXMLFactory(ResponseXMLFactory):
|
||||
""" Factory for producing <optionresponse> XML"""
|
||||
|
||||
@@ -620,7 +626,7 @@ class StringResponseXMLFactory(ResponseXMLFactory):
|
||||
|
||||
def create_response_element(self, **kwargs):
|
||||
""" Create a <stringresponse> XML element.
|
||||
|
||||
|
||||
Uses **kwargs:
|
||||
|
||||
*answer*: The correct answer (a string) [REQUIRED]
|
||||
@@ -642,7 +648,7 @@ class StringResponseXMLFactory(ResponseXMLFactory):
|
||||
# Create the <stringresponse> element
|
||||
response_element = etree.Element("stringresponse")
|
||||
|
||||
# Set the answer attribute
|
||||
# Set the answer attribute
|
||||
response_element.set("answer", str(answer))
|
||||
|
||||
# Set the case sensitivity
|
||||
@@ -667,6 +673,7 @@ class StringResponseXMLFactory(ResponseXMLFactory):
|
||||
def create_input_element(self, **kwargs):
|
||||
return ResponseXMLFactory.textline_input_xml(**kwargs)
|
||||
|
||||
|
||||
class AnnotationResponseXMLFactory(ResponseXMLFactory):
|
||||
""" Factory for creating <annotationresponse> XML trees """
|
||||
def create_response_element(self, **kwargs):
|
||||
@@ -679,17 +686,17 @@ class AnnotationResponseXMLFactory(ResponseXMLFactory):
|
||||
input_element = etree.Element("annotationinput")
|
||||
|
||||
text_children = [
|
||||
{'tag': 'title', 'text': kwargs.get('title', 'super cool annotation') },
|
||||
{'tag': 'text', 'text': kwargs.get('text', 'texty text') },
|
||||
{'tag': 'comment', 'text':kwargs.get('comment', 'blah blah erudite comment blah blah') },
|
||||
{'tag': 'comment_prompt', 'text': kwargs.get('comment_prompt', 'type a commentary below') },
|
||||
{'tag': 'tag_prompt', 'text': kwargs.get('tag_prompt', 'select one tag') }
|
||||
{'tag': 'title', 'text': kwargs.get('title', 'super cool annotation')},
|
||||
{'tag': 'text', 'text': kwargs.get('text', 'texty text')},
|
||||
{'tag': 'comment', 'text':kwargs.get('comment', 'blah blah erudite comment blah blah')},
|
||||
{'tag': 'comment_prompt', 'text': kwargs.get('comment_prompt', 'type a commentary below')},
|
||||
{'tag': 'tag_prompt', 'text': kwargs.get('tag_prompt', 'select one tag')}
|
||||
]
|
||||
|
||||
for child in text_children:
|
||||
etree.SubElement(input_element, child['tag']).text = child['text']
|
||||
|
||||
default_options = [('green', 'correct'),('eggs', 'incorrect'),('ham', 'partially-correct')]
|
||||
default_options = [('green', 'correct'),('eggs', 'incorrect'), ('ham', 'partially-correct')]
|
||||
options = kwargs.get('options', default_options)
|
||||
options_element = etree.SubElement(input_element, 'options')
|
||||
|
||||
@@ -698,4 +705,3 @@ class AnnotationResponseXMLFactory(ResponseXMLFactory):
|
||||
option_element.text = description
|
||||
|
||||
return input_element
|
||||
|
||||
|
||||
@@ -8,41 +8,66 @@ from xmodule.raw_module import RawDescriptor
|
||||
from .x_module import XModule
|
||||
from xblock.core import Integer, Scope, BlockScope, ModelType, String, Boolean, Object, Float, List
|
||||
from xmodule.open_ended_grading_classes.combined_open_ended_modulev1 import CombinedOpenEndedV1Module, CombinedOpenEndedV1Descriptor
|
||||
from collections import namedtuple
|
||||
|
||||
log = logging.getLogger("mitx.courseware")
|
||||
|
||||
|
||||
V1_SETTINGS_ATTRIBUTES = ["display_name", "attempts", "is_graded", "accept_file_upload",
|
||||
"skip_spelling_checks", "due", "graceperiod", "max_score"]
|
||||
"skip_spelling_checks", "due", "graceperiod", "max_score"]
|
||||
|
||||
V1_STUDENT_ATTRIBUTES = ["current_task_number", "task_states", "state",
|
||||
"student_attempts", "ready_to_reset"]
|
||||
"student_attempts", "ready_to_reset"]
|
||||
|
||||
V1_ATTRIBUTES = V1_SETTINGS_ATTRIBUTES + V1_STUDENT_ATTRIBUTES
|
||||
|
||||
VERSION_TUPLES = (
|
||||
('1', CombinedOpenEndedV1Descriptor, CombinedOpenEndedV1Module, V1_SETTINGS_ATTRIBUTES, V1_STUDENT_ATTRIBUTES),
|
||||
)
|
||||
VersionTuple = namedtuple('VersionTuple', ['descriptor', 'module', 'settings_attributes', 'student_attributes'])
|
||||
VERSION_TUPLES = {
|
||||
1: VersionTuple(CombinedOpenEndedV1Descriptor, CombinedOpenEndedV1Module, V1_SETTINGS_ATTRIBUTES,
|
||||
V1_STUDENT_ATTRIBUTES),
|
||||
}
|
||||
|
||||
DEFAULT_VERSION = 1
|
||||
DEFAULT_VERSION = str(DEFAULT_VERSION)
|
||||
|
||||
|
||||
class VersionInteger(Integer):
|
||||
"""
|
||||
A model type that converts from strings to integers when reading from json.
|
||||
Also does error checking to see if version is correct or not.
|
||||
"""
|
||||
|
||||
def from_json(self, value):
|
||||
try:
|
||||
value = int(value)
|
||||
if value not in VERSION_TUPLES:
|
||||
version_error_string = "Could not find version {0}, using version {1} instead"
|
||||
log.error(version_error_string.format(value, DEFAULT_VERSION))
|
||||
value = DEFAULT_VERSION
|
||||
except:
|
||||
value = DEFAULT_VERSION
|
||||
return value
|
||||
|
||||
|
||||
class CombinedOpenEndedFields(object):
|
||||
display_name = String(help="Display name for this module", default="Open Ended Grading", scope=Scope.settings)
|
||||
current_task_number = Integer(help="Current task that the student is on.", default=0, scope=Scope.student_state)
|
||||
task_states = List(help="List of state dictionaries of each task within this module.", scope=Scope.student_state)
|
||||
state = String(help="Which step within the current task that the student is on.", default="initial", scope=Scope.student_state)
|
||||
student_attempts = Integer(help="Number of attempts taken by the student on this problem", default=0, scope=Scope.student_state)
|
||||
ready_to_reset = Boolean(help="If the problem is ready to be reset or not.", default=False, scope=Scope.student_state)
|
||||
state = String(help="Which step within the current task that the student is on.", default="initial",
|
||||
scope=Scope.student_state)
|
||||
student_attempts = Integer(help="Number of attempts taken by the student on this problem", default=0,
|
||||
scope=Scope.student_state)
|
||||
ready_to_reset = Boolean(help="If the problem is ready to be reset or not.", default=False,
|
||||
scope=Scope.student_state)
|
||||
attempts = Integer(help="Maximum number of attempts that a student is allowed.", default=1, scope=Scope.settings)
|
||||
is_graded = Boolean(help="Whether or not the problem is graded.", default=False, scope=Scope.settings)
|
||||
accept_file_upload = Boolean(help="Whether or not the problem accepts file uploads.", default=False, scope=Scope.settings)
|
||||
skip_spelling_checks = Boolean(help="Whether or not to skip initial spelling checks.", default=True, scope=Scope.settings)
|
||||
is_graded = Boolean(help="Whether or not the problem is graded.", default=False, scope=Scope.settings)
|
||||
accept_file_upload = Boolean(help="Whether or not the problem accepts file uploads.", default=False,
|
||||
scope=Scope.settings)
|
||||
skip_spelling_checks = Boolean(help="Whether or not to skip initial spelling checks.", default=True,
|
||||
scope=Scope.settings)
|
||||
due = String(help="Date that this problem is due by", default=None, scope=Scope.settings)
|
||||
graceperiod = String(help="Amount of time after the due date that submissions will be accepted", default=None, scope=Scope.settings)
|
||||
graceperiod = String(help="Amount of time after the due date that submissions will be accepted", default=None,
|
||||
scope=Scope.settings)
|
||||
max_score = Integer(help="Maximum score for the problem.", default=1, scope=Scope.settings)
|
||||
version = Integer(help="Current version number", default=DEFAULT_VERSION, scope=Scope.settings)
|
||||
version = VersionInteger(help="Current version number", default=DEFAULT_VERSION, scope=Scope.settings)
|
||||
data = String(help="XML data for the problem", scope=Scope.content)
|
||||
|
||||
|
||||
@@ -130,23 +155,10 @@ class CombinedOpenEndedModule(CombinedOpenEndedFields, XModule):
|
||||
if self.task_states is None:
|
||||
self.task_states = []
|
||||
|
||||
versions = [i[0] for i in VERSION_TUPLES]
|
||||
descriptors = [i[1] for i in VERSION_TUPLES]
|
||||
modules = [i[2] for i in VERSION_TUPLES]
|
||||
settings_attributes = [i[3] for i in VERSION_TUPLES]
|
||||
student_attributes = [i[4] for i in VERSION_TUPLES]
|
||||
version_error_string = "Could not find version {0}, using version {1} instead"
|
||||
version_tuple = VERSION_TUPLES[self.version]
|
||||
|
||||
try:
|
||||
version_index = versions.index(self.version)
|
||||
except:
|
||||
#This is a dev_facing_error
|
||||
log.error(version_error_string.format(self.version, DEFAULT_VERSION))
|
||||
self.version = DEFAULT_VERSION
|
||||
version_index = versions.index(self.version)
|
||||
|
||||
self.student_attributes = student_attributes[version_index]
|
||||
self.settings_attributes = settings_attributes[version_index]
|
||||
self.student_attributes = version_tuple.student_attributes
|
||||
self.settings_attributes = version_tuple.settings_attributes
|
||||
|
||||
attributes = self.student_attributes + self.settings_attributes
|
||||
|
||||
@@ -154,10 +166,11 @@ class CombinedOpenEndedModule(CombinedOpenEndedFields, XModule):
|
||||
'rewrite_content_links': self.rewrite_content_links,
|
||||
}
|
||||
instance_state = {k: getattr(self, k) for k in attributes}
|
||||
self.child_descriptor = descriptors[version_index](self.system)
|
||||
self.child_definition = descriptors[version_index].definition_from_xml(etree.fromstring(self.data), self.system)
|
||||
self.child_module = modules[version_index](self.system, location, self.child_definition, self.child_descriptor,
|
||||
instance_state=instance_state, static_data=static_data, attributes=attributes)
|
||||
self.child_descriptor = version_tuple.descriptor(self.system)
|
||||
self.child_definition = version_tuple.descriptor.definition_from_xml(etree.fromstring(self.data), self.system)
|
||||
self.child_module = version_tuple.module(self.system, location, self.child_definition, self.child_descriptor,
|
||||
instance_state=instance_state, static_data=static_data,
|
||||
attributes=attributes)
|
||||
self.save_instance_data()
|
||||
|
||||
def get_html(self):
|
||||
|
||||
@@ -131,6 +131,7 @@ section.poll_question {
|
||||
box-shadow: rgb(97, 184, 225) 0px 1px 0px 0px inset;
|
||||
color: rgb(255, 255, 255);
|
||||
text-shadow: rgb(7, 103, 148) 0px 1px 0px;
|
||||
background-image: none;
|
||||
}
|
||||
|
||||
.text {
|
||||
|
||||
@@ -8,7 +8,7 @@ from collections import namedtuple
|
||||
from fs.osfs import OSFS
|
||||
from itertools import repeat
|
||||
from path import path
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import datetime
|
||||
|
||||
from importlib import import_module
|
||||
from xmodule.errortracker import null_error_tracker, exc_info_to_str
|
||||
@@ -246,6 +246,7 @@ class MongoModuleStore(ModuleStoreBase):
|
||||
self.fs_root = path(fs_root)
|
||||
self.error_tracker = error_tracker
|
||||
self.render_template = render_template
|
||||
self.ignore_write_events_on_courses = []
|
||||
|
||||
def get_metadata_inheritance_tree(self, location):
|
||||
'''
|
||||
@@ -303,6 +304,7 @@ class MongoModuleStore(ModuleStoreBase):
|
||||
# this is likely a leaf node, so let's record what metadata we need to inherit
|
||||
metadata_to_inherit[child] = my_metadata
|
||||
|
||||
|
||||
if root is not None:
|
||||
_compute_inherited_metadata(root)
|
||||
|
||||
@@ -329,8 +331,13 @@ class MongoModuleStore(ModuleStoreBase):
|
||||
|
||||
return tree
|
||||
|
||||
def refresh_cached_metadata_inheritance_tree(self, location):
|
||||
pseudo_course_id = '/'.join([location.org, location.course])
|
||||
if pseudo_course_id not in self.ignore_write_events_on_courses:
|
||||
self.get_cached_metadata_inheritance_tree(location, force_refresh = True)
|
||||
|
||||
def clear_cached_metadata_inheritance_tree(self, location):
|
||||
key_name = '{0}/{1}'.format(location.org, location.course)
|
||||
key_name = '{0}/{1}'.format(location.org, location.course)
|
||||
if self.metadata_inheritance_cache is not None:
|
||||
self.metadata_inheritance_cache.delete(key_name)
|
||||
|
||||
@@ -375,7 +382,7 @@ class MongoModuleStore(ModuleStoreBase):
|
||||
|
||||
return data
|
||||
|
||||
def _load_item(self, item, data_cache):
|
||||
def _load_item(self, item, data_cache, should_apply_metadata_inheritence=True):
|
||||
"""
|
||||
Load an XModuleDescriptor from item, using the children stored in data_cache
|
||||
"""
|
||||
@@ -389,9 +396,7 @@ class MongoModuleStore(ModuleStoreBase):
|
||||
|
||||
metadata_inheritance_tree = None
|
||||
|
||||
# if we are loading a course object, there is no parent to inherit the metadata from
|
||||
# so don't bother getting it
|
||||
if item['location']['category'] != 'course':
|
||||
if should_apply_metadata_inheritence:
|
||||
metadata_inheritance_tree = self.get_cached_metadata_inheritance_tree(Location(item['location']))
|
||||
|
||||
# TODO (cdodge): When the 'split module store' work has been completed, we should remove
|
||||
@@ -414,7 +419,10 @@ class MongoModuleStore(ModuleStoreBase):
|
||||
"""
|
||||
data_cache = self._cache_children(items, depth)
|
||||
|
||||
return [self._load_item(item, data_cache) for item in items]
|
||||
# if we are loading a course object, if we're not prefetching children (depth != 0) then don't
|
||||
# bother with the metadata inheritence
|
||||
return [self._load_item(item, data_cache,
|
||||
should_apply_metadata_inheritence=(item['location']['category'] != 'course' or depth != 0)) for item in items]
|
||||
|
||||
def get_courses(self):
|
||||
'''
|
||||
@@ -497,7 +505,12 @@ class MongoModuleStore(ModuleStoreBase):
|
||||
try:
|
||||
source_item = self.collection.find_one(location_to_query(source))
|
||||
source_item['_id'] = Location(location).dict()
|
||||
self.collection.insert(source_item)
|
||||
self.collection.insert(
|
||||
source_item,
|
||||
# Must include this to avoid the django debug toolbar (which defines the deprecated "safe=False")
|
||||
# from overriding our default value set in the init method.
|
||||
safe=self.collection.safe
|
||||
)
|
||||
item = self._load_items([source_item])[0]
|
||||
|
||||
# VS[compat] cdodge: This is a hack because static_tabs also have references from the course module, so
|
||||
@@ -519,7 +532,7 @@ class MongoModuleStore(ModuleStoreBase):
|
||||
raise DuplicateItemError(location)
|
||||
|
||||
# recompute (and update) the metadata inheritance tree which is cached
|
||||
self.get_cached_metadata_inheritance_tree(Location(location), force_refresh = True)
|
||||
self.refresh_cached_metadata_inheritance_tree(Location(location))
|
||||
|
||||
def get_course_for_item(self, location, depth=0):
|
||||
'''
|
||||
@@ -560,6 +573,9 @@ class MongoModuleStore(ModuleStoreBase):
|
||||
{'$set': update},
|
||||
multi=False,
|
||||
upsert=True,
|
||||
# Must include this to avoid the django debug toolbar (which defines the deprecated "safe=False")
|
||||
# from overriding our default value set in the init method.
|
||||
safe=self.collection.safe
|
||||
)
|
||||
if result['n'] == 0:
|
||||
raise ItemNotFoundError(location)
|
||||
@@ -586,7 +602,7 @@ class MongoModuleStore(ModuleStoreBase):
|
||||
|
||||
self._update_single_item(location, {'definition.children': children})
|
||||
# recompute (and update) the metadata inheritance tree which is cached
|
||||
self.get_cached_metadata_inheritance_tree(Location(location), force_refresh = True)
|
||||
self.refresh_cached_metadata_inheritance_tree(Location(location))
|
||||
|
||||
def update_metadata(self, location, metadata):
|
||||
"""
|
||||
@@ -612,7 +628,7 @@ class MongoModuleStore(ModuleStoreBase):
|
||||
|
||||
self._update_single_item(location, {'metadata': metadata})
|
||||
# recompute (and update) the metadata inheritance tree which is cached
|
||||
self.get_cached_metadata_inheritance_tree(loc, force_refresh = True)
|
||||
self.refresh_cached_metadata_inheritance_tree(loc)
|
||||
|
||||
def delete_item(self, location):
|
||||
"""
|
||||
@@ -630,10 +646,12 @@ class MongoModuleStore(ModuleStoreBase):
|
||||
course.tabs = [tab for tab in existing_tabs if tab.get('url_slug') != location.name]
|
||||
self.update_metadata(course.location, own_metadata(course))
|
||||
|
||||
self.collection.remove({'_id': Location(location).dict()})
|
||||
self.collection.remove({'_id': Location(location).dict()},
|
||||
# Must include this to avoid the django debug toolbar (which defines the deprecated "safe=False")
|
||||
# from overriding our default value set in the init method.
|
||||
safe=self.collection.safe)
|
||||
# recompute (and update) the metadata inheritance tree which is cached
|
||||
self.get_cached_metadata_inheritance_tree(Location(location), force_refresh = True)
|
||||
|
||||
self.refresh_cached_metadata_inheritance_tree(Location(location))
|
||||
|
||||
def get_parent_locations(self, location, course_id):
|
||||
'''Find all locations that are the parents of this location in this
|
||||
|
||||
@@ -25,8 +25,7 @@ class XModuleCourseFactory(Factory):
|
||||
|
||||
@classmethod
|
||||
def _create(cls, target_class, *args, **kwargs):
|
||||
# This logic was taken from the create_new_course method in
|
||||
# cms/djangoapps/contentstore/views.py
|
||||
|
||||
template = Location('i4x', 'edx', 'templates', 'course', 'Empty')
|
||||
org = kwargs.get('org')
|
||||
number = kwargs.get('number')
|
||||
@@ -43,8 +42,7 @@ class XModuleCourseFactory(Factory):
|
||||
if display_name is not None:
|
||||
new_course.display_name = display_name
|
||||
|
||||
new_course.start = gmtime()
|
||||
|
||||
new_course.lms.start = gmtime()
|
||||
new_course.tabs = [{"type": "courseware"},
|
||||
{"type": "course_info", "name": "Course Info"},
|
||||
{"type": "discussion", "name": "Discussion"},
|
||||
@@ -81,21 +79,41 @@ class XModuleItemFactory(Factory):
|
||||
@classmethod
|
||||
def _create(cls, target_class, *args, **kwargs):
|
||||
"""
|
||||
kwargs must include parent_location, template. Can contain display_name
|
||||
target_class is ignored
|
||||
Uses *kwargs*:
|
||||
|
||||
*parent_location* (required): the location of the parent module
|
||||
(e.g. the parent course or section)
|
||||
|
||||
*template* (required): the template to create the item from
|
||||
(e.g. i4x://templates/section/Empty)
|
||||
|
||||
*data* (optional): the data for the item
|
||||
(e.g. XML problem definition for a problem item)
|
||||
|
||||
*display_name* (optional): the display name of the item
|
||||
|
||||
*metadata* (optional): dictionary of metadata attributes
|
||||
|
||||
*target_class* is ignored
|
||||
"""
|
||||
|
||||
DETACHED_CATEGORIES = ['about', 'static_tab', 'course_info']
|
||||
|
||||
parent_location = Location(kwargs.get('parent_location'))
|
||||
template = Location(kwargs.get('template'))
|
||||
data = kwargs.get('data')
|
||||
display_name = kwargs.get('display_name')
|
||||
metadata = kwargs.get('metadata', {})
|
||||
|
||||
store = modulestore('direct')
|
||||
|
||||
# This code was based off that in cms/djangoapps/contentstore/views.py
|
||||
parent = store.get_item(parent_location)
|
||||
dest_location = parent_location._replace(category=template.category, name=uuid4().hex)
|
||||
|
||||
# If a display name is set, use that
|
||||
dest_name = display_name.replace(" ", "_") if display_name is not None else uuid4().hex
|
||||
dest_location = parent_location._replace(category=template.category,
|
||||
name=dest_name)
|
||||
|
||||
new_item = store.clone_item(template, dest_location)
|
||||
|
||||
@@ -103,7 +121,14 @@ class XModuleItemFactory(Factory):
|
||||
if display_name is not None:
|
||||
new_item.display_name = display_name
|
||||
|
||||
store.update_metadata(new_item.location.url(), own_metadata(new_item))
|
||||
# Add additional metadata or override current metadata
|
||||
item_metadata = own_metadata(new_item)
|
||||
item_metadata.update(metadata)
|
||||
store.update_metadata(new_item.location.url(), item_metadata)
|
||||
|
||||
# replace the data with the optional *data* parameter
|
||||
if data is not None:
|
||||
store.update_item(new_item.location, data)
|
||||
|
||||
if new_item.location.category not in DETACHED_CATEGORIES:
|
||||
store.update_children(parent_location, parent.children + [new_item.location.url()])
|
||||
|
||||
@@ -4,6 +4,8 @@ import mimetypes
|
||||
from lxml.html import rewrite_links as lxml_rewrite_links
|
||||
from path import path
|
||||
|
||||
from xblock.core import Scope
|
||||
|
||||
from .xml import XMLModuleStore
|
||||
from .exceptions import DuplicateItemError
|
||||
from xmodule.modulestore import Location
|
||||
@@ -201,100 +203,127 @@ def import_from_xml(store, data_dir, course_dirs=None,
|
||||
course_items = []
|
||||
for course_id in module_store.modules.keys():
|
||||
|
||||
course_data_path = None
|
||||
course_location = None
|
||||
if target_location_namespace is not None:
|
||||
pseudo_course_id = '/'.join([target_location_namespace.org, target_location_namespace.course])
|
||||
else:
|
||||
course_id_components = course_id.split('/')
|
||||
pseudo_course_id = '/'.join([course_id_components[0], course_id_components[1]])
|
||||
|
||||
if verbose:
|
||||
log.debug("Scanning {0} for course module...".format(course_id))
|
||||
try:
|
||||
# turn off all write signalling while importing as this is a high volume operation
|
||||
if pseudo_course_id not in store.ignore_write_events_on_courses:
|
||||
store.ignore_write_events_on_courses.append(pseudo_course_id)
|
||||
|
||||
# Quick scan to get course module as we need some info from there. Also we need to make sure that the
|
||||
# course module is committed first into the store
|
||||
for module in module_store.modules[course_id].itervalues():
|
||||
if module.category == 'course':
|
||||
course_data_path = path(data_dir) / module.data_dir
|
||||
course_location = module.location
|
||||
|
||||
module = remap_namespace(module, target_location_namespace)
|
||||
|
||||
# cdodge: more hacks (what else). Seems like we have a problem when importing a course (like 6.002) which
|
||||
# does not have any tabs defined in the policy file. The import goes fine and then displays fine in LMS,
|
||||
# but if someone tries to add a new tab in the CMS, then the LMS barfs because it expects that -
|
||||
# if there is *any* tabs - then there at least needs to be some predefined ones
|
||||
if module.tabs is None or len(module.tabs) == 0:
|
||||
module.tabs = [{"type": "courseware"},
|
||||
{"type": "course_info", "name": "Course Info"},
|
||||
{"type": "discussion", "name": "Discussion"},
|
||||
{"type": "wiki", "name": "Wiki"}] # note, add 'progress' when we can support it on Edge
|
||||
|
||||
|
||||
if hasattr(module, 'data'):
|
||||
store.update_item(module.location, module.data)
|
||||
store.update_children(module.location, module.children)
|
||||
store.update_metadata(module.location, dict(own_metadata(module)))
|
||||
|
||||
# a bit of a hack, but typically the "course image" which is shown on marketing pages is hard coded to /images/course_image.jpg
|
||||
# so let's make sure we import in case there are no other references to it in the modules
|
||||
verify_content_links(module, course_data_path, static_content_store, '/static/images/course_image.jpg')
|
||||
|
||||
course_items.append(module)
|
||||
|
||||
|
||||
# then import all the static content
|
||||
if static_content_store is not None:
|
||||
_namespace_rename = target_location_namespace if target_location_namespace is not None else course_location
|
||||
|
||||
# first pass to find everything in /static/
|
||||
import_static_content(module_store.modules[course_id], course_location, course_data_path, static_content_store,
|
||||
_namespace_rename, subpath='static', verbose=verbose)
|
||||
|
||||
# finally loop through all the modules
|
||||
for module in module_store.modules[course_id].itervalues():
|
||||
|
||||
if module.category == 'course':
|
||||
# we've already saved the course module up at the top of the loop
|
||||
# so just skip over it in the inner loop
|
||||
continue
|
||||
|
||||
# remap module to the new namespace
|
||||
if target_location_namespace is not None:
|
||||
module = remap_namespace(module, target_location_namespace)
|
||||
course_data_path = None
|
||||
course_location = None
|
||||
|
||||
if verbose:
|
||||
log.debug('importing module location {0}'.format(module.location))
|
||||
log.debug("Scanning {0} for course module...".format(course_id))
|
||||
|
||||
if hasattr(module, 'data'):
|
||||
module_data = module.data
|
||||
# Quick scan to get course module as we need some info from there. Also we need to make sure that the
|
||||
# course module is committed first into the store
|
||||
for module in module_store.modules[course_id].itervalues():
|
||||
if module.category == 'course':
|
||||
course_data_path = path(data_dir) / module.data_dir
|
||||
course_location = module.location
|
||||
|
||||
# cdodge: now go through any link references to '/static/' and make sure we've imported
|
||||
# it as a StaticContent asset
|
||||
try:
|
||||
remap_dict = {}
|
||||
module = remap_namespace(module, target_location_namespace)
|
||||
|
||||
# use the rewrite_links as a utility means to enumerate through all links
|
||||
# in the module data. We use that to load that reference into our asset store
|
||||
# IMPORTANT: There appears to be a bug in lxml.rewrite_link which makes us not be able to
|
||||
# do the rewrites natively in that code.
|
||||
# For example, what I'm seeing is <img src='foo.jpg' /> -> <img src='bar.jpg'>
|
||||
# Note the dropped element closing tag. This causes the LMS to fail when rendering modules - that's
|
||||
# no good, so we have to do this kludge
|
||||
if isinstance(module_data, str) or isinstance(module_data, unicode): # some module 'data' fields are non strings which blows up the link traversal code
|
||||
lxml_rewrite_links(module_data, lambda link: verify_content_links(module, course_data_path,
|
||||
static_content_store, link, remap_dict))
|
||||
# cdodge: more hacks (what else). Seems like we have a problem when importing a course (like 6.002) which
|
||||
# does not have any tabs defined in the policy file. The import goes fine and then displays fine in LMS,
|
||||
# but if someone tries to add a new tab in the CMS, then the LMS barfs because it expects that -
|
||||
# if there is *any* tabs - then there at least needs to be some predefined ones
|
||||
if module.tabs is None or len(module.tabs) == 0:
|
||||
module.tabs = [{"type": "courseware"},
|
||||
{"type": "course_info", "name": "Course Info"},
|
||||
{"type": "discussion", "name": "Discussion"},
|
||||
{"type": "wiki", "name": "Wiki"}] # note, add 'progress' when we can support it on Edge
|
||||
|
||||
for key in remap_dict.keys():
|
||||
module_data = module_data.replace(key, remap_dict[key])
|
||||
|
||||
except Exception, e:
|
||||
logging.exception("failed to rewrite links on {0}. Continuing...".format(module.location))
|
||||
if hasattr(module, 'data'):
|
||||
store.update_item(module.location, module.data)
|
||||
store.update_children(module.location, module.children)
|
||||
store.update_metadata(module.location, dict(own_metadata(module)))
|
||||
|
||||
store.update_item(module.location, module_data)
|
||||
# a bit of a hack, but typically the "course image" which is shown on marketing pages is hard coded to /images/course_image.jpg
|
||||
# so let's make sure we import in case there are no other references to it in the modules
|
||||
verify_content_links(module, course_data_path, static_content_store, '/static/images/course_image.jpg')
|
||||
|
||||
if hasattr(module, 'children') and module.children != []:
|
||||
store.update_children(module.location, module.children)
|
||||
course_items.append(module)
|
||||
|
||||
# NOTE: It's important to use own_metadata here to avoid writing
|
||||
# inherited metadata everywhere.
|
||||
store.update_metadata(module.location, dict(own_metadata(module)))
|
||||
|
||||
# then import all the static content
|
||||
if static_content_store is not None:
|
||||
_namespace_rename = target_location_namespace if target_location_namespace is not None else course_location
|
||||
|
||||
# first pass to find everything in /static/
|
||||
import_static_content(module_store.modules[course_id], course_location, course_data_path, static_content_store,
|
||||
_namespace_rename, subpath='static', verbose=verbose)
|
||||
|
||||
# finally loop through all the modules
|
||||
for module in module_store.modules[course_id].itervalues():
|
||||
|
||||
if module.category == 'course':
|
||||
# we've already saved the course module up at the top of the loop
|
||||
# so just skip over it in the inner loop
|
||||
continue
|
||||
|
||||
# remap module to the new namespace
|
||||
if target_location_namespace is not None:
|
||||
module = remap_namespace(module, target_location_namespace)
|
||||
|
||||
if verbose:
|
||||
log.debug('importing module location {0}'.format(module.location))
|
||||
|
||||
content = {}
|
||||
for field in module.fields:
|
||||
if field.scope != Scope.content:
|
||||
continue
|
||||
try:
|
||||
content[field.name] = module._model_data[field.name]
|
||||
except KeyError:
|
||||
# Ignore any missing keys in _model_data
|
||||
pass
|
||||
|
||||
if 'data' in content:
|
||||
module_data = content['data']
|
||||
|
||||
# cdodge: now go through any link references to '/static/' and make sure we've imported
|
||||
# it as a StaticContent asset
|
||||
try:
|
||||
remap_dict = {}
|
||||
|
||||
# use the rewrite_links as a utility means to enumerate through all links
|
||||
# in the module data. We use that to load that reference into our asset store
|
||||
# IMPORTANT: There appears to be a bug in lxml.rewrite_link which makes us not be able to
|
||||
# do the rewrites natively in that code.
|
||||
# For example, what I'm seeing is <img src='foo.jpg' /> -> <img src='bar.jpg'>
|
||||
# Note the dropped element closing tag. This causes the LMS to fail when rendering modules - that's
|
||||
# no good, so we have to do this kludge
|
||||
if isinstance(module_data, str) or isinstance(module_data, unicode): # some module 'data' fields are non strings which blows up the link traversal code
|
||||
lxml_rewrite_links(module_data, lambda link: verify_content_links(module, course_data_path,
|
||||
static_content_store, link, remap_dict))
|
||||
|
||||
for key in remap_dict.keys():
|
||||
module_data = module_data.replace(key, remap_dict[key])
|
||||
|
||||
except Exception, e:
|
||||
logging.exception("failed to rewrite links on {0}. Continuing...".format(module.location))
|
||||
|
||||
store.update_item(module.location, content)
|
||||
|
||||
if hasattr(module, 'children') and module.children != []:
|
||||
store.update_children(module.location, module.children)
|
||||
|
||||
# NOTE: It's important to use own_metadata here to avoid writing
|
||||
# inherited metadata everywhere.
|
||||
store.update_metadata(module.location, dict(own_metadata(module)))
|
||||
finally:
|
||||
# turn back on all write signalling
|
||||
if pseudo_course_id in store.ignore_write_events_on_courses:
|
||||
store.ignore_write_events_on_courses.remove(pseudo_course_id)
|
||||
store.refresh_cached_metadata_inheritance_tree(target_location_namespace if
|
||||
target_location_namespace is not None else course_location)
|
||||
|
||||
return module_store, course_items
|
||||
|
||||
|
||||
@@ -128,7 +128,9 @@ if Backbone?
|
||||
type: "POST"
|
||||
success: (response, textStatus) =>
|
||||
if textStatus == 'success'
|
||||
@model.set('pinned', true)
|
||||
@model.set('pinned', true)
|
||||
error: =>
|
||||
$('.admin-pin').text("Pinning not currently available")
|
||||
|
||||
unPin: ->
|
||||
url = @model.urlFor("unPinThread")
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
// studio - utilities - mixins and extends
|
||||
// ====================
|
||||
|
||||
// font-sizing
|
||||
@function em($pxval, $base: 16) {
|
||||
@return #{$pxval / $base}em;
|
||||
}
|
||||
|
||||
@mixin font-size($sizeValue: 1.6){
|
||||
@mixin font-size($sizeValue: 16){
|
||||
font-size: $sizeValue + px;
|
||||
font-size: ($sizeValue/10) + rem;
|
||||
}
|
||||
@@ -64,4 +67,106 @@
|
||||
:-ms-input-placeholder {
|
||||
color: $color;
|
||||
}
|
||||
}
|
||||
|
||||
// ====================
|
||||
|
||||
// extends - visual
|
||||
.faded-hr-divider {
|
||||
@include background-image(linear-gradient(180deg, rgba(200,200,200, 0) 0%,
|
||||
rgba(200,200,200, 1) 50%,
|
||||
rgba(200,200,200, 0)));
|
||||
height: 1px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.faded-hr-divider-medium {
|
||||
@include background-image(linear-gradient(180deg, rgba(240,240,240, 0) 0%,
|
||||
rgba(240,240,240, 1) 50%,
|
||||
rgba(240,240,240, 0)));
|
||||
height: 1px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.faded-hr-divider-light {
|
||||
@include background-image(linear-gradient(180deg, rgba(255,255,255, 0) 0%,
|
||||
rgba(255,255,255, 0.8) 50%,
|
||||
rgba(255,255,255, 0)));
|
||||
height: 1px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.faded-vertical-divider {
|
||||
@include background-image(linear-gradient(90deg, rgba(200,200,200, 0) 0%,
|
||||
rgba(200,200,200, 1) 50%,
|
||||
rgba(200,200,200, 0)));
|
||||
height: 100%;
|
||||
width: 1px;
|
||||
}
|
||||
|
||||
.faded-vertical-divider-light {
|
||||
@include background-image(linear-gradient(90deg, rgba(255,255,255, 0) 0%,
|
||||
rgba(255,255,255, 0.6) 50%,
|
||||
rgba(255,255,255, 0)));
|
||||
height: 100%;
|
||||
width: 1px;
|
||||
}
|
||||
|
||||
.vertical-divider {
|
||||
@extend .faded-vertical-divider;
|
||||
position: relative;
|
||||
|
||||
&::after {
|
||||
@extend .faded-vertical-divider-light;
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
left: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
.horizontal-divider {
|
||||
border: none;
|
||||
@extend .faded-hr-divider;
|
||||
position: relative;
|
||||
|
||||
&::after {
|
||||
@extend .faded-hr-divider-light;
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
.fade-right-hr-divider {
|
||||
@include background-image(linear-gradient(180deg, rgba(200,200,200, 0) 0%,
|
||||
rgba(200,200,200, 1)));
|
||||
border: none;
|
||||
}
|
||||
|
||||
.fade-left-hr-divider {
|
||||
@include background-image(linear-gradient(180deg, rgba(200,200,200, 1) 0%,
|
||||
rgba(200,200,200, 0)));
|
||||
border: none;
|
||||
}
|
||||
|
||||
// extends - ui
|
||||
.window {
|
||||
@include clearfix();
|
||||
@include border-radius(3px);
|
||||
@include box-shadow(0 1px 1px $shadow-l1);
|
||||
margin-bottom: $baseline;
|
||||
border: 1px solid $gray-l2;
|
||||
background: $white;
|
||||
}
|
||||
|
||||
.elem-d1 {
|
||||
@include clearfix();
|
||||
@include box-sizing(border-box);
|
||||
}
|
||||
|
||||
.elem-d2 {
|
||||
@include clearfix();
|
||||
@include box-sizing(border-box);
|
||||
}
|
||||
@@ -13,14 +13,19 @@
|
||||
<script src="{% static 'js/vendor/jasmine-jquery.js' %}"></script>
|
||||
<script src="{% static 'console-runner.js' %}"></script>
|
||||
|
||||
{% load compressed %}
|
||||
{# static files #}
|
||||
{% for url in suite.static_files %}
|
||||
<script src="{{ STATIC_URL }}{{ url }}"></script>
|
||||
{% endfor %}
|
||||
|
||||
{% compressed_js 'js-test-source' %}
|
||||
|
||||
{# source files #}
|
||||
{% for url in suite.js_files %}
|
||||
<script src="{{ url }}"></script>
|
||||
{% endfor %}
|
||||
|
||||
{% load compressed %}
|
||||
{# static files #}
|
||||
{% compressed_js 'js-test-source' %}
|
||||
|
||||
{# spec files #}
|
||||
{% compressed_js 'spec' %}
|
||||
|
||||
@@ -7,4 +7,9 @@
|
||||
<customtag tag="S1" slug="discuss_91" impl="discuss"/>
|
||||
<customtag page="70" slug="book_92" impl="book"/>
|
||||
<customtag lecnum="1" slug="slides_93" impl="slides"/>
|
||||
<poll_question name="T1_changemind_poll_foo" display_name="Change your answer" reset="false">
|
||||
<p>Have you changed your mind?</p>
|
||||
<answer id="yes">Yes</answer>
|
||||
<answer id="no">No</answer>
|
||||
</poll_question>
|
||||
</sequential>
|
||||
|
||||
@@ -313,14 +313,18 @@ There is an important split in demographic data gathered for the students who si
|
||||
- This student signed up before this information was collected
|
||||
* - `''` (blank)
|
||||
- User did not specify level of education.
|
||||
* - `'p'`
|
||||
- Doctorate
|
||||
* - `'p_se'`
|
||||
- Doctorate in science or engineering
|
||||
- Doctorate in science or engineering (no longer used)
|
||||
* - `'p_oth'`
|
||||
- Doctorate in another field
|
||||
- Doctorate in another field (no longer used)
|
||||
* - `'m'`
|
||||
- Master's or professional degree
|
||||
* - `'b'`
|
||||
- Bachelor's degree
|
||||
* - `'a'`
|
||||
- Associate's degree
|
||||
* - `'hs'`
|
||||
- Secondary/high school
|
||||
* - `'jhs'`
|
||||
@@ -624,4 +628,4 @@ The generatedcertificate table tracks certificate state for students who have be
|
||||
|
||||
`grade`
|
||||
-------
|
||||
The grade of the student recorded at the time the certificate was generated. This may be different than the current grade since grading is only done once for a course when it ends.
|
||||
The grade of the student recorded at the time the certificate was generated. This may be different than the current grade since grading is only done once for a course when it ends.
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
from lettuce import world, step
|
||||
from django.core.management import call_command
|
||||
from nose.tools import assert_equals, assert_in
|
||||
from lettuce.django import django_url
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from student.models import CourseEnrollment
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.django import _MODULESTORES, modulestore
|
||||
from xmodule.templates import update_templates
|
||||
import time
|
||||
|
||||
from logging import getLogger
|
||||
@@ -73,7 +74,8 @@ def should_see_in_the_page(step, text):
|
||||
@step('I am logged in$')
|
||||
def i_am_logged_in(step):
|
||||
world.create_user('robot')
|
||||
world.log_in('robot@edx.org', 'test')
|
||||
world.log_in('robot', 'test')
|
||||
world.browser.visit(django_url('/'))
|
||||
|
||||
|
||||
@step('I am not logged in$')
|
||||
@@ -81,12 +83,56 @@ def i_am_not_logged_in(step):
|
||||
world.browser.cookies.delete()
|
||||
|
||||
|
||||
@step(u'I am registered for a course$')
|
||||
def i_am_registered_for_a_course(step):
|
||||
TEST_COURSE_ORG = 'edx'
|
||||
TEST_COURSE_NAME = 'Test Course'
|
||||
TEST_SECTION_NAME = "Problem"
|
||||
|
||||
|
||||
@step(u'The course "([^"]*)" exists$')
|
||||
def create_course(step, course):
|
||||
|
||||
# First clear the modulestore so we don't try to recreate
|
||||
# the same course twice
|
||||
# This also ensures that the necessary templates are loaded
|
||||
flush_xmodule_store()
|
||||
|
||||
# Create the course
|
||||
# We always use the same org and display name,
|
||||
# but vary the course identifier (e.g. 600x or 191x)
|
||||
course = world.CourseFactory.create(org=TEST_COURSE_ORG,
|
||||
number=course,
|
||||
display_name=TEST_COURSE_NAME)
|
||||
|
||||
# Add a section to the course to contain problems
|
||||
section = world.ItemFactory.create(parent_location=course.location,
|
||||
display_name=TEST_SECTION_NAME)
|
||||
|
||||
problem_section = world.ItemFactory.create(parent_location=section.location,
|
||||
template='i4x://edx/templates/sequential/Empty',
|
||||
display_name=TEST_SECTION_NAME)
|
||||
|
||||
|
||||
@step(u'I am registered for the course "([^"]*)"$')
|
||||
def i_am_registered_for_the_course(step, course):
|
||||
# Create the course
|
||||
create_course(step, course)
|
||||
|
||||
# Create the user
|
||||
world.create_user('robot')
|
||||
u = User.objects.get(username='robot')
|
||||
CourseEnrollment.objects.create(user=u, course_id='MITx/6.002x/2012_Fall')
|
||||
world.log_in('robot@edx.org', 'test')
|
||||
|
||||
# If the user is not already enrolled, enroll the user.
|
||||
# TODO: change to factory
|
||||
CourseEnrollment.objects.get_or_create(user=u, course_id=course_id(course))
|
||||
|
||||
world.log_in('robot', 'test')
|
||||
|
||||
|
||||
@step(u'The course "([^"]*)" has extra tab "([^"]*)"$')
|
||||
def add_tab_to_course(step, course, extra_tab_name):
|
||||
section_item = world.ItemFactory.create(parent_location=course_location(course),
|
||||
template="i4x://edx/templates/static_tab/Empty",
|
||||
display_name=str(extra_tab_name))
|
||||
|
||||
|
||||
@step(u'I am an edX user$')
|
||||
@@ -97,3 +143,37 @@ def i_am_an_edx_user(step):
|
||||
@step(u'User "([^"]*)" is an edX user$')
|
||||
def registered_edx_user(step, uname):
|
||||
world.create_user(uname)
|
||||
|
||||
|
||||
def flush_xmodule_store():
|
||||
# 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()"
|
||||
_MODULESTORES = {}
|
||||
modulestore().collection.drop()
|
||||
update_templates()
|
||||
|
||||
|
||||
def course_id(course_num):
|
||||
return "%s/%s/%s" % (TEST_COURSE_ORG, course_num,
|
||||
TEST_COURSE_NAME.replace(" ", "_"))
|
||||
|
||||
|
||||
def course_location(course_num):
|
||||
return Location(loc_or_tag="i4x",
|
||||
org=TEST_COURSE_ORG,
|
||||
course=course_num,
|
||||
category='course',
|
||||
name=TEST_COURSE_NAME.replace(" ", "_"))
|
||||
|
||||
|
||||
def section_location(course_num):
|
||||
return Location(loc_or_tag="i4x",
|
||||
org=TEST_COURSE_ORG,
|
||||
course=course_num,
|
||||
category='sequential',
|
||||
name=TEST_SECTION_NAME.replace(" ", "_"))
|
||||
|
||||
@@ -9,6 +9,7 @@ logger = getLogger(__name__)
|
||||
|
||||
## support functions
|
||||
|
||||
|
||||
def get_courses():
|
||||
'''
|
||||
Returns dict of lists of courses available, keyed by course.org (ie university).
|
||||
@@ -82,13 +83,13 @@ def get_courseware_with_tabs(course_id):
|
||||
course = get_course_by_id(course_id)
|
||||
chapters = [chapter for chapter in course.get_children() if not chapter.lms.hide_from_toc]
|
||||
courseware = [{'chapter_name': c.display_name_with_default,
|
||||
'sections': [{'section_name': s.display_name_with_default,
|
||||
'sections': [{'section_name': s.display_name_with_default,
|
||||
'clickable_tab_count': len(s.get_children()) if (type(s) == seq_module.SequenceDescriptor) else 0,
|
||||
'tabs': [{'children_count': len(t.get_children()) if (type(t) == vertical_module.VerticalDescriptor) else 0,
|
||||
'class': t.__class__.__name__}
|
||||
for t in s.get_children()]}
|
||||
'class': t.__class__.__name__}
|
||||
for t in s.get_children()]}
|
||||
for s in c.get_children() if not s.lms.hide_from_toc]}
|
||||
for c in chapters]
|
||||
for c in chapters]
|
||||
|
||||
return courseware
|
||||
|
||||
@@ -167,7 +168,6 @@ def process_section(element, num_tabs=0):
|
||||
assert False, "Class for element not recognized!!"
|
||||
|
||||
|
||||
|
||||
def process_problem(element, problem_id):
|
||||
'''
|
||||
Process problem attempts to
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
Feature: View the Courseware Tab
|
||||
As a student in an edX course
|
||||
In order to work on the course
|
||||
I want to view the info on the courseware tab
|
||||
|
||||
Scenario: I can get to the courseware tab when logged in
|
||||
Given I am registered for a course
|
||||
And I log in
|
||||
And I click on View Courseware
|
||||
When I click on the "Courseware" tab
|
||||
Then the "Courseware" tab is active
|
||||
@@ -3,21 +3,18 @@ Feature: All the high level tabs should work
|
||||
As a student
|
||||
I want to navigate through the high level tabs
|
||||
|
||||
# Note this didn't work as a scenario outline because
|
||||
# before each scenario was not flushing the database
|
||||
# TODO: break this apart so that if one fails the others
|
||||
# will still run
|
||||
Scenario: A student can see all tabs of the course
|
||||
Given I am registered for a course
|
||||
And I log in
|
||||
And I click on View Courseware
|
||||
When I click on the "Courseware" tab
|
||||
Then the page title should be "6.002x Courseware"
|
||||
When I click on the "Course Info" tab
|
||||
Then the page title should be "6.002x Course Info"
|
||||
When I click on the "Textbook" tab
|
||||
Then the page title should be "6.002x Textbook"
|
||||
When I click on the "Wiki" tab
|
||||
Then the page title should be "6.002x | edX Wiki"
|
||||
When I click on the "Progress" tab
|
||||
Then the page title should be "6.002x Progress"
|
||||
Scenario: I can navigate to all high -level tabs in a course
|
||||
Given: I am registered for the course "6.002x"
|
||||
And The course "6.002x" has extra tab "Custom Tab"
|
||||
And I am logged in
|
||||
And I click on View Courseware
|
||||
When I click on the "<TabName>" tab
|
||||
Then the page title should contain "<PageTitle>"
|
||||
|
||||
Examples:
|
||||
| TabName | PageTitle |
|
||||
| Courseware | 6.002x Courseware |
|
||||
| Course Info | 6.002x Course Info |
|
||||
| Custom Tab | 6.002x Custom Tab |
|
||||
| Wiki | edX Wiki |
|
||||
| Progress | 6.002x Progress |
|
||||
|
||||
@@ -39,9 +39,9 @@ Feature: Homepage for web users
|
||||
| MITx |
|
||||
| HarvardX |
|
||||
| BerkeleyX |
|
||||
| UTx |
|
||||
| UTx |
|
||||
| WellesleyX |
|
||||
| GeorgetownX |
|
||||
| GeorgetownX |
|
||||
|
||||
# # TODO: Add scenario that tests the courses available
|
||||
# # using a policy or a configuration file
|
||||
|
||||
@@ -34,6 +34,7 @@ def click_the_dropdown(step):
|
||||
|
||||
#### helper functions
|
||||
|
||||
|
||||
def user_is_an_unactivated_user(uname):
|
||||
u = User.objects.get(username=uname)
|
||||
u.is_active = False
|
||||
|
||||
@@ -3,10 +3,10 @@ Feature: Open ended grading
|
||||
In order to complete the courseware questions
|
||||
I want the machine learning grading to be functional
|
||||
|
||||
# Commenting these all out right now until we can
|
||||
# Commenting these all out right now until we can
|
||||
# make a reference implementation for a course with
|
||||
# an open ended grading problem that is always available
|
||||
#
|
||||
#
|
||||
# Scenario: An answer that is too short is rejected
|
||||
# Given I navigate to an openended question
|
||||
# And I enter the answer "z"
|
||||
|
||||
77
lms/djangoapps/courseware/features/problems.feature
Normal file
77
lms/djangoapps/courseware/features/problems.feature
Normal file
@@ -0,0 +1,77 @@
|
||||
Feature: Answer problems
|
||||
As a student in an edX course
|
||||
In order to test my understanding of the material
|
||||
I want to answer problems
|
||||
|
||||
Scenario: I can answer a problem correctly
|
||||
Given External graders respond "correct"
|
||||
And I am viewing a "<ProblemType>" problem
|
||||
When I answer a "<ProblemType>" problem "correctly"
|
||||
Then My "<ProblemType>" answer is marked "correct"
|
||||
|
||||
Examples:
|
||||
| ProblemType |
|
||||
| drop down |
|
||||
| multiple choice |
|
||||
| checkbox |
|
||||
| string |
|
||||
| numerical |
|
||||
| formula |
|
||||
| script |
|
||||
| code |
|
||||
|
||||
Scenario: I can answer a problem incorrectly
|
||||
Given External graders respond "incorrect"
|
||||
And I am viewing a "<ProblemType>" problem
|
||||
When I answer a "<ProblemType>" problem "incorrectly"
|
||||
Then My "<ProblemType>" answer is marked "incorrect"
|
||||
|
||||
Examples:
|
||||
| ProblemType |
|
||||
| drop down |
|
||||
| multiple choice |
|
||||
| checkbox |
|
||||
| string |
|
||||
| numerical |
|
||||
| formula |
|
||||
| script |
|
||||
| code |
|
||||
|
||||
Scenario: I can submit a blank answer
|
||||
Given I am viewing a "<ProblemType>" problem
|
||||
When I check a problem
|
||||
Then My "<ProblemType>" answer is marked "incorrect"
|
||||
|
||||
Examples:
|
||||
| ProblemType |
|
||||
| drop down |
|
||||
| multiple choice |
|
||||
| checkbox |
|
||||
| string |
|
||||
| numerical |
|
||||
| formula |
|
||||
| script |
|
||||
|
||||
|
||||
Scenario: I can reset a problem
|
||||
Given I am viewing a "<ProblemType>" problem
|
||||
And I answer a "<ProblemType>" problem "<Correctness>ly"
|
||||
When I reset the problem
|
||||
Then My "<ProblemType>" answer is marked "unanswered"
|
||||
|
||||
Examples:
|
||||
| ProblemType | Correctness |
|
||||
| drop down | correct |
|
||||
| drop down | incorrect |
|
||||
| multiple choice | correct |
|
||||
| multiple choice | incorrect |
|
||||
| checkbox | correct |
|
||||
| checkbox | incorrect |
|
||||
| string | correct |
|
||||
| string | incorrect |
|
||||
| numerical | correct |
|
||||
| numerical | incorrect |
|
||||
| formula | correct |
|
||||
| formula | incorrect |
|
||||
| script | correct |
|
||||
| script | incorrect |
|
||||
296
lms/djangoapps/courseware/features/problems.py
Normal file
296
lms/djangoapps/courseware/features/problems.py
Normal file
@@ -0,0 +1,296 @@
|
||||
from lettuce import world, step
|
||||
from lettuce.django import django_url
|
||||
import random
|
||||
import textwrap
|
||||
import time
|
||||
from common import i_am_registered_for_the_course, TEST_SECTION_NAME, section_location
|
||||
from capa.tests.response_xml_factory import OptionResponseXMLFactory, \
|
||||
ChoiceResponseXMLFactory, MultipleChoiceResponseXMLFactory, \
|
||||
StringResponseXMLFactory, NumericalResponseXMLFactory, \
|
||||
FormulaResponseXMLFactory, CustomResponseXMLFactory, \
|
||||
CodeResponseXMLFactory
|
||||
|
||||
# Factories from capa.tests.response_xml_factory that we will use
|
||||
# to generate the problem XML, with the keyword args used to configure
|
||||
# the output.
|
||||
PROBLEM_FACTORY_DICT = {
|
||||
'drop down': {
|
||||
'factory': OptionResponseXMLFactory(),
|
||||
'kwargs': {
|
||||
'question_text': 'The correct answer is Option 2',
|
||||
'options': ['Option 1', 'Option 2', 'Option 3', 'Option 4'],
|
||||
'correct_option': 'Option 2'}},
|
||||
|
||||
'multiple choice': {
|
||||
'factory': MultipleChoiceResponseXMLFactory(),
|
||||
'kwargs': {
|
||||
'question_text': 'The correct answer is Choice 3',
|
||||
'choices': [False, False, True, False],
|
||||
'choice_names': ['choice_1', 'choice_2', 'choice_3', 'choice_4']}},
|
||||
|
||||
'checkbox': {
|
||||
'factory': ChoiceResponseXMLFactory(),
|
||||
'kwargs': {
|
||||
'question_text': 'The correct answer is Choices 1 and 3',
|
||||
'choice_type': 'checkbox',
|
||||
'choices': [True, False, True, False, False],
|
||||
'choice_names': ['Choice 1', 'Choice 2', 'Choice 3', 'Choice 4']}},
|
||||
|
||||
'string': {
|
||||
'factory': StringResponseXMLFactory(),
|
||||
'kwargs': {
|
||||
'question_text': 'The answer is "correct string"',
|
||||
'case_sensitive': False,
|
||||
'answer': 'correct string'}},
|
||||
|
||||
'numerical': {
|
||||
'factory': NumericalResponseXMLFactory(),
|
||||
'kwargs': {
|
||||
'question_text': 'The answer is pi + 1',
|
||||
'answer': '4.14159',
|
||||
'tolerance': '0.00001',
|
||||
'math_display': True}},
|
||||
|
||||
'formula': {
|
||||
'factory': FormulaResponseXMLFactory(),
|
||||
'kwargs': {
|
||||
'question_text': 'The solution is [mathjax]x^2+2x+y[/mathjax]',
|
||||
'sample_dict': {'x': (-100, 100), 'y': (-100, 100)},
|
||||
'num_samples': 10,
|
||||
'tolerance': 0.00001,
|
||||
'math_display': True,
|
||||
'answer': 'x^2+2*x+y'}},
|
||||
|
||||
'script': {
|
||||
'factory': CustomResponseXMLFactory(),
|
||||
'kwargs': {
|
||||
'question_text': 'Enter two integers that sum to 10.',
|
||||
'cfn': 'test_add_to_ten',
|
||||
'expect': '10',
|
||||
'num_inputs': 2,
|
||||
'script': textwrap.dedent("""
|
||||
def test_add_to_ten(expect,ans):
|
||||
try:
|
||||
a1=int(ans[0])
|
||||
a2=int(ans[1])
|
||||
except ValueError:
|
||||
a1=0
|
||||
a2=0
|
||||
return (a1+a2)==int(expect)
|
||||
""")}},
|
||||
'code': {
|
||||
'factory': CodeResponseXMLFactory(),
|
||||
'kwargs': {
|
||||
'question_text': 'Submit code to an external grader',
|
||||
'initial_display': 'print "Hello world!"',
|
||||
'grader_payload': '{"grader": "ps1/Spring2013/test_grader.py"}', }},
|
||||
}
|
||||
|
||||
|
||||
def add_problem_to_course(course, problem_type):
|
||||
|
||||
assert(problem_type in PROBLEM_FACTORY_DICT)
|
||||
|
||||
# Generate the problem XML using capa.tests.response_xml_factory
|
||||
factory_dict = PROBLEM_FACTORY_DICT[problem_type]
|
||||
problem_xml = factory_dict['factory'].build_xml(**factory_dict['kwargs'])
|
||||
|
||||
# Create a problem item using our generated XML
|
||||
# We set rerandomize=always in the metadata so that the "Reset" button
|
||||
# will appear.
|
||||
problem_item = world.ItemFactory.create(parent_location=section_location(course),
|
||||
template="i4x://edx/templates/problem/Blank_Common_Problem",
|
||||
display_name=str(problem_type),
|
||||
data=problem_xml,
|
||||
metadata={'rerandomize': 'always'})
|
||||
|
||||
|
||||
@step(u'I am viewing a "([^"]*)" problem')
|
||||
def view_problem(step, problem_type):
|
||||
i_am_registered_for_the_course(step, 'model_course')
|
||||
|
||||
# Ensure that the course has this problem type
|
||||
add_problem_to_course('model_course', problem_type)
|
||||
|
||||
# Go to the one section in the factory-created course
|
||||
# which should be loaded with the correct problem
|
||||
chapter_name = TEST_SECTION_NAME.replace(" ", "_")
|
||||
section_name = chapter_name
|
||||
url = django_url('/courses/edx/model_course/Test_Course/courseware/%s/%s' %
|
||||
(chapter_name, section_name))
|
||||
|
||||
world.browser.visit(url)
|
||||
|
||||
|
||||
@step(u'External graders respond "([^"]*)"')
|
||||
def set_external_grader_response(step, correctness):
|
||||
assert(correctness in ['correct', 'incorrect'])
|
||||
|
||||
response_dict = {'correct': True if correctness == 'correct' else False,
|
||||
'score': 1 if correctness == 'correct' else 0,
|
||||
'msg': 'Your problem was graded %s' % correctness}
|
||||
|
||||
# Set the fake xqueue server to always respond
|
||||
# correct/incorrect when asked to grade a problem
|
||||
world.xqueue_server.set_grade_response(response_dict)
|
||||
|
||||
|
||||
@step(u'I answer a "([^"]*)" problem "([^"]*)ly"')
|
||||
def answer_problem(step, problem_type, correctness):
|
||||
""" Mark a given problem type correct or incorrect, then submit it.
|
||||
|
||||
*problem_type* is a string representing the type of problem (e.g. 'drop down')
|
||||
*correctness* is in ['correct', 'incorrect']
|
||||
"""
|
||||
|
||||
assert(correctness in ['correct', 'incorrect'])
|
||||
|
||||
if problem_type == "drop down":
|
||||
select_name = "input_i4x-edx-model_course-problem-drop_down_2_1"
|
||||
option_text = 'Option 2' if correctness == 'correct' else 'Option 3'
|
||||
world.browser.select(select_name, option_text)
|
||||
|
||||
elif problem_type == "multiple choice":
|
||||
if correctness == 'correct':
|
||||
inputfield('multiple choice', choice='choice_3').check()
|
||||
else:
|
||||
inputfield('multiple choice', choice='choice_2').check()
|
||||
|
||||
elif problem_type == "checkbox":
|
||||
if correctness == 'correct':
|
||||
inputfield('checkbox', choice='choice_0').check()
|
||||
inputfield('checkbox', choice='choice_2').check()
|
||||
else:
|
||||
inputfield('checkbox', choice='choice_3').check()
|
||||
|
||||
elif problem_type == 'string':
|
||||
textvalue = 'correct string' if correctness == 'correct' else 'incorrect'
|
||||
inputfield('string').fill(textvalue)
|
||||
|
||||
elif problem_type == 'numerical':
|
||||
textvalue = "pi + 1" if correctness == 'correct' else str(random.randint(-2, 2))
|
||||
inputfield('numerical').fill(textvalue)
|
||||
|
||||
elif problem_type == 'formula':
|
||||
textvalue = "x^2+2*x+y" if correctness == 'correct' else 'x^2'
|
||||
inputfield('formula').fill(textvalue)
|
||||
|
||||
elif problem_type == 'script':
|
||||
# Correct answer is any two integers that sum to 10
|
||||
first_addend = random.randint(-100, 100)
|
||||
second_addend = 10 - first_addend
|
||||
|
||||
# If we want an incorrect answer, then change
|
||||
# the second addend so they no longer sum to 10
|
||||
if correctness == 'incorrect':
|
||||
second_addend += random.randint(1, 10)
|
||||
|
||||
inputfield('script', input_num=1).fill(str(first_addend))
|
||||
inputfield('script', input_num=2).fill(str(second_addend))
|
||||
|
||||
elif problem_type == 'code':
|
||||
# The fake xqueue server is configured to respond
|
||||
# correct / incorrect no matter what we submit.
|
||||
# Furthermore, since the inline code response uses
|
||||
# JavaScript to make the code display nicely, it's difficult
|
||||
# to programatically input text
|
||||
# (there's not <textarea> we can just fill text into)
|
||||
# For this reason, we submit the initial code in the response
|
||||
# (configured in the problem XML above)
|
||||
pass
|
||||
|
||||
# Submit the problem
|
||||
check_problem(step)
|
||||
|
||||
|
||||
@step(u'I check a problem')
|
||||
def check_problem(step):
|
||||
world.css_click("input.check")
|
||||
|
||||
|
||||
@step(u'I reset the problem')
|
||||
def reset_problem(step):
|
||||
world.css_click('input.reset')
|
||||
|
||||
|
||||
# Dictionaries that map problem types to the css selectors
|
||||
# for correct/incorrect/unanswered marks.
|
||||
# The elements are lists of selectors because a particular problem type
|
||||
# might be marked in multiple ways.
|
||||
# For example, multiple choice is marked incorrect differently
|
||||
# depending on whether the user selects an incorrect
|
||||
# item or submits without selecting any item)
|
||||
CORRECTNESS_SELECTORS = {
|
||||
'correct': {'drop down': ['span.correct'],
|
||||
'multiple choice': ['label.choicegroup_correct'],
|
||||
'checkbox': ['span.correct'],
|
||||
'string': ['div.correct'],
|
||||
'numerical': ['div.correct'],
|
||||
'formula': ['div.correct'],
|
||||
'script': ['div.correct'],
|
||||
'code': ['span.correct']},
|
||||
|
||||
'incorrect': {'drop down': ['span.incorrect'],
|
||||
'multiple choice': ['label.choicegroup_incorrect',
|
||||
'span.incorrect'],
|
||||
'checkbox': ['span.incorrect'],
|
||||
'string': ['div.incorrect'],
|
||||
'numerical': ['div.incorrect'],
|
||||
'formula': ['div.incorrect'],
|
||||
'script': ['div.incorrect'],
|
||||
'code': ['span.incorrect']},
|
||||
|
||||
'unanswered': {'drop down': ['span.unanswered'],
|
||||
'multiple choice': ['span.unanswered'],
|
||||
'checkbox': ['span.unanswered'],
|
||||
'string': ['div.unanswered'],
|
||||
'numerical': ['div.unanswered'],
|
||||
'formula': ['div.unanswered'],
|
||||
'script': ['div.unanswered'],
|
||||
'code': ['span.unanswered'] }}
|
||||
|
||||
|
||||
@step(u'My "([^"]*)" answer is marked "([^"]*)"')
|
||||
def assert_answer_mark(step, problem_type, correctness):
|
||||
""" Assert that the expected answer mark is visible for a given problem type.
|
||||
|
||||
*problem_type* is a string identifying the type of problem (e.g. 'drop down')
|
||||
*correctness* is in ['correct', 'incorrect', 'unanswered']
|
||||
"""
|
||||
|
||||
# Determine which selector(s) to look for based on correctness
|
||||
assert(correctness in CORRECTNESS_SELECTORS)
|
||||
selector_dict = CORRECTNESS_SELECTORS[correctness]
|
||||
assert(problem_type in selector_dict)
|
||||
|
||||
# At least one of the correct selectors should be present
|
||||
for sel in selector_dict[problem_type]:
|
||||
has_expected = world.browser.is_element_present_by_css(sel, wait_time=4)
|
||||
|
||||
# As soon as we find the selector, break out of the loop
|
||||
if has_expected:
|
||||
break
|
||||
|
||||
# Expect that we found the expected selector
|
||||
assert(has_expected)
|
||||
|
||||
def inputfield(problem_type, choice=None, input_num=1):
|
||||
""" Return the <input> element for *problem_type*.
|
||||
For example, if problem_type is 'string', return
|
||||
the text field for the string problem in the test course.
|
||||
|
||||
*choice* is the name of the checkbox input in a group
|
||||
of checkboxes. """
|
||||
|
||||
sel = ("input#input_i4x-edx-model_course-problem-%s_2_%s" %
|
||||
(problem_type.replace(" ", "_"), str(input_num)))
|
||||
|
||||
if choice is not None:
|
||||
base = "_choice_" if problem_type == "multiple choice" else "_"
|
||||
sel = sel + base + str(choice)
|
||||
|
||||
# If the input element doesn't exist, fail immediately
|
||||
assert(world.browser.is_element_present_by_css(sel, wait_time=4))
|
||||
|
||||
# Retrieve the input element
|
||||
return world.browser.find_by_css(sel)
|
||||
@@ -4,13 +4,14 @@ Feature: Register for a course
|
||||
I want to register for a class on the edX website
|
||||
|
||||
Scenario: I can register for a course
|
||||
Given I am logged in
|
||||
Given The course "6.002x" exists
|
||||
And I am logged in
|
||||
And I visit the courses page
|
||||
When I register for the course numbered "6.002x"
|
||||
When I register for the course "6.002x"
|
||||
Then I should see the course numbered "6.002x" in my dashboard
|
||||
|
||||
Scenario: I can unregister for a course
|
||||
Given I am registered for a course
|
||||
Given I am registered for the course "6.002x"
|
||||
And I visit the dashboard
|
||||
When I click the link with the text "Unregister"
|
||||
And I press the "Unregister" button in the Unenroll dialog
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
from lettuce import world, step
|
||||
from lettuce.django import django_url
|
||||
from common import TEST_COURSE_ORG, TEST_COURSE_NAME
|
||||
|
||||
|
||||
@step('I register for the course numbered "([^"]*)"$')
|
||||
@step('I register for the course "([^"]*)"$')
|
||||
def i_register_for_the_course(step, course):
|
||||
courses_section = world.browser.find_by_css('section.courses')
|
||||
course_link_css = 'article[id*="%s"] > div' % course
|
||||
course_link = courses_section.find_by_css(course_link_css).first
|
||||
course_link.click()
|
||||
cleaned_name = TEST_COURSE_NAME.replace(' ', '_')
|
||||
url = django_url('courses/%s/%s/%s/about' % (TEST_COURSE_ORG, course, cleaned_name))
|
||||
world.browser.visit(url)
|
||||
|
||||
intro_section = world.browser.find_by_css('section.intro')
|
||||
register_link = intro_section.find_by_css('a.register')
|
||||
|
||||
@@ -60,4 +60,4 @@ Feature: There are courses on the homepage
|
||||
# Scenario: Navigate through course BerkeleyX/CS184.1x/2012_Fall
|
||||
# Given I am registered for course "BerkeleyX/CS184.1x/2012_Fall"
|
||||
# And I log in
|
||||
# Then I verify all the content of each course
|
||||
# Then I verify all the content of each course
|
||||
|
||||
@@ -81,7 +81,7 @@ def browse_course(course_id):
|
||||
num_rendered_sections = len(rendered_sections)
|
||||
|
||||
msg = ('%d sections expected, %d sections found on page, %s - %d - %s' %
|
||||
(num_sections, num_rendered_sections, course_id, chapter_it, chapters[chapter_it]['chapter_name']))
|
||||
(num_sections, num_rendered_sections, course_id, chapter_it, chapters[chapter_it]['chapter_name']))
|
||||
#logger.debug(msg)
|
||||
assert num_sections == num_rendered_sections, msg
|
||||
|
||||
@@ -112,7 +112,7 @@ def browse_course(course_id):
|
||||
num_rendered_tabs = 0
|
||||
|
||||
msg = ('%d tabs expected, %d tabs found, %s - %d - %s' %
|
||||
(num_tabs, num_rendered_tabs, course_id, section_it, sections[section_it]['section_name']))
|
||||
(num_tabs, num_rendered_tabs, course_id, section_it, sections[section_it]['section_name']))
|
||||
#logger.debug(msg)
|
||||
|
||||
# Save the HTML to a file for later comparison
|
||||
@@ -137,7 +137,7 @@ def browse_course(course_id):
|
||||
rendered_items = world.browser.find_by_css('div#seq_content > section > ol > li > section')
|
||||
num_rendered_items = len(rendered_items)
|
||||
msg = ('%d items expected, %d items found, %s - %d - %s - tab %d' %
|
||||
(tab_children, num_rendered_items, course_id, section_it, sections[section_it]['section_name'], tab_it))
|
||||
(tab_children, num_rendered_items, course_id, section_it, sections[section_it]['section_name'], tab_it))
|
||||
#logger.debug(msg)
|
||||
assert tab_children == num_rendered_items, msg
|
||||
|
||||
|
||||
32
lms/djangoapps/courseware/features/xqueue_setup.py
Normal file
32
lms/djangoapps/courseware/features/xqueue_setup.py
Normal file
@@ -0,0 +1,32 @@
|
||||
from courseware.mock_xqueue_server.mock_xqueue_server import MockXQueueServer
|
||||
from lettuce import before, after, world
|
||||
from django.conf import settings
|
||||
import threading
|
||||
|
||||
@before.all
|
||||
def setup_mock_xqueue_server():
|
||||
|
||||
# Retrieve the local port from settings
|
||||
server_port = settings.XQUEUE_PORT
|
||||
|
||||
# Create the mock server instance
|
||||
server = MockXQueueServer(server_port)
|
||||
|
||||
# Start the server running in a separate daemon thread
|
||||
# Because the thread is a daemon, it will terminate
|
||||
# when the main thread terminates.
|
||||
server_thread = threading.Thread(target=server.serve_forever)
|
||||
server_thread.daemon = True
|
||||
server_thread.start()
|
||||
|
||||
# Store the server instance in lettuce's world
|
||||
# so that other steps can access it
|
||||
# (and we can shut it down later)
|
||||
world.xqueue_server = server
|
||||
|
||||
|
||||
@after.all
|
||||
def teardown_mock_xqueue_server(total):
|
||||
|
||||
# Stop the xqueue server and free up the port
|
||||
world.xqueue_server.shutdown()
|
||||
@@ -159,6 +159,7 @@ def grade(student, request, course, model_data_cache=None, keep_raw_scores=False
|
||||
# If we haven't seen a single problem in the section, we don't have to grade it at all! We can assume 0%
|
||||
for moduledescriptor in section['xmoduledescriptors']:
|
||||
# Create a fake key to pull out a StudentModule object from the ModelDataCache
|
||||
|
||||
key = LmsKeyValueStore.Key(
|
||||
Scope.student_state,
|
||||
student.id,
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler
|
||||
import json
|
||||
import urllib
|
||||
import urlparse
|
||||
import threading
|
||||
|
||||
from logging import getLogger
|
||||
logger = getLogger(__name__)
|
||||
|
||||
|
||||
class MockXQueueRequestHandler(BaseHTTPRequestHandler):
|
||||
'''
|
||||
A handler for XQueue POST requests.
|
||||
'''
|
||||
|
||||
protocol = "HTTP/1.0"
|
||||
|
||||
def do_HEAD(self):
|
||||
self._send_head()
|
||||
|
||||
def do_POST(self):
|
||||
'''
|
||||
Handle a POST request from the client
|
||||
|
||||
Sends back an immediate success/failure response.
|
||||
It then POSTS back to the client
|
||||
with grading results, as configured in MockXQueueServer.
|
||||
'''
|
||||
self._send_head()
|
||||
|
||||
# Retrieve the POST data
|
||||
post_dict = self._post_dict()
|
||||
|
||||
# Log the request
|
||||
logger.debug("XQueue received POST request %s to path %s" %
|
||||
(str(post_dict), self.path))
|
||||
|
||||
# Respond only to grading requests
|
||||
if self._is_grade_request():
|
||||
try:
|
||||
xqueue_header = json.loads(post_dict['xqueue_header'])
|
||||
xqueue_body = json.loads(post_dict['xqueue_body'])
|
||||
|
||||
callback_url = xqueue_header['lms_callback_url']
|
||||
|
||||
except KeyError:
|
||||
# If the message doesn't have a header or body,
|
||||
# then it's malformed.
|
||||
# Respond with failure
|
||||
error_msg = "XQueue received invalid grade request"
|
||||
self._send_immediate_response(False, message=error_msg)
|
||||
|
||||
except ValueError:
|
||||
# If we could not decode the body or header,
|
||||
# respond with failure
|
||||
|
||||
error_msg = "XQueue could not decode grade request"
|
||||
self._send_immediate_response(False, message=error_msg)
|
||||
|
||||
else:
|
||||
# Send an immediate response of success
|
||||
# The grade request is formed correctly
|
||||
self._send_immediate_response(True)
|
||||
|
||||
# Wait a bit before POSTing back to the callback url with the
|
||||
# grade result configured by the server
|
||||
# Otherwise, the problem will not realize it's
|
||||
# queued and it will keep waiting for a response
|
||||
# indefinitely
|
||||
delayed_grade_func = lambda: self._send_grade_response(callback_url,
|
||||
xqueue_header)
|
||||
|
||||
timer = threading.Timer(2, delayed_grade_func)
|
||||
timer.start()
|
||||
|
||||
# If we get a request that's not to the grading submission
|
||||
# URL, return an error
|
||||
else:
|
||||
error_message = "Invalid request URL"
|
||||
self._send_immediate_response(False, message=error_message)
|
||||
|
||||
|
||||
def _send_head(self):
|
||||
'''
|
||||
Send the response code and MIME headers
|
||||
'''
|
||||
if self._is_grade_request():
|
||||
self.send_response(200)
|
||||
else:
|
||||
self.send_response(500)
|
||||
|
||||
self.send_header('Content-type', 'text/plain')
|
||||
self.end_headers()
|
||||
|
||||
def _post_dict(self):
|
||||
'''
|
||||
Retrieve the POST parameters from the client as a dictionary
|
||||
'''
|
||||
|
||||
try:
|
||||
length = int(self.headers.getheader('content-length'))
|
||||
|
||||
post_dict = urlparse.parse_qs(self.rfile.read(length))
|
||||
|
||||
# The POST dict will contain a list of values
|
||||
# for each key.
|
||||
# None of our parameters are lists, however,
|
||||
# so we map [val] --> val
|
||||
# If the list contains multiple entries,
|
||||
# we pick the first one
|
||||
post_dict = dict(map(lambda (key, list_val): (key, list_val[0]),
|
||||
post_dict.items()))
|
||||
|
||||
except:
|
||||
# We return an empty dict here, on the assumption
|
||||
# that when we later check that the request has
|
||||
# the correct fields, it won't find them,
|
||||
# and will therefore send an error response
|
||||
return {}
|
||||
|
||||
return post_dict
|
||||
|
||||
def _send_immediate_response(self, success, message=""):
|
||||
'''
|
||||
Send an immediate success/failure message
|
||||
back to the client
|
||||
'''
|
||||
|
||||
# Send the response indicating success/failure
|
||||
response_str = json.dumps({'return_code': 0 if success else 1,
|
||||
'content': message})
|
||||
|
||||
# Log the response
|
||||
logger.debug("XQueue: sent response %s" % response_str)
|
||||
|
||||
self.wfile.write(response_str)
|
||||
|
||||
def _send_grade_response(self, postback_url, xqueue_header):
|
||||
'''
|
||||
POST the grade response back to the client
|
||||
using the response provided by the server configuration
|
||||
'''
|
||||
response_dict = {'xqueue_header': json.dumps(xqueue_header),
|
||||
'xqueue_body': json.dumps(self.server.grade_response())}
|
||||
|
||||
# Log the response
|
||||
logger.debug("XQueue: sent grading response %s" % str(response_dict))
|
||||
|
||||
MockXQueueRequestHandler.post_to_url(postback_url, response_dict)
|
||||
|
||||
def _is_grade_request(self):
|
||||
return 'xqueue/submit' in self.path
|
||||
|
||||
@staticmethod
|
||||
def post_to_url(url, param_dict):
|
||||
'''
|
||||
POST *param_dict* to *url*
|
||||
We make this a separate function so we can easily patch
|
||||
it during testing.
|
||||
'''
|
||||
urllib.urlopen(url, urllib.urlencode(param_dict))
|
||||
|
||||
|
||||
class MockXQueueServer(HTTPServer):
|
||||
'''
|
||||
A mock XQueue grading server that responds
|
||||
to POST requests to localhost.
|
||||
'''
|
||||
|
||||
def __init__(self, port_num,
|
||||
grade_response_dict={'correct': True, 'score': 1, 'msg': ''}):
|
||||
'''
|
||||
Initialize the mock XQueue server instance.
|
||||
|
||||
*port_num* is the localhost port to listen to
|
||||
|
||||
*grade_response_dict* is a dictionary that will be JSON-serialized
|
||||
and sent in response to XQueue grading requests.
|
||||
'''
|
||||
|
||||
self.set_grade_response(grade_response_dict)
|
||||
|
||||
handler = MockXQueueRequestHandler
|
||||
address = ('', port_num)
|
||||
HTTPServer.__init__(self, address, handler)
|
||||
|
||||
def shutdown(self):
|
||||
'''
|
||||
Stop the server and free up the port
|
||||
'''
|
||||
# First call superclass shutdown()
|
||||
HTTPServer.shutdown(self)
|
||||
|
||||
# We also need to manually close the socket
|
||||
self.socket.close()
|
||||
|
||||
def grade_response(self):
|
||||
return self._grade_response
|
||||
|
||||
def set_grade_response(self, grade_response_dict):
|
||||
|
||||
# Check that the grade response has the right keys
|
||||
assert('correct' in grade_response_dict and
|
||||
'score' in grade_response_dict and
|
||||
'msg' in grade_response_dict)
|
||||
|
||||
# Wrap the message in <div> tags to ensure that it is valid XML
|
||||
grade_response_dict['msg'] = "<div>%s</div>" % grade_response_dict['msg']
|
||||
|
||||
# Save the response dictionary
|
||||
self._grade_response = grade_response_dict
|
||||
@@ -0,0 +1,78 @@
|
||||
import mock
|
||||
import unittest
|
||||
import threading
|
||||
import json
|
||||
import urllib
|
||||
import urlparse
|
||||
import time
|
||||
from mock_xqueue_server import MockXQueueServer, MockXQueueRequestHandler
|
||||
|
||||
|
||||
class MockXQueueServerTest(unittest.TestCase):
|
||||
'''
|
||||
A mock version of the XQueue server that listens on a local
|
||||
port and responds with pre-defined grade messages.
|
||||
|
||||
Used for lettuce BDD tests in lms/courseware/features/problems.feature
|
||||
and lms/courseware/features/problems.py
|
||||
|
||||
This is temporary and will be removed when XQueue is
|
||||
rewritten using celery.
|
||||
'''
|
||||
|
||||
def setUp(self):
|
||||
|
||||
# Create the server
|
||||
server_port = 8034
|
||||
self.server_url = 'http://127.0.0.1:%d' % server_port
|
||||
self.server = MockXQueueServer(server_port,
|
||||
{'correct': True, 'score': 1, 'msg': ''})
|
||||
|
||||
# Start the server in a separate daemon thread
|
||||
server_thread = threading.Thread(target=self.server.serve_forever)
|
||||
server_thread.daemon = True
|
||||
server_thread.start()
|
||||
|
||||
def tearDown(self):
|
||||
|
||||
# Stop the server, freeing up the port
|
||||
self.server.shutdown()
|
||||
|
||||
def test_grade_request(self):
|
||||
|
||||
# Patch post_to_url() so we can intercept
|
||||
# outgoing POST requests from the server
|
||||
MockXQueueRequestHandler.post_to_url = mock.Mock()
|
||||
|
||||
# Send a grade request
|
||||
callback_url = 'http://127.0.0.1:8000/test_callback'
|
||||
|
||||
grade_header = json.dumps({'lms_callback_url': callback_url,
|
||||
'lms_key': 'test_queuekey',
|
||||
'queue_name': 'test_queue'})
|
||||
|
||||
grade_body = json.dumps({'student_info': 'test',
|
||||
'grader_payload': 'test',
|
||||
'student_response': 'test'})
|
||||
|
||||
grade_request = {'xqueue_header': grade_header,
|
||||
'xqueue_body': grade_body}
|
||||
|
||||
response_handle = urllib.urlopen(self.server_url + '/xqueue/submit',
|
||||
urllib.urlencode(grade_request))
|
||||
|
||||
response_dict = json.loads(response_handle.read())
|
||||
|
||||
# Expect that the response is success
|
||||
self.assertEqual(response_dict['return_code'], 0)
|
||||
|
||||
# Wait a bit before checking that the server posted back
|
||||
time.sleep(3)
|
||||
|
||||
# Expect that the server tries to post back the grading info
|
||||
xqueue_body = json.dumps({'correct': True, 'score': 1,
|
||||
'msg': '<div></div>'})
|
||||
expected_callback_dict = {'xqueue_header': grade_header,
|
||||
'xqueue_body': xqueue_body}
|
||||
MockXQueueRequestHandler.post_to_url.assert_called_with(callback_url,
|
||||
expected_callback_dict)
|
||||
@@ -1,3 +1,7 @@
|
||||
"""
|
||||
Classes to provide the LMS runtime data storage to XBlocks
|
||||
"""
|
||||
|
||||
import json
|
||||
from collections import namedtuple, defaultdict
|
||||
from itertools import chain
|
||||
@@ -14,10 +18,16 @@ from xblock.core import Scope
|
||||
|
||||
|
||||
class InvalidWriteError(Exception):
|
||||
pass
|
||||
"""
|
||||
Raised to indicate that writing to a particular key
|
||||
in the KeyValueStore is disabled
|
||||
"""
|
||||
|
||||
|
||||
def chunks(items, chunk_size):
|
||||
"""
|
||||
Yields the values from items in chunks of size chunk_size
|
||||
"""
|
||||
items = list(items)
|
||||
return (items[i:i + chunk_size] for i in xrange(0, len(items), chunk_size))
|
||||
|
||||
@@ -67,6 +77,15 @@ class ModelDataCache(object):
|
||||
"""
|
||||
|
||||
def get_child_descriptors(descriptor, depth, descriptor_filter):
|
||||
"""
|
||||
Return a list of all child descriptors down to the specified depth
|
||||
that match the descriptor filter. Includes `descriptor`
|
||||
|
||||
descriptor: The parent to search inside
|
||||
depth: The number of levels to descend, or None for infinite depth
|
||||
descriptor_filter(descriptor): A function that returns True
|
||||
if descriptor should be included in the results
|
||||
"""
|
||||
if descriptor_filter(descriptor):
|
||||
descriptors = [descriptor]
|
||||
else:
|
||||
@@ -121,7 +140,7 @@ class ModelDataCache(object):
|
||||
'module_state_key__in',
|
||||
(descriptor.location.url() for descriptor in self.descriptors),
|
||||
course_id=self.course_id,
|
||||
student=self.user,
|
||||
student=self.user.pk,
|
||||
)
|
||||
elif scope == Scope.content:
|
||||
return self._chunked_query(
|
||||
@@ -145,13 +164,13 @@ class ModelDataCache(object):
|
||||
XModuleStudentPrefsField,
|
||||
'module_type__in',
|
||||
set(descriptor.location.category for descriptor in self.descriptors),
|
||||
student=self.user,
|
||||
student=self.user.pk,
|
||||
field_name__in=set(field.name for field in fields),
|
||||
)
|
||||
elif scope == Scope.student_info:
|
||||
return self._query(
|
||||
XModuleStudentInfoField,
|
||||
student=self.user,
|
||||
student=self.user.pk,
|
||||
field_name__in=set(field.name for field in fields),
|
||||
)
|
||||
else:
|
||||
@@ -168,6 +187,9 @@ class ModelDataCache(object):
|
||||
return scope_map
|
||||
|
||||
def _cache_key_from_kvs_key(self, key):
|
||||
"""
|
||||
Return the key used in the ModelDataCache for the specified KeyValueStore key
|
||||
"""
|
||||
if key.scope == Scope.student_state:
|
||||
return (key.scope, key.block_scope_id.url())
|
||||
elif key.scope == Scope.content:
|
||||
@@ -180,6 +202,10 @@ class ModelDataCache(object):
|
||||
return (key.scope, key.field_name)
|
||||
|
||||
def _cache_key_from_field_object(self, scope, field_object):
|
||||
"""
|
||||
Return the key used in the ModelDataCache for the specified scope and
|
||||
field
|
||||
"""
|
||||
if scope == Scope.student_state:
|
||||
return (scope, field_object.module_state_key)
|
||||
elif scope == Scope.content:
|
||||
@@ -230,7 +256,7 @@ class ModelDataCache(object):
|
||||
usage_id='%s-%s' % (self.course_id, key.block_scope_id.url()),
|
||||
)
|
||||
elif key.scope == Scope.student_preferences:
|
||||
field_object, _= XModuleStudentPrefsField.objects.get_or_create(
|
||||
field_object, _ = XModuleStudentPrefsField.objects.get_or_create(
|
||||
field_name=key.field_name,
|
||||
module_type=key.block_scope_id,
|
||||
student=self.user,
|
||||
@@ -276,6 +302,7 @@ class LmsKeyValueStore(KeyValueStore):
|
||||
Scope.student_info,
|
||||
Scope.children,
|
||||
)
|
||||
|
||||
def __init__(self, descriptor_model_data, model_data_cache):
|
||||
self._descriptor_model_data = descriptor_model_data
|
||||
self._model_data_cache = model_data_cache
|
||||
@@ -357,4 +384,3 @@ class LmsKeyValueStore(KeyValueStore):
|
||||
|
||||
|
||||
LmsUsage = namedtuple('LmsUsage', 'id, def_id')
|
||||
|
||||
|
||||
@@ -116,6 +116,10 @@ def create_thread(request, course_id, commentable_id):
|
||||
|
||||
thread.save()
|
||||
|
||||
#patch for backward compatibility to comments service
|
||||
if not 'pinned' in thread.attributes:
|
||||
thread['pinned'] = False
|
||||
|
||||
if post.get('auto_subscribe', 'false').lower() == 'true':
|
||||
user = cc.User.from_django_user(request.user)
|
||||
user.follow(thread)
|
||||
|
||||
@@ -98,6 +98,11 @@ def get_threads(request, course_id, discussion_id=None, per_page=THREADS_PER_PAG
|
||||
else:
|
||||
thread['group_name'] = ""
|
||||
thread['group_string'] = "This post visible to everyone."
|
||||
|
||||
#patch for backward compatibility to comments service
|
||||
if not 'pinned' in thread:
|
||||
thread['pinned'] = False
|
||||
|
||||
|
||||
query_params['page'] = page
|
||||
query_params['num_pages'] = num_pages
|
||||
@@ -245,6 +250,11 @@ def single_thread(request, course_id, discussion_id, thread_id):
|
||||
|
||||
try:
|
||||
thread = cc.Thread.find(thread_id).retrieve(recursive=True, user_id=request.user.id)
|
||||
|
||||
#patch for backward compatibility with comments service
|
||||
if not 'pinned' in thread.attributes:
|
||||
thread['pinned'] = False
|
||||
|
||||
except (cc.utils.CommentClientError, cc.utils.CommentClientUnknownError) as err:
|
||||
log.error("Error loading single thread.")
|
||||
raise Http404
|
||||
@@ -285,6 +295,10 @@ def single_thread(request, course_id, discussion_id, thread_id):
|
||||
if thread.get('group_id') and not thread.get('group_name'):
|
||||
thread['group_name'] = get_cohort_by_id(course_id, thread.get('group_id')).name
|
||||
|
||||
#patch for backward compatibility with comments service
|
||||
if not "pinned" in thread:
|
||||
thread["pinned"] = False
|
||||
|
||||
threads = [utils.safe_content(thread) for thread in threads]
|
||||
|
||||
#recent_active_threads = cc.search_recent_active_threads(
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user