Commit 9fdac385 by marco

Merge branch 'master' into ux/marco/textbook

parents f45cc6fb 4d266fa3
......@@ -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]
......
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()
......
import factory
from student.models import User, UserProfile, Registration
from datetime import datetime
import uuid
class UserProfileFactory(factory.Factory):
FACTORY_FOR = UserProfile
user = None
name = 'Robot Studio'
courseware = 'course.xml'
class RegistrationFactory(factory.Factory):
FACTORY_FOR = Registration
user = None
activation_key = uuid.uuid4().hex
class UserFactory(factory.Factory):
FACTORY_FOR = User
username = 'robot-studio'
email = 'robot+studio@edx.org'
password = 'test'
first_name = 'Robot'
last_name = 'Studio'
is_staff = False
is_active = True
is_superuser = False
last_login = datetime.now()
date_joined = datetime.now()
from lettuce import world, step
from 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'])
......
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
{
"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;
......
// 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;
}
}
\ No newline at end of file
section.cal {
@include box-sizing(border-box);
@include clearfix;
padding: 20px;
> header {
display: none;
@include clearfix;
margin-bottom: 10px;
opacity: .4;
@include transition;
text-shadow: 0 1px 0 #fff;
&:hover {
opacity: 1;
}
h2 {
@include inline-block();
text-transform: uppercase;
letter-spacing: 1px;
font-size: 14px;
padding: 6px 6px 6px 0;
font-size: 12px;
margin: 0;
}
ul {
@include inline-block;
float: right;
margin: 0;
padding: 0;
&.actions {
float: left;
}
li {
@include inline-block;
margin-right: 6px;
border-right: 1px solid #ddd;
padding: 0 6px 0 0;
&:last-child {
border-right: 0;
margin-right: 0;
padding-right: 0;
}
a {
@include inline-block();
font-size: 12px;
@include inline-block;
margin: 0 6px;
font-style: italic;
}
ul {
@include inline-block();
margin: 0;
li {
@include inline-block();
padding: 0;
border-left: 0;
}
}
}
}
}
ol {
list-style: none;
@include clearfix;
border: 1px solid lighten( $dark-blue , 30% );
background: #FFF;
width: 100%;
@include box-sizing(border-box);
margin: 0;
padding: 0;
@include box-shadow(0 0 5px lighten($dark-blue, 45%));
@include border-radius(3px);
overflow: hidden;
> li {
border-right: 1px solid lighten($dark-blue, 40%);
border-bottom: 1px solid lighten($dark-blue, 40%);
@include box-sizing(border-box);
float: left;
width: flex-grid(3) + ((flex-gutter() * 3) / 4);
background-color: $light-blue;
@include box-shadow(inset 0 0 0 1px lighten($light-blue, 8%));
&:hover {
li.create-module {
opacity: 1;
}
}
&:nth-child(4n) {
border-right: 0;
}
header {
border-bottom: 1px solid lighten($dark-blue, 40%);
@include box-shadow(0 2px 2px $light-blue);
display: block;
margin-bottom: 2px;
background: #FFF;
h1 {
font-size: 14px;
text-transform: uppercase;
border-bottom: 1px solid lighten($dark-blue, 60%);
padding: 6px;
color: $bright-blue;
margin: 0;
a {
color: $bright-blue;
display: block;
padding: 6px;
margin: -6px;
&:hover {
color: darken($bright-blue, 10%);
background: lighten($yellow, 10%);
}
}
}
ul {
margin: 0;
padding: 0;
li {
background: #fff;
color: #888;
border-bottom: 0;
font-size: 12px;
@include box-shadow(none);
}
}
}
ul {
list-style: none;
margin: 0 0 1px 0;
padding: 0;
li {
border-bottom: 1px solid darken($light-blue, 6%);
// @include box-shadow(0 1px 0 lighten($light-blue, 4%));
overflow: hidden;
position: relative;
text-shadow: 0 1px 0 #fff;
&:hover {
background-color: lighten($yellow, 14%);
a.draggable {
background-color: lighten($yellow, 14%);
opacity: 1;
}
}
&.editable {
padding: 3px 6px;
}
a {
color: lighten($dark-blue, 10%);
display: block;
padding: 6px 35px 6px 6px;
&:hover {
background-color: lighten($yellow, 10%);
}
&.draggable {
background-color: $light-blue;
opacity: .3;
padding: 0;
&:hover {
background-color: lighten($yellow, 10%);
}
}
}
&.create-module {
position: relative;
opacity: 0;
@include transition(all 3s ease-in-out);
background: darken($light-blue, 2%);
> div {
background: $dark-blue;
@include box-shadow(0 0 5px darken($light-blue, 60%));
@include box-sizing(border-box);
display: none;
margin-left: 3%;
padding: 10px;
@include position(absolute, 30px 0 0 0);
width: 90%;
z-index: 99;
ul {
li {
border-bottom: 0;
background: none;
input {
@include box-sizing(border-box);
width: 100%;
}
select {
@include box-sizing(border-box);
width: 100%;
option {
font-size: 14px;
}
}
div {
margin-top: 10px;
}
a {
color: $light-blue;
float: right;
&:first-child {
float: left;
}
&:hover {
color: #fff;
}
}
}
}
}
}
}
}
}
}
section.new-section {
margin: 10px 0 40px;
@include inline-block();
position: relative;
> a {
@extend .button;
display: block;
}
section {
display: none;
@include position(absolute, 30px 0 0 0);
background: rgba(#000, .8);
min-width: 300px;
padding: 10px;
@include box-sizing(border-box);
@include border-radius(3px);
z-index: 99;
&:before {
content: " ";
display: block;
background: rgba(#000, .8);
width: 10px;
height: 10px;
@include position(absolute, -5px 0 0 20%);
@include transform(rotate(45deg));
}
form {
ul {
list-style: none;
li {
border-bottom: 0;
background: none;
margin-bottom: 6px;
input {
width: 100%;
@include box-sizing(border-box);
border-color: #000;
padding: 6px;
}
select {
width: 100%;
@include box-sizing(border-box);
option {
font-size: 14px;
}
}
a {
float: right;
&:first-child {
float: left;
}
}
}
}
}
}
}
}
body.content
section.cal {
width: flex-grid(3);
float: left;
overflow: scroll;
@include box-sizing(border-box);
opacity: .4;
@include transition();
&:hover {
opacity: 1;
}
> header {
@include transition;
overflow: hidden;
> a {
display: none;
}
ul {
float: none;
display: block;
li {
ul {
display: inline;
}
}
}
}
ol {
li {
@include box-sizing(border-box);
width: 100%;
border-right: 0;
&.create-module {
display: none;
}
}
}
}
// studio - utilities - mixins and extends
// ====================
@mixin clearfix {
&:after {
content: '';
......
.faded-hr-divider {
@include background-image(linear-gradient(180deg, rgba(200,200,200, 0) 0%,
rgba(200,200,200, 1) 50%,
rgba(200,200,200, 0)));
height: 1px;
width: 100%;
}
.faded-hr-divider-medium {
@include background-image(linear-gradient(180deg, rgba(240,240,240, 0) 0%,
rgba(240,240,240, 1) 50%,
rgba(240,240,240, 0)));
height: 1px;
width: 100%;
}
.faded-hr-divider-light {
@include background-image(linear-gradient(180deg, rgba(255,255,255, 0) 0%,
rgba(255,255,255, 0.8) 50%,
rgba(255,255,255, 0)));
height: 1px;
width: 100%;
}
.faded-vertical-divider {
@include background-image(linear-gradient(90deg, rgba(200,200,200, 0) 0%,
rgba(200,200,200, 1) 50%,
rgba(200,200,200, 0)));
height: 100%;
width: 1px;
}
.faded-vertical-divider-light {
@include background-image(linear-gradient(90deg, rgba(255,255,255, 0) 0%,
rgba(255,255,255, 0.6) 50%,
rgba(255,255,255, 0)));
height: 100%;
width: 1px;
}
.vertical-divider {
@extend .faded-vertical-divider;
position: relative;
&::after {
@extend .faded-vertical-divider-light;
content: "";
display: block;
position: absolute;
left: 1px;
}
}
.horizontal-divider {
border: none;
@extend .faded-hr-divider;
position: relative;
&::after {
@extend .faded-hr-divider-light;
content: "";
display: block;
position: absolute;
top: 1px;
}
}
.fade-right-hr-divider {
@include background-image(linear-gradient(180deg, rgba(200,200,200, 0) 0%,
rgba(200,200,200, 1)));
border: none;
}
.fade-left-hr-divider {
@include background-image(linear-gradient(180deg, rgba(200,200,200, 1) 0%,
rgba(200,200,200, 0)));
border: none;
}
\ No newline at end of file
// This is a temporary page, which will be replaced once we have a more extensive course catalog and marketing site for edX labs.
.class-landing {
.main-wrapper {
width: 700px !important;
margin: 100px auto;
}
.class-info {
padding: 30px 40px 40px;
@extend .window;
hgroup {
padding-bottom: 26px;
border-bottom: 1px solid $mediumGrey;
}
h1 {
float: none;
font-size: 30px;
font-weight: 300;
margin: 0;
}
h2 {
color: #5d6779;
}
.class-actions {
@include clearfix;
padding: 15px 0;
margin-bottom: 18px;
border-bottom: 1px solid $mediumGrey;
}
.log-in-form {
@include clearfix;
padding: 15px 0 20px;
margin-bottom: 18px;
border-bottom: 1px solid $mediumGrey;
.log-in-submit-button {
@include blue-button;
padding: 6px 20px 8px;
margin: 24px 0 0;
}
.column {
float: left;
width: 41%;
margin-right: 1%;
&.submit {
width: 16%;
margin-right: 0;
}
label {
float: left;
}
}
input {
width: 100%;
font-family: $sans-serif;
font-size: 13px;
}
.forgot-button {
float: right;
margin-bottom: 6px;
font-size: 12px;
}
}
.sign-up-button {
@include blue-button;
display: block;
width: 250px;
margin: auto;
}
.log-in-button {
@include white-button;
float: right;
}
.sign-up-button,
.log-in-button {
padding: 8px 0 12px;
font-size: 18px;
font-weight: 300;
text-align: center;
}
.class-description {
margin-top: 30px;
font-size: 14px;
}
p + p {
margin-top: 22px;
}
}
.edx-labs-logo-small {
display: block;
width: 124px;
height: 30px;
margin: auto;
background: url(../img/edx-labs-logo-small.png) no-repeat;
text-indent: -9999px;
overflow: hidden;
}
.edge-logo {
display: block;
width: 143px;
height: 39px;
margin: auto;
background: url(../images/edge-logo-small.png) no-repeat;
text-indent: -9999px;
overflow: hidden;
}
}
\ No newline at end of file
body {
@include clearfix();
height: 100%;
font: 14px $body-font-family;
background-color: lighten($dark-blue, 62%);
background-image: url('/static/img/noise.png');
> section {
display: table;
table-layout: fixed;
width: 100%;
}
> header {
background: $dark-blue;
@include background-image(url('/static/img/noise.png'), linear-gradient(lighten($dark-blue, 10%), $dark-blue));
border-bottom: 1px solid darken($dark-blue, 15%);
@include box-shadow(inset 0 -1px 0 lighten($dark-blue, 10%));
@include box-sizing(border-box);
color: #fff;
display: block;
float: none;
padding: 0 20px;
text-shadow: 0 -1px 0 darken($dark-blue, 15%);
width: 100%;
nav {
@include clearfix;
> a {
@include hide-text;
background: url('/static/img/menu.png') 0 center no-repeat;
border-right: 1px solid darken($dark-blue, 10%);
@include box-shadow(1px 0 0 lighten($dark-blue, 10%));
display: block;
float: left;
height: 19px;
padding: 8px 10px 8px 0;
width: 14px;
&:hover, &:focus {
opacity: .7;
}
}
h2 {
border-right: 1px solid darken($dark-blue, 10%);
@include box-shadow(1px 0 0 lighten($dark-blue, 10%));
float: left;
font-size: 14px;
margin: 0;
text-transform: uppercase;
-webkit-font-smoothing: antialiased;
a {
color: #fff;
padding: 8px 20px;
display: block;
&:hover {
background-color: rgba(darken($dark-blue, 15%), .5);
color: $yellow;
}
}
}
a {
color: rgba(#fff, .8);
&:hover {
color: rgba(#fff, .6);
}
}
ul {
float: left;
margin: 0;
padding: 0;
@include clearfix;
&.user-nav {
float: right;
border-left: 1px solid darken($dark-blue, 10%);
}
li {
border-right: 1px solid darken($dark-blue, 10%);
float: left;
@include box-shadow(1px 0 0 lighten($dark-blue, 10%));
a {
padding: 8px 20px;
display: block;
&:hover {
background-color: rgba(darken($dark-blue, 15%), .5);
color: $yellow;
}
&.new-module {
&:before {
@include inline-block;
content: "+";
font-weight: bold;
margin-right: 10px;
}
}
}
}
}
}
}
&.content {
section.main-content {
border-left: 2px solid $dark-blue;
@include box-sizing(border-box);
width: flex-grid(9) + flex-gutter();
float: left;
@include box-shadow( -2px 0 0 lighten($dark-blue, 55%));
@include transition();
background: #FFF;
}
}
}
.component {
font-family: 'Open Sans', Verdana, Arial, Helvetica, sans-serif;
font-size: 16px;
line-height: 1.6;
color: #3c3c3c;
a {
color: #1d9dd9;
text-decoration: none;
}
p {
font-size: 16px;
line-height: 1.6;
}
h1 {
float: none;
}
h2 {
color: #646464;
font-size: 19px;
font-weight: 300;
letter-spacing: 1px;
margin-bottom: 15px;
margin-left: 0;
text-transform: uppercase;
}
h3 {
font-size: 19px;
font-weight: 400;
}
h4 {
background: none;
padding: 0;
border: none;
@include box-shadow(none);
font-size: 16px;
font-weight: 400;
}
code {
margin: 0 2px;
padding: 0px 5px;
border-radius: 3px;
border: 1px solid #eaeaea;
white-space: nowrap;
font-family: Monaco, monospace;
font-size: 14px;
background-color: #f8f8f8;
}
p + h2, ul + h2, ol + h2, p + h3 {
margin-top: 40px;
}
p + p, ul + p, ol + p {
margin-top: 20px;
}
p {
color: #3c3c3c;
font: normal 1em/1.6em;
margin: 0px;
}
}
\ No newline at end of file
.edx-studio-logo-large {
display: block;
width: 224px;
height: 45px;
margin: 100px auto 30px;
background: url(../img/edx-studio-large.png) no-repeat;
}
.sign-up-box,
.log-in-box {
width: 500px;
margin: auto;
border-radius: 3px;
header {
height: 36px;
border-radius: 3px 3px 0 0;
border: 1px solid #2c2e33;
@include linear-gradient(top, #686b76, #54565e);
color: #fff;
@include box-shadow(0 1px 1px rgba(0, 0, 0, 0.2), 0 -1px 0px rgba(255, 255, 255, 0.05) inset, 0 1px 0 rgba(255, 255, 255, .25) inset);
h1 {
float: none;
margin: 5px 0;
font-size: 15px;
font-weight: 300;
text-align: center;
}
}
form {
padding: 40px;
border: 1px solid $darkGrey;
border-top-width: 0;
border-radius: 0 0 3px 3px;
background: #fff;
@include box-shadow(0 1px 2px rgba(0, 0, 0, .1));
}
label {
display: block;
margin-bottom: 5px;
font-size: 13px;
font-weight: 700;
}
input[type="text"],
input[type="email"],
input[type="password"] {
width: 100%;
font-size: 20px;
font-weight: 300;
}
.row {
@include clearfix;
margin-bottom: 24px;
.split {
float: left;
width: 48%;
&:first-child {
margin-right: 4%;
}
}
}
.form-actions {
@include clearfix;
margin-top: 32px;
margin-bottom: 5px;
text-align: center;
}
.log-in-button,
.create-account-button {
@include blue-button;
padding: 8px 0 10px;
font-family: $sans-serif;
@include transition(all .15s);
}
.create-account-button {
padding: 10px 40px 12px;
margin-bottom: 10px;
}
.enrolled {
font-size: 14px;
}
.sign-up-button {
@include white-button;
padding: 7px 0 9px;
}
.log-in-button,
.sign-up-button {
@include box-sizing(border-box);
float: left;
width: 45%;
}
.or {
float: left;
display: inline-block;
width: 10%;
font-size: 15px;
line-height: 36px;
color: $darkGrey;
text-align: center;
}
.forgot-button {
float: right;
font-size: 11px;
font-weight: 400;
line-height: 21px;
}
.log-in-extra {
margin-top: 10px;
text-align: right;
font-size: 13px;
}
#login_error,
#register_error {
display: none;
margin-bottom: 30px;
padding: 5px 10px;
border-radius: 3px;
background: $error-red;
font-size: 14px;
color: #fff;
}
}
\ No newline at end of file
section.video-new, section.video-edit, section.problem-new, section.problem-edit {
position: absolute;
top: 72px;
right: 0;
background: #fff;
width: flex-grid(6);
@include box-shadow(0 0 6px #666);
border: 1px solid #333;
border-right: 0;
z-index: 4;
> header {
background: #666;
@include clearfix;
color: #fff;
padding: 6px;
border-bottom: 1px solid #333;
-webkit-font-smoothing: antialiased;
h2 {
float: left;
font-size: 14px;
}
a {
color: #fff;
&.save-update {
float: right;
}
&.cancel {
float: left;
}
}
}
> section {
padding: 20px;
> header {
h1 {
font-size: 24px;
margin: 12px 0;
}
section {
&.status-settings {
ul {
list-style: none;
@include border-radius(2px);
border: 1px solid #999;
@include inline-block();
li {
@include inline-block();
border-right: 1px solid #999;
padding: 6px;
&:last-child {
border-right: 0;
}
&.current {
background: #eee;
}
}
}
a.settings {
@include inline-block();
margin: 0 20px;
border: 1px solid #999;
padding: 6px;
}
select {
float: right;
}
}
&.meta {
background: #eee;
padding: 10px;
margin: 20px 0;
@include clearfix();
div {
float: left;
margin-right: 20px;
h2 {
font-size: 14px;
@include inline-block();
}
p {
@include inline-block();
}
}
}
}
}
section.notes {
margin-top: 20px;
padding: 6px;
background: #eee;
border: 1px solid #ccc;
textarea {
@include box-sizing(border-box);
display: block;
width: 100%;
}
h2 {
font-size: 14px;
margin-bottom: 6px;
}
input[type="submit"]{
margin-top: 10px;
}
}
}
}
section.problem-new, section.problem-edit {
> section {
textarea {
@include box-sizing(border-box);
display: block;
width: 100%;
}
div.preview {
background: #eee;
@include box-sizing(border-box);
height: 40px;
padding: 10px;
width: 100%;
}
a.save {
@extend .button;
@include inline-block();
margin-top: 20px;
}
}
}
// studio - utilities - reset
// ====================
// * {
// @include box-sizing(border-box);
// }
html, body, div, span, applet, object, iframe,
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;
}
}
*/
}
\ No newline at end of file
section#unit-wrapper {
section.filters {
@include clearfix;
display: none;
opacity: .4;
margin-bottom: 10px;
@include transition;
&:hover {
opacity: 1;
}
h2 {
@include inline-block();
text-transform: uppercase;
letter-spacing: 1px;
font-size: 14px;
padding: 6px 6px 6px 0;
font-size: 12px;
margin: 0;
}
ul {
@include clearfix();
list-style: none;
margin: 0;
padding: 0;
li {
@include inline-block;
margin-right: 6px;
border-right: 1px solid #ddd;
padding-right: 6px;
&.search {
float: right;
border: 0;
}
a {
&.more {
font-size: 12px;
@include inline-block;
margin: 0 6px;
font-style: italic;
}
}
}
}
}
div.content {
display: table;
border: 1px solid lighten($dark-blue, 40%);
width: 100%;
@include border-radius(3px);
@include box-shadow(0 0 4px lighten($dark-blue, 50%));
section {
header {
background: #fff;
padding: 6px;
border-bottom: 1px solid lighten($dark-blue, 60%);
@include clearfix;
h2 {
color: $bright-blue;
// float: left;
font-size: 14px;
letter-spacing: 1px;
// line-height: 20px;
text-transform: uppercase;
margin: 0;
}
}
&.modules {
@include box-sizing(border-box);
display: table-cell;
width: flex-grid(6, 9);
border-right: 1px solid lighten($dark-blue, 40%);
&.empty {
text-align: center;
vertical-align: middle;
a {
@extend .button;
@include inline-block();
margin-top: 10px;
}
}
ol {
list-style: none;
margin: 0;
padding: 0;
li {
border-bottom: 1px solid lighten($dark-blue, 60%);
a {
color: #000;
}
ol {
list-style: none;
margin: 0;
padding: 0;
li {
padding: 6px;
position: relative;
&:last-child {
border-bottom: 0;
}
&:hover {
background-color: lighten($yellow, 10%);
a.draggable {
opacity: 1;
}
}
a.draggable {
float: right;
opacity: .4;
}
&.group {
padding: 0;
header {
padding: 6px;
background: none;
h3 {
font-size: 14px;
margin: 0;
}
}
ol {
border-left: 4px solid #999;
border-bottom: 0;
margin: 0;
padding: 0;
li {
&:last-child {
border-bottom: 0;
}
}
}
}
}
}
}
}
}
&.scratch-pad {
@include box-sizing(border-box);
display: table-cell;
width: flex-grid(3, 9) + flex-gutter(9);
vertical-align: top;
ol {
list-style: none;
margin: 0;
padding: 0;
li {
background: $light-blue;
&:last-child {
border-bottom: 0;
}
&.new-module a {
background-color: darken($light-blue, 2%);
border-bottom: 1px solid darken($light-blue, 8%);
&:hover {
background-color: lighten($yellow, 10%);
}
}
a {
color: $dark-blue;
}
ul {
list-style: none;
margin: 0;
padding: 0;
li {
padding: 6px;
border-collapse: collapse;
border-bottom: 1px solid darken($light-blue, 8%);
position: relative;
&:last-child {
border-bottom: 1px solid darken($light-blue, 8%);
}
&:hover {
background-color: lighten($yellow, 10%);
a.draggable {
opacity: 1;
}
}
&.empty {
padding: 12px;
a {
@extend .button;
display: block;
text-align: center;
}
}
a.draggable {
opacity: .3;
}
}
}
}
}
}
}
}
}
// studio - utilities - variables
// ====================
$baseline: 20px;
// 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);
\ No newline at end of file
$lightBluishGrey2: rgb(213, 220, 228);
$error-red: rgb(253, 87, 87);
\ No newline at end of file
section.video-new, section.video-edit {
> section {
section.upload {
padding: 6px;
margin-bottom: 10px;
border: 1px solid #ddd;
a.upload-button {
@extend .button;
@include inline-block();
}
}
section.in-use {
h2 {
font-size: 14px;
}
div {
background: #eee;
text-align: center;
padding: 6px;
}
}
a.save-update {
@extend .button;
@include inline-block();
margin-top: 20px;
}
}
}
section.week-edit,
section.week-new,
section.sequence-edit {
> header {
border-bottom: 2px solid #333;
@include clearfix();
div {
@include clearfix();
padding: 6px 20px;
h1 {
font-size: 18px;
text-transform: uppercase;
letter-spacing: 1px;
float: left;
}
p {
float: right;
}
&.week {
background: #eee;
font-size: 12px;
border-bottom: 1px solid #ccc;
h2 {
font-size: 12px;
@include inline-block();
margin-right: 20px;
}
ul {
list-style: none;
@include inline-block();
li {
@include inline-block();
margin-right: 10px;
p {
float: none;
}
}
}
}
}
section.goals {
background: #eee;
padding: 6px 20px;
border-top: 1px solid #ccc;
ul {
list-style: none;
color: #999;
li {
margin-bottom: 6px;
&:last-child {
margin-bottom: 0;
}
}
}
}
}
> section.content {
@include box-sizing(border-box);
padding: 20px;
section.filters {
@include clearfix;
margin-bottom: 10px;
background: #efefef;
border: 1px solid #ddd;
ul {
@include clearfix();
list-style: none;
padding: 6px;
li {
@include inline-block();
&.advanced {
float: right;
}
}
}
}
> div {
display: table;
border: 1px solid;
width: 100%;
section {
header {
background: #eee;
padding: 6px;
border-bottom: 1px solid #ccc;
@include clearfix;
h2 {
text-transform: uppercase;
letter-spacing: 1px;
font-size: 12px;
float: left;
}
}
&.modules {
@include box-sizing(border-box);
display: table-cell;
width: flex-grid(6, 9);
border-right: 1px solid #333;
&.empty {
text-align: center;
vertical-align: middle;
a {
@extend .button;
@include inline-block();
margin-top: 10px;
}
}
ol {
list-style: none;
border-bottom: 1px solid #333;
li {
border-bottom: 1px solid #333;
&:last-child{
border-bottom: 0;
}
a {
color: #000;
}
ol {
list-style: none;
li {
padding: 6px;
&:hover {
a.draggable {
opacity: 1;
}
}
a.draggable {
float: right;
opacity: .5;
}
&.group {
padding: 0;
header {
padding: 6px;
background: none;
h3 {
font-size: 14px;
}
}
ol {
border-left: 4px solid #999;
border-bottom: 0;
li {
&:last-child {
border-bottom: 0;
}
}
}
}
}
}
}
}
}
&.scratch-pad {
@include box-sizing(border-box);
display: table-cell;
width: flex-grid(3, 9) + flex-gutter(9);
vertical-align: top;
ol {
list-style: none;
border-bottom: 1px solid #999;
li {
border-bottom: 1px solid #999;
background: #f9f9f9;
&:last-child {
border-bottom: 0;
}
ul {
list-style: none;
li {
padding: 6px;
&:last-child {
border-bottom: 0;
}
&:hover {
a.draggable {
opacity: 1;
}
}
&.empty {
padding: 12px;
a {
@extend .button;
display: block;
text-align: center;
}
}
a.draggable {
float: right;
opacity: .3;
}
a {
color: #000;
}
}
}
}
}
}
}
}
}
}
// studio - css architecture
// ====================
// bourbon libs and resets
@import 'bourbon/bourbon';
@import 'bourbon/addons/button';
@import 'vendor/normalize';
@import 'keyframes';
@import 'reset';
// utilities
@import 'variables';
@import 'mixins';
@import 'cms_mixins';
// assets
@import 'assets/fonts';
@import 'assets/graphics';
@import 'assets/keyframes';
// base
@import 'base';
// elements
@import 'elements/header';
@import 'elements/footer';
@import 'elements/navigation';
@import 'elements/forms';
@import 'elements/modal';
@import 'elements/alerts';
@import 'elements/jquery-ui-calendar';
// specific views
@import 'views/account';
@import 'views/assets';
@import 'views/updates';
@import 'views/dashboard';
@import 'views/export';
@import 'views/index';
@import 'views/import';
@import 'views/outline';
@import 'views/settings';
@import 'views/static-pages';
@import 'views/subsection';
@import 'views/unit';
@import 'views/users';
@import "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';
@import 'content-types';
@import 'assets/content-types';
// xblock-related
@import 'module/module-styles.scss';
@import 'descriptor/module-styles.scss';
// studio - elements - alerts, notifications, prompts
// ====================
// notifications
.wrapper-notification {
@include clearfix();
......
//studio global footer
// studio - elements - global footer
// ====================
.wrapper-footer {
margin: ($baseline*1.5) 0 $baseline 0;
padding: $baseline;
......
// studio - elements - forms
// ====================
// forms - general
input[type="text"],
input[type="email"],
input[type="password"],
textarea.text {
padding: 6px 8px 8px;
@include box-sizing(border-box);
border: 1px solid $mediumGrey;
border-radius: 2px;
@include linear-gradient($lightGrey, tint($lightGrey, 90%));
background-color: $lightGrey;
@include box-shadow(0 1px 2px rgba(0, 0, 0, .1) inset);
font-family: 'Open Sans', sans-serif;
font-size: 11px;
color: $baseFontColor;
outline: 0;
&::-webkit-input-placeholder,
&:-moz-placeholder,
&:-ms-input-placeholder {
color: #979faf;
}
&:focus {
@include linear-gradient($paleYellow, tint($paleYellow, 90%));
outline: 0;
}
}
// forms - specific
input.search {
padding: 6px 15px 8px 30px;
@include box-sizing(border-box);
border: 1px solid $darkGrey;
border-radius: 20px;
background: url(../img/search-icon.png) no-repeat 8px 7px #edf1f5;
font-family: 'Open Sans', sans-serif;
color: $baseFontColor;
outline: 0;
&::-webkit-input-placeholder {
color: #979faf;
}
}
label {
font-size: 12px;
}
code {
padding: 0 4px;
border-radius: 3px;
background: #eee;
font-family: Monaco, monospace;
}
.CodeMirror {
font-size: 13px;
border: 1px solid $darkGrey;
background: #fff;
}
.text-editor {
width: 100%;
min-height: 80px;
padding: 10px;
@include box-sizing(border-box);
border: 1px solid $mediumGrey;
@include linear-gradient(top, rgba(255, 255, 255, 0), rgba(255, 255, 255, 0.3));
background-color: #edf1f5;
@include box-shadow(0 1px 2px rgba(0, 0, 0, 0.1) inset);
font-family: Monaco, monospace;
}
\ No newline at end of file
// studio global header and navigation
// studio - elements - global header
// ====================
.wrapper-header {
......
// studio - elements - JQUI calendar
// ====================
.ui-datepicker {
border-color: $darkGrey;
border-radius: 2px;
......
// studio - elements - modal windows
// ====================
.modal-cover {
display: none;
position: fixed;
......
// studio - elements - navigation
// ====================
// common
// ====================
// primary
// ====================
// right hand side
// ====================
// tabs
// ====================
// dropdown
// ====================
//
\ No newline at end of file
// Studio - Sign In/Up
// studio - views - sign up/in
// ====================
body.signup, body.signin {
.wrapper-content {
......
.uploads {
// studio - views - assets
// ====================
body.course.uploads {
input.asset-search-input {
float: left;
width: 260px;
......
.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;
}
// studio - views - user dashboard
// ====================
.class-link {
z-index: 100;
display: block;
padding: 20px 25px;
line-height: 1.3;
&:hover {
background: $paleYellow;
body.dashboard {
.my-classes {
margin-top: $baseline;
}
+ .view-live-button {
opacity: 1.0;
pointer-events: auto;
.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;
}
.class-name {
display: block;
font-size: 19px;
font-weight: 300;
}
.detail {
font-size: 14px;
font-weight: 400;
margin-right: 20px;
color: #3c3c3c;
}
.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;
// 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;
.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;
}
.column {
float: left;
width: 48%;
}
.row {
margin-bottom: 15px;
@include clearfix;
}
.column:first-child {
margin-right: 4%;
}
.column {
float: left;
width: 48%;
}
.course-info {
width: 600px;
}
.column:first-child {
margin-right: 4%;
}
label {
display: block;
font-size: 13px;
font-weight: 700;
}
.course-info {
width: 600px;
}
.new-course-org,
.new-course-number,
.new-course-name {
width: 100%;
}
label {
display: block;
font-size: 13px;
font-weight: 700;
}
.new-course-name {
font-size: 19px;
font-weight: 300;
}
.new-course-org,
.new-course-number,
.new-course-name {
width: 100%;
}
.new-course-save {
@include blue-button;
}
.new-course-name {
font-size: 19px;
font-weight: 300;
}
.new-course-save {
@include blue-button;
}
.new-course-cancel {
@include white-button;
.new-course-cancel {
@include white-button;
}
}
}
\ No newline at end of file
.export {
// studio - views - course export
// ====================
body.course.export {
.export-overview {
@extend .window;
@include clearfix;
......@@ -118,6 +122,4 @@
}
}
}
}
\ No newline at end of file
.import {
// studio - views - course import
// ====================
body.course.import {
.import-overview {
@extend .window;
@include clearfix;
......
// how it works/not signed in index
.index {
// studio - views - how it works
// ====================
body.index {
&.not-signedin {
......
// studio - views - course outline
// ====================
input.courseware-unit-search-input {
float: left;
width: 260px;
background-color: #fff;
}
body.course.outline {
.branch {
.section-item {
@include clearfix();
input.courseware-unit-search-input {
float: left;
width: 260px;
background-color: #fff;
}
.details {
display: block;
float: left;
margin-bottom: 0;
width: 650px;
}
.branch {
.gradable-status {
float: right;
position: relative;
top: -4px;
right: 50px;
width: 145px;
.section-item {
@include clearfix();
.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;
.details {
display: block;
float: left;
margin-bottom: 0;
width: 650px;
}
.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);
.gradable-status {
float: right;
position: relative;
top: -4px;
right: 50px;
width: 145px;
li {
width: 115px;
margin-bottom: 3px;
padding-bottom: 3px;
border-bottom: 1px solid $lightGrey;
.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;
}
&:last-child {
margin-bottom: 0;
padding-bottom: 0;
border: none;
.menu-toggle {
z-index: 10;
position: absolute;
top: 0;
right: 5px;
padding: 5px;
color: $mediumGrey;
&:hover, &.is-active {
color: $blue;
}
}
a {
color: $darkGrey;
.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;
a {
color: $blue;
&.is-selected {
font-weight: bold;
&.is-selected {
font-weight: bold;
}
}
}
}
// dropdown state
&.is-active {
// dropdown state
&.is-active {
.menu {
z-index: 1000;
display: block;
opacity: 1.0;
}
.menu {
z-index: 1000;
display: block;
opacity: 1.0;
}
.menu-toggle {
z-index: 10000;
.menu-toggle {
z-index: 10000;
}
}
}
// set state
&.is-set {
// set state
&.is-set {
.menu-toggle {
color: $blue;
}
.menu-toggle {
color: $blue;
}
.status-label {
display: block;
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;
}
.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));
label {
float: left;
line-height: 29px;
}
&:first-child {
margin-top: 0;
}
.datepair {
float: left;
margin-left: 10px;
}
&.collapsed {
padding-bottom: 0;
}
.section-published-date {
position: absolute;
top: 19px;
right: 90px;
padding: 4px 10px;
border-radius: 3px;
background: $lightGrey;
text-align: right;
label {
float: left;
line-height: 29px;
}
.published-status {
font-size: 12px;
margin-right: 15px;
.datepair {
float: left;
margin-left: 10px;
}
strong {
font-weight: bold;
}
}
.section-published-date {
position: absolute;
top: 19px;
right: 90px;
padding: 4px 10px;
border-radius: 3px;
background: $lightGrey;
text-align: right;
.schedule-button {
@include blue-button;
}
.published-status {
font-size: 12px;
margin-right: 15px;
.edit-button {
@include blue-button;
}
strong {
font-weight: bold;
}
}
.schedule-button,
.edit-button {
font-size: 11px;
padding: 3px 15px 5px;
}
}
.schedule-button {
@include blue-button;
}
.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;
}
.edit-button {
@include blue-button;
}
.datepair .date {
width: 80px;
}
.schedule-button,
.edit-button {
font-size: 11px;
padding: 3px 15px 5px;
}
}
.datepair .time {
width: 65px;
}
.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;
}
&.collapsed .subsection-list,
.collapsed .subsection-list,
.collapsed > ol {
display: none !important;
}
.datepair .date {
width: 80px;
}
header {
min-height: 75px;
@include clearfix();
.datepair .time {
width: 65px;
}
.item-details, .section-published-date {
&.collapsed .subsection-list,
.collapsed .subsection-list,
.collapsed > ol {
display: none !important;
}
}
header {
min-height: 75px;
@include clearfix();
.item-details {
display: inline-block;
padding: 20px 0 10px 0;
@include clearfix();
.item-details, .section-published-date {
.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);
.item-details {
display: inline-block;
padding: 20px 0 10px 0;
@include clearfix();
&:hover {
color: $orange;
.section-name {
float: left;
margin-right: 10px;
width: 350px;
font-size: 19px;
font-weight: bold;
color: $blue;
}
}
.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;
}
.section-name-span {
cursor: pointer;
@include transition(color .15s);
.cancel-button {
@include white-button;
padding: 7px 20px 7px;
}
}
&:hover {
color: $orange;
}
}
.section-published-date {
float: right;
width: 265px;
margin-right: 220px;
@include border-radius(3px);
background: $lightGrey;
.section-name-edit {
position: relative;
width: 400px;
background: $white;
.published-status {
font-size: 12px;
margin-right: 15px;
input {
font-size: 16px;
}
.save-button {
@include blue-button;
padding: 7px 20px 7px;
margin-right: 5px;
}
strong {
font-weight: bold;
.cancel-button {
@include white-button;
padding: 7px 20px 7px;
}
}
.schedule-button {
@include blue-button;
}
.section-published-date {
float: right;
width: 265px;
margin-right: 220px;
@include border-radius(3px);
background: $lightGrey;
.edit-button {
@include blue-button;
}
.published-status {
font-size: 12px;
margin-right: 15px;
.schedule-button,
.edit-button {
font-size: 11px;
padding: 3px 15px 5px;
strong {
font-weight: bold;
}
}
}
.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;
.schedule-button {
@include blue-button;
}
.menu-toggle {
z-index: 10;
position: absolute;
top: 2px;
right: 5px;
padding: 5px;
color: $lightGrey;
.edit-button {
@include blue-button;
}
&:hover, &.is-active {
color: $blue;
.schedule-button,
.edit-button {
font-size: 11px;
padding: 3px 15px 5px;
}
}
.menu {
z-index: 1;
display: none;
opacity: 0.0;
.gradable-status {
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);
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;
}
li {
width: 115px;
margin-bottom: 3px;
padding-bottom: 3px;
border-bottom: 1px solid $lightGrey;
.menu-toggle {
z-index: 10;
position: absolute;
top: 2px;
right: 5px;
padding: 5px;
color: $lightGrey;
&:last-child {
margin-bottom: 0;
padding-bottom: 0;
border: none;
&:hover, &.is-active {
color: $blue;
}
}
a {
color: $darkGrey;
.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 {
a {
&.is-selected {
font-weight: bold;
&.is-selected {
font-weight: bold;
}
}
}
// dropdown state
&.is-active {
.menu {
z-index: 1000;
display: block;
opacity: 1.0;
}
.menu-toggle {
z-index: 10000;
}
}
// dropdown state
&.is-active {
// set state
&.is-set {
.menu {
z-index: 1000;
.menu-toggle {
color: $blue;
}
.status-label {
display: block;
opacity: 1.0;
color: $blue;
}
}
float: left;
padding: 21px 0 0;
}
}
.menu-toggle {
z-index: 10000;
.item-actions {
margin-top: 21px;
margin-right: 12px;
.edit-button,
.delete-button {
margin-top: -3px;
}
}
}
// set state
&.is-set {
.expand-collapse-icon {
float: left;
margin: 29px 6px 16px 16px;
@include transition(none);
.menu-toggle {
color: $blue;
&.expand {
background-position: 0 0;
}
&.collapsed {
}
}
.status-label {
display: block;
color: $blue;
.drag-handle {
margin-left: 11px;
}
}
}
float: left;
padding: 21px 0 0;
h3 {
font-size: 19px;
font-weight: 700;
color: $blue;
}
}
.item-actions {
margin-top: 21px;
margin-right: 12px;
.section-name-span {
cursor: pointer;
@include transition(color .15s);
.edit-button,
.delete-button {
margin-top: -3px;
}
}
&:hover {
color: $orange;
}
}
.expand-collapse-icon {
float: left;
margin: 29px 6px 16px 16px;
@include transition(none);
.section-name-form {
margin-bottom: 15px;
}
&.expand {
background-position: 0 0;
}
.section-name-edit {
input {
font-size: 16px;
}
.save-button {
@include blue-button;
padding: 7px 20px 7px;
margin-right: 5px;
}
&.collapsed {
}
}
.cancel-button {
@include white-button;
padding: 7px 20px 7px;
}
}
.drag-handle {
margin-left: 11px;
}
}
h4 {
font-size: 12px;
color: #878e9d;
h3 {
font-size: 19px;
font-weight: 700;
color: $blue;
}
strong {
font-weight: bold;
}
}
.section-name-span {
cursor: pointer;
@include transition(color .15s);
.list-header {
@include linear-gradient(top, transparent, rgba(0, 0, 0, .1));
background-color: #ced2db;
border-radius: 3px 3px 0 0;
}
&:hover {
color: $orange;
}
}
.subsection-list {
margin: 0 12px;
.section-name-form {
margin-bottom: 15px;
}
> ol {
@include tree-view;
border-top-width: 0;
}
}
.section-name-edit {
input {
font-size: 16px;
}
.save-button {
@include blue-button;
padding: 7px 20px 7px;
margin-right: 5px;
}
&.new-section {
.cancel-button {
@include white-button;
padding: 7px 20px 7px;
}
}
header {
height: auto;
@include clearfix();
}
h4 {
font-size: 12px;
color: #878e9d;
.expand-collapse-icon {
visibility: hidden;
}
strong {
font-weight: bold;
}
}
.item-details {
padding: 25px 0 0 0;
.list-header {
@include linear-gradient(top, transparent, rgba(0, 0, 0, .1));
background-color: #ced2db;
border-radius: 3px 3px 0 0;
}
.section-name {
float: none;
width: 100%;
}
}
}
}
.subsection-list {
margin: 0 12px;
.toggle-button-sections {
display: none;
position: relative;
float: right;
margin-top: 10px;
> ol {
@include tree-view;
border-top-width: 0;
}
}
font-size: 13px;
color: $darkGrey;
&.new-section {
&.is-shown {
display: block;
}
header {
height: auto;
@include clearfix();
}
.ss-icon {
@include border-radius(20px);
position: relative;
top: -1px;
display: inline-block;
margin-right: 2px;
line-height: 5px;
font-size: 11px;
}
.expand-collapse-icon {
visibility: hidden;
}
.label {
display: inline-block;
}
}
.item-details {
padding: 25px 0 0 0;
.new-section-name,
.new-subsection-name-input {
width: 515px;
}
.section-name {
float: none;
width: 100%;
}
}
}
}
.new-section-name-save,
.new-subsection-name-save {
@include blue-button;
padding: 4px 20px 7px;
margin: 0 5px;
color: #fff !important;
}
.toggle-button-sections {
display: none;
position: relative;
float: right;
margin-top: 10px;
.new-section-name-cancel,
.new-subsection-name-cancel {
@include white-button;
padding: 4px 20px 7px;
color: #8891a1 !important;
}
font-size: 13px;
color: $darkGrey;
.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));
}
&.is-shown {
display: block;
}
.preview {
background: url(../img/preview.jpg) center top no-repeat;
}
.ss-icon {
@include border-radius(20px);
position: relative;
top: -1px;
display: inline-block;
margin-right: 2px;
line-height: 5px;
font-size: 11px;
}
.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;
}
.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;
}
h3 {
font-size: 34px;
font-weight: 300;
}
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;
}
.picker {
margin: 30px 0 65px;
}
h3 {
font-size: 34px;
font-weight: 300;
}
.description {
margin-top: 30px;
font-size: 14px;
line-height: 20px;
}
.picker {
margin: 30px 0 65px;
}
strong {
font-weight: 700;
}
.description {
margin-top: 30px;
font-size: 14px;
line-height: 20px;
}
.start-date,
.start-time {
font-size: 19px;
}
strong {
font-weight: 700;
}
.save-button {
@include blue-button;
margin-right: 10px;
}
.start-date,
.start-time {
font-size: 19px;
}
.cancel-button {
@include white-button;
}
.save-button {
@include blue-button;
margin-right: 10px;
}
.save-button,
.cancel-button {
font-size: 16px;
}
}
.cancel-button {
@include white-button;
}
.collapse-all-button {
float: right;
margin-top: 10px;
font-size: 13px;
color: $darkGrey;
}
.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;
}
}
// sort/drag and drop
.ui-droppable {
@include transition (padding 0.5s ease-in-out 0s);
min-height: 20px;
padding: 0;
// hiding unit button - temporary fix until this semantically corrected
.new-unit-item {
display: none;
}
}
&.dropover {
padding: 15px 0;
}
}
ol.ui-droppable .branch:first-child .section-item {
border-top: none;
}
.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;
}
}
\ No newline at end of file
// Studio - Course Settings
// studio - views - course settings
// ====================
body.course.settings {
.content-primary, .content-supplementary {
......
.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;
}
......
.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;
}
// studio - views - course subsection
// ====================
.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;
body.course.subsection {
.unit-settings {
.window-contents {
padding: 10px 20px;
}
.menu-toggle {
z-index: 100;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 20px;
background: transparent;
.unit-actions {
border-bottom: none;
padding-bottom: 0;
}
&:hover, &.is-active {
color: $blue;
.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;
}
}
.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;
input[type="radio"] {
margin-right: 7px;
}
.status {
font-size: 12px;
@include border-radius(4px);
@include box-shadow(0 1px 2px rgba(0, 0, 0, .2));
@include transition(opacity .15s);
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 {
margin-bottom: 3px;
padding-bottom: 3px;
border-bottom: 1px solid $lightGrey;
&:last-child {
margin-bottom: 0;
padding-bottom: 0;
border: none;
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;
}
a {
.new-unit-item {
margin-left: 20px;
}
}
ol ol {
.section-item {
padding-left: 34px;
}
&.is-selected {
font-weight: bold;
.new-unit-item {
margin: 0 0 10px 41px;
}
}
}
}
// dropdown state
&.is-active {
.subsection-body {
padding: 32px 40px;
@include clearfix;
.menu {
z-index: 10000;
display: block;
opacity: 1.0;
> div {
margin-bottom: 40px;
}
input {
font-size: 14px;
}
.unit-subtitle {
display: block;
width: 100%;
}
.sortable-unit-list {
ol {
@include tree-view;
}
}
.menu-toggle {
z-index: 1000;
.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;
}
}
// set state
&.is-set {
.policy-list-element {
.save-button,
.cancel-button {
display: none;
}
.menu-toggle {
color: $blue;
.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 {
display: block;
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;
}
}
}
}
......
.unit .main-wrapper {
@include clearfix();
margin: 40px;
}
// studio - views - unit
// ====================
//Problem Selector tab menu requirements
.js .tabs .tab {
display: none;
}
//end problem selector reqs
body.course.unit {
.main-column {
clear: both;
float: left;
width: 70%;
}
.unit-body.published {
.components > li {
border: none;
.unit .main-wrapper {
@include clearfix();
margin: 40px;
}
.rendered-component {
padding: 0 20px;
}
//Problem Selector tab menu requirements
.js .tabs .tab {
display: none;
}
}
//end problem selector reqs
.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;
.main-column {
clear: both;
float: left;
width: 70%;
}
li {
float: left;
}
.unit-body.published {
.components > li {
border: none;
a,
.current-page {
display: block;
padding: 15px 35px 15px 30px;
font-size: 14px;
background: url(../img/breadcrumb-arrow.png) no-repeat right center;
.rendered-component {
padding: 0 20px;
}
}
}
h2 {
margin: 30px 40px 30px 0;
color: #646464;
font-size: 19px;
font-weight: 300;
letter-spacing: 1px;
text-transform: uppercase;
}
.unit-body {
.unit-name-input {
padding: 20px 40px;
.components {
label {
display: block;
}
> li {
position: relative;
z-index: 10;
margin: 20px 40px;
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;
}
}
.title {
margin: 0 0 15px 0;
color: $mediumGrey;
h2 {
margin: 30px 40px 30px 0;
color: #646464;
font-size: 19px;
font-weight: 300;
letter-spacing: 1px;
text-transform: uppercase;
}
.value {
}
}
.components {
&.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;
}
> li {
position: relative;
z-index: 10;
margin: 20px 40px;
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 {
.title {
margin: 0 0 15px 0;
color: $mediumGrey;
a,
li {
display: inline-block;
.value {
}
}
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);
&.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;
.name {
position: absolute;
bottom: 5px;
left: 0;
width: 100%;
padding: 10px;
@include box-sizing(border-box);
color: #fff;
}
.new-component-button {
display: block;
padding: 20px;
text-align: center;
color: #edf1f5;
}
}
.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;
h5 {
margin: 20px 0px;
color: #fff;
font-weight: 600;
font-size: 18px;
}
.problem-type-tabs {
.rendered-component {
display: none;
background: #fff;
border-radius: 3px 3px 0 0;
}
// specific menu types
&.new-component-problem {
padding-bottom:10px;
.new-component-type {
.ss-icon, .editor-indicator {
a,
li {
display: inline-block;
}
.problem-type-tabs {
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-type,
.new-component-template {
@include clearfix;
.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;
a {
position: relative;
border: 1px solid $darkGreen;
background: tint($green,20%);
color: #fff;
.cancel-button {
margin: 20px 0px 10px 10px;
@include white-button;
}
&:hover {
background: $brightGreen;
.problem-type-tabs {
display: none;
}
}
}
.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);
// specific menu types
&.new-component-problem {
padding-bottom:10px;
.ss-icon, .editor-indicator {
display: inline-block;
}
li:first-child {
margin-left: 20px;
.problem-type-tabs {
display: inline-block;
}
}
}
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;
.new-component-type,
.new-component-template {
@include clearfix;
&:hover {
opacity:1;
background-color: tint($lightBluishGrey, 20%);
}
a {
position: relative;
border: 1px solid $darkGreen;
background: tint($green,20%);
color: #fff;
&.ui-state-active {
border: 0px;
@include active;
opacity:1;
&:hover {
background: $brightGreen;
}
}
}
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 {
.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);
a {
background: #fff;
border: 0px;
color: #3c3c3c;
@include transition (none);
&:hover {
background: tint($green,30%);
color: #fff;
@include transition(background-color .15s);
li:first-child {
margin-left: 20px;
}
}
li {
border:none;
border-bottom: 1px dashed $lightGrey;
color: #fff;
}
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;
li:first-child {
a {
border-top: 0px;
&:hover {
opacity:1;
background-color: tint($lightBluishGrey, 20%);
}
&.ui-state-active {
border: 0px;
@include active;
opacity:1;
}
}
}
li:nth-child(2) {
a {
border-radius: 0px;
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);
}
}
a {
@include clearfix();
display: block;
padding: 7px 20px;
border-bottom: none;
font-weight: 500;
.new-component-template {
.name {
float: left;
a {
background: #fff;
border: 0px;
color: #3c3c3c;
@include transition (none);
.ss-icon {
@include transition(opacity .15s);
display: inline-block;
top: 1px;
margin-right: 5px;
opacity: 0.5;
width: 17;
height: 21px;
vertical-align: middle;
&:hover {
background: tint($green,30%);
color: #fff;
@include transition(background-color .15s);
}
}
.editor-indicator {
@include transition(opacity .15s);
float: right;
position: relative;
top: 3px;
font-size: 12px;
opacity: 0.3;
li {
border:none;
border-bottom: 1px dashed $lightGrey;
color: #fff;
}
.ss-icon, .editor-indicator {
display: none;
li:first-child {
a {
border-top: 0px;
}
}
&:hover {
color: #fff;
li:nth-child(2) {
a {
border-radius: 0px;
}
}
.ss-icon {
opacity: 1.0;
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 {
opacity: 1.0;
@include transition(opacity .15s);
float: right;
position: relative;
top: 3px;
font-size: 12px;
opacity: 0.3;
}
}
}
// specific editor types
.empty {
.ss-icon, .editor-indicator {
display: none;
}
a {
line-height: 1.4;
font-weight: 400;
background: #fff;
color: #3c3c3c;
&:hover {
color: #fff;
.ss-icon {
opacity: 1.0;
}
&:hover {
background: tint($green,30%);
color: #fff;
.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;
.new-component {
text-align: center;
h5 {
color: $darkGreen;
h5 {
color: $darkGreen;
}
}
}
}
}
}
.component {
border: 1px solid $lightBluishGrey2;
border-radius: 3px;
background: #fff;
@include transition(none);
.component {
border: 1px solid $lightBluishGrey2;
border-radius: 3px;
background: #fff;
@include transition(none);
&:hover {
border-color: #6696d7;
&:hover {
border-color: #6696d7;
.drag-handle {
background-color: $blue;
border-color: $blue;
}
}
.drag-handle {
background-color: $blue;
border-color: $blue;
&.editing {
border: 1px solid $lightBluishGrey2;
z-index: auto;
.drag-handle,
.component-actions {
display: none;
}
}
}
&.editing {
border: 1px solid $lightBluishGrey2;
z-index: auto;
.drag-handle,
.component-actions {
display: none;
&.component-placeholder {
border-color: #6696d7;
}
}
&.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);
}
}
.component-actions {
position: absolute;
top: 7px;
right: 9px;
}
.xmodule_display {
padding: 40px 20px 20px;
overflow-x: auto;
.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);
h1 {
float: none;
margin-left: 0;
}
}
}
.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;
}
}
.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;
.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;
.metadata_edit {
margin-bottom: 20px;
font-size: 13px;
li {
margin-bottom: 10px;
}
li {
label {
display: inline-block;
margin-right: 10px;
}
}
h3 {
margin-bottom: 10px;
font-size: 18px;
font-weight: 700;
}
label {
display: inline-block;
margin-right: 10px;
h5 {
margin-bottom: 8px;
color: #fff;
font-weight: 700;
}
}
h3 {
margin-bottom: 10px;
font-size: 18px;
font-weight: 700;
.save-button {
margin-top: 10px;
margin: 15px 8px 0 0;
}
}
}
h5 {
margin-bottom: 8px;
color: #fff;
font-weight: 700;
.unit-settings {
.window-contents {
padding: 10px 20px;
}
.save-button {
margin-top: 10px;
margin: 15px 8px 0 0;
.unit-actions {
border-bottom: none;
padding-bottom: 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;
.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;
div {
margin-top: 15px;
}
}
}
input[type="radio"] {
margin-right: 7px;
}
input[type="radio"] {
margin-right: 7px;
}
.status {
font-size: 12px;
.status {
font-size: 12px;
strong {
font-weight: 700;
strong {
font-weight: 700;
}
}
}
.preview-button, .view-button {
@include white-button;
margin-bottom: 10px;
}
.preview-button, .view-button {
@include white-button;
margin-bottom: 10px;
}
.publish-button {
@include orange-button;
}
.publish-button {
@include orange-button;
}
.delete-button {
@include blue-button;
}
.delete-button {
@include blue-button;
}
.delete-draft {
display: inline-block;
}
.delete-draft {
display: inline-block;
}
.delete-button,
.preview-button,
.publish-button,
.view-button {
font-size: 11px;
margin-top: 10px;
padding: 6px 15px 8px;
.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;
}
.unit-history {
&.collapsed {
h4 {
border-bottom: none;
border-radius: 3px;
}
.window-contents {
display: none;
.window-contents {
display: none;
}
}
}
ol {
border: 1px solid #ced2db;
ol {
border: 1px solid #ced2db;
li {
display: block;
padding: 6px 8px 8px 10px;
background: #edf1f5;
font-size: 12px;
li {
display: block;
padding: 6px 8px 8px 10px;
background: #edf1f5;
font-size: 12px;
&:hover {
background: #fffcf1;
&:hover {
background: #fffcf1;
.item-actions {
display: block;
.item-actions {
display: block;
}
}
}
&.checked {
background: #d1dae3;
}
&.checked {
background: #d1dae3;
}
.item-actions {
display: none;
}
.item-actions {
display: none;
}
input[type="radio"] {
margin-right: 7px;
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;
.unit-location {
.url {
width: 100%;
font-size: 11px;
padding: 2px 8px 4px;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
@include box-sizing(border-box);
margin-bottom: 10px;
@include box-shadow(none);
}
ol {
.draft-tag,
.hidden-tag,
.private-tag,
.has-new-draft-tag {
font-size: 8px;
}
.window-contents > ol {
@include tree-view;
.section-item {
padding-left: 20px;
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);
}
.new-unit-item {
margin-left: 20px;
}
}
ol {
.section-item {
padding-left: 20px;
}
ol ol {
.section-item {
padding-left: 34px;
.new-unit-item {
margin-left: 20px;
}
}
.new-unit-item {
margin: 0 0 10px 41px;
ol ol {
.section-item {
padding-left: 34px;
}
.new-unit-item {
margin: 0 0 10px 41px;
}
}
}
}
}
.edit-state-draft {
.visibility,
.edit-state-draft {
.visibility,
.edit-draft-message,
.view-button {
display: none;
}
.edit-draft-message,
.view-button {
display: none;
}
.published-alert {
display: block;
.published-alert {
display: block;
}
}
}
.edit-state-public {
.delete-draft,
.component-actions,
.new-component-item,
.editing-draft-alert,
.publish-draft-message,
.preview-button {
display: none;
}
.edit-state-public {
.delete-draft,
.component-actions,
.new-component-item,
.editing-draft-alert,
.publish-draft-message,
.preview-button {
display: none;
}
.published-alert {
display: block;
}
.published-alert {
display: block;
}
.drag-handle {
display: none !important;
.drag-handle {
display: none !important;
}
}
}
.edit-state-private {
.delete-draft,
.publish-draft,
.editing-draft-alert,
.create-draft,
.view-button {
display: none;
.edit-state-private {
.delete-draft,
.publish-draft,
.editing-draft-alert,
.create-draft,
.view-button {
display: none;
}
}
}
......@@ -664,4 +678,4 @@ body.unit {
padding-top: 0;
}
}
}
}
\ No newline at end of file
.course-info {
// studio - views - course updates
// ====================
body.course.updates {
h2 {
margin-bottom: 24px;
font-size: 22px;
......
.users {
// studio - views - course users
// ====================
body.course.users {
.new-user-form {
display: none;
padding: 15px 20px;
......
<%inherit file="base.html" />
<%block name="title">Page Not Found</%block>
<%block name="content">
<div class="wrapper-content wrapper">
<section class="content">
<h1>Page not found</h1>
<p>The page that you were looking for was not found. Go back to the <a href="/">homepage</a> or let us know about any pages that may have been moved at <a href="mailto:technical@edx.org">technical@edx.org</a>.</p>
</section>
</div>
</%block>
\ No newline at end of file
<%inherit file="base.html" />
<%block name="title">Server Error</%block>
<%block name="content">
<div class="wrapper-content wrapper">
<section class="content">
<h1>Currently the <em>edX</em> servers are down</h1>
<p>Our staff is currently working to get the site back up as soon as possible. Please email us at <a href="mailto:technical@edx.org">technical@edx.org</a> to report any problems or downtime.</p>
</section>
</div>
</%block>
\ No newline at end of file
......@@ -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"),
......
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'):
......
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
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
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):
'''
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
@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
"""
pass
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)
@world.absorb
class RegistrationFactory(sf.RegistrationFactory):
"""
Activation key for registering the user account
"""
pass
# 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))
@world.absorb
class GroupFactory(sf.GroupFactory):
"""
Groups for user permissions for courses
"""
pass
if new_item.location.category not in DETACHED_CATEGORIES:
store.update_children(parent_location, parent.children + [new_item.location.url()])
return new_item
@world.absorb
class CourseEnrollmentAllowedFactory(sf.CourseEnrollmentAllowed):
"""
Users allowed to enroll in the course outside of the usual window
"""
pass
class Item:
@world.absorb
class CourseFactory(xf.CourseFactory):
"""
Courseware courses
"""
pass
class ItemFactory(XModuleItemFactory):
FACTORY_FOR = Item
parent_location = 'i4x://MITx/999/course/Robot_Super_Course'
template = 'i4x://edx/templates/chapter/Empty'
display_name = 'Section One'
@world.absorb
class ItemFactory(xf.ItemFactory):
"""
Everything included inside a course
"""
pass
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()
# wait for the page to redraw
assert world.browser.is_element_present_by_css('.content-wrapper', 10)
def log_in(username, password):
'''
Log the user in programatically
'''
# 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()
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"
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)
version_tuple = VERSION_TUPLES[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 verbose:
log.debug("Scanning {0} for course module...".format(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)))
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]])
# 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')
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)
course_items.append(module)
course_data_path = None
course_location = None
if verbose:
log.debug("Scanning {0} for course module...".format(course_id))
# 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
# 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
# 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)
module = remap_namespace(module, target_location_namespace)
# finally loop through all the modules
for module in module_store.modules[course_id].itervalues():
# 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 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 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)))
if verbose:
log.debug('importing module location {0}'.format(module.location))
# 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, 'data'):
module_data = module.data
course_items.append(module)
# 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))
# 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
for key in remap_dict.keys():
module_data = module_data.replace(key, remap_dict[key])
# 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)
except Exception, e:
logging.exception("failed to rewrite links on {0}. Continuing...".format(module.location))
# finally loop through all the modules
for module in module_store.modules[course_id].itervalues():
store.update_item(module.location, module_data)
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
if hasattr(module, 'children') and module.children != []:
store.update_children(module.location, module.children)
# remap module to the new namespace
if target_location_namespace is not None:
module = remap_namespace(module, target_location_namespace)
# NOTE: It's important to use own_metadata here to avoid writing
# inherited metadata everywhere.
store.update_metadata(module.location, dict(own_metadata(module)))
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")
......
// 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);
}
\ No newline at end of file
......@@ -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.
\ No newline at end of file
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.
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
......
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"
......
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 |
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
......
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
\ No newline at end of file
# 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
......
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,
......
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
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)
"""
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(
......
......@@ -8,16 +8,32 @@ from .test import *
# otherwise the browser will not render the pages correctly
DEBUG = True
# Show the courses that are in the data directory
COURSES_ROOT = ENV_ROOT / "data"
DATA_DIR = COURSES_ROOT
# Use the mongo store for acceptance tests
modulestore_options = {
'default_class': 'xmodule.raw_module.RawDescriptor',
'host': 'localhost',
'db': 'test_xmodule',
'collection': 'modulestore',
'fs_root': GITHUB_REPO_ROOT,
'render_template': 'mitxmako.shortcuts.render_to_string',
}
MODULESTORE = {
'default': {
'ENGINE': 'xmodule.modulestore.xml.XMLModuleStore',
'OPTIONS': {
'data_dir': DATA_DIR,
'default_class': 'xmodule.hidden_module.HiddenDescriptor',
}
'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore',
'OPTIONS': modulestore_options
},
'direct': {
'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore',
'OPTIONS': modulestore_options
}
}
CONTENTSTORE = {
'ENGINE': 'xmodule.contentstore.mongo.MongoContentStore',
'OPTIONS': {
'host': 'localhost',
'db': 'test_xcontent',
}
}
......@@ -32,6 +48,18 @@ DATABASES = {
}
}
# Set up XQueue information so that the lms will send
# requests to a mock XQueue server running locally
XQUEUE_PORT = 8027
XQUEUE_INTERFACE = {
"url": "http://127.0.0.1:%d" % XQUEUE_PORT,
"django_auth": {
"username": "lms",
"password": "***REMOVED***"
},
"basic_auth": ('anant', 'agarwal'),
}
# Do not display the YouTube videos in the browser while running the
# acceptance tests. This makes them faster and more reliable
MITX_FEATURES['STUB_VIDEO_FOR_TESTING'] = True
......
......@@ -5,8 +5,5 @@
"/static/js/vendor/jquery-ui.min.js",
"/static/js/vendor/jquery.leanModal.min.js",
"/static/js/vendor/flot/jquery.flot.js"
],
"static_files": [
"js/application.js"
]
}
......@@ -4,9 +4,6 @@ describe 'Calculator', ->
@calculator = new Calculator
describe 'bind', ->
beforeEach ->
Calculator.bind()
it 'bind the calculator button', ->
expect($('.calc')).toHandleWith 'click', @calculator.toggle
......@@ -31,12 +28,19 @@ describe 'Calculator', ->
$('form#calculator').submit()
describe 'toggle', ->
it 'toggle the calculator and focus the input', ->
spyOn $.fn, 'focus'
@calculator.toggle(jQuery.Event("click"))
it 'focuses the input when toggled', ->
# Since the focus is called asynchronously, we need to
# wait until focus() is called.
didFocus = false
runs ->
spyOn($.fn, 'focus').andCallFake (elementName) -> didFocus = true
@calculator.toggle(jQuery.Event("click"))
waitsFor (-> didFocus), "focus() should have been called on the input", 1000
expect($('li.calc-main')).toHaveClass('open')
expect($('#calculator_wrapper #calculator_input').focus).toHaveBeenCalled()
runs ->
expect($('#calculator_wrapper #calculator_input').focus).toHaveBeenCalled()
it 'toggle the close button on the calculator button', ->
@calculator.toggle(jQuery.Event("click"))
......
......@@ -22,18 +22,23 @@ describe 'Tab', ->
it 'bind the tabs', ->
expect($.fn.tabs).toHaveBeenCalledWith show: @tab.onShow
# As of jQuery 1.9, the onShow callback is deprecated
# http://jqueryui.com/upgrade-guide/1.9/#deprecated-show-event-renamed-to-activate
# The code below tests that onShow does what is expected,
# but note that onShow will NOT be called when the user
# clicks on the tab if we're using jQuery version >= 1.9
describe 'onShow', ->
beforeEach ->
@tab = new Tab 1, @items
$('[href="#tab-1-0"]').click()
@tab.onShow($('#tab-1-0'), {'index': 1})
it 'replace content in the container', ->
$('[href="#tab-1-1"]').click()
@tab.onShow($('#tab-1-1'), {'index': 1})
expect($('#tab-1-0').html()).toEqual ''
expect($('#tab-1-1').html()).toEqual 'Video 2'
expect($('#tab-1-2').html()).toEqual ''
it 'trigger contentChanged event on the element', ->
spyOnEvent @tab.el, 'contentChanged'
$('[href="#tab-1-1"]').click()
@tab.onShow($('#tab-1-1'), {'index': 1})
expect('contentChanged').toHaveBeenTriggeredOn @tab.el
......@@ -32,11 +32,9 @@ describe 'Navigation', ->
heightStyle: 'content'
it 'binds the accordionchange event', ->
Navigation.bind()
expect($('#accordion')).toHandleWith 'accordionchange', @navigation.log
it 'bind the navigation toggle', ->
Navigation.bind()
expect($('#open_close_accordion a')).toHandleWith 'click', @navigation.toggle
describe 'when the #accordion does not exists', ->
......@@ -45,7 +43,6 @@ describe 'Navigation', ->
it 'does not activate the accordion', ->
spyOn $.fn, 'accordion'
Navigation.bind()
expect($('#accordion').accordion).wasNotCalled()
describe 'toggle', ->
......
......@@ -45,10 +45,10 @@
</header>
<div class="post-body">${'<%- body %>'}</div>
% if course and has_permission(user, 'openclose_thread', course.id):
<div class="admin-pin discussion-pin notpinned" data-role="thread-pin" data-tooltip="pin this thread">
<i class="icon"></i><span class="pin-label">Pin Thread</span></div>
%else:
${"<% if (pinned) { %>"}
<div class="discussion-pin notpinned" data-role="thread-pin" data-tooltip="pin this thread">
......@@ -57,9 +57,6 @@
% endif
${'<% if (obj.courseware_url) { %>'}
<div class="post-context">
(this post is about <a href="${'<%- courseware_url%>'}">${'<%- courseware_title %>'}</a>)
......
......@@ -73,41 +73,6 @@
</article>
-->
<article id="associate-legal-counsel" class="job">
<div class="inner-wrapper">
<h3><strong>ASSOCIATE LEGAL COUNSEL</strong></h3>
<p>We are seeking a talented lawyer with the ability to operate independently in a fast-paced environment and work proactively with all members of the edX team. You must have thorough knowledge of intellectual property law, contracts and licensing. </p>
<p><strong>Key Responsibilities: </strong></p>
<ul>
<li>Drive the negotiating, reviewing, drafting and overseeing of a wide range of transactional arrangements, including collaborations related to the provision of online education, inbound and outbound licensing of intellectual property, strategic partnerships, nondisclosure agreements, and services agreements.</li>
<li>Provide counseling on the legal implications/considerations of business and technical strategies and projects, with special emphasis on regulations related to higher education, data security and privacy.</li>
<li>Provide advice and support company-wide on a variety of legal issues in a timely and effective manner.</li>
<li>Assist on other matters as needed.</li>
</ul>
<p><strong>Requirements:</strong></p>
<ul>
<li>JD from an accredited law school</li>
<li>Massachusetts bar admission required</li>
<li>2-3 years of transactional experience at a major law firm and/or as an in-house counselor</li>
<li>Substantial IP licensing experience</li>
<li>Knowledge of copyright, trademark and patent law</li>
<li>Experience with open source content and open source software preferred</li>
<li>Outstanding communications skills (written and oral)</li>
<li>Experience with drafting and legal review of Internet privacy policies and terms of use.</li>
<li>Understanding of how to balance legal risks with business objectives</li>
<li>Ability to develop an innovative approach to legal issues in support of strategic business initiatives</li>
<li>An internal business and customer focused proactive attitude with ability to prioritize effectively</li>
<li>Experience with higher education preferred but not required</li>
</ul>
<p>If you are interested in this position, please send an email to <a href="mailto:jobs@edx.org">jobs@edx.org</a>.</p>
</div>
</article>
<article id="director-of-education-services" class="job">
<div class="inner-wrapper">
<h3><strong>DIRECTOR OF EDUCATIONAL SERVICES</strong></h3>
......@@ -369,40 +334,6 @@
</article>
<article id="director-engineering-open-source" class="job">
<div class="inner-wrapper">
<h3><strong>DIRECTOR ENGINEERING, OPEN SOURCE COMMUNITY MANAGER</strong></h3>
<p>In edX courses, students make (and break) electronic circuits, they manipulate molecules on the fly and they do it all at once, in their tens of thousands. We have great Professors and great Universities. But we can’t possibly keep up with all the great ideas out there, so we’re making our platform open source, to turn up the volume on great education. To do that well, we’ll need a Director of Engineering who can lead our Open Source Community efforts.</p>
<p><strong>Responsibilities:</strong></p>
<ul>
<li>Define and implement software design standards that make the open source community most welcome and productive.</li>
<li>Work with others to establish the governance standards for the edX Open Source Platform, establish the infrastructure, and manage the team to deliver releases and leverage our University partners and stakeholders to</li> make the edX platform the world’s best learning platform.
<li>Help the organization recognize the benefits and limitations inherent in open source solutions.</li>
<li>Establish best practices and key tool usage, especially those based on industry standards.</li>
<li>Provide visibility for the leadership team into the concerns and challenges faced by the open source community.</li>
<li>Foster a thriving community by providing the communication, documentation and feedback that they need to be enthusiastic.</li>
<li>Maximize the good code design coming from the open source community.</li>
<li>Provide the wit and firmness that the community needs to channel their energy productively.</li>
<li>Tactfully balance the internal needs of the organization to pursue new opportunities with the community’s need to participate in the platform’s evolution.</li>
<li>Shorten lines of communication and build trust across entire team</li>
</ul>
<p><strong>Qualifications:</strong></p>
<ul>
<li>Bachelors, preferably Masters in Computer Science</li>
<li>Solid communication skills, especially written</li>
<li>Committed to Agile practice, Scrum and Kanban</li>
<li>Charm and humor</li>
<li>Deep familiarity with Open Source, participant and contributor</li>
<li>Python, Django, Javascript</li>
<li>Commitment to support your technical recommendations, both within and beyond the organization.</li>
</ul>
<p>If you are interested in this position, please send an email to <a href="mailto:jobs@edx.org">jobs@edx.org</a>.</p>
</div>
</article>
<article id="software-engineer" class="job">
<div class="inner-wrapper">
<h3><strong>SOFTWARE ENGINEER</strong></h3>
......@@ -441,7 +372,6 @@
<section class="jobs-sidebar">
<h2>Positions</h2>
<nav>
<a href="#associate-legal-counsel">Associate Legal Counsel</a>
<a href="#director-of-education-services">Director of Education Services</a>
<a href="#manager-of-training-services">Manager of Training Services</a>
<a href="#instructional-designer">Instructional Designer</a>
......@@ -449,7 +379,6 @@
<a href="#project-manager-pmo">Project Manager (PMO)</a>
<a href="#director-of-product-management">Director of Product Management</a>
<a href="#content-engineer">Content Engineer</a>
<a href="#director-engineering-open-source">Director Engineering, Open Source Community Manager</a>
<a href="#software-engineer">Software Engineer</a>
</nav>
<h2>How to Apply</h2>
......
Django==1.3.1
flup==1.0.3.dev-20110405
lxml==2.3.4
Mako==0.7.0
Markdown==2.1.1
markdown2==1.4.2
python-memcached==1.48
numpy==1.6.1
Pygments==1.5
boto==2.3.0
django-storages==1.1.4
django-masquerade==0.1.5
fs==0.4.0
django-jasmine==0.3.2
path.py==2.2.2
requests==0.12.1
BeautifulSoup==3.2.1
BeautifulSoup4==4.1.1
newrelic==1.3.0.289
ipython==0.12.1
django-pipeline==1.2.12
django-staticfiles==1.2.1
glob2==0.3
sympy==0.7.1
pymongo==2.2.1
rednose==0.3.3
mock==0.8.0
GitPython==0.3.2.RC1
PyYAML==3.10
feedparser==5.1.2
MySQL-python==1.2.3
matplotlib==1.1.0
scipy==0.10.1
akismet==0.2.0
Coffin==0.3.6
django-celery==2.2.7
django-countries==1.0.5
django-followit==0.0.3
django-keyedcache==1.4-6
django-kombu==0.9.2
django-mako==0.1.5pre
django-recaptcha-works==0.3.4
django-robots==0.8.1
django-ses==0.4.1
django-threaded-multihost==1.4-1
html5lib==0.90
Jinja2==2.6
oauth2==1.5.211
pystache==0.3.1
python-openid==2.2.5
South==0.7.5
Unidecode==0.04.9
dogstatsd-python==0.2.1
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment