Merge branch 'master' of github.com:MITx/mitx into merge
Conflicts: lms/urls.py
This commit is contained in:
@@ -90,10 +90,12 @@ def add_histogram(get_html, module):
|
||||
|
||||
# TODO (ichuang): Remove after fall 2012 LMS migration done
|
||||
if settings.MITX_FEATURES.get('ENABLE_LMS_MIGRATION'):
|
||||
[filepath, filename] = module.definition.get('filename','')
|
||||
[filepath, filename] = module.definition.get('filename', ['', None])
|
||||
osfs = module.system.filestore
|
||||
if filename is not None and osfs.exists(filename):
|
||||
filepath = filename # if original, unmangled filename exists then use it (github doesn't like symlinks)
|
||||
# if original, unmangled filename exists then use it (github
|
||||
# doesn't like symlinks)
|
||||
filepath = filename
|
||||
data_dir = osfs.root_path.rsplit('/')[-1]
|
||||
edit_link = "https://github.com/MITx/%s/tree/master/%s" % (data_dir,filepath)
|
||||
else:
|
||||
|
||||
@@ -204,7 +204,7 @@ def extract_choices(element):
|
||||
raise Exception("[courseware.capa.inputtypes.extract_choices] \
|
||||
Expected a <choice> tag; got %s instead"
|
||||
% choice.tag)
|
||||
choice_text = ''.join([etree.tostring(x) for x in choice])
|
||||
choice_text = ''.join([x.text for x in choice])
|
||||
|
||||
choices.append((choice.get("name"), choice_text))
|
||||
|
||||
|
||||
@@ -800,6 +800,12 @@ class CodeResponse(LoncapaResponse):
|
||||
'''
|
||||
Grade student code using an external queueing server, called 'xqueue'
|
||||
|
||||
Expects 'xqueue' dict in ModuleSystem with the following keys:
|
||||
system.xqueue = { 'interface': XqueueInterface object,
|
||||
'callback_url': Per-StudentModule callback URL where results are posted (string),
|
||||
'default_queuename': Default queuename to submit request (string)
|
||||
}
|
||||
|
||||
External requests are only submitted for student submission grading
|
||||
(i.e. and not for getting reference answers)
|
||||
'''
|
||||
@@ -873,15 +879,16 @@ class CodeResponse(LoncapaResponse):
|
||||
'edX_cmd': 'get_score',
|
||||
'edX_tests': self.tests,
|
||||
'processor': self.code,
|
||||
'edX_student_response': unicode(submission), # unicode on File object returns its filename
|
||||
}
|
||||
|
||||
# Submit request
|
||||
if hasattr(submission, 'read'): # Test for whether submission is a file
|
||||
if is_file(submission):
|
||||
contents.update({'edX_student_response': submission.name})
|
||||
(error, msg) = qinterface.send_to_queue(header=xheader,
|
||||
body=json.dumps(contents),
|
||||
file_to_upload=submission)
|
||||
else:
|
||||
contents.update({'edX_student_response': submission})
|
||||
(error, msg) = qinterface.send_to_queue(header=xheader,
|
||||
body=json.dumps(contents))
|
||||
|
||||
|
||||
@@ -39,5 +39,18 @@ def convert_files_to_filenames(answers):
|
||||
'''
|
||||
new_answers = dict()
|
||||
for answer_id in answers.keys():
|
||||
new_answers[answer_id] = unicode(answers[answer_id])
|
||||
if is_file(answers[answer_id]):
|
||||
new_answers[answer_id] = answers[answer_id].name
|
||||
else:
|
||||
new_answers[answer_id] = answers[answer_id]
|
||||
return new_answers
|
||||
|
||||
def is_file(file_to_test):
|
||||
'''
|
||||
Duck typing to check if 'file_to_test' is a File object
|
||||
'''
|
||||
is_file = True
|
||||
for method in ['read', 'name']:
|
||||
if not hasattr(file_to_test, method):
|
||||
is_file = False
|
||||
return is_file
|
||||
|
||||
@@ -103,7 +103,9 @@ class ABTestDescriptor(RawDescriptor, XmlDescriptor):
|
||||
experiment = xml_object.get('experiment')
|
||||
|
||||
if experiment is None:
|
||||
raise InvalidDefinitionError("ABTests must specify an experiment. Not found in:\n{xml}".format(xml=etree.tostring(xml_object, pretty_print=True)))
|
||||
raise InvalidDefinitionError(
|
||||
"ABTests must specify an experiment. Not found in:\n{xml}"
|
||||
.format(xml=etree.tostring(xml_object, pretty_print=True)))
|
||||
|
||||
definition = {
|
||||
'data': {
|
||||
@@ -127,7 +129,9 @@ class ABTestDescriptor(RawDescriptor, XmlDescriptor):
|
||||
definition['data']['group_content'][name] = child_content_urls
|
||||
definition['children'].extend(child_content_urls)
|
||||
|
||||
default_portion = 1 - sum(portion for (name, portion) in definition['data']['group_portions'].items())
|
||||
default_portion = 1 - sum(
|
||||
portion for (name, portion) in definition['data']['group_portions'].items())
|
||||
|
||||
if default_portion < 0:
|
||||
raise InvalidDefinitionError("ABTest portions must add up to less than or equal to 1")
|
||||
|
||||
|
||||
@@ -119,9 +119,9 @@ class CapaModule(XModule):
|
||||
if self.show_answer == "":
|
||||
self.show_answer = "closed"
|
||||
|
||||
if instance_state != None:
|
||||
if instance_state is not None:
|
||||
instance_state = json.loads(instance_state)
|
||||
if instance_state != None and 'attempts' in instance_state:
|
||||
if instance_state is not None and 'attempts' in instance_state:
|
||||
self.attempts = instance_state['attempts']
|
||||
|
||||
self.name = only_one(dom2.xpath('/problem/@name'))
|
||||
@@ -130,7 +130,7 @@ class CapaModule(XModule):
|
||||
if weight_string:
|
||||
self.weight = float(weight_string)
|
||||
else:
|
||||
self.weight = 1
|
||||
self.weight = None
|
||||
|
||||
if self.rerandomize == 'never':
|
||||
seed = 1
|
||||
@@ -238,7 +238,7 @@ class CapaModule(XModule):
|
||||
content = {'name': self.metadata['display_name'],
|
||||
'html': html,
|
||||
'weight': self.weight,
|
||||
}
|
||||
}
|
||||
|
||||
# We using strings as truthy values, because the terminology of the
|
||||
# check button is context-specific.
|
||||
@@ -563,6 +563,11 @@ class CapaDescriptor(RawDescriptor):
|
||||
|
||||
module_class = CapaModule
|
||||
|
||||
# Capa modules have some additional metadata:
|
||||
# TODO (vshnayder): do problems have any other metadata? Do they
|
||||
# actually use type and points?
|
||||
metadata_attributes = RawDescriptor.metadata_attributes + ('type', 'points')
|
||||
|
||||
# VS[compat]
|
||||
# TODO (cpennington): Delete this method once all fall 2012 course are being
|
||||
# edited in the cms
|
||||
@@ -572,8 +577,3 @@ class CapaDescriptor(RawDescriptor):
|
||||
'problems/' + path[8:],
|
||||
path[8:],
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def split_to_file(cls, xml_object):
|
||||
'''Problems always written in their own files'''
|
||||
return True
|
||||
|
||||
@@ -3,6 +3,7 @@ import time
|
||||
import dateutil.parser
|
||||
import logging
|
||||
|
||||
from xmodule.util.decorators import lazyproperty
|
||||
from xmodule.graders import load_grading_policy
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.seq_module import SequenceDescriptor, SequenceModule
|
||||
@@ -12,13 +13,9 @@ log = logging.getLogger(__name__)
|
||||
|
||||
class CourseDescriptor(SequenceDescriptor):
|
||||
module_class = SequenceModule
|
||||
metadata_attributes = SequenceDescriptor.metadata_attributes + ('org', 'course')
|
||||
|
||||
def __init__(self, system, definition=None, **kwargs):
|
||||
super(CourseDescriptor, self).__init__(system, definition, **kwargs)
|
||||
|
||||
self._grader = None
|
||||
self._grade_cutoffs = None
|
||||
|
||||
msg = None
|
||||
try:
|
||||
@@ -39,34 +36,84 @@ class CourseDescriptor(SequenceDescriptor):
|
||||
|
||||
def has_started(self):
|
||||
return time.gmtime() > self.start
|
||||
|
||||
|
||||
@property
|
||||
def grader(self):
|
||||
self.__load_grading_policy()
|
||||
return self._grader
|
||||
|
||||
return self.__grading_policy['GRADER']
|
||||
|
||||
@property
|
||||
def grade_cutoffs(self):
|
||||
self.__load_grading_policy()
|
||||
return self._grade_cutoffs
|
||||
|
||||
|
||||
def __load_grading_policy(self):
|
||||
if not self._grader or not self._grade_cutoffs:
|
||||
policy_string = ""
|
||||
|
||||
try:
|
||||
with self.system.resources_fs.open("grading_policy.json") as grading_policy_file:
|
||||
policy_string = grading_policy_file.read()
|
||||
except (IOError, ResourceNotFoundError):
|
||||
log.warning("Unable to load course settings file from grading_policy.json in course " + self.id)
|
||||
|
||||
grading_policy = load_grading_policy(policy_string)
|
||||
|
||||
self._grader = grading_policy['GRADER']
|
||||
self._grade_cutoffs = grading_policy['GRADE_CUTOFFS']
|
||||
|
||||
|
||||
return self.__grading_policy['GRADE_CUTOFFS']
|
||||
|
||||
@lazyproperty
|
||||
def __grading_policy(self):
|
||||
policy_string = ""
|
||||
|
||||
try:
|
||||
with self.system.resources_fs.open("grading_policy.json") as grading_policy_file:
|
||||
policy_string = grading_policy_file.read()
|
||||
except (IOError, ResourceNotFoundError):
|
||||
log.warning("Unable to load course settings file from grading_policy.json in course " + self.id)
|
||||
|
||||
grading_policy = load_grading_policy(policy_string)
|
||||
|
||||
return grading_policy
|
||||
|
||||
|
||||
@lazyproperty
|
||||
def grading_context(self):
|
||||
"""
|
||||
This returns a dictionary with keys necessary for quickly grading
|
||||
a student. They are used by grades.grade()
|
||||
|
||||
The grading context has two keys:
|
||||
graded_sections - This contains the sections that are graded, as
|
||||
well as all possible children modules that can affect the
|
||||
grading. This allows some sections to be skipped if the student
|
||||
hasn't seen any part of it.
|
||||
|
||||
The format is a dictionary keyed by section-type. The values are
|
||||
arrays of dictionaries containing
|
||||
"section_descriptor" : The section descriptor
|
||||
"xmoduledescriptors" : An array of xmoduledescriptors that
|
||||
could possibly be in the section, for any student
|
||||
|
||||
all_descriptors - This contains a list of all xmodules that can
|
||||
effect grading a student. This is used to efficiently fetch
|
||||
all the xmodule state for a StudentModuleCache without walking
|
||||
the descriptor tree again.
|
||||
|
||||
|
||||
"""
|
||||
|
||||
all_descriptors = []
|
||||
graded_sections = {}
|
||||
|
||||
def yield_descriptor_descendents(module_descriptor):
|
||||
for child in module_descriptor.get_children():
|
||||
yield child
|
||||
for module_descriptor in yield_descriptor_descendents(child):
|
||||
yield module_descriptor
|
||||
|
||||
for c in self.get_children():
|
||||
sections = []
|
||||
for s in c.get_children():
|
||||
if s.metadata.get('graded', False):
|
||||
# TODO: Only include modules that have a score here
|
||||
xmoduledescriptors = [child for child in yield_descriptor_descendents(s)]
|
||||
|
||||
section_description = { 'section_descriptor' : s, 'xmoduledescriptors' : xmoduledescriptors}
|
||||
|
||||
section_format = s.metadata.get('format', "")
|
||||
graded_sections[ section_format ] = graded_sections.get( section_format, [] ) + [section_description]
|
||||
|
||||
all_descriptors.extend(xmoduledescriptors)
|
||||
all_descriptors.append(s)
|
||||
|
||||
return { 'graded_sections' : graded_sections,
|
||||
'all_descriptors' : all_descriptors,}
|
||||
|
||||
|
||||
@staticmethod
|
||||
def id_to_location(course_id):
|
||||
'''Convert the given course_id (org/course/name) to a location object.
|
||||
|
||||
@@ -207,7 +207,7 @@ div.video {
|
||||
h3 {
|
||||
color: #999;
|
||||
float: left;
|
||||
font-size: 12px;
|
||||
font-size: em(14);
|
||||
font-weight: normal;
|
||||
letter-spacing: 1px;
|
||||
padding: 0 lh(.25) 0 lh(.5);
|
||||
@@ -221,6 +221,7 @@ div.video {
|
||||
margin-bottom: 0;
|
||||
padding: 0 lh(.5) 0 0;
|
||||
line-height: 46px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
&:hover, &:active, &:focus {
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import sys
|
||||
import hashlib
|
||||
import logging
|
||||
import random
|
||||
import string
|
||||
import sys
|
||||
|
||||
from pkg_resources import resource_string
|
||||
from lxml import etree
|
||||
@@ -35,7 +38,8 @@ class ErrorDescriptor(EditingDescriptor):
|
||||
error_msg='Error not available'):
|
||||
'''Create an instance of this descriptor from the supplied data.
|
||||
|
||||
Does not try to parse the data--just stores it.
|
||||
Does not require that xml_data be parseable--just stores it and exports
|
||||
as-is if not.
|
||||
|
||||
Takes an extra, optional, parameter--the error that caused an
|
||||
issue. (should be a string, or convert usefully into one).
|
||||
@@ -45,6 +49,13 @@ class ErrorDescriptor(EditingDescriptor):
|
||||
definition = {'data': inner}
|
||||
inner['error_msg'] = str(error_msg)
|
||||
|
||||
# Pick a unique url_name -- the sha1 hash of the xml_data.
|
||||
# NOTE: We could try to pull out the url_name of the errored descriptor,
|
||||
# but url_names aren't guaranteed to be unique between descriptor types,
|
||||
# and ErrorDescriptor can wrap any type. When the wrapped module is fixed,
|
||||
# it will be written out with the original url_name.
|
||||
url_name = hashlib.sha1(xml_data).hexdigest()
|
||||
|
||||
try:
|
||||
# If this is already an error tag, don't want to re-wrap it.
|
||||
xml_obj = etree.fromstring(xml_data)
|
||||
@@ -63,7 +74,7 @@ class ErrorDescriptor(EditingDescriptor):
|
||||
inner['contents'] = xml_data
|
||||
# TODO (vshnayder): Do we need a unique slug here? Just pick a random
|
||||
# 64-bit num?
|
||||
location = ['i4x', org, course, 'error', 'slug']
|
||||
location = ['i4x', org, course, 'error', url_name]
|
||||
metadata = {} # stays in the xml_data
|
||||
|
||||
return cls(system, definition, location=location, metadata=metadata)
|
||||
|
||||
@@ -13,6 +13,7 @@ from .html_checker import check_html
|
||||
|
||||
log = logging.getLogger("mitx.courseware")
|
||||
|
||||
|
||||
class HtmlModule(XModule):
|
||||
def get_html(self):
|
||||
return self.html
|
||||
@@ -36,18 +37,19 @@ class HtmlDescriptor(XmlDescriptor, EditingDescriptor):
|
||||
# are being edited in the cms
|
||||
@classmethod
|
||||
def backcompat_paths(cls, path):
|
||||
origpath = path
|
||||
if path.endswith('.html.xml'):
|
||||
path = path[:-9] + '.html' #backcompat--look for html instead of xml
|
||||
path = path[:-9] + '.html' # backcompat--look for html instead of xml
|
||||
candidates = []
|
||||
while os.sep in path:
|
||||
candidates.append(path)
|
||||
_, _, path = path.partition(os.sep)
|
||||
|
||||
# also look for .html versions instead of .xml
|
||||
if origpath.endswith('.xml'):
|
||||
candidates.append(origpath[:-4] + '.html')
|
||||
return candidates
|
||||
nc = []
|
||||
for candidate in candidates:
|
||||
if candidate.endswith('.xml'):
|
||||
nc.append(candidate[:-4] + '.html')
|
||||
return candidates + nc
|
||||
|
||||
# NOTE: html descriptors are special. We do not want to parse and
|
||||
# export them ourselves, because that can break things (e.g. lxml
|
||||
@@ -69,7 +71,7 @@ class HtmlDescriptor(XmlDescriptor, EditingDescriptor):
|
||||
if filename is None:
|
||||
definition_xml = copy.deepcopy(xml_object)
|
||||
cls.clean_metadata_from_xml(definition_xml)
|
||||
return {'data' : stringify_children(definition_xml)}
|
||||
return {'data': stringify_children(definition_xml)}
|
||||
else:
|
||||
filepath = cls._format_filepath(xml_object.tag, filename)
|
||||
|
||||
@@ -80,7 +82,7 @@ class HtmlDescriptor(XmlDescriptor, EditingDescriptor):
|
||||
# online and has imported all current (fall 2012) courses from xml
|
||||
if not system.resources_fs.exists(filepath):
|
||||
candidates = cls.backcompat_paths(filepath)
|
||||
#log.debug("candidates = {0}".format(candidates))
|
||||
log.debug("candidates = {0}".format(candidates))
|
||||
for candidate in candidates:
|
||||
if system.resources_fs.exists(candidate):
|
||||
filepath = candidate
|
||||
@@ -95,7 +97,7 @@ class HtmlDescriptor(XmlDescriptor, EditingDescriptor):
|
||||
log.warning(msg)
|
||||
system.error_tracker("Warning: " + msg)
|
||||
|
||||
definition = {'data' : html}
|
||||
definition = {'data': html}
|
||||
|
||||
# TODO (ichuang): remove this after migration
|
||||
# for Fall 2012 LMS migration: keep filename (and unmangled filename)
|
||||
@@ -109,17 +111,11 @@ class HtmlDescriptor(XmlDescriptor, EditingDescriptor):
|
||||
# add more info and re-raise
|
||||
raise Exception(msg), None, sys.exc_info()[2]
|
||||
|
||||
@classmethod
|
||||
def split_to_file(cls, xml_object):
|
||||
'''Never include inline html'''
|
||||
return True
|
||||
|
||||
|
||||
# TODO (vshnayder): make export put things in the right places.
|
||||
|
||||
def definition_to_xml(self, resource_fs):
|
||||
'''If the contents are valid xml, write them to filename.xml. Otherwise,
|
||||
write just the <html filename=""> tag to filename.xml, and the html
|
||||
write just <html filename="" [meta-attrs="..."]> to filename.xml, and the html
|
||||
string to filename.html.
|
||||
'''
|
||||
try:
|
||||
@@ -138,4 +134,3 @@ class HtmlDescriptor(XmlDescriptor, EditingDescriptor):
|
||||
elt = etree.Element('html')
|
||||
elt.set("filename", self.url_name)
|
||||
return elt
|
||||
|
||||
|
||||
@@ -188,21 +188,26 @@ class XMLModuleStore(ModuleStoreBase):
|
||||
course_file = StringIO(clean_out_mako_templating(course_file.read()))
|
||||
|
||||
course_data = etree.parse(course_file).getroot()
|
||||
|
||||
org = course_data.get('org')
|
||||
|
||||
if org is None:
|
||||
log.error("No 'org' attribute set for course in {dir}. "
|
||||
msg = ("No 'org' attribute set for course in {dir}. "
|
||||
"Using default 'edx'".format(dir=course_dir))
|
||||
log.error(msg)
|
||||
tracker(msg)
|
||||
org = 'edx'
|
||||
|
||||
course = course_data.get('course')
|
||||
|
||||
if course is None:
|
||||
log.error("No 'course' attribute set for course in {dir}."
|
||||
msg = ("No 'course' attribute set for course in {dir}."
|
||||
" Using default '{default}'".format(
|
||||
dir=course_dir,
|
||||
default=course_dir
|
||||
))
|
||||
log.error(msg)
|
||||
tracker(msg)
|
||||
course = course_dir
|
||||
|
||||
system = ImportSystem(self, org, course, course_dir, tracker)
|
||||
|
||||
@@ -122,16 +122,3 @@ class SequenceDescriptor(MakoModuleDescriptor, XmlDescriptor):
|
||||
etree.fromstring(child.export_to_xml(resource_fs)))
|
||||
return xml_object
|
||||
|
||||
@classmethod
|
||||
def split_to_file(cls, xml_object):
|
||||
# Note: if we end up needing subclasses, can port this logic there.
|
||||
yes = ('chapter',)
|
||||
no = ('course',)
|
||||
|
||||
if xml_object.tag in yes:
|
||||
return True
|
||||
elif xml_object.tag in no:
|
||||
return False
|
||||
|
||||
# otherwise maybe--delegate to superclass.
|
||||
return XmlDescriptor.split_to_file(xml_object)
|
||||
|
||||
@@ -15,6 +15,7 @@ import xmodule
|
||||
import capa.calc as calc
|
||||
import capa.capa_problem as lcp
|
||||
from capa.correctmap import CorrectMap
|
||||
from capa.util import convert_files_to_filenames
|
||||
from xmodule import graders, x_module
|
||||
from xmodule.x_module import ModuleSystem
|
||||
from xmodule.graders import Score, aggregate_scores
|
||||
@@ -31,7 +32,7 @@ i4xs = ModuleSystem(
|
||||
user=Mock(),
|
||||
filestore=fs.osfs.OSFS(os.path.dirname(os.path.realpath(__file__))),
|
||||
debug=True,
|
||||
xqueue_callback_url='/',
|
||||
xqueue={'interface':None, 'callback_url':'/', 'default_queuename': 'testqueue'},
|
||||
is_staff=False
|
||||
)
|
||||
|
||||
@@ -278,7 +279,6 @@ class StringResponseWithHintTest(unittest.TestCase):
|
||||
class CodeResponseTest(unittest.TestCase):
|
||||
'''
|
||||
Test CodeResponse
|
||||
|
||||
'''
|
||||
def test_update_score(self):
|
||||
problem_file = os.path.dirname(__file__) + "/test_files/coderesponse.xml"
|
||||
@@ -327,7 +327,18 @@ class CodeResponseTest(unittest.TestCase):
|
||||
self.assertFalse(test_lcp.correct_map.is_queued(answer_ids[j])) # Should be dequeued, message delivered
|
||||
else:
|
||||
self.assertTrue(test_lcp.correct_map.is_queued(answer_ids[j])) # Should be queued, message undelivered
|
||||
|
||||
|
||||
def test_convert_files_to_filenames(self):
|
||||
problem_file = os.path.dirname(__file__) + "/test_files/coderesponse.xml"
|
||||
fp = open(problem_file)
|
||||
answers_with_file = {'1_2_1': 'String-based answer',
|
||||
'1_3_1': ['answer1', 'answer2', 'answer3'],
|
||||
'1_4_1': fp}
|
||||
answers_converted = convert_files_to_filenames(answers_with_file)
|
||||
self.assertEquals(answers_converted['1_2_1'], 'String-based answer')
|
||||
self.assertEquals(answers_converted['1_3_1'], ['answer1', 'answer2', 'answer3'])
|
||||
self.assertEquals(answers_converted['1_4_1'], fp.name)
|
||||
|
||||
|
||||
class ChoiceResponseTest(unittest.TestCase):
|
||||
|
||||
|
||||
@@ -1,36 +1,107 @@
|
||||
from xmodule.modulestore.xml import XMLModuleStore
|
||||
from nose.tools import assert_equals
|
||||
from nose import SkipTest
|
||||
from tempfile import mkdtemp
|
||||
import unittest
|
||||
|
||||
from fs.osfs import OSFS
|
||||
from nose.tools import assert_equals, assert_true
|
||||
from path import path
|
||||
from tempfile import mkdtemp
|
||||
|
||||
from xmodule.modulestore.xml import XMLModuleStore
|
||||
|
||||
# from ~/mitx_all/mitx/common/lib/xmodule/xmodule/tests/
|
||||
# to ~/mitx_all/mitx/common/test
|
||||
TEST_DIR = path(__file__).abspath().dirname()
|
||||
for i in range(4):
|
||||
TEST_DIR = TEST_DIR.dirname()
|
||||
TEST_DIR = TEST_DIR / 'test'
|
||||
|
||||
DATA_DIR = TEST_DIR / 'data'
|
||||
|
||||
|
||||
def check_export_roundtrip(data_dir):
|
||||
print "Starting import"
|
||||
initial_import = XMLModuleStore('org', 'course', data_dir, eager=True)
|
||||
initial_course = initial_import.course
|
||||
def strip_metadata(descriptor, key):
|
||||
"""
|
||||
Recursively strips tag from all children.
|
||||
"""
|
||||
print "strip {key} from {desc}".format(key=key, desc=descriptor.location.url())
|
||||
descriptor.metadata.pop(key, None)
|
||||
for d in descriptor.get_children():
|
||||
strip_metadata(d, key)
|
||||
|
||||
print "Starting export"
|
||||
export_dir = mkdtemp()
|
||||
fs = OSFS(export_dir)
|
||||
xml = initial_course.export_to_xml(fs)
|
||||
with fs.open('course.xml', 'w') as course_xml:
|
||||
course_xml.write(xml)
|
||||
|
||||
print "Starting second import"
|
||||
second_import = XMLModuleStore('org', 'course', export_dir, eager=True)
|
||||
|
||||
print "Checking key equality"
|
||||
assert_equals(initial_import.modules.keys(), second_import.modules.keys())
|
||||
|
||||
print "Checking module equality"
|
||||
for location in initial_import.modules.keys():
|
||||
print "Checking", location
|
||||
assert_equals(initial_import.modules[location], second_import.modules[location])
|
||||
def strip_filenames(descriptor):
|
||||
"""
|
||||
Recursively strips 'filename' from all children's definitions.
|
||||
"""
|
||||
print "strip filename from {desc}".format(desc=descriptor.location.url())
|
||||
descriptor.definition.pop('filename', None)
|
||||
for d in descriptor.get_children():
|
||||
strip_filenames(d)
|
||||
|
||||
|
||||
def test_toy_roundtrip():
|
||||
dir = ""
|
||||
# TODO: add paths and make this run.
|
||||
raise SkipTest()
|
||||
check_export_roundtrip(dir)
|
||||
|
||||
class RoundTripTestCase(unittest.TestCase):
|
||||
'''Check that our test courses roundtrip properly'''
|
||||
def check_export_roundtrip(self, data_dir, course_dir):
|
||||
print "Starting import"
|
||||
initial_import = XMLModuleStore(data_dir, eager=True, course_dirs=[course_dir])
|
||||
|
||||
courses = initial_import.get_courses()
|
||||
self.assertEquals(len(courses), 1)
|
||||
initial_course = courses[0]
|
||||
|
||||
print "Starting export"
|
||||
export_dir = mkdtemp()
|
||||
print "export_dir: {0}".format(export_dir)
|
||||
fs = OSFS(export_dir)
|
||||
export_course_dir = 'export'
|
||||
export_fs = fs.makeopendir(export_course_dir)
|
||||
|
||||
xml = initial_course.export_to_xml(export_fs)
|
||||
with export_fs.open('course.xml', 'w') as course_xml:
|
||||
course_xml.write(xml)
|
||||
|
||||
print "Starting second import"
|
||||
second_import = XMLModuleStore(export_dir, eager=True,
|
||||
course_dirs=[export_course_dir])
|
||||
|
||||
courses2 = second_import.get_courses()
|
||||
self.assertEquals(len(courses2), 1)
|
||||
exported_course = courses2[0]
|
||||
|
||||
print "Checking course equality"
|
||||
# HACK: data_dir metadata tags break equality because they
|
||||
# aren't real metadata, and depend on paths. Remove them.
|
||||
strip_metadata(initial_course, 'data_dir')
|
||||
strip_metadata(exported_course, 'data_dir')
|
||||
|
||||
# HACK: filenames change when changing file formats
|
||||
# during imports from old-style courses. Ignore them.
|
||||
strip_filenames(initial_course)
|
||||
strip_filenames(exported_course)
|
||||
|
||||
self.assertEquals(initial_course, exported_course)
|
||||
|
||||
print "Checking key equality"
|
||||
self.assertEquals(sorted(initial_import.modules.keys()),
|
||||
sorted(second_import.modules.keys()))
|
||||
|
||||
print "Checking module equality"
|
||||
for location in initial_import.modules.keys():
|
||||
print "Checking", location
|
||||
if location.category == 'html':
|
||||
print ("Skipping html modules--they can't import in"
|
||||
" final form without writing files...")
|
||||
continue
|
||||
self.assertEquals(initial_import.modules[location],
|
||||
second_import.modules[location])
|
||||
|
||||
|
||||
def setUp(self):
|
||||
self.maxDiff = None
|
||||
|
||||
def test_toy_roundtrip(self):
|
||||
self.check_export_roundtrip(DATA_DIR, "toy")
|
||||
|
||||
def test_simple_roundtrip(self):
|
||||
self.check_export_roundtrip(DATA_DIR, "simple")
|
||||
|
||||
def test_full_roundtrip(self):
|
||||
self.check_export_roundtrip(DATA_DIR, "full")
|
||||
|
||||
@@ -5,6 +5,7 @@ from fs.memoryfs import MemoryFS
|
||||
from lxml import etree
|
||||
|
||||
from xmodule.x_module import XMLParsingSystem, XModuleDescriptor
|
||||
from xmodule.xml_module import is_pointer_tag
|
||||
from xmodule.errortracker import make_error_tracker
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
@@ -46,22 +47,17 @@ class DummySystem(XMLParsingSystem):
|
||||
raise Exception("Shouldn't be called")
|
||||
|
||||
|
||||
|
||||
|
||||
class ImportTestCase(unittest.TestCase):
|
||||
'''Make sure module imports work properly, including for malformed inputs'''
|
||||
|
||||
|
||||
@staticmethod
|
||||
def get_system():
|
||||
'''Get a dummy system'''
|
||||
return DummySystem()
|
||||
|
||||
def test_fallback(self):
|
||||
'''Make sure that malformed xml loads as an ErrorDescriptor.'''
|
||||
'''Check that malformed xml loads as an ErrorDescriptor.'''
|
||||
|
||||
bad_xml = '''<sequential display_name="oops"><video url="hi"></sequential>'''
|
||||
|
||||
system = self.get_system()
|
||||
|
||||
descriptor = XModuleDescriptor.load_from_xml(bad_xml, system, 'org', 'course',
|
||||
@@ -70,6 +66,22 @@ class ImportTestCase(unittest.TestCase):
|
||||
self.assertEqual(descriptor.__class__.__name__,
|
||||
'ErrorDescriptor')
|
||||
|
||||
|
||||
def test_unique_url_names(self):
|
||||
'''Check that each error gets its very own url_name'''
|
||||
bad_xml = '''<sequential display_name="oops"><video url="hi"></sequential>'''
|
||||
bad_xml2 = '''<sequential url_name="oops"><video url="hi"></sequential>'''
|
||||
system = self.get_system()
|
||||
|
||||
descriptor1 = XModuleDescriptor.load_from_xml(bad_xml, system, 'org',
|
||||
'course', None)
|
||||
|
||||
descriptor2 = XModuleDescriptor.load_from_xml(bad_xml2, system, 'org',
|
||||
'course', None)
|
||||
|
||||
self.assertNotEqual(descriptor1.location, descriptor2.location)
|
||||
|
||||
|
||||
def test_reimport(self):
|
||||
'''Make sure an already-exported error xml tag loads properly'''
|
||||
|
||||
@@ -111,30 +123,65 @@ class ImportTestCase(unittest.TestCase):
|
||||
xml_out = etree.fromstring(xml_str_out)
|
||||
self.assertEqual(xml_out.tag, 'sequential')
|
||||
|
||||
def test_metadata_inherit(self):
|
||||
"""Make sure metadata inherits properly"""
|
||||
def test_metadata_import_export(self):
|
||||
"""Two checks:
|
||||
- unknown metadata is preserved across import-export
|
||||
- inherited metadata doesn't leak to children.
|
||||
"""
|
||||
system = self.get_system()
|
||||
v = "1 hour"
|
||||
start_xml = '''<course graceperiod="{grace}" url_name="test1" display_name="myseq">
|
||||
<chapter url="hi" url_name="ch" display_name="CH">
|
||||
<html url_name="h" display_name="H">Two houses, ...</html></chapter>
|
||||
</course>'''.format(grace=v)
|
||||
v = '1 hour'
|
||||
org = 'foo'
|
||||
course = 'bbhh'
|
||||
url_name = 'test1'
|
||||
start_xml = '''
|
||||
<course org="{org}" course="{course}"
|
||||
graceperiod="{grace}" url_name="{url_name}" unicorn="purple">
|
||||
<chapter url="hi" url_name="ch" display_name="CH">
|
||||
<html url_name="h" display_name="H">Two houses, ...</html>
|
||||
</chapter>
|
||||
</course>'''.format(grace=v, org=org, course=course, url_name=url_name)
|
||||
descriptor = XModuleDescriptor.load_from_xml(start_xml, system,
|
||||
'org', 'course')
|
||||
org, course)
|
||||
|
||||
print "Errors: {0}".format(system.errorlog.errors)
|
||||
print descriptor, descriptor.metadata
|
||||
self.assertEqual(descriptor.metadata['graceperiod'], v)
|
||||
self.assertEqual(descriptor.metadata['unicorn'], 'purple')
|
||||
|
||||
# Check that the child inherits correctly
|
||||
# Check that the child inherits graceperiod correctly
|
||||
child = descriptor.get_children()[0]
|
||||
self.assertEqual(child.metadata['graceperiod'], v)
|
||||
|
||||
# Now export and see if the chapter tag has a graceperiod attribute
|
||||
# check that the child does _not_ inherit any unicorns
|
||||
self.assertTrue('unicorn' not in child.metadata)
|
||||
|
||||
# Now export and check things
|
||||
resource_fs = MemoryFS()
|
||||
exported_xml = descriptor.export_to_xml(resource_fs)
|
||||
|
||||
# Check that the exported xml is just a pointer
|
||||
print "Exported xml:", exported_xml
|
||||
root = etree.fromstring(exported_xml)
|
||||
chapter_tag = root[0]
|
||||
self.assertEqual(chapter_tag.tag, 'chapter')
|
||||
self.assertFalse('graceperiod' in chapter_tag.attrib)
|
||||
pointer = etree.fromstring(exported_xml)
|
||||
self.assertTrue(is_pointer_tag(pointer))
|
||||
# but it's a special case course pointer
|
||||
self.assertEqual(pointer.attrib['course'], course)
|
||||
self.assertEqual(pointer.attrib['org'], org)
|
||||
|
||||
# Does the course still have unicorns?
|
||||
with resource_fs.open('course/{url_name}.xml'.format(url_name=url_name)) as f:
|
||||
course_xml = etree.fromstring(f.read())
|
||||
|
||||
self.assertEqual(course_xml.attrib['unicorn'], 'purple')
|
||||
|
||||
# the course and org tags should be _only_ in the pointer
|
||||
self.assertTrue('course' not in course_xml.attrib)
|
||||
self.assertTrue('org' not in course_xml.attrib)
|
||||
|
||||
# did we successfully strip the url_name from the definition contents?
|
||||
self.assertTrue('url_name' not in course_xml.attrib)
|
||||
|
||||
# Does the chapter tag now have a graceperiod attribute?
|
||||
# hardcoded path to child
|
||||
with resource_fs.open('chapter/ch.xml') as f:
|
||||
chapter_xml = etree.fromstring(f.read())
|
||||
self.assertEqual(chapter_xml.tag, 'chapter')
|
||||
self.assertFalse('graceperiod' in chapter_xml.attrib)
|
||||
|
||||
0
common/lib/xmodule/xmodule/util/__init__.py
Normal file
0
common/lib/xmodule/xmodule/util/__init__.py
Normal file
36
common/lib/xmodule/xmodule/util/decorators.py
Normal file
36
common/lib/xmodule/xmodule/util/decorators.py
Normal file
@@ -0,0 +1,36 @@
|
||||
|
||||
|
||||
def lazyproperty(fn):
|
||||
"""
|
||||
Use this decorator for lazy generation of properties that
|
||||
are expensive to compute. From http://stackoverflow.com/a/3013910/86828
|
||||
|
||||
|
||||
Example:
|
||||
class Test(object):
|
||||
|
||||
@lazyproperty
|
||||
def a(self):
|
||||
print 'generating "a"'
|
||||
return range(5)
|
||||
|
||||
Interactive Session:
|
||||
>>> t = Test()
|
||||
>>> t.__dict__
|
||||
{}
|
||||
>>> t.a
|
||||
generating "a"
|
||||
[0, 1, 2, 3, 4]
|
||||
>>> t.__dict__
|
||||
{'_lazy_a': [0, 1, 2, 3, 4]}
|
||||
>>> t.a
|
||||
[0, 1, 2, 3, 4]
|
||||
"""
|
||||
|
||||
attr_name = '_lazy_' + fn.__name__
|
||||
@property
|
||||
def _lazyprop(self):
|
||||
if not hasattr(self, attr_name):
|
||||
setattr(self, attr_name, fn(self))
|
||||
return getattr(self, attr_name)
|
||||
return _lazyprop
|
||||
@@ -6,6 +6,7 @@ from fs.errors import ResourceNotFoundError
|
||||
from functools import partial
|
||||
from lxml import etree
|
||||
from lxml.etree import XMLSyntaxError
|
||||
from pprint import pprint
|
||||
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.errortracker import exc_info_to_str
|
||||
@@ -550,9 +551,9 @@ class XModuleDescriptor(Plugin, HTMLSnippet):
|
||||
|
||||
if not eq:
|
||||
for attr in self.equality_attributes:
|
||||
print(getattr(self, attr, None),
|
||||
getattr(other, attr, None),
|
||||
getattr(self, attr, None) == getattr(other, attr, None))
|
||||
pprint((getattr(self, attr, None),
|
||||
getattr(other, attr, None),
|
||||
getattr(self, attr, None) == getattr(other, attr, None)))
|
||||
|
||||
return eq
|
||||
|
||||
@@ -643,7 +644,7 @@ class ModuleSystem(object):
|
||||
user=None,
|
||||
filestore=None,
|
||||
debug=False,
|
||||
xqueue = None,
|
||||
xqueue=None,
|
||||
is_staff=False):
|
||||
'''
|
||||
Create a closure around the system environment.
|
||||
|
||||
@@ -11,22 +11,44 @@ import sys
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
_AttrMapBase = namedtuple('_AttrMap', 'metadata_key to_metadata from_metadata')
|
||||
|
||||
def is_pointer_tag(xml_obj):
|
||||
"""
|
||||
Check if xml_obj is a pointer tag: <blah url_name="something" />.
|
||||
No children, one attribute named url_name.
|
||||
|
||||
Special case for course roots: the pointer is
|
||||
<course url_name="something" org="myorg" course="course">
|
||||
|
||||
xml_obj: an etree Element
|
||||
|
||||
Returns a bool.
|
||||
"""
|
||||
if xml_obj.tag != "course":
|
||||
expected_attr = set(['url_name'])
|
||||
else:
|
||||
expected_attr = set(['url_name', 'course', 'org'])
|
||||
|
||||
actual_attr = set(xml_obj.attrib.keys())
|
||||
return len(xml_obj) == 0 and actual_attr == expected_attr
|
||||
|
||||
|
||||
|
||||
_AttrMapBase = namedtuple('_AttrMap', 'from_xml to_xml')
|
||||
|
||||
class AttrMap(_AttrMapBase):
|
||||
"""
|
||||
A class that specifies a metadata_key, and two functions:
|
||||
A class that specifies two functions:
|
||||
|
||||
to_metadata: convert value from the xml representation into
|
||||
from_xml: convert value from the xml representation into
|
||||
an internal python representation
|
||||
|
||||
from_metadata: convert the internal python representation into
|
||||
to_xml: convert the internal python representation into
|
||||
the value to store in the xml.
|
||||
"""
|
||||
def __new__(_cls, metadata_key,
|
||||
to_metadata=lambda x: x,
|
||||
from_metadata=lambda x: x):
|
||||
return _AttrMapBase.__new__(_cls, metadata_key, to_metadata, from_metadata)
|
||||
def __new__(_cls, from_xml=lambda x: x,
|
||||
to_xml=lambda x: x):
|
||||
return _AttrMapBase.__new__(_cls, from_xml, to_xml)
|
||||
|
||||
|
||||
class XmlDescriptor(XModuleDescriptor):
|
||||
@@ -39,19 +61,28 @@ class XmlDescriptor(XModuleDescriptor):
|
||||
|
||||
# The attributes will be removed from the definition xml passed
|
||||
# to definition_from_xml, and from the xml returned by definition_to_xml
|
||||
|
||||
# Note -- url_name isn't in this list because it's handled specially on
|
||||
# import and export.
|
||||
|
||||
# TODO (vshnayder): Do we need a list of metadata we actually
|
||||
# understand? And if we do, is this the place?
|
||||
# Related: What's the right behavior for clean_metadata?
|
||||
metadata_attributes = ('format', 'graceperiod', 'showanswer', 'rerandomize',
|
||||
'start', 'due', 'graded', 'display_name', 'url_name', 'hide_from_toc',
|
||||
'ispublic', # if True, then course is listed for all users; see
|
||||
'ispublic', # if True, then course is listed for all users; see
|
||||
# VS[compat] Remove once unused.
|
||||
'name', 'slug')
|
||||
|
||||
metadata_to_strip = ('data_dir',
|
||||
# VS[compat] -- remove the below attrs once everything is in the CMS
|
||||
'course', 'org', 'url_name', 'filename')
|
||||
|
||||
# A dictionary mapping xml attribute names AttrMaps that describe how
|
||||
# to import and export them
|
||||
xml_attribute_map = {
|
||||
# type conversion: want True/False in python, "true"/"false" in xml
|
||||
'graded': AttrMap('graded',
|
||||
lambda val: val == 'true',
|
||||
'graded': AttrMap(lambda val: val == 'true',
|
||||
lambda val: str(val).lower()),
|
||||
}
|
||||
|
||||
@@ -101,12 +132,32 @@ class XmlDescriptor(XModuleDescriptor):
|
||||
"""
|
||||
return etree.parse(file_object).getroot()
|
||||
|
||||
@classmethod
|
||||
def load_file(cls, filepath, fs, location):
|
||||
'''
|
||||
Open the specified file in fs, and call cls.file_to_xml on it,
|
||||
returning the lxml object.
|
||||
|
||||
Add details and reraise on error.
|
||||
'''
|
||||
try:
|
||||
with fs.open(filepath) as file:
|
||||
return cls.file_to_xml(file)
|
||||
except Exception as err:
|
||||
# Add info about where we are, but keep the traceback
|
||||
msg = 'Unable to load file contents at path %s for item %s: %s ' % (
|
||||
filepath, location.url(), str(err))
|
||||
raise Exception, msg, sys.exc_info()[2]
|
||||
|
||||
|
||||
@classmethod
|
||||
def load_definition(cls, xml_object, system, location):
|
||||
'''Load a descriptor definition from the specified xml_object.
|
||||
Subclasses should not need to override this except in special
|
||||
cases (e.g. html module)'''
|
||||
|
||||
# VS[compat] -- the filename tag should go away once everything is
|
||||
# converted. (note: make sure html files still work once this goes away)
|
||||
filename = xml_object.get('filename')
|
||||
if filename is None:
|
||||
definition_xml = copy.deepcopy(xml_object)
|
||||
@@ -120,22 +171,14 @@ class XmlDescriptor(XModuleDescriptor):
|
||||
# again in the correct format. This should go away once the CMS is
|
||||
# online and has imported all current (fall 2012) courses from xml
|
||||
if not system.resources_fs.exists(filepath) and hasattr(
|
||||
cls,
|
||||
'backcompat_paths'):
|
||||
cls, 'backcompat_paths'):
|
||||
candidates = cls.backcompat_paths(filepath)
|
||||
for candidate in candidates:
|
||||
if system.resources_fs.exists(candidate):
|
||||
filepath = candidate
|
||||
break
|
||||
|
||||
try:
|
||||
with system.resources_fs.open(filepath) as file:
|
||||
definition_xml = cls.file_to_xml(file)
|
||||
except Exception:
|
||||
msg = 'Unable to load file contents at path %s for item %s' % (
|
||||
filepath, location.url())
|
||||
# Add info about where we are, but keep the traceback
|
||||
raise Exception, msg, sys.exc_info()[2]
|
||||
definition_xml = cls.load_file(filepath, system.resources_fs, location)
|
||||
|
||||
cls.clean_metadata_from_xml(definition_xml)
|
||||
definition = cls.definition_from_xml(definition_xml, system)
|
||||
@@ -146,6 +189,28 @@ class XmlDescriptor(XModuleDescriptor):
|
||||
|
||||
return definition
|
||||
|
||||
@classmethod
|
||||
def load_metadata(cls, xml_object):
|
||||
"""
|
||||
Read the metadata attributes from this xml_object.
|
||||
|
||||
Returns a dictionary {key: value}.
|
||||
"""
|
||||
metadata = {}
|
||||
for attr in xml_object.attrib:
|
||||
val = xml_object.get(attr)
|
||||
if val is not None:
|
||||
# VS[compat]. Remove after all key translations done
|
||||
attr = cls._translate(attr)
|
||||
|
||||
if attr in cls.metadata_to_strip:
|
||||
# don't load these
|
||||
continue
|
||||
|
||||
attr_map = cls.xml_attribute_map.get(attr, AttrMap())
|
||||
metadata[attr] = attr_map.from_xml(val)
|
||||
return metadata
|
||||
|
||||
|
||||
@classmethod
|
||||
def from_xml(cls, xml_data, system, org=None, course=None):
|
||||
@@ -160,26 +225,27 @@ class XmlDescriptor(XModuleDescriptor):
|
||||
url identifiers
|
||||
"""
|
||||
xml_object = etree.fromstring(xml_data)
|
||||
# VS[compat] -- just have the url_name lookup once translation is done
|
||||
slug = xml_object.get('url_name', xml_object.get('slug'))
|
||||
location = Location('i4x', org, course, xml_object.tag, slug)
|
||||
# VS[compat] -- just have the url_name lookup, once translation is done
|
||||
url_name = xml_object.get('url_name', xml_object.get('slug'))
|
||||
location = Location('i4x', org, course, xml_object.tag, url_name)
|
||||
|
||||
def load_metadata():
|
||||
metadata = {}
|
||||
for attr in cls.metadata_attributes:
|
||||
val = xml_object.get(attr)
|
||||
if val is not None:
|
||||
# VS[compat]. Remove after all key translations done
|
||||
attr = cls._translate(attr)
|
||||
# VS[compat] -- detect new-style each-in-a-file mode
|
||||
if is_pointer_tag(xml_object):
|
||||
# new style:
|
||||
# read the actual definition file--named using url_name
|
||||
filepath = cls._format_filepath(xml_object.tag, url_name)
|
||||
definition_xml = cls.load_file(filepath, system.resources_fs, location)
|
||||
else:
|
||||
definition_xml = xml_object
|
||||
|
||||
attr_map = cls.xml_attribute_map.get(attr, AttrMap(attr))
|
||||
metadata[attr_map.metadata_key] = attr_map.to_metadata(val)
|
||||
return metadata
|
||||
definition = cls.load_definition(definition_xml, system, location)
|
||||
# VS[compat] -- make Ike's github preview links work in both old and
|
||||
# new file layouts
|
||||
if is_pointer_tag(xml_object):
|
||||
# new style -- contents actually at filepath
|
||||
definition['filename'] = [filepath, filepath]
|
||||
|
||||
definition = cls.load_definition(xml_object, system, location)
|
||||
metadata = load_metadata()
|
||||
# VS[compat] -- just have the url_name lookup once translation is done
|
||||
slug = xml_object.get('url_name', xml_object.get('slug'))
|
||||
metadata = cls.load_metadata(definition_xml)
|
||||
return cls(
|
||||
system,
|
||||
definition,
|
||||
@@ -193,20 +259,6 @@ class XmlDescriptor(XModuleDescriptor):
|
||||
name=name,
|
||||
ext=cls.filename_extension)
|
||||
|
||||
@classmethod
|
||||
def split_to_file(cls, xml_object):
|
||||
'''
|
||||
Decide whether to write this object to a separate file or not.
|
||||
|
||||
xml_object: an xml definition of an instance of cls.
|
||||
|
||||
This default implementation will split if this has more than 7
|
||||
descendant tags.
|
||||
|
||||
Can be overridden by subclasses.
|
||||
'''
|
||||
return len(list(xml_object.iter())) > 7
|
||||
|
||||
def export_to_xml(self, resource_fs):
|
||||
"""
|
||||
Returns an xml string representing this module, and all modules
|
||||
@@ -227,42 +279,39 @@ class XmlDescriptor(XModuleDescriptor):
|
||||
xml_object = self.definition_to_xml(resource_fs)
|
||||
self.__class__.clean_metadata_from_xml(xml_object)
|
||||
|
||||
# Set the tag first, so it's right if writing to a file
|
||||
# Set the tag so we get the file path right
|
||||
xml_object.tag = self.category
|
||||
|
||||
# Write it to a file if necessary
|
||||
if self.split_to_file(xml_object):
|
||||
# Put this object in its own file
|
||||
filepath = self.__class__._format_filepath(self.category, self.url_name)
|
||||
resource_fs.makedir(os.path.dirname(filepath), allow_recreate=True)
|
||||
with resource_fs.open(filepath, 'w') as file:
|
||||
file.write(etree.tostring(xml_object, pretty_print=True))
|
||||
# ...and remove all of its children here
|
||||
for child in xml_object:
|
||||
xml_object.remove(child)
|
||||
# also need to remove the text of this object.
|
||||
xml_object.text = ''
|
||||
# and the tail for good measure...
|
||||
xml_object.tail = ''
|
||||
def val_for_xml(attr):
|
||||
"""Get the value for this attribute that we want to store.
|
||||
(Possible format conversion through an AttrMap).
|
||||
"""
|
||||
attr_map = self.xml_attribute_map.get(attr, AttrMap())
|
||||
return attr_map.to_xml(self.own_metadata[attr])
|
||||
|
||||
# Add the non-inherited metadata
|
||||
for attr in sorted(self.own_metadata):
|
||||
# don't want e.g. data_dir
|
||||
if attr not in self.metadata_to_strip:
|
||||
xml_object.set(attr, val_for_xml(attr))
|
||||
|
||||
xml_object.set('filename', self.url_name)
|
||||
# Write the definition to a file
|
||||
filepath = self.__class__._format_filepath(self.category, self.url_name)
|
||||
resource_fs.makedir(os.path.dirname(filepath), allow_recreate=True)
|
||||
with resource_fs.open(filepath, 'w') as file:
|
||||
file.write(etree.tostring(xml_object, pretty_print=True))
|
||||
|
||||
# Add the metadata
|
||||
xml_object.set('url_name', self.url_name)
|
||||
for attr in self.metadata_attributes:
|
||||
attr_map = self.xml_attribute_map.get(attr, AttrMap(attr))
|
||||
metadata_key = attr_map.metadata_key
|
||||
# And return just a pointer with the category and filename.
|
||||
record_object = etree.Element(self.category)
|
||||
record_object.set('url_name', self.url_name)
|
||||
|
||||
if (metadata_key not in self.metadata or
|
||||
metadata_key in self._inherited_metadata):
|
||||
continue
|
||||
# Special case for course pointers:
|
||||
if self.category == 'course':
|
||||
# add org and course attributes on the pointer tag
|
||||
record_object.set('org', self.location.org)
|
||||
record_object.set('course', self.location.course)
|
||||
|
||||
val = attr_map.from_metadata(self.metadata[metadata_key])
|
||||
xml_object.set(attr, val)
|
||||
|
||||
# Now we just have to make it beautiful
|
||||
return etree.tostring(xml_object, pretty_print=True)
|
||||
return etree.tostring(record_object, pretty_print=True)
|
||||
|
||||
def definition_to_xml(self, resource_fs):
|
||||
"""
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<chapter name="Overview">
|
||||
<video name="Welcome" youtube="0.75:izygArpw-Qo,1.0:p2Q6BrNhdh8,1.25:1EeWXzPdhSA,1.50:rABDYkeK0x8"/>
|
||||
<videosequence format="Lecture Sequence" name="A simple sequence">
|
||||
<html id="toylab" filename="toylab"/>
|
||||
<html name="toylab" filename="toylab"/>
|
||||
<video name="S0V1: Video Resources" youtube="0.75:EuzkdzfR0i8,1.0:1bK-WdDi6Qw,1.25:0v1VzoDVUTM,1.50:Bxk_-ZJb240"/>
|
||||
</videosequence>
|
||||
<section name="Lecture 2">
|
||||
@@ -15,7 +15,7 @@
|
||||
<chapter name="Chapter 2">
|
||||
<section name="Problem Set 1">
|
||||
<sequential>
|
||||
<problem type="lecture" showanswer="attempted" rerandomize="true" title="A simple coding problem" name="Simple coding problem" filename="ps01-simple"/>
|
||||
<problem type="lecture" showanswer="attempted" rerandomize="true" display_name="A simple coding problem" name="Simple coding problem" filename="ps01-simple"/>
|
||||
</sequential>
|
||||
</section>
|
||||
<video name="Lost Video" youtube="1.0:TBvX7HzxexQ"/>
|
||||
|
||||
@@ -28,3 +28,18 @@ Check out the course data directories that you want to work with into the
|
||||
Replace `../data` with your `GITHUB_REPO_ROOT` if it's not the default value.
|
||||
|
||||
This will import all courses in your data directory into mongodb
|
||||
|
||||
## Unit tests
|
||||
|
||||
This runs all the tests (long, uses collectstatic):
|
||||
|
||||
rake test
|
||||
|
||||
xmodule can be tested independently, with this:
|
||||
|
||||
rake test_common/lib/xmodule
|
||||
|
||||
To see all available rake commands, do this:
|
||||
|
||||
rake -T
|
||||
|
||||
@@ -52,7 +52,7 @@ def certificate_request(request):
|
||||
return return_error(survey_response['error'])
|
||||
|
||||
grade = None
|
||||
student_gradesheet = grades.grade_sheet(request.user)
|
||||
student_gradesheet = grades.grade(request.user, request, course)
|
||||
grade = student_gradesheet['grade']
|
||||
|
||||
if not grade:
|
||||
@@ -65,7 +65,7 @@ def certificate_request(request):
|
||||
else:
|
||||
#This is not a POST, we should render the page with the form
|
||||
|
||||
grade_sheet = grades.grade_sheet(request.user)
|
||||
student_gradesheet = grades.grade(request.user, request, course)
|
||||
certificate_state = certificate_state_for_student(request.user, grade_sheet['grade'])
|
||||
|
||||
if certificate_state['state'] != "requestable":
|
||||
|
||||
@@ -46,12 +46,15 @@ def check_course(course_id, course_must_be_open=True, course_required=True):
|
||||
|
||||
|
||||
def course_image_url(course):
|
||||
return staticfiles_storage.url(course.metadata['data_dir'] + "/images/course_image.jpg")
|
||||
return staticfiles_storage.url(course.metadata['data_dir'] +
|
||||
"/images/course_image.jpg")
|
||||
|
||||
|
||||
def get_course_about_section(course, section_key):
|
||||
"""
|
||||
This returns the snippet of html to be rendered on the course about page, given the key for the section.
|
||||
This returns the snippet of html to be rendered on the course about page,
|
||||
given the key for the section.
|
||||
|
||||
Valid keys:
|
||||
- overview
|
||||
- title
|
||||
@@ -70,18 +73,23 @@ def get_course_about_section(course, section_key):
|
||||
- more_info
|
||||
"""
|
||||
|
||||
# Many of these are stored as html files instead of some semantic markup. This can change without effecting
|
||||
# this interface when we find a good format for defining so many snippets of text/html.
|
||||
# Many of these are stored as html files instead of some semantic
|
||||
# markup. This can change without effecting this interface when we find a
|
||||
# good format for defining so many snippets of text/html.
|
||||
|
||||
# TODO: Remove number, instructors from this list
|
||||
if section_key in ['short_description', 'description', 'key_dates', 'video', 'course_staff_short', 'course_staff_extended',
|
||||
'requirements', 'syllabus', 'textbook', 'faq', 'more_info', 'number', 'instructors', 'overview',
|
||||
'effort', 'end_date', 'prerequisites']:
|
||||
if section_key in ['short_description', 'description', 'key_dates', 'video',
|
||||
'course_staff_short', 'course_staff_extended',
|
||||
'requirements', 'syllabus', 'textbook', 'faq', 'more_info',
|
||||
'number', 'instructors', 'overview',
|
||||
'effort', 'end_date', 'prerequisites']:
|
||||
try:
|
||||
with course.system.resources_fs.open(path("about") / section_key + ".html") as htmlFile:
|
||||
return replace_urls(htmlFile.read().decode('utf-8'), course.metadata['data_dir'])
|
||||
return replace_urls(htmlFile.read().decode('utf-8'),
|
||||
course.metadata['data_dir'])
|
||||
except ResourceNotFoundError:
|
||||
log.warning("Missing about section {key} in course {url}".format(key=section_key, url=course.location.url()))
|
||||
log.warning("Missing about section {key} in course {url}".format(
|
||||
key=section_key, url=course.location.url()))
|
||||
return None
|
||||
elif section_key == "title":
|
||||
return course.metadata.get('display_name', course.url_name)
|
||||
@@ -95,7 +103,9 @@ def get_course_about_section(course, section_key):
|
||||
|
||||
def get_course_info_section(course, section_key):
|
||||
"""
|
||||
This returns the snippet of html to be rendered on the course info page, given the key for the section.
|
||||
This returns the snippet of html to be rendered on the course info page,
|
||||
given the key for the section.
|
||||
|
||||
Valid keys:
|
||||
- handouts
|
||||
- guest_handouts
|
||||
@@ -103,43 +113,51 @@ def get_course_info_section(course, section_key):
|
||||
- guest_updates
|
||||
"""
|
||||
|
||||
# Many of these are stored as html files instead of some semantic markup. This can change without effecting
|
||||
# this interface when we find a good format for defining so many snippets of text/html.
|
||||
# Many of these are stored as html files instead of some semantic
|
||||
# markup. This can change without effecting this interface when we find a
|
||||
# good format for defining so many snippets of text/html.
|
||||
|
||||
if section_key in ['handouts', 'guest_handouts', 'updates', 'guest_updates']:
|
||||
try:
|
||||
with course.system.resources_fs.open(path("info") / section_key + ".html") as htmlFile:
|
||||
return replace_urls(htmlFile.read().decode('utf-8'), course.metadata['data_dir'])
|
||||
return replace_urls(htmlFile.read().decode('utf-8'),
|
||||
course.metadata['data_dir'])
|
||||
except ResourceNotFoundError:
|
||||
log.exception("Missing info section {key} in course {url}".format(key=section_key, url=course.location.url()))
|
||||
log.exception("Missing info section {key} in course {url}".format(
|
||||
key=section_key, url=course.location.url()))
|
||||
return "! Info section missing !"
|
||||
|
||||
raise KeyError("Invalid about key " + str(section_key))
|
||||
|
||||
def course_staff_group_name(course):
|
||||
'''
|
||||
course should be either a CourseDescriptor instance, or a string (the .course entry of a Location)
|
||||
course should be either a CourseDescriptor instance, or a string (the
|
||||
.course entry of a Location)
|
||||
'''
|
||||
if isinstance(course,str):
|
||||
if isinstance(course, str) or isinstance(course, unicode):
|
||||
coursename = course
|
||||
else:
|
||||
coursename = course.metadata.get('data_dir','UnknownCourseName')
|
||||
if not coursename: # Fall 2012: not all course.xml have metadata correct yet
|
||||
coursename = course.metadata.get('course','')
|
||||
# should be a CourseDescriptor, so grab its location.course:
|
||||
coursename = course.location.course
|
||||
return 'staff_%s' % coursename
|
||||
|
||||
def has_staff_access_to_course(user,course):
|
||||
def has_staff_access_to_course(user, course):
|
||||
'''
|
||||
Returns True if the given user has staff access to the course.
|
||||
This means that user is in the staff_* group, or is an overall admin.
|
||||
|
||||
course is the course field of the location being accessed.
|
||||
'''
|
||||
if user is None or (not user.is_authenticated()) or course is None:
|
||||
return False
|
||||
if user.is_staff:
|
||||
return True
|
||||
user_groups = [x[1] for x in user.groups.values_list()] # note this is the Auth group, not UserTestGroup
|
||||
|
||||
# note this is the Auth group, not UserTestGroup
|
||||
user_groups = [x[1] for x in user.groups.values_list()]
|
||||
staff_group = course_staff_group_name(course)
|
||||
log.debug('course %s user %s groups %s' % (staff_group, user, user_groups))
|
||||
log.debug('course %s, staff_group %s, user %s, groups %s' % (
|
||||
course, staff_group, user, user_groups))
|
||||
if staff_group in user_groups:
|
||||
return True
|
||||
return False
|
||||
@@ -154,7 +172,8 @@ def get_courses_by_university(user):
|
||||
Returns dict of lists of courses available, keyed by course.org (ie university).
|
||||
Courses are sorted by course.number.
|
||||
|
||||
if ACCESS_REQUIRE_STAFF_FOR_COURSE then list only includes those accessible to user.
|
||||
if ACCESS_REQUIRE_STAFF_FOR_COURSE then list only includes those accessible
|
||||
to user.
|
||||
'''
|
||||
# TODO: Clean up how 'error' is done.
|
||||
# filter out any courses that errored.
|
||||
@@ -163,9 +182,9 @@ def get_courses_by_university(user):
|
||||
courses = sorted(courses, key=lambda course: course.number)
|
||||
universities = defaultdict(list)
|
||||
for course in courses:
|
||||
if settings.MITX_FEATURES.get('ENABLE_LMS_MIGRATION'):
|
||||
if settings.MITX_FEATURES.get('ACCESS_REQUIRE_STAFF_FOR_COURSE'):
|
||||
if not has_access_to_course(user,course):
|
||||
continue
|
||||
universities[course.org].append(course)
|
||||
return universities
|
||||
|
||||
|
||||
|
||||
@@ -3,26 +3,135 @@ import logging
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
from models import StudentModuleCache
|
||||
from module_render import get_module, get_instance_module
|
||||
from xmodule import graders
|
||||
from xmodule.graders import Score
|
||||
from models import StudentModule
|
||||
|
||||
_log = logging.getLogger("mitx.courseware")
|
||||
log = logging.getLogger("mitx.courseware")
|
||||
|
||||
|
||||
def grade_sheet(student, course, grader, student_module_cache):
|
||||
def yield_module_descendents(module):
|
||||
for child in module.get_display_items():
|
||||
yield child
|
||||
for module in yield_module_descendents(child):
|
||||
yield module
|
||||
|
||||
def grade(student, request, course, student_module_cache=None):
|
||||
"""
|
||||
This pulls a summary of all problems in the course. It returns a dictionary
|
||||
with two datastructures:
|
||||
This grades a student as quickly as possible. It retuns the
|
||||
output from the course grader, augmented with the final letter
|
||||
grade. The keys in the output are:
|
||||
|
||||
- grade : A final letter grade.
|
||||
- percent : The final percent for the class (rounded up).
|
||||
- section_breakdown : A breakdown of each section that makes
|
||||
up the grade. (For display)
|
||||
- grade_breakdown : A breakdown of the major components that
|
||||
make up the final grade. (For display)
|
||||
|
||||
More information on the format is in the docstring for CourseGrader.
|
||||
"""
|
||||
|
||||
grading_context = course.grading_context
|
||||
|
||||
if student_module_cache == None:
|
||||
student_module_cache = StudentModuleCache(student, grading_context['all_descriptors'])
|
||||
|
||||
totaled_scores = {}
|
||||
# This next complicated loop is just to collect the totaled_scores, which is
|
||||
# passed to the grader
|
||||
for section_format, sections in grading_context['graded_sections'].iteritems():
|
||||
format_scores = []
|
||||
for section in sections:
|
||||
section_descriptor = section['section_descriptor']
|
||||
section_name = section_descriptor.metadata.get('display_name')
|
||||
|
||||
should_grade_section = 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']:
|
||||
if student_module_cache.lookup(moduledescriptor.category, moduledescriptor.location.url() ):
|
||||
should_grade_section = True
|
||||
break
|
||||
|
||||
if should_grade_section:
|
||||
scores = []
|
||||
# TODO: We need the request to pass into here. If we could forgo that, our arguments
|
||||
# would be simpler
|
||||
section_module = get_module(student, request, section_descriptor.location, student_module_cache)
|
||||
|
||||
# TODO: We may be able to speed this up by only getting a list of children IDs from section_module
|
||||
# Then, we may not need to instatiate any problems if they are already in the database
|
||||
for module in yield_module_descendents(section_module):
|
||||
(correct, total) = get_score(student, module, student_module_cache)
|
||||
if correct is None and total is None:
|
||||
continue
|
||||
|
||||
if settings.GENERATE_PROFILE_SCORES:
|
||||
if total > 1:
|
||||
correct = random.randrange(max(total - 2, 1), total + 1)
|
||||
else:
|
||||
correct = total
|
||||
|
||||
graded = module.metadata.get("graded", False)
|
||||
if not total > 0:
|
||||
#We simply cannot grade a problem that is 12/0, because we might need it as a percentage
|
||||
graded = False
|
||||
|
||||
scores.append(Score(correct, total, graded, module.metadata.get('display_name')))
|
||||
|
||||
section_total, graded_total = graders.aggregate_scores(scores, section_name)
|
||||
else:
|
||||
section_total = Score(0.0, 1.0, False, section_name)
|
||||
graded_total = Score(0.0, 1.0, True, section_name)
|
||||
|
||||
#Add the graded total to totaled_scores
|
||||
if graded_total.possible > 0:
|
||||
format_scores.append(graded_total)
|
||||
else:
|
||||
log.exception("Unable to grade a section with a total possible score of zero. " + str(section_descriptor.id))
|
||||
|
||||
totaled_scores[section_format] = format_scores
|
||||
|
||||
grade_summary = course.grader.grade(totaled_scores)
|
||||
|
||||
# We round the grade here, to make sure that the grade is an whole percentage and
|
||||
# doesn't get displayed differently than it gets grades
|
||||
grade_summary['percent'] = round(grade_summary['percent'] * 100 + 0.05) / 100
|
||||
|
||||
letter_grade = grade_for_percentage(course.grade_cutoffs, grade_summary['percent'])
|
||||
grade_summary['grade'] = letter_grade
|
||||
|
||||
return grade_summary
|
||||
|
||||
- courseware_summary is a summary of all sections with problems in the
|
||||
course. It is organized as an array of chapters, each containing an array of
|
||||
sections, each containing an array of scores. This contains information for
|
||||
graded and ungraded problems, and is good for displaying a course summary
|
||||
with due dates, etc.
|
||||
def grade_for_percentage(grade_cutoffs, percentage):
|
||||
"""
|
||||
Returns a letter grade 'A' 'B' 'C' or None.
|
||||
|
||||
Arguments
|
||||
- grade_cutoffs is a dictionary mapping a grade to the lowest
|
||||
possible percentage to earn that grade.
|
||||
- percentage is the final percent across all problems in a course
|
||||
"""
|
||||
|
||||
letter_grade = None
|
||||
for possible_grade in ['A', 'B', 'C']:
|
||||
if percentage >= grade_cutoffs[possible_grade]:
|
||||
letter_grade = possible_grade
|
||||
break
|
||||
|
||||
return letter_grade
|
||||
|
||||
- grade_summary is the output from the course grader. More information on
|
||||
the format is in the docstring for CourseGrader.
|
||||
def progress_summary(student, course, grader, student_module_cache):
|
||||
"""
|
||||
This pulls a summary of all problems in the course.
|
||||
|
||||
Returns
|
||||
- courseware_summary is a summary of all sections with problems in the course.
|
||||
It is organized as an array of chapters, each containing an array of sections,
|
||||
each containing an array of scores. This contains information for graded and
|
||||
ungraded problems, and is good for displaying a course summary with due dates,
|
||||
etc.
|
||||
|
||||
Arguments:
|
||||
student: A User object for the student to grade
|
||||
@@ -30,49 +139,24 @@ def grade_sheet(student, course, grader, student_module_cache):
|
||||
student_module_cache: A StudentModuleCache initialized with all
|
||||
instance_modules for the student
|
||||
"""
|
||||
totaled_scores = {}
|
||||
chapters = []
|
||||
for c in course.get_children():
|
||||
sections = []
|
||||
for s in c.get_children():
|
||||
def yield_descendents(module):
|
||||
yield module
|
||||
for child in module.get_display_items():
|
||||
for module in yield_descendents(child):
|
||||
yield module
|
||||
|
||||
graded = s.metadata.get('graded', False)
|
||||
scores = []
|
||||
for module in yield_descendents(s):
|
||||
for module in yield_module_descendents(s):
|
||||
(correct, total) = get_score(student, module, student_module_cache)
|
||||
|
||||
if correct is None and total is None:
|
||||
continue
|
||||
|
||||
if settings.GENERATE_PROFILE_SCORES:
|
||||
if total > 1:
|
||||
correct = random.randrange(max(total - 2, 1), total + 1)
|
||||
else:
|
||||
correct = total
|
||||
|
||||
if not total > 0:
|
||||
#We simply cannot grade a problem that is 12/0, because we
|
||||
#might need it as a percentage
|
||||
graded = False
|
||||
|
||||
scores.append(Score(correct, total, graded,
|
||||
module.metadata.get('display_name')))
|
||||
scores.append(Score(correct, total, graded,
|
||||
module.metadata.get('display_name')))
|
||||
|
||||
section_total, graded_total = graders.aggregate_scores(
|
||||
scores, s.metadata.get('display_name'))
|
||||
|
||||
#Add the graded total to totaled_scores
|
||||
format = s.metadata.get('format', "")
|
||||
if format and graded_total.possible > 0:
|
||||
format_scores = totaled_scores.get(format, [])
|
||||
format_scores.append(graded_total)
|
||||
totaled_scores[format] = format_scores
|
||||
|
||||
sections.append({
|
||||
'display_name': s.display_name,
|
||||
'url_name': s.url_name,
|
||||
@@ -88,13 +172,10 @@ def grade_sheet(student, course, grader, student_module_cache):
|
||||
'url_name': c.url_name,
|
||||
'sections': sections})
|
||||
|
||||
grade_summary = grader.grade(totaled_scores)
|
||||
|
||||
return {'courseware_summary': chapters,
|
||||
'grade_summary': grade_summary}
|
||||
return chapters
|
||||
|
||||
|
||||
def get_score(user, problem, cache):
|
||||
def get_score(user, problem, student_module_cache):
|
||||
"""
|
||||
Return the score for a user on a problem
|
||||
|
||||
@@ -105,17 +186,18 @@ def get_score(user, problem, cache):
|
||||
correct = 0.0
|
||||
|
||||
# If the ID is not in the cache, add the item
|
||||
instance_module = cache.lookup(problem.category, problem.id)
|
||||
if instance_module is None:
|
||||
instance_module = StudentModule(module_type=problem.category,
|
||||
module_state_key=problem.id,
|
||||
student=user,
|
||||
state=None,
|
||||
grade=0,
|
||||
max_grade=problem.max_score(),
|
||||
done='i')
|
||||
cache.append(instance_module)
|
||||
instance_module.save()
|
||||
instance_module = get_instance_module(user, problem, student_module_cache)
|
||||
# instance_module = student_module_cache.lookup(problem.category, problem.id)
|
||||
# if instance_module is None:
|
||||
# instance_module = StudentModule(module_type=problem.category,
|
||||
# module_state_key=problem.id,
|
||||
# student=user,
|
||||
# state=None,
|
||||
# grade=0,
|
||||
# max_grade=problem.max_score(),
|
||||
# done='i')
|
||||
# cache.append(instance_module)
|
||||
# instance_module.save()
|
||||
|
||||
# If this problem is ungraded/ungradable, bail
|
||||
if instance_module.max_grade is None:
|
||||
@@ -126,8 +208,11 @@ def get_score(user, problem, cache):
|
||||
|
||||
if correct is not None and total is not None:
|
||||
#Now we re-weight the problem, if specified
|
||||
weight = getattr(problem, 'weight', 1)
|
||||
if weight != 1:
|
||||
weight = getattr(problem, 'weight', None)
|
||||
if weight is not None:
|
||||
if total == 0:
|
||||
log.exception("Cannot reweight a problem with zero weight. Problem: " + str(instance_module))
|
||||
return (correct, total)
|
||||
correct = correct * weight / total
|
||||
total = weight
|
||||
|
||||
|
||||
@@ -78,8 +78,8 @@ class Command(BaseCommand):
|
||||
|
||||
# TODO (cpennington): Get coursename in a legitimate way
|
||||
course_location = 'i4x://edx/6002xs12/course/6.002_Spring_2012'
|
||||
student_module_cache = StudentModuleCache(sample_user, modulestore().get_item(course_location))
|
||||
(course, _, _, _) = get_module(sample_user, None, course_location, student_module_cache)
|
||||
student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(sample_user, modulestore().get_item(course_location))
|
||||
course = get_module(sample_user, None, course_location, student_module_cache)
|
||||
|
||||
to_run = [
|
||||
#TODO (vshnayder) : make check_rendering work (use module_render.py),
|
||||
|
||||
@@ -67,17 +67,19 @@ class StudentModuleCache(object):
|
||||
"""
|
||||
A cache of StudentModules for a specific student
|
||||
"""
|
||||
def __init__(self, user, descriptor, depth=None):
|
||||
def __init__(self, user, descriptors):
|
||||
'''
|
||||
Find any StudentModule objects that are needed by any child modules of the
|
||||
supplied descriptor. Avoids making multiple queries to the database
|
||||
|
||||
descriptor: An XModuleDescriptor
|
||||
depth is the number of levels of descendent modules to load StudentModules for, in addition to
|
||||
the supplied descriptor. If depth is None, load all descendent StudentModules
|
||||
supplied descriptor, or caches only the StudentModule objects specifically
|
||||
for every descriptor in descriptors. Avoids making multiple queries to the
|
||||
database.
|
||||
|
||||
Arguments
|
||||
user: The user for which to fetch maching StudentModules
|
||||
descriptors: An array of XModuleDescriptors.
|
||||
'''
|
||||
if user.is_authenticated():
|
||||
module_ids = self._get_module_state_keys(descriptor, depth)
|
||||
module_ids = self._get_module_state_keys(descriptors)
|
||||
|
||||
# This works around a limitation in sqlite3 on the number of parameters
|
||||
# that can be put into a single query
|
||||
@@ -91,27 +93,52 @@ class StudentModuleCache(object):
|
||||
|
||||
else:
|
||||
self.cache = []
|
||||
|
||||
def _get_module_state_keys(self, descriptor, depth):
|
||||
'''
|
||||
Get a list of the state_keys needed for StudentModules
|
||||
required for this module descriptor
|
||||
|
||||
|
||||
|
||||
@classmethod
|
||||
def cache_for_descriptor_descendents(cls, user, descriptor, depth=None, descriptor_filter=lambda descriptor: True):
|
||||
"""
|
||||
descriptor: An XModuleDescriptor
|
||||
depth is the number of levels of descendent modules to load StudentModules for, in addition to
|
||||
the supplied descriptor. If depth is None, load all descendent StudentModules
|
||||
descriptor_filter is a function that accepts a descriptor and return wether the StudentModule
|
||||
should be cached
|
||||
"""
|
||||
|
||||
def get_child_descriptors(descriptor, depth, descriptor_filter):
|
||||
if descriptor_filter(descriptor):
|
||||
descriptors = [descriptor]
|
||||
else:
|
||||
descriptors = []
|
||||
|
||||
if depth is None or depth > 0:
|
||||
new_depth = depth - 1 if depth is not None else depth
|
||||
|
||||
for child in descriptor.get_children():
|
||||
descriptors.extend(get_child_descriptors(child, new_depth, descriptor_filter))
|
||||
|
||||
return descriptors
|
||||
|
||||
|
||||
descriptors = get_child_descriptors(descriptor, depth, descriptor_filter)
|
||||
|
||||
return StudentModuleCache(user, descriptors)
|
||||
|
||||
def _get_module_state_keys(self, descriptors):
|
||||
'''
|
||||
keys = [descriptor.location.url()]
|
||||
|
||||
shared_state_key = getattr(descriptor, 'shared_state_key', None)
|
||||
if shared_state_key is not None:
|
||||
keys.append(shared_state_key)
|
||||
|
||||
if depth is None or depth > 0:
|
||||
new_depth = depth - 1 if depth is not None else depth
|
||||
|
||||
for child in descriptor.get_children():
|
||||
keys.extend(self._get_module_state_keys(child, new_depth))
|
||||
Get a list of the state_keys needed for StudentModules
|
||||
required for this module descriptor
|
||||
|
||||
descriptor_filter is a function that accepts a descriptor and return wether the StudentModule
|
||||
should be cached
|
||||
'''
|
||||
keys = []
|
||||
for descriptor in descriptors:
|
||||
keys.append(descriptor.location.url())
|
||||
|
||||
shared_state_key = getattr(descriptor, 'shared_state_key', None)
|
||||
if shared_state_key is not None:
|
||||
keys.append(shared_state_key)
|
||||
|
||||
return keys
|
||||
|
||||
|
||||
@@ -50,9 +50,9 @@ def toc_for_course(user, request, course, active_chapter, active_section):
|
||||
|
||||
chapters with name 'hidden' are skipped.
|
||||
'''
|
||||
|
||||
student_module_cache = StudentModuleCache(user, course, depth=2)
|
||||
(course, _, _, _) = get_module(user, request, course.location, student_module_cache)
|
||||
|
||||
student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(user, course, depth=2)
|
||||
course = get_module(user, request, course.location, student_module_cache)
|
||||
|
||||
chapters = list()
|
||||
for chapter in course.get_display_items():
|
||||
@@ -121,25 +121,26 @@ def get_module(user, request, location, student_module_cache, position=None):
|
||||
- position : extra information from URL for user-specified
|
||||
position within module
|
||||
|
||||
Returns:
|
||||
- a tuple (xmodule instance, instance_module, shared_module, module category).
|
||||
instance_module is a StudentModule specific to this module for this student,
|
||||
or None if this is an anonymous user
|
||||
shared_module is a StudentModule specific to all modules with the same
|
||||
'shared_state_key' attribute, or None if the module does not elect to
|
||||
share state
|
||||
Returns: xmodule instance
|
||||
|
||||
'''
|
||||
descriptor = modulestore().get_item(location)
|
||||
|
||||
instance_module = student_module_cache.lookup(descriptor.category,
|
||||
descriptor.location.url())
|
||||
|
||||
shared_state_key = getattr(descriptor, 'shared_state_key', None)
|
||||
if shared_state_key is not None:
|
||||
shared_module = student_module_cache.lookup(descriptor.category,
|
||||
shared_state_key)
|
||||
|
||||
#TODO Only check the cache if this module can possibly have state
|
||||
if user.is_authenticated():
|
||||
instance_module = student_module_cache.lookup(descriptor.category,
|
||||
descriptor.location.url())
|
||||
|
||||
shared_state_key = getattr(descriptor, 'shared_state_key', None)
|
||||
if shared_state_key is not None:
|
||||
shared_module = student_module_cache.lookup(descriptor.category,
|
||||
shared_state_key)
|
||||
else:
|
||||
shared_module = None
|
||||
else:
|
||||
instance_module = None
|
||||
shared_module = None
|
||||
|
||||
|
||||
instance_state = instance_module.state if instance_module is not None else {}
|
||||
shared_state = shared_module.state if shared_module is not None else None
|
||||
@@ -163,9 +164,8 @@ def get_module(user, request, location, student_module_cache, position=None):
|
||||
'default_queuename': xqueue_default_queuename.replace(' ','_') }
|
||||
|
||||
def _get_module(location):
|
||||
(module, _, _, _) = get_module(user, request, location,
|
||||
return get_module(user, request, location,
|
||||
student_module_cache, position)
|
||||
return module
|
||||
|
||||
# TODO (cpennington): When modules are shared between courses, the static
|
||||
# prefix is going to have to be specific to the module, not the directory
|
||||
@@ -198,31 +198,59 @@ def get_module(user, request, location, student_module_cache, position=None):
|
||||
if has_staff_access_to_course(user, module.location.course):
|
||||
module.get_html = add_histogram(module.get_html, module)
|
||||
|
||||
# If StudentModule for this instance wasn't already in the database,
|
||||
# and this isn't a guest user, create it.
|
||||
return module
|
||||
|
||||
def get_instance_module(user, module, student_module_cache):
|
||||
"""
|
||||
Returns instance_module is a StudentModule specific to this module for this student,
|
||||
or None if this is an anonymous user
|
||||
"""
|
||||
if user.is_authenticated():
|
||||
instance_module = student_module_cache.lookup(module.category,
|
||||
module.location.url())
|
||||
|
||||
if not instance_module:
|
||||
instance_module = StudentModule(
|
||||
student=user,
|
||||
module_type=descriptor.category,
|
||||
module_type=module.category,
|
||||
module_state_key=module.id,
|
||||
state=module.get_instance_state(),
|
||||
max_grade=module.max_score())
|
||||
instance_module.save()
|
||||
# Add to cache. The caller and the system context have references
|
||||
# to it, so the change persists past the return
|
||||
student_module_cache.append(instance_module)
|
||||
if not shared_module and shared_state_key is not None:
|
||||
shared_module = StudentModule(
|
||||
student=user,
|
||||
module_type=descriptor.category,
|
||||
module_state_key=shared_state_key,
|
||||
state=module.get_shared_state())
|
||||
shared_module.save()
|
||||
student_module_cache.append(shared_module)
|
||||
|
||||
return (module, instance_module, shared_module, descriptor.category)
|
||||
|
||||
|
||||
return instance_module
|
||||
else:
|
||||
return None
|
||||
|
||||
def get_shared_instance_module(user, module, student_module_cache):
|
||||
"""
|
||||
Return shared_module is a StudentModule specific to all modules with the same
|
||||
'shared_state_key' attribute, or None if the module does not elect to
|
||||
share state
|
||||
"""
|
||||
if user.is_authenticated():
|
||||
# To get the shared_state_key, we need to descriptor
|
||||
descriptor = modulestore().get_item(module.location)
|
||||
|
||||
shared_state_key = getattr(module, 'shared_state_key', None)
|
||||
if shared_state_key is not None:
|
||||
shared_module = student_module_cache.lookup(module.category,
|
||||
shared_state_key)
|
||||
if not shared_module:
|
||||
shared_module = StudentModule(
|
||||
student=user,
|
||||
module_type=descriptor.category,
|
||||
module_state_key=shared_state_key,
|
||||
state=module.get_shared_state())
|
||||
shared_module.save()
|
||||
student_module_cache.append(shared_module)
|
||||
else:
|
||||
shared_module = None
|
||||
|
||||
return shared_module
|
||||
else:
|
||||
return None
|
||||
|
||||
@csrf_exempt
|
||||
def xqueue_callback(request, userid, id, dispatch):
|
||||
@@ -240,12 +268,13 @@ def xqueue_callback(request, userid, id, dispatch):
|
||||
# Retrieve target StudentModule
|
||||
user = User.objects.get(id=userid)
|
||||
|
||||
student_module_cache = StudentModuleCache(user, modulestore().get_item(id))
|
||||
instance, instance_module, shared_module, module_type = get_module(request.user, request, id, student_module_cache)
|
||||
student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(user, modulestore().get_item(id))
|
||||
instance = get_module(user, request, id, student_module_cache)
|
||||
instance_module = get_instance_module(user, instance, student_module_cache)
|
||||
|
||||
if instance_module is None:
|
||||
log.debug("Couldn't find module '%s' for user '%s'",
|
||||
id, request.user)
|
||||
id, user)
|
||||
raise Http404
|
||||
|
||||
oldgrade = instance_module.grade
|
||||
@@ -285,16 +314,18 @@ def modx_dispatch(request, dispatch=None, id=None):
|
||||
- id -- the module id. Used to look up the XModule instance
|
||||
'''
|
||||
# ''' (fix emacs broken parsing)
|
||||
|
||||
# Check for submitted files
|
||||
p = request.POST.copy()
|
||||
if request.FILES:
|
||||
for inputfile_id in request.FILES.keys():
|
||||
p[inputfile_id] = request.FILES[inputfile_id]
|
||||
|
||||
student_module_cache = StudentModuleCache(request.user, modulestore().get_item(id))
|
||||
instance, instance_module, shared_module, module_type = get_module(request.user, request, id, student_module_cache)
|
||||
student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(request.user, modulestore().get_item(id))
|
||||
instance = get_module(request.user, request, id, student_module_cache)
|
||||
|
||||
instance_module = get_instance_module(request.user, instance, student_module_cache)
|
||||
shared_module = get_shared_instance_module(request.user, instance, student_module_cache)
|
||||
|
||||
# Don't track state for anonymous users (who don't have student modules)
|
||||
if instance_module is not None:
|
||||
oldgrade = instance_module.grade
|
||||
|
||||
@@ -135,10 +135,25 @@ class ActivateLoginTestCase(TestCase):
|
||||
class PageLoader(ActivateLoginTestCase):
|
||||
''' Base class that adds a function to load all pages in a modulestore '''
|
||||
|
||||
|
||||
def enroll(self, course):
|
||||
resp = self.client.post('/change_enrollment', {
|
||||
'enrollment_action': 'enroll',
|
||||
'course_id': course.id,
|
||||
})
|
||||
data = parse_json(resp)
|
||||
self.assertTrue(data['success'])
|
||||
|
||||
def check_pages_load(self, course_name, data_dir, modstore):
|
||||
print "Checking course {0} in {1}".format(course_name, data_dir)
|
||||
import_from_xml(modstore, data_dir, [course_name])
|
||||
|
||||
# enroll in the course before trying to access pages
|
||||
courses = modstore.get_courses()
|
||||
self.assertEqual(len(courses), 1)
|
||||
course = courses[0]
|
||||
self.enroll(course)
|
||||
|
||||
n = 0
|
||||
num_bad = 0
|
||||
all_ok = True
|
||||
|
||||
@@ -48,7 +48,6 @@ log = logging.getLogger("mitx.courseware")
|
||||
|
||||
template_imports = {'urllib': urllib}
|
||||
|
||||
|
||||
def user_groups(user):
|
||||
if not user.is_authenticated():
|
||||
return []
|
||||
@@ -59,6 +58,8 @@ def user_groups(user):
|
||||
|
||||
# Kill caching on dev machines -- we switch groups a lot
|
||||
group_names = cache.get(key)
|
||||
if settings.DEBUG:
|
||||
group_names = None
|
||||
|
||||
if group_names is None:
|
||||
group_names = [u.name for u in UserTestGroup.objects.filter(users=user)]
|
||||
@@ -82,18 +83,17 @@ def gradebook(request, course_id):
|
||||
if 'course_admin' not in user_groups(request.user):
|
||||
raise Http404
|
||||
course = check_course(course_id)
|
||||
|
||||
|
||||
student_objects = User.objects.all()[:100]
|
||||
student_info = []
|
||||
|
||||
for student in student_objects:
|
||||
student_module_cache = StudentModuleCache(student, course)
|
||||
course, _, _, _ = get_module(request.user, request, course.location, student_module_cache)
|
||||
|
||||
#TODO: Only select students who are in the course
|
||||
for student in student_objects:
|
||||
student_info.append({
|
||||
'username': student.username,
|
||||
'id': student.id,
|
||||
'email': student.email,
|
||||
'grade_info': grades.grade_sheet(student, course, student_module_cache),
|
||||
'grade_summary': grades.grade(student, request, course),
|
||||
'realname': UserProfile.objects.get(user=student).name
|
||||
})
|
||||
|
||||
@@ -116,18 +116,23 @@ def profile(request, course_id, student_id=None):
|
||||
|
||||
user_info = UserProfile.objects.get(user=student)
|
||||
|
||||
student_module_cache = StudentModuleCache(request.user, course)
|
||||
course_module, _, _, _ = get_module(request.user, request, course.location, student_module_cache)
|
||||
|
||||
student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(request.user, course)
|
||||
course_module = get_module(request.user, request, course.location, student_module_cache)
|
||||
|
||||
courseware_summary = grades.progress_summary(student, course_module, course.grader, student_module_cache)
|
||||
grade_summary = grades.grade(request.user, request, course, student_module_cache)
|
||||
|
||||
context = {'name': user_info.name,
|
||||
'username': student.username,
|
||||
'location': user_info.location,
|
||||
'language': user_info.language,
|
||||
'email': student.email,
|
||||
'course': course,
|
||||
'csrf': csrf(request)['csrf_token']
|
||||
'csrf': csrf(request)['csrf_token'],
|
||||
'courseware_summary' : courseware_summary,
|
||||
'grade_summary' : grade_summary
|
||||
}
|
||||
context.update(grades.grade_sheet(student, course_module, course.grader, student_module_cache))
|
||||
context.update()
|
||||
|
||||
return render_to_response('profile.html', context)
|
||||
|
||||
@@ -198,11 +203,12 @@ def index(request, course_id, chapter=None, section=None,
|
||||
if look_for_module:
|
||||
section_descriptor = get_section(course, chapter, section)
|
||||
if section_descriptor is not None:
|
||||
student_module_cache = StudentModuleCache(request.user,
|
||||
student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(
|
||||
request.user,
|
||||
section_descriptor)
|
||||
module, _, _, _ = get_module(request.user, request,
|
||||
section_descriptor.location,
|
||||
student_module_cache)
|
||||
module = get_module(request.user, request,
|
||||
section_descriptor.location,
|
||||
student_module_cache)
|
||||
context['content'] = module.get_html()
|
||||
else:
|
||||
log.warning("Couldn't find a section descriptor for course_id '{0}',"
|
||||
|
||||
@@ -158,6 +158,9 @@ COURSE_SETTINGS = {'6.002x_Fall_2012': {'number' : '6.002x',
|
||||
}
|
||||
}
|
||||
|
||||
# IP addresses that are allowed to reload the course, etc.
|
||||
# TODO (vshnayder): Will probably need to change as we get real access control in.
|
||||
LMS_MIGRATION_ALLOWED_IPS = []
|
||||
|
||||
############################### XModule Store ##################################
|
||||
MODULESTORE = {
|
||||
|
||||
@@ -16,3 +16,8 @@ MITX_FEATURES['ENABLE_TEXTBOOK'] = False
|
||||
MITX_FEATURES['ENABLE_DISCUSSION'] = False
|
||||
MITX_FEATURES['ACCESS_REQUIRE_STAFF_FOR_COURSE'] = True # require that user be in the staff_* group to be able to enroll
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# disable django debug toolbars
|
||||
|
||||
INSTALLED_APPS = tuple([ app for app in INSTALLED_APPS if not app.startswith('debug_toolbar') ])
|
||||
MIDDLEWARE_CLASSES = tuple([ mcl for mcl in MIDDLEWARE_CLASSES if not mcl.startswith('debug_toolbar') ])
|
||||
|
||||
BIN
lms/static/images/textbook/textbook-left.png
Normal file
BIN
lms/static/images/textbook/textbook-left.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 783 B |
BIN
lms/static/images/textbook/textbook-right.png
Normal file
BIN
lms/static/images/textbook/textbook-right.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 822 B |
@@ -20,7 +20,7 @@ div.info-wrapper {
|
||||
|
||||
> li {
|
||||
@extend .clearfix;
|
||||
border-bottom: 1px solid #e3e3e3;
|
||||
border-bottom: 1px solid lighten($border-color, 10%);
|
||||
margin-bottom: lh();
|
||||
padding-bottom: lh(.5);
|
||||
list-style-type: disk;
|
||||
@@ -76,42 +76,29 @@ div.info-wrapper {
|
||||
h1 {
|
||||
@extend .bottom-border;
|
||||
padding: lh(.5) lh(.5);
|
||||
}
|
||||
|
||||
header {
|
||||
|
||||
// h1 {
|
||||
// font-weight: 100;
|
||||
// font-style: italic;
|
||||
// }
|
||||
|
||||
p {
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
margin-bottom: 0;
|
||||
margin-top: 4px;
|
||||
}
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
ol {
|
||||
background: none;
|
||||
list-style: none;
|
||||
padding-left: 0;
|
||||
margin: 0;
|
||||
|
||||
li {
|
||||
@extend .clearfix;
|
||||
background: none;
|
||||
border-bottom: 1px solid #d3d3d3;
|
||||
@include box-shadow(0 1px 0 #eee);
|
||||
border-bottom: 1px solid $border-color;
|
||||
@include box-sizing(border-box);
|
||||
padding: em(7) lh(.75);
|
||||
position: relative;
|
||||
font-size: 1em;
|
||||
|
||||
&.expandable,
|
||||
&.collapsable {
|
||||
h4 {
|
||||
font-style: $body-font-size;
|
||||
font-weight: normal;
|
||||
font-size: 1em;
|
||||
padding-left: 18px;
|
||||
}
|
||||
}
|
||||
@@ -122,16 +109,12 @@ div.info-wrapper {
|
||||
|
||||
li {
|
||||
border-bottom: 0;
|
||||
border-top: 1px solid #d3d3d3;
|
||||
border-top: 1px solid $border-color;
|
||||
@include box-shadow(inset 0 1px 0 #eee);
|
||||
padding-left: lh(1.5);
|
||||
font-size: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: #e9e9e9;
|
||||
}
|
||||
|
||||
div.hitarea {
|
||||
background-image: url('../images/treeview-default.gif');
|
||||
display: block;
|
||||
@@ -159,14 +142,12 @@ div.info-wrapper {
|
||||
h3 {
|
||||
border-bottom: 0;
|
||||
@include box-shadow(none);
|
||||
color: #999;
|
||||
font-size: $body-font-size;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
color: #aaa;
|
||||
font-size: 1em;
|
||||
margin-bottom: em(6);
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: $body-font-size;
|
||||
letter-spacing: 0;
|
||||
margin: 0;
|
||||
text-transform: none;
|
||||
@@ -191,14 +172,8 @@ div.info-wrapper {
|
||||
}
|
||||
|
||||
a {
|
||||
color: lighten($text-color, 10%);
|
||||
@include inline-block();
|
||||
text-decoration: none;
|
||||
@include transition();
|
||||
|
||||
&:hover {
|
||||
color: $mit-red;
|
||||
}
|
||||
line-height: lh();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,38 +10,26 @@ div.profile-wrapper {
|
||||
|
||||
header {
|
||||
@extend .bottom-border;
|
||||
margin: 0 ;
|
||||
padding: lh(.5) lh();
|
||||
margin: 0;
|
||||
padding: lh(.5);
|
||||
|
||||
h1 {
|
||||
font-size: 18px;
|
||||
margin: 0;
|
||||
padding-right: 30px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
position: absolute;
|
||||
right: lh(.5);
|
||||
text-transform: uppercase;
|
||||
top: 13px;
|
||||
|
||||
&:hover {
|
||||
color: #555;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
||||
li {
|
||||
border-bottom: 1px solid #d3d3d3;
|
||||
@include box-shadow(0 1px 0 #eee);
|
||||
color: lighten($text-color, 10%);
|
||||
display: block;
|
||||
padding: 7px lh();
|
||||
padding: lh(.5) 0 lh(.5) lh(.5);
|
||||
position: relative;
|
||||
text-decoration: none;
|
||||
@include transition();
|
||||
@@ -144,11 +132,14 @@ div.profile-wrapper {
|
||||
@extend .content;
|
||||
|
||||
header {
|
||||
@extend h1.top-header;
|
||||
@extend .clearfix;
|
||||
@extend h1.top-header;
|
||||
margin-bottom: lh();
|
||||
|
||||
h1 {
|
||||
float: left;
|
||||
font-size: 1em;
|
||||
font-weight: 100;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
@@ -162,6 +153,7 @@ div.profile-wrapper {
|
||||
border-top: 1px solid #e3e3e3;
|
||||
list-style: none;
|
||||
margin-top: lh();
|
||||
padding-left: 0;
|
||||
|
||||
> li {
|
||||
@extend .clearfix;
|
||||
@@ -178,9 +170,11 @@ div.profile-wrapper {
|
||||
border-right: 1px dashed #ddd;
|
||||
@include box-sizing(border-box);
|
||||
display: table-cell;
|
||||
letter-spacing: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
padding-right: flex-gutter(9);
|
||||
text-transform: none;
|
||||
width: flex-grid(2, 9);
|
||||
}
|
||||
|
||||
@@ -203,14 +197,39 @@ div.profile-wrapper {
|
||||
|
||||
h3 {
|
||||
color: #666;
|
||||
|
||||
span {
|
||||
color: #999;
|
||||
font-size: em(14);
|
||||
font-weight: 100;
|
||||
}
|
||||
}
|
||||
|
||||
ol {
|
||||
list-style: none;
|
||||
p {
|
||||
color: #999;
|
||||
font-size: em(14);
|
||||
}
|
||||
|
||||
li {
|
||||
display: inline-block;
|
||||
padding-right: 1em;
|
||||
section.scores {
|
||||
margin: lh(.5) 0;
|
||||
|
||||
h3 {
|
||||
font-size: em(14);
|
||||
@include inline-block;
|
||||
}
|
||||
|
||||
ol {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
@include inline-block;
|
||||
|
||||
li {
|
||||
@include inline-block;
|
||||
font-size: em(14);
|
||||
font-weight: normal;
|
||||
padding-right: 1em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ div.book-wrapper {
|
||||
@include box-sizing(border-box);
|
||||
|
||||
ul#booknav {
|
||||
font-size: 12px;
|
||||
font-size: $body-font-size;
|
||||
|
||||
a {
|
||||
color: #000;
|
||||
@@ -39,8 +39,7 @@ div.book-wrapper {
|
||||
}
|
||||
|
||||
> li {
|
||||
border-bottom: 1px solid #d3d3d3;
|
||||
@include box-shadow(0 1px 0 #eee);
|
||||
border-bottom: 1px solid $border-color;
|
||||
padding: 7px 7px 7px 30px;
|
||||
}
|
||||
}
|
||||
@@ -48,9 +47,11 @@ div.book-wrapper {
|
||||
|
||||
section.book {
|
||||
@extend .content;
|
||||
padding-right: 0;
|
||||
padding-bottom: 0;
|
||||
padding-top: 0;
|
||||
|
||||
nav {
|
||||
@extend .topbar;
|
||||
@extend .clearfix;
|
||||
|
||||
a {
|
||||
@@ -62,32 +63,57 @@ div.book-wrapper {
|
||||
@extend .clearfix;
|
||||
|
||||
li {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: flex-grid(2, 8);
|
||||
|
||||
a {
|
||||
display: table;
|
||||
@include box-sizing(border-box);
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
vertical-align: middle;
|
||||
@include transition;
|
||||
background-color: rgba(#000, .7);
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
opacity: 0;
|
||||
filter: alpha(opacity=0);
|
||||
text-indent: -9999px;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
filter: alpha(opacity=100);
|
||||
|
||||
&.last {
|
||||
}
|
||||
|
||||
&.next {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.last {
|
||||
display: block;
|
||||
float: left;
|
||||
left: 0;
|
||||
|
||||
a {
|
||||
border-left: 0;
|
||||
border-right: 1px solid darken(#f6efd4, 20%);
|
||||
@include box-shadow(inset -1px 0 0 lighten(#f6efd4, 5%));
|
||||
background-image: url('../images/textbook/textbook-left.png');
|
||||
}
|
||||
}
|
||||
|
||||
&.next {
|
||||
display: block;
|
||||
float: right;
|
||||
right: 0;
|
||||
|
||||
a {
|
||||
background-image: url('../images/textbook/textbook-right.png');
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.bottom-nav {
|
||||
border-bottom: 0;
|
||||
border-top: 1px solid #EDDFAA;
|
||||
margin-bottom: -(lh());
|
||||
margin-top: lh();
|
||||
}
|
||||
@@ -95,9 +121,10 @@ div.book-wrapper {
|
||||
|
||||
section.page {
|
||||
text-align: center;
|
||||
position: relative;
|
||||
border: 1px solid $border-color;
|
||||
|
||||
img {
|
||||
border: 1px solid $border-color;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
body {
|
||||
font-family: $sans-serif;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
body, h1, h2, h3, h4, h5, h6, p, p a:link, p a:visited, a {
|
||||
font-family: $sans-serif;
|
||||
}
|
||||
|
||||
@@ -19,3 +15,9 @@ table {
|
||||
table-layout: fixed;
|
||||
}
|
||||
}
|
||||
|
||||
form {
|
||||
label {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
h1.top-header {
|
||||
border-bottom: 1px solid #e3e3e3;
|
||||
text-align: left;
|
||||
font-size: 24px;
|
||||
font-size: em(24);
|
||||
font-weight: 100;
|
||||
padding-bottom: lh();
|
||||
}
|
||||
@@ -51,7 +51,6 @@ h1.top-header {
|
||||
|
||||
.sidebar {
|
||||
border-right: 1px solid #C8C8C8;
|
||||
@include box-shadow(inset -1px 0 0 #e6e6e6);
|
||||
@include box-sizing(border-box);
|
||||
display: table-cell;
|
||||
font-family: $sans-serif;
|
||||
@@ -75,7 +74,7 @@ h1.top-header {
|
||||
}
|
||||
|
||||
.bottom-border {
|
||||
border-bottom: 1px solid #d3d3d3;
|
||||
border-bottom: 1px solid $border-color;
|
||||
}
|
||||
|
||||
@media print {
|
||||
|
||||
@@ -4,78 +4,70 @@ div#wiki_panel {
|
||||
|
||||
h2 {
|
||||
@extend .bottom-border;
|
||||
font-size: 18px;
|
||||
margin: 0 ;
|
||||
padding: lh(.5) lh();
|
||||
}
|
||||
|
||||
input[type="button"] {
|
||||
background: transparent;
|
||||
border: none;
|
||||
@include box-shadow(none);
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
margin: 0px;
|
||||
padding: 7px lh();
|
||||
text-align: left;
|
||||
@include transition();
|
||||
width: 100%;
|
||||
padding: lh(.5) lh() lh(.5) 0;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
ul {
|
||||
padding-left: 0;
|
||||
margin: 0;
|
||||
|
||||
li {
|
||||
@include box-shadow(inset 0 1px 0 0 #eee);
|
||||
border-top: 1px solid #d3d3d3;
|
||||
|
||||
&:hover {
|
||||
background: #efefef;
|
||||
@include background-image(linear-gradient(-90deg, rgb(245,245,245), rgb(225,225,225)));
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
border: none;
|
||||
}
|
||||
@extend .bottom-border;
|
||||
|
||||
&.search {
|
||||
padding: 10px lh();
|
||||
padding: 10px lh() 10px 0;
|
||||
|
||||
label {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.create-article {
|
||||
h3 {
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
padding: 7px lh();
|
||||
padding: 7px lh() 7px 0;
|
||||
|
||||
&:hover {
|
||||
background: #efefef;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
form {
|
||||
input[type="submit"]{
|
||||
@extend .light-button;
|
||||
text-transform: none;
|
||||
text-shadow: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
div#wiki_create_form {
|
||||
@extend .clearfix;
|
||||
background: #dadada;
|
||||
border-bottom: 1px solid #d3d3d3;
|
||||
padding: 15px;
|
||||
padding: lh(.5) lh() lh(.5) 0;
|
||||
|
||||
label {
|
||||
font-family: $sans-serif;
|
||||
margin-bottom: lh(.5);
|
||||
}
|
||||
|
||||
input[type="text"] {
|
||||
@include box-sizing(border-box);
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
width: 100%;
|
||||
margin-bottom: lh(.5);
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
|
||||
li {
|
||||
float: left;
|
||||
border-bottom: 0;
|
||||
|
||||
&#cancel {
|
||||
float: right;
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0; }
|
||||
|
||||
.wrapper, .subpage, section.copyright, section.tos, section.privacy-policy, section.honor-code, header.announcement div, section.index-content, footer {
|
||||
margin: 0;
|
||||
overflow: hidden; }
|
||||
|
||||
div#enroll form {
|
||||
display: none; }
|
||||
@@ -1,1017 +0,0 @@
|
||||
/*
|
||||
html5doctor.com Reset Stylesheet
|
||||
v1.6.1
|
||||
Last Updated: 2010-09-17
|
||||
Author: Richard Clark - http://richclarkdesign.com
|
||||
Twitter: @rich_clark
|
||||
*/
|
||||
html, body, div, span, object, iframe,
|
||||
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
|
||||
abbr, address, cite, code,
|
||||
del, dfn, em, img, ins, kbd, q, samp,
|
||||
small, strong, var,
|
||||
b, i,
|
||||
dl, dt, dd, ol, ul, li,
|
||||
fieldset, form, label, legend,
|
||||
table, caption, tbody, tfoot, thead, tr, th, td,
|
||||
article, aside, canvas, details, figcaption, figure,
|
||||
footer, header, hgroup, menu, nav, section, summary,
|
||||
time, mark, audio, video {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
font-size: 100%;
|
||||
vertical-align: baseline;
|
||||
background: transparent; }
|
||||
|
||||
body {
|
||||
line-height: 1; }
|
||||
|
||||
article, aside, details, figcaption, figure,
|
||||
footer, header, hgroup, menu, nav, section {
|
||||
display: block; }
|
||||
|
||||
nav ul {
|
||||
list-style: none; }
|
||||
|
||||
blockquote, q {
|
||||
quotes: none; }
|
||||
|
||||
blockquote:before, blockquote:after,
|
||||
q:before, q:after {
|
||||
content: '';
|
||||
content: none; }
|
||||
|
||||
a {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-size: 100%;
|
||||
vertical-align: baseline;
|
||||
background: transparent; }
|
||||
|
||||
/* change colours to suit your needs */
|
||||
ins {
|
||||
background-color: #ff9;
|
||||
color: #000;
|
||||
text-decoration: none; }
|
||||
|
||||
/* change colours to suit your needs */
|
||||
mark {
|
||||
background-color: #ff9;
|
||||
color: #000;
|
||||
font-style: italic;
|
||||
font-weight: bold; }
|
||||
|
||||
del {
|
||||
text-decoration: line-through; }
|
||||
|
||||
abbr[title], dfn[title] {
|
||||
border-bottom: 1px dotted;
|
||||
cursor: help; }
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0; }
|
||||
|
||||
/* change border colour to suit your needs */
|
||||
hr {
|
||||
display: block;
|
||||
height: 1px;
|
||||
border: 0;
|
||||
border-top: 1px solid #cccccc;
|
||||
margin: 1em 0;
|
||||
padding: 0; }
|
||||
|
||||
input, select {
|
||||
vertical-align: middle; }
|
||||
|
||||
/* Generated by Font Squirrel (http://www.fontsquirrel.com) on January 25, 2012 05:06:34 PM America/New_York */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
src: url("../fonts/OpenSans-Regular-webfont.eot");
|
||||
src: url("../fonts/OpenSans-Regular-webfont.eot?#iefix") format("embedded-opentype"), url("../fonts/OpenSans-Regular-webfont.woff") format("woff"), url("../fonts/OpenSans-Regular-webfont.ttf") format("truetype"), url("../fonts/OpenSans-Regular-webfont.svg#OpenSansRegular") format("svg");
|
||||
font-weight: 600;
|
||||
font-style: normal; }
|
||||
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
src: url("../fonts/OpenSans-Italic-webfont.eot");
|
||||
src: url("../fonts/OpenSans-Italic-webfont.eot?#iefix") format("embedded-opentype"), url("../fonts/OpenSans-Italic-webfont.woff") format("woff"), url("../fonts/OpenSans-Italic-webfont.ttf") format("truetype"), url("../fonts/OpenSans-Italic-webfont.svg#OpenSansItalic") format("svg");
|
||||
font-weight: 400;
|
||||
font-style: italic; }
|
||||
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
src: url("../fonts/OpenSans-Bold-webfont.eot");
|
||||
src: url("../fonts/OpenSans-Bold-webfont.eot?#iefix") format("embedded-opentype"), url("../fonts/OpenSans-Bold-webfont.woff") format("woff"), url("../fonts/OpenSans-Bold-webfont.ttf") format("truetype"), url("../fonts/OpenSans-Bold-webfont.svg#OpenSansBold") format("svg");
|
||||
font-weight: 700;
|
||||
font-style: normal; }
|
||||
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
src: url("../fonts/OpenSans-BoldItalic-webfont.eot");
|
||||
src: url("../fonts/OpenSans-BoldItalic-webfont.eot?#iefix") format("embedded-opentype"), url("../fonts/OpenSans-BoldItalic-webfont.woff") format("woff"), url("../fonts/OpenSans-BoldItalic-webfont.ttf") format("truetype"), url("../fonts/OpenSans-BoldItalic-webfont.svg#OpenSansBoldItalic") format("svg");
|
||||
font-weight: 700;
|
||||
font-style: italic; }
|
||||
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
src: url("../fonts/OpenSans-ExtraBold-webfont.eot");
|
||||
src: url("../fonts/OpenSans-ExtraBold-webfont.eot?#iefix") format("embedded-opentype"), url("../fonts/OpenSans-ExtraBold-webfont.woff") format("woff"), url("../fonts/OpenSans-ExtraBold-webfont.ttf") format("truetype"), url("../fonts/OpenSans-ExtraBold-webfont.svg#OpenSansExtrabold") format("svg");
|
||||
font-weight: 800;
|
||||
font-style: normal; }
|
||||
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
src: url("../fonts/OpenSans-ExtraBoldItalic-webfont.eot");
|
||||
src: url("../fonts/OpenSans-ExtraBoldItalic-webfont.eot?#iefix") format("embedded-opentype"), url("../fonts/OpenSans-ExtraBoldItalic-webfont.woff") format("woff"), url("../fonts/OpenSans-ExtraBoldItalic-webfont.ttf") format("truetype"), url("../fonts/OpenSans-ExtraBoldItalic-webfont.svg#OpenSansExtraboldItalic") format("svg");
|
||||
font-weight: 800;
|
||||
font-style: italic; }
|
||||
|
||||
.wrapper, .subpage, section.copyright, section.tos, section.privacy-policy, section.honor-code, header.announcement div, footer, section.index-content {
|
||||
-webkit-box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
margin: 0 auto;
|
||||
max-width: 1400px;
|
||||
padding: 25.888px;
|
||||
width: 100%; }
|
||||
|
||||
.subpage > div, section.copyright > div, section.tos > div, section.privacy-policy > div, section.honor-code > div {
|
||||
padding-left: 34.171%; }
|
||||
@media screen and (max-width: 940px) {
|
||||
.subpage > div, section.copyright > div, section.tos > div, section.privacy-policy > div, section.honor-code > div {
|
||||
padding-left: 0; } }
|
||||
.subpage > div p, section.copyright > div p, section.tos > div p, section.privacy-policy > div p, section.honor-code > div p {
|
||||
margin-bottom: 25.888px;
|
||||
line-height: 25.888px; }
|
||||
.subpage > div h1, section.copyright > div h1, section.tos > div h1, section.privacy-policy > div h1, section.honor-code > div h1 {
|
||||
margin-bottom: 12.944px; }
|
||||
.subpage > div h2, section.copyright > div h2, section.tos > div h2, section.privacy-policy > div h2, section.honor-code > div h2 {
|
||||
font: 18px "Open Sans", Helvetica, Arial, sans-serif;
|
||||
color: #000;
|
||||
margin-bottom: 12.944px; }
|
||||
.subpage > div ul, section.copyright > div ul, section.tos > div ul, section.privacy-policy > div ul, section.honor-code > div ul {
|
||||
list-style: disc outside none; }
|
||||
.subpage > div ul li, section.copyright > div ul li, section.tos > div ul li, section.privacy-policy > div ul li, section.honor-code > div ul li {
|
||||
list-style: disc outside none;
|
||||
line-height: 25.888px; }
|
||||
.subpage > div dl, section.copyright > div dl, section.tos > div dl, section.privacy-policy > div dl, section.honor-code > div dl {
|
||||
margin-bottom: 25.888px; }
|
||||
.subpage > div dl dd, section.copyright > div dl dd, section.tos > div dl dd, section.privacy-policy > div dl dd, section.honor-code > div dl dd {
|
||||
margin-bottom: 12.944px; }
|
||||
|
||||
.clearfix:after, .subpage:after, section.copyright:after, section.tos:after, section.privacy-policy:after, section.honor-code:after, header.announcement div section:after, footer:after, section.index-content:after, section.index-content section:after, section.index-content section.about section:after, div.leanModal_box#enroll ol:after {
|
||||
content: ".";
|
||||
display: block;
|
||||
height: 0;
|
||||
clear: both;
|
||||
visibility: hidden; }
|
||||
|
||||
.button, header.announcement div section.course section a, section.index-content section.course a, section.index-content section.staff a, section.index-content section.about-course section.cta a.enroll {
|
||||
-webkit-border-radius: 3px;
|
||||
-moz-border-radius: 3px;
|
||||
-ms-border-radius: 3px;
|
||||
-o-border-radius: 3px;
|
||||
border-radius: 3px;
|
||||
display: -moz-inline-box;
|
||||
-moz-box-orient: vertical;
|
||||
display: inline-block;
|
||||
vertical-align: baseline;
|
||||
zoom: 1;
|
||||
*display: inline;
|
||||
*vertical-align: auto;
|
||||
-webkit-transition-property: all;
|
||||
-moz-transition-property: all;
|
||||
-ms-transition-property: all;
|
||||
-o-transition-property: all;
|
||||
transition-property: all;
|
||||
-webkit-transition-duration: 0.15s;
|
||||
-moz-transition-duration: 0.15s;
|
||||
-ms-transition-duration: 0.15s;
|
||||
-o-transition-duration: 0.15s;
|
||||
transition-duration: 0.15s;
|
||||
-webkit-transition-timing-function: ease-out;
|
||||
-moz-transition-timing-function: ease-out;
|
||||
-ms-transition-timing-function: ease-out;
|
||||
-o-transition-timing-function: ease-out;
|
||||
transition-timing-function: ease-out;
|
||||
-webkit-transition-delay: 0;
|
||||
-moz-transition-delay: 0;
|
||||
-ms-transition-delay: 0;
|
||||
-o-transition-delay: 0;
|
||||
transition-delay: 0;
|
||||
background-color: #993333;
|
||||
border: 1px solid #732626;
|
||||
color: #fff;
|
||||
margin: 25.888px 0 12.944px;
|
||||
padding: 6.472px 12.944px;
|
||||
text-decoration: none;
|
||||
font-style: normal;
|
||||
-webkit-box-shadow: inset 0 1px 0 #b83d3d;
|
||||
-moz-box-shadow: inset 0 1px 0 #b83d3d;
|
||||
box-shadow: inset 0 1px 0 #b83d3d;
|
||||
-webkit-font-smoothing: antialiased; }
|
||||
.button:hover, header.announcement div section.course section a:hover, section.index-content section.course a:hover, section.index-content section.staff a:hover, section.index-content section.about-course section.cta a.enroll:hover {
|
||||
background-color: #732626;
|
||||
border-color: #4d1919; }
|
||||
.button span, header.announcement div section.course section a span, section.index-content section.course a span, section.index-content section.staff a span, section.index-content section.about-course section.cta a.enroll span {
|
||||
font-family: Garamond, Baskerville, "Baskerville Old Face", "Hoefler Text", "Times New Roman", serif;
|
||||
font-style: italic; }
|
||||
|
||||
p.ie-warning {
|
||||
display: block !important;
|
||||
line-height: 1.3em;
|
||||
background: yellow;
|
||||
margin-bottom: 25.888px;
|
||||
padding: 25.888px; }
|
||||
|
||||
body {
|
||||
background-color: #fff;
|
||||
color: #444;
|
||||
font: 16px Georgia, serif; }
|
||||
body :focus {
|
||||
outline-color: #ccc; }
|
||||
body h1 {
|
||||
font: 800 24px "Open Sans", Helvetica, Arial, sans-serif; }
|
||||
body li {
|
||||
margin-bottom: 25.888px; }
|
||||
body em {
|
||||
font-style: italic; }
|
||||
body a {
|
||||
color: #993333;
|
||||
font-style: italic;
|
||||
text-decoration: none; }
|
||||
body a:hover, body a:focus {
|
||||
color: #732626; }
|
||||
body input[type="email"], body input[type="number"], body input[type="password"], body input[type="search"], body input[type="tel"], body input[type="text"], body input[type="url"], body input[type="color"], body input[type="date"], body input[type="datetime"], body input[type="datetime-local"], body input[type="month"], body input[type="time"], body input[type="week"], body textarea {
|
||||
-webkit-box-shadow: 0 -1px 0 white;
|
||||
-moz-box-shadow: 0 -1px 0 white;
|
||||
box-shadow: 0 -1px 0 white;
|
||||
background-color: #eeeeee;
|
||||
background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #eeeeee), color-stop(100%, white));
|
||||
background-image: -webkit-linear-gradient(top, #eeeeee, white);
|
||||
background-image: -moz-linear-gradient(top, #eeeeee, white);
|
||||
background-image: -ms-linear-gradient(top, #eeeeee, white);
|
||||
background-image: -o-linear-gradient(top, #eeeeee, white);
|
||||
background-image: linear-gradient(top, #eeeeee, white);
|
||||
border: 1px solid #999;
|
||||
font: 16px Georgia, serif;
|
||||
padding: 4px;
|
||||
width: 100%; }
|
||||
body input[type="email"]:focus, body input[type="number"]:focus, body input[type="password"]:focus, body input[type="search"]:focus, body input[type="tel"]:focus, body input[type="text"]:focus, body input[type="url"]:focus, body input[type="color"]:focus, body input[type="date"]:focus, body input[type="datetime"]:focus, body input[type="datetime-local"]:focus, body input[type="month"]:focus, body input[type="time"]:focus, body input[type="week"]:focus, body textarea:focus {
|
||||
border-color: #993333; }
|
||||
|
||||
header.announcement {
|
||||
-webkit-background-size: cover;
|
||||
-moz-background-size: cover;
|
||||
-ms-background-size: cover;
|
||||
-o-background-size: cover;
|
||||
background-size: cover;
|
||||
background: #333;
|
||||
border-bottom: 1px solid #000;
|
||||
color: #fff;
|
||||
-webkit-font-smoothing: antialiased; }
|
||||
header.announcement.home {
|
||||
background: #e3e3e3 url("../images/marketing/shot-5-medium.jpg"); }
|
||||
@media screen and (min-width: 1200px) {
|
||||
header.announcement.home {
|
||||
background: #e3e3e3 url("../images/marketing/shot-5-large.jpg"); } }
|
||||
header.announcement.home div {
|
||||
padding: 258.88px 25.888px 77.664px; }
|
||||
@media screen and (max-width:780px) {
|
||||
header.announcement.home div {
|
||||
padding: 64.72px 25.888px 51.776px; } }
|
||||
header.announcement.home div nav h1 {
|
||||
margin-right: 0; }
|
||||
header.announcement.home div nav a.login {
|
||||
display: none; }
|
||||
header.announcement.course {
|
||||
background: #e3e3e3 url("../images/marketing/course-bg-small.jpg"); }
|
||||
@media screen and (min-width: 1200px) {
|
||||
header.announcement.course {
|
||||
background: #e3e3e3 url("../images/marketing/course-bg-large.jpg"); } }
|
||||
@media screen and (max-width: 1199px) and (min-width: 700px) {
|
||||
header.announcement.course {
|
||||
background: #e3e3e3 url("../images/marketing/course-bg-medium.jpg"); } }
|
||||
header.announcement.course div {
|
||||
padding: 103.552px 25.888px 51.776px; }
|
||||
@media screen and (max-width:780px) {
|
||||
header.announcement.course div {
|
||||
padding: 64.72px 25.888px 51.776px; } }
|
||||
header.announcement div {
|
||||
position: relative; }
|
||||
header.announcement div nav {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 25.888px;
|
||||
-webkit-border-radius: 0 0 3px 3px;
|
||||
-moz-border-radius: 0 0 3px 3px;
|
||||
-ms-border-radius: 0 0 3px 3px;
|
||||
-o-border-radius: 0 0 3px 3px;
|
||||
border-radius: 0 0 3px 3px;
|
||||
background: #333;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
padding: 12.944px 25.888px; }
|
||||
header.announcement div nav h1 {
|
||||
display: -moz-inline-box;
|
||||
-moz-box-orient: vertical;
|
||||
display: inline-block;
|
||||
vertical-align: baseline;
|
||||
zoom: 1;
|
||||
*display: inline;
|
||||
*vertical-align: auto;
|
||||
margin-right: 12.944px; }
|
||||
header.announcement div nav h1 a {
|
||||
font: italic 800 18px "Open Sans", Helvetica, Arial, sans-serif;
|
||||
color: #fff;
|
||||
text-decoration: none; }
|
||||
header.announcement div nav h1 a:hover, header.announcement div nav h1 a:focus {
|
||||
color: #999; }
|
||||
header.announcement div nav a.login {
|
||||
text-decoration: none;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-family: "Open Sans", Helvetica, Arial, sans-serif; }
|
||||
header.announcement div nav a.login:hover, header.announcement div nav a.login:focus {
|
||||
color: #999; }
|
||||
header.announcement div section {
|
||||
background: #993333;
|
||||
display: -moz-inline-box;
|
||||
-moz-box-orient: vertical;
|
||||
display: inline-block;
|
||||
vertical-align: baseline;
|
||||
zoom: 1;
|
||||
*display: inline;
|
||||
*vertical-align: auto;
|
||||
margin-left: 34.171%;
|
||||
padding: 25.888px 38.832px; }
|
||||
@media screen and (max-width: 780px) {
|
||||
header.announcement div section {
|
||||
margin-left: 0; } }
|
||||
header.announcement div section h1 {
|
||||
font-family: "Open Sans";
|
||||
font-size: 30px;
|
||||
font-weight: 800;
|
||||
display: -moz-inline-box;
|
||||
-moz-box-orient: vertical;
|
||||
display: inline-block;
|
||||
vertical-align: baseline;
|
||||
zoom: 1;
|
||||
*display: inline;
|
||||
*vertical-align: auto;
|
||||
line-height: 1.2em;
|
||||
margin: 0 25.888px 0 0; }
|
||||
header.announcement div section h2 {
|
||||
font-family: "Open Sans";
|
||||
font-size: 24px;
|
||||
font-weight: 400;
|
||||
display: -moz-inline-box;
|
||||
-moz-box-orient: vertical;
|
||||
display: inline-block;
|
||||
vertical-align: baseline;
|
||||
zoom: 1;
|
||||
*display: inline;
|
||||
*vertical-align: auto;
|
||||
line-height: 1.2em; }
|
||||
header.announcement div section.course section {
|
||||
float: left;
|
||||
margin-left: 0;
|
||||
margin-right: 3.817%;
|
||||
padding: 0;
|
||||
width: 48.092%; }
|
||||
@media screen and (max-width: 780px) {
|
||||
header.announcement div section.course section {
|
||||
float: none;
|
||||
width: 100%;
|
||||
margin-right: 0; } }
|
||||
header.announcement div section.course section a {
|
||||
background-color: #4d1919;
|
||||
border-color: #260d0d;
|
||||
-webkit-box-shadow: inset 0 1px 0 #732626, 0 1px 0 #ac3939;
|
||||
-moz-box-shadow: inset 0 1px 0 #732626, 0 1px 0 #ac3939;
|
||||
box-shadow: inset 0 1px 0 #732626, 0 1px 0 #ac3939;
|
||||
display: block;
|
||||
padding: 12.944px 25.888px;
|
||||
text-align: center; }
|
||||
header.announcement div section.course section a:hover {
|
||||
background-color: #732626;
|
||||
border-color: #4d1919; }
|
||||
header.announcement div section.course p {
|
||||
width: 48.092%;
|
||||
line-height: 25.888px;
|
||||
float: left; }
|
||||
@media screen and (max-width: 780px) {
|
||||
header.announcement div section.course p {
|
||||
float: none;
|
||||
width: 100%; } }
|
||||
|
||||
footer {
|
||||
padding-top: 0; }
|
||||
footer div.footer-wrapper {
|
||||
border-top: 1px solid #e5e5e5;
|
||||
padding: 25.888px 0;
|
||||
background: url("../images/marketing/mit-logo.png") right center no-repeat; }
|
||||
@media screen and (max-width: 780px) {
|
||||
footer div.footer-wrapper {
|
||||
background-position: left bottom;
|
||||
padding-bottom: 77.664px; } }
|
||||
footer div.footer-wrapper a {
|
||||
color: #888;
|
||||
text-decoration: none;
|
||||
-webkit-transition-property: all;
|
||||
-moz-transition-property: all;
|
||||
-ms-transition-property: all;
|
||||
-o-transition-property: all;
|
||||
transition-property: all;
|
||||
-webkit-transition-duration: 0.15s;
|
||||
-moz-transition-duration: 0.15s;
|
||||
-ms-transition-duration: 0.15s;
|
||||
-o-transition-duration: 0.15s;
|
||||
transition-duration: 0.15s;
|
||||
-webkit-transition-timing-function: ease-out;
|
||||
-moz-transition-timing-function: ease-out;
|
||||
-ms-transition-timing-function: ease-out;
|
||||
-o-transition-timing-function: ease-out;
|
||||
transition-timing-function: ease-out;
|
||||
-webkit-transition-delay: 0;
|
||||
-moz-transition-delay: 0;
|
||||
-ms-transition-delay: 0;
|
||||
-o-transition-delay: 0;
|
||||
transition-delay: 0; }
|
||||
footer div.footer-wrapper a:hover, footer div.footer-wrapper a:focus {
|
||||
color: #666; }
|
||||
footer div.footer-wrapper p {
|
||||
display: -moz-inline-box;
|
||||
-moz-box-orient: vertical;
|
||||
display: inline-block;
|
||||
vertical-align: baseline;
|
||||
zoom: 1;
|
||||
*display: inline;
|
||||
*vertical-align: auto;
|
||||
margin-right: 25.888px; }
|
||||
footer div.footer-wrapper ul {
|
||||
display: -moz-inline-box;
|
||||
-moz-box-orient: vertical;
|
||||
display: inline-block;
|
||||
vertical-align: baseline;
|
||||
zoom: 1;
|
||||
*display: inline;
|
||||
*vertical-align: auto; }
|
||||
@media screen and (max-width: 780px) {
|
||||
footer div.footer-wrapper ul {
|
||||
margin-top: 25.888px; } }
|
||||
footer div.footer-wrapper ul li {
|
||||
display: -moz-inline-box;
|
||||
-moz-box-orient: vertical;
|
||||
display: inline-block;
|
||||
vertical-align: baseline;
|
||||
zoom: 1;
|
||||
*display: inline;
|
||||
*vertical-align: auto;
|
||||
margin-bottom: 0; }
|
||||
footer div.footer-wrapper ul li:after {
|
||||
content: ' |';
|
||||
display: inline;
|
||||
color: #ccc; }
|
||||
footer div.footer-wrapper ul li:last-child:after {
|
||||
content: none; }
|
||||
footer div.footer-wrapper ul.social {
|
||||
float: right;
|
||||
margin-right: 60px;
|
||||
position: relative;
|
||||
top: -5px; }
|
||||
@media screen and (max-width: 780px) {
|
||||
footer div.footer-wrapper ul.social {
|
||||
float: none; } }
|
||||
footer div.footer-wrapper ul.social li {
|
||||
float: left;
|
||||
margin-right: 12.944px; }
|
||||
footer div.footer-wrapper ul.social li:after {
|
||||
content: none;
|
||||
display: none; }
|
||||
footer div.footer-wrapper ul.social li a {
|
||||
display: block;
|
||||
height: 29px;
|
||||
width: 28px;
|
||||
text-indent: -9999px; }
|
||||
footer div.footer-wrapper ul.social li a:hover {
|
||||
opacity: .8; }
|
||||
footer div.footer-wrapper ul.social li.twitter a {
|
||||
background: url("../images/marketing/twitter.png") 0 0 no-repeat; }
|
||||
footer div.footer-wrapper ul.social li.facebook a {
|
||||
background: url("../images/marketing/facebook.png") 0 0 no-repeat; }
|
||||
footer div.footer-wrapper ul.social li.linkedin a {
|
||||
background: url("../images/marketing/linkedin.png") 0 0 no-repeat; }
|
||||
|
||||
section.index-content section {
|
||||
float: left; }
|
||||
@media screen and (max-width: 780px) {
|
||||
section.index-content section {
|
||||
float: none;
|
||||
width: auto;
|
||||
margin-right: 0; } }
|
||||
section.index-content section h1 {
|
||||
font-size: 800 24px "Open Sans";
|
||||
margin-bottom: 25.888px; }
|
||||
section.index-content section p {
|
||||
line-height: 25.888px;
|
||||
margin-bottom: 25.888px; }
|
||||
section.index-content section ul {
|
||||
margin: 0; }
|
||||
section.index-content section.about {
|
||||
-webkit-box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
border-right: 1px solid #e5e5e5;
|
||||
margin-right: 2.513%;
|
||||
padding-right: 1.256%;
|
||||
width: 65.829%; }
|
||||
@media screen and (max-width: 780px) {
|
||||
section.index-content section.about {
|
||||
width: 100%;
|
||||
border-right: 0;
|
||||
margin-right: 0;
|
||||
padding-right: 0; } }
|
||||
section.index-content section.about section {
|
||||
margin-bottom: 25.888px; }
|
||||
section.index-content section.about section p {
|
||||
width: 48.092%;
|
||||
float: left; }
|
||||
@media screen and (max-width: 780px) {
|
||||
section.index-content section.about section p {
|
||||
float: none;
|
||||
width: auto; } }
|
||||
section.index-content section.about section p:nth-child(odd) {
|
||||
margin-right: 3.817%; }
|
||||
@media screen and (max-width: 780px) {
|
||||
section.index-content section.about section p:nth-child(odd) {
|
||||
margin-right: 0; } }
|
||||
section.index-content section.about section.intro section {
|
||||
margin-bottom: 0; }
|
||||
section.index-content section.about section.intro section.intro-text {
|
||||
margin-right: 3.817%;
|
||||
width: 48.092%; }
|
||||
@media screen and (max-width: 780px) {
|
||||
section.index-content section.about section.intro section.intro-text {
|
||||
margin-right: 0;
|
||||
width: auto; } }
|
||||
section.index-content section.about section.intro section.intro-text p {
|
||||
margin-right: 0;
|
||||
width: auto;
|
||||
float: none; }
|
||||
section.index-content section.about section.intro section.intro-video {
|
||||
width: 48.092%; }
|
||||
@media screen and (max-width: 780px) {
|
||||
section.index-content section.about section.intro section.intro-video {
|
||||
width: auto; } }
|
||||
section.index-content section.about section.intro section.intro-video a {
|
||||
display: block;
|
||||
width: 100%; }
|
||||
section.index-content section.about section.intro section.intro-video a img {
|
||||
width: 100%; }
|
||||
section.index-content section.about section.intro section.intro-video a span {
|
||||
display: none; }
|
||||
section.index-content section.about section.features {
|
||||
border-top: 1px solid #E5E5E5;
|
||||
padding-top: 25.888px;
|
||||
margin-bottom: 0; }
|
||||
section.index-content section.about section.features h2 {
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
color: #888;
|
||||
margin-bottom: 25.888px;
|
||||
font-weight: normal;
|
||||
font-size: 14px; }
|
||||
section.index-content section.about section.features h2 span {
|
||||
text-transform: none; }
|
||||
section.index-content section.about section.features p {
|
||||
width: auto;
|
||||
clear: both; }
|
||||
section.index-content section.about section.features p strong {
|
||||
font-family: "Open sans";
|
||||
font-weight: 800; }
|
||||
section.index-content section.about section.features p a {
|
||||
color: #993333;
|
||||
text-decoration: none;
|
||||
-webkit-transition-property: all;
|
||||
-moz-transition-property: all;
|
||||
-ms-transition-property: all;
|
||||
-o-transition-property: all;
|
||||
transition-property: all;
|
||||
-webkit-transition-duration: 0.15s;
|
||||
-moz-transition-duration: 0.15s;
|
||||
-ms-transition-duration: 0.15s;
|
||||
-o-transition-duration: 0.15s;
|
||||
transition-duration: 0.15s;
|
||||
-webkit-transition-timing-function: ease-out;
|
||||
-moz-transition-timing-function: ease-out;
|
||||
-ms-transition-timing-function: ease-out;
|
||||
-o-transition-timing-function: ease-out;
|
||||
transition-timing-function: ease-out;
|
||||
-webkit-transition-delay: 0;
|
||||
-moz-transition-delay: 0;
|
||||
-ms-transition-delay: 0;
|
||||
-o-transition-delay: 0;
|
||||
transition-delay: 0; }
|
||||
section.index-content section.about section.features p a:hover, section.index-content section.about section.features p a:focus {
|
||||
color: #602020; }
|
||||
section.index-content section.about section.features ul {
|
||||
margin-bottom: 0; }
|
||||
section.index-content section.about section.features ul li {
|
||||
line-height: 25.888px;
|
||||
width: 48.092%;
|
||||
float: left;
|
||||
margin-bottom: 12.944px; }
|
||||
@media screen and (max-width: 780px) {
|
||||
section.index-content section.about section.features ul li {
|
||||
width: auto;
|
||||
float: none; } }
|
||||
section.index-content section.about section.features ul li:nth-child(odd) {
|
||||
margin-right: 3.817%; }
|
||||
@media screen and (max-width: 780px) {
|
||||
section.index-content section.about section.features ul li:nth-child(odd) {
|
||||
margin-right: 0; } }
|
||||
section.index-content section.course, section.index-content section.staff {
|
||||
width: 31.658%; }
|
||||
@media screen and (max-width: 780px) {
|
||||
section.index-content section.course, section.index-content section.staff {
|
||||
width: auto; } }
|
||||
section.index-content section.course h1, section.index-content section.staff h1 {
|
||||
color: #888;
|
||||
font: normal 16px Georgia, serif;
|
||||
font-size: 14px;
|
||||
letter-spacing: 1px;
|
||||
margin-bottom: 25.888px;
|
||||
text-transform: uppercase; }
|
||||
section.index-content section.course h2, section.index-content section.staff h2 {
|
||||
font: 800 24px "Open Sans", Helvetica, Arial, sans-serif; }
|
||||
section.index-content section.course h3, section.index-content section.staff h3 {
|
||||
font: 400 18px "Open Sans", Helvetica, Arial, sans-serif; }
|
||||
section.index-content section.course a span.arrow, section.index-content section.staff a span.arrow {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
font-style: normal;
|
||||
display: -moz-inline-box;
|
||||
-moz-box-orient: vertical;
|
||||
display: inline-block;
|
||||
vertical-align: baseline;
|
||||
zoom: 1;
|
||||
*display: inline;
|
||||
*vertical-align: auto;
|
||||
padding-left: 10px; }
|
||||
section.index-content section.course ul, section.index-content section.staff ul {
|
||||
list-style: none; }
|
||||
section.index-content section.course ul li img, section.index-content section.staff ul li img {
|
||||
float: left;
|
||||
margin-right: 12.944px; }
|
||||
section.index-content section.course h2 {
|
||||
padding-top: 129.44px;
|
||||
background: url("../images/marketing/circuits-bg.jpg") 0 0 no-repeat;
|
||||
-webkit-background-size: contain;
|
||||
-moz-background-size: contain;
|
||||
-ms-background-size: contain;
|
||||
-o-background-size: contain;
|
||||
background-size: contain; }
|
||||
@media screen and (max-width: 998px) and (min-width: 781px) {
|
||||
section.index-content section.course h2 {
|
||||
background: url("../images/marketing/circuits-medium-bg.jpg") 0 0 no-repeat; } }
|
||||
@media screen and (max-width: 780px) {
|
||||
section.index-content section.course h2 {
|
||||
padding-top: 129.44px;
|
||||
background: url("../images/marketing/circuits-bg.jpg") 0 0 no-repeat; } }
|
||||
@media screen and (min-width: 500px) and (max-width: 781px) {
|
||||
section.index-content section.course h2 {
|
||||
padding-top: 207.104px; } }
|
||||
section.index-content section.course div.announcement p.announcement-button a {
|
||||
margin-top: 0; }
|
||||
section.index-content section.course div.announcement img {
|
||||
max-width: 100%;
|
||||
margin-bottom: 25.888px; }
|
||||
section.index-content section.about-course {
|
||||
-webkit-box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
border-right: 1px solid #e5e5e5;
|
||||
margin-right: 2.513%;
|
||||
padding-right: 1.256%;
|
||||
width: 65.829%; }
|
||||
@media screen and (max-width: 780px) {
|
||||
section.index-content section.about-course {
|
||||
width: auto;
|
||||
border-right: 0;
|
||||
margin-right: 0;
|
||||
padding-right: 0; } }
|
||||
section.index-content section.about-course section {
|
||||
width: 48.092%; }
|
||||
@media screen and (max-width: 780px) {
|
||||
section.index-content section.about-course section {
|
||||
width: auto; } }
|
||||
section.index-content section.about-course section.about-info {
|
||||
margin-right: 3.817%; }
|
||||
@media screen and (max-width: 780px) {
|
||||
section.index-content section.about-course section.about-info {
|
||||
margin-right: 0; } }
|
||||
section.index-content section.about-course section.requirements {
|
||||
clear: both;
|
||||
width: 100%;
|
||||
border-top: 1px solid #E5E5E5;
|
||||
padding-top: 25.888px;
|
||||
margin-bottom: 0; }
|
||||
section.index-content section.about-course section.requirements p {
|
||||
float: left;
|
||||
width: 48.092%;
|
||||
margin-right: 3.817%; }
|
||||
@media screen and (max-width: 780px) {
|
||||
section.index-content section.about-course section.requirements p {
|
||||
margin-right: 0;
|
||||
float: none;
|
||||
width: auto; } }
|
||||
section.index-content section.about-course section.requirements p:nth-child(odd) {
|
||||
margin-right: 0; }
|
||||
section.index-content section.about-course section.cta {
|
||||
width: 100%;
|
||||
text-align: center; }
|
||||
section.index-content section.about-course section.cta a.enroll {
|
||||
padding: 12.944px 51.776px;
|
||||
display: -moz-inline-box;
|
||||
-moz-box-orient: vertical;
|
||||
display: inline-block;
|
||||
vertical-align: baseline;
|
||||
zoom: 1;
|
||||
*display: inline;
|
||||
*vertical-align: auto;
|
||||
text-align: center;
|
||||
font: 800 18px "Open Sans", Helvetica, Arial, sans-serif; }
|
||||
section.index-content section.staff h1 {
|
||||
margin-top: 25.888px; }
|
||||
|
||||
#lean_overlay {
|
||||
background: #000;
|
||||
display: none;
|
||||
height: 100%;
|
||||
left: 0px;
|
||||
position: fixed;
|
||||
top: 0px;
|
||||
width: 100%;
|
||||
z-index: 100; }
|
||||
|
||||
div.leanModal_box {
|
||||
background: #fff;
|
||||
border: none;
|
||||
-webkit-border-radius: 3px;
|
||||
-moz-border-radius: 3px;
|
||||
-ms-border-radius: 3px;
|
||||
-o-border-radius: 3px;
|
||||
border-radius: 3px;
|
||||
-webkit-box-shadow: 0 0 6px black;
|
||||
-moz-box-shadow: 0 0 6px black;
|
||||
box-shadow: 0 0 6px black;
|
||||
-webkit-box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
display: none;
|
||||
padding: 51.776px;
|
||||
text-align: left; }
|
||||
div.leanModal_box a.modal_close {
|
||||
color: #aaa;
|
||||
display: block;
|
||||
font-style: normal;
|
||||
height: 14px;
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
top: 12px;
|
||||
width: 14px;
|
||||
z-index: 2; }
|
||||
div.leanModal_box a.modal_close:hover {
|
||||
color: #993333;
|
||||
text-decoration: none; }
|
||||
div.leanModal_box h1 {
|
||||
border-bottom: 1px solid #eee;
|
||||
font-size: 24px;
|
||||
margin-bottom: 25.888px;
|
||||
margin-top: 0;
|
||||
padding-bottom: 25.888px;
|
||||
text-align: left; }
|
||||
div.leanModal_box#enroll {
|
||||
max-width: 600px; }
|
||||
div.leanModal_box#enroll ol {
|
||||
padding-top: 25.888px; }
|
||||
div.leanModal_box#enroll ol li.terms, div.leanModal_box#enroll ol li.honor-code {
|
||||
float: none;
|
||||
width: auto; }
|
||||
div.leanModal_box#enroll ol li div.tip {
|
||||
display: none; }
|
||||
div.leanModal_box#enroll ol li:hover div.tip {
|
||||
background: #333;
|
||||
color: #fff;
|
||||
display: block;
|
||||
font-size: 16px;
|
||||
line-height: 25.888px;
|
||||
margin: 0 0 0 -10px;
|
||||
padding: 10px;
|
||||
position: absolute;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
width: 500px; }
|
||||
div.leanModal_box form {
|
||||
text-align: left; }
|
||||
div.leanModal_box form div#enroll_error, div.leanModal_box form div#login_error, div.leanModal_box form div#pwd_error {
|
||||
background-color: #333333;
|
||||
border: black;
|
||||
color: #fff;
|
||||
font-family: "Open sans";
|
||||
font-weight: bold;
|
||||
letter-spacing: 1px;
|
||||
margin: -25.888px -25.888px 25.888px;
|
||||
padding: 12.944px;
|
||||
text-shadow: 0 1px 0 #1a1a1a;
|
||||
-webkit-font-smoothing: antialiased; }
|
||||
div.leanModal_box form div#enroll_error:empty, div.leanModal_box form div#login_error:empty, div.leanModal_box form div#pwd_error:empty {
|
||||
padding: 0; }
|
||||
div.leanModal_box form ol {
|
||||
list-style: none;
|
||||
margin-bottom: 25.888px; }
|
||||
div.leanModal_box form ol li {
|
||||
margin-bottom: 12.944px; }
|
||||
div.leanModal_box form ol li.terms, div.leanModal_box form ol li.remember {
|
||||
border-top: 1px solid #eee;
|
||||
clear: both;
|
||||
float: none;
|
||||
padding-top: 25.888px;
|
||||
width: auto; }
|
||||
div.leanModal_box form ol li.honor-code {
|
||||
float: none;
|
||||
width: auto; }
|
||||
div.leanModal_box form ol li label {
|
||||
display: block;
|
||||
font-weight: bold; }
|
||||
div.leanModal_box form ol li input[type="email"], div.leanModal_box form ol li input[type="number"], div.leanModal_box form ol li input[type="password"], div.leanModal_box form ol li input[type="search"], div.leanModal_box form ol li input[type="tel"], div.leanModal_box form ol li input[type="text"], div.leanModal_box form ol li input[type="url"], div.leanModal_box form ol li input[type="color"], div.leanModal_box form ol li input[type="date"], div.leanModal_box form ol li input[type="datetime"], div.leanModal_box form ol li input[type="datetime-local"], div.leanModal_box form ol li input[type="month"], div.leanModal_box form ol li input[type="time"], div.leanModal_box form ol li input[type="week"], div.leanModal_box form ol li textarea {
|
||||
-webkit-box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
width: 100%; }
|
||||
div.leanModal_box form ol li input[type="checkbox"] {
|
||||
margin-right: 10px; }
|
||||
div.leanModal_box form ol li ul {
|
||||
list-style: disc outside none;
|
||||
margin: 12.944px 0 25.888px 25.888px; }
|
||||
div.leanModal_box form ol li ul li {
|
||||
color: #666;
|
||||
float: none;
|
||||
font-size: 14px;
|
||||
list-style: disc outside none;
|
||||
margin-bottom: 12.944px; }
|
||||
div.leanModal_box form input[type="button"], div.leanModal_box form input[type="submit"] {
|
||||
border: 1px solid #691b1b;
|
||||
-webkit-border-radius: 3px;
|
||||
-moz-border-radius: 3px;
|
||||
-ms-border-radius: 3px;
|
||||
-o-border-radius: 3px;
|
||||
border-radius: 3px;
|
||||
-webkit-box-shadow: inset 0 1px 0 0 #bc5c5c;
|
||||
-moz-box-shadow: inset 0 1px 0 0 #bc5c5c;
|
||||
box-shadow: inset 0 1px 0 0 #bc5c5c;
|
||||
color: white;
|
||||
display: inline;
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
background-color: #993333;
|
||||
background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #993333), color-stop(100%, #761e1e));
|
||||
background-image: -webkit-linear-gradient(top, #993333, #761e1e);
|
||||
background-image: -moz-linear-gradient(top, #993333, #761e1e);
|
||||
background-image: -ms-linear-gradient(top, #993333, #761e1e);
|
||||
background-image: -o-linear-gradient(top, #993333, #761e1e);
|
||||
background-image: linear-gradient(top, #993333, #761e1e);
|
||||
padding: 6px 18px 7px;
|
||||
text-shadow: 0 1px 0 #5d1414;
|
||||
-webkit-background-clip: padding-box;
|
||||
font-size: 18px;
|
||||
padding: 12.944px; }
|
||||
div.leanModal_box form input[type="button"]:hover, div.leanModal_box form input[type="submit"]:hover {
|
||||
-webkit-box-shadow: inset 0 1px 0 0 #a44141;
|
||||
-moz-box-shadow: inset 0 1px 0 0 #a44141;
|
||||
box-shadow: inset 0 1px 0 0 #a44141;
|
||||
cursor: pointer;
|
||||
background-color: #823030;
|
||||
background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #823030), color-stop(100%, #691c1c));
|
||||
background-image: -webkit-linear-gradient(top, #823030, #691c1c);
|
||||
background-image: -moz-linear-gradient(top, #823030, #691c1c);
|
||||
background-image: -ms-linear-gradient(top, #823030, #691c1c);
|
||||
background-image: -o-linear-gradient(top, #823030, #691c1c);
|
||||
background-image: linear-gradient(top, #823030, #691c1c); }
|
||||
div.leanModal_box form input[type="button"]:active, div.leanModal_box form input[type="submit"]:active {
|
||||
border: 1px solid #691b1b;
|
||||
-webkit-box-shadow: inset 0 0 8px 4px #5c1919, inset 0 0 8px 4px #5c1919, 0 1px 1px 0 #eeeeee;
|
||||
-moz-box-shadow: inset 0 0 8px 4px #5c1919, inset 0 0 8px 4px #5c1919, 0 1px 1px 0 #eeeeee;
|
||||
box-shadow: inset 0 0 8px 4px #5c1919, inset 0 0 8px 4px #5c1919, 0 1px 1px 0 #eeeeee; }
|
||||
|
||||
div#login {
|
||||
min-width: 400px; }
|
||||
div#login header {
|
||||
border-bottom: 1px solid #ddd;
|
||||
margin-bottom: 25.888px;
|
||||
padding-bottom: 25.888px; }
|
||||
div#login header h1 {
|
||||
border-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
margin-bottom: 6.472px; }
|
||||
div#login ol li {
|
||||
float: none;
|
||||
width: auto; }
|
||||
|
||||
div.lost-password {
|
||||
margin-top: 25.888px;
|
||||
text-align: left; }
|
||||
div.lost-password a {
|
||||
color: #999; }
|
||||
div.lost-password a:hover {
|
||||
color: #444; }
|
||||
|
||||
div#pwd_reset p {
|
||||
margin-bottom: 25.888px; }
|
||||
div#pwd_reset input[type="email"] {
|
||||
margin-bottom: 25.888px; }
|
||||
|
||||
div#apply_name_change,
|
||||
div#change_email,
|
||||
div#unenroll,
|
||||
div#deactivate-account {
|
||||
max-width: 700px; }
|
||||
div#apply_name_change ul,
|
||||
div#change_email ul,
|
||||
div#unenroll ul,
|
||||
div#deactivate-account ul {
|
||||
list-style: none; }
|
||||
div#apply_name_change ul li,
|
||||
div#change_email ul li,
|
||||
div#unenroll ul li,
|
||||
div#deactivate-account ul li {
|
||||
margin-bottom: 12.944px; }
|
||||
div#apply_name_change ul li textarea, div#apply_name_change ul li input[type="email"], div#apply_name_change ul li input[type="number"], div#apply_name_change ul li input[type="password"], div#apply_name_change ul li input[type="search"], div#apply_name_change ul li input[type="tel"], div#apply_name_change ul li input[type="text"], div#apply_name_change ul li input[type="url"], div#apply_name_change ul li input[type="color"], div#apply_name_change ul li input[type="date"], div#apply_name_change ul li input[type="datetime"], div#apply_name_change ul li input[type="datetime-local"], div#apply_name_change ul li input[type="month"], div#apply_name_change ul li input[type="time"], div#apply_name_change ul li input[type="week"],
|
||||
div#change_email ul li textarea,
|
||||
div#change_email ul li input[type="email"],
|
||||
div#change_email ul li input[type="number"],
|
||||
div#change_email ul li input[type="password"],
|
||||
div#change_email ul li input[type="search"],
|
||||
div#change_email ul li input[type="tel"],
|
||||
div#change_email ul li input[type="text"],
|
||||
div#change_email ul li input[type="url"],
|
||||
div#change_email ul li input[type="color"],
|
||||
div#change_email ul li input[type="date"],
|
||||
div#change_email ul li input[type="datetime"],
|
||||
div#change_email ul li input[type="datetime-local"],
|
||||
div#change_email ul li input[type="month"],
|
||||
div#change_email ul li input[type="time"],
|
||||
div#change_email ul li input[type="week"],
|
||||
div#unenroll ul li textarea,
|
||||
div#unenroll ul li input[type="email"],
|
||||
div#unenroll ul li input[type="number"],
|
||||
div#unenroll ul li input[type="password"],
|
||||
div#unenroll ul li input[type="search"],
|
||||
div#unenroll ul li input[type="tel"],
|
||||
div#unenroll ul li input[type="text"],
|
||||
div#unenroll ul li input[type="url"],
|
||||
div#unenroll ul li input[type="color"],
|
||||
div#unenroll ul li input[type="date"],
|
||||
div#unenroll ul li input[type="datetime"],
|
||||
div#unenroll ul li input[type="datetime-local"],
|
||||
div#unenroll ul li input[type="month"],
|
||||
div#unenroll ul li input[type="time"],
|
||||
div#unenroll ul li input[type="week"],
|
||||
div#deactivate-account ul li textarea,
|
||||
div#deactivate-account ul li input[type="email"],
|
||||
div#deactivate-account ul li input[type="number"],
|
||||
div#deactivate-account ul li input[type="password"],
|
||||
div#deactivate-account ul li input[type="search"],
|
||||
div#deactivate-account ul li input[type="tel"],
|
||||
div#deactivate-account ul li input[type="text"],
|
||||
div#deactivate-account ul li input[type="url"],
|
||||
div#deactivate-account ul li input[type="color"],
|
||||
div#deactivate-account ul li input[type="date"],
|
||||
div#deactivate-account ul li input[type="datetime"],
|
||||
div#deactivate-account ul li input[type="datetime-local"],
|
||||
div#deactivate-account ul li input[type="month"],
|
||||
div#deactivate-account ul li input[type="time"],
|
||||
div#deactivate-account ul li input[type="week"] {
|
||||
-webkit-box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
width: 100%; }
|
||||
div#apply_name_change ul li textarea,
|
||||
div#change_email ul li textarea,
|
||||
div#unenroll ul li textarea,
|
||||
div#deactivate-account ul li textarea {
|
||||
height: 60px; }
|
||||
div#apply_name_change ul li input[type="submit"],
|
||||
div#change_email ul li input[type="submit"],
|
||||
div#unenroll ul li input[type="submit"],
|
||||
div#deactivate-account ul li input[type="submit"] {
|
||||
white-space: normal; }
|
||||
|
||||
div#feedback_div form ol li {
|
||||
float: none;
|
||||
width: 100%; }
|
||||
div#feedback_div form ol li textarea#feedback_message {
|
||||
height: 100px; }
|
||||
@@ -299,3 +299,7 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.leanModal_box {
|
||||
@extend .modal;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
</%block>
|
||||
|
||||
<%block name="headextra">
|
||||
<%static:css group='course'/>
|
||||
|
||||
<style type="text/css">
|
||||
.grade_a {color:green;}
|
||||
@@ -19,7 +20,8 @@
|
||||
|
||||
</%block>
|
||||
|
||||
<%include file="navigation.html" args="active_page=''" />
|
||||
<%include file="course_navigation.html" args="active_page=''" />
|
||||
|
||||
<section class="container">
|
||||
<div class="gradebook-wrapper">
|
||||
<section class="gradebook-content">
|
||||
@@ -28,7 +30,7 @@
|
||||
%if len(students) > 0:
|
||||
<table>
|
||||
<%
|
||||
templateSummary = students[0]['grade_info']['grade_summary']
|
||||
templateSummary = students[0]['grade_summary']
|
||||
%>
|
||||
|
||||
|
||||
@@ -42,15 +44,15 @@
|
||||
|
||||
<%def name="percent_data(percentage)">
|
||||
<%
|
||||
data_class = "grade_none"
|
||||
if percentage > .87:
|
||||
data_class = "grade_a"
|
||||
elif percentage > .70:
|
||||
data_class = "grade_b"
|
||||
elif percentage > .6:
|
||||
data_class = "grade_c"
|
||||
elif percentage > 0:
|
||||
data_class = "grade_f"
|
||||
letter_grade = 'None'
|
||||
if percentage > 0:
|
||||
letter_grade = 'F'
|
||||
for grade in ['A', 'B', 'C']:
|
||||
if percentage >= course.grade_cutoffs[grade]:
|
||||
letter_grade = grade
|
||||
break
|
||||
|
||||
data_class = "grade_" + letter_grade
|
||||
%>
|
||||
<td class="${data_class}">${ "{0:.0%}".format( percentage ) }</td>
|
||||
</%def>
|
||||
@@ -58,10 +60,10 @@
|
||||
%for student in students:
|
||||
<tr>
|
||||
<td><a href="/profile/${student['id']}/">${student['username']}</a></td>
|
||||
%for section in student['grade_info']['grade_summary']['section_breakdown']:
|
||||
%for section in student['grade_summary']['section_breakdown']:
|
||||
${percent_data( section['percent'] )}
|
||||
%endfor
|
||||
<th>${percent_data( student['grade_info']['grade_summary']['percent'])}</th>
|
||||
<th>${percent_data( student['grade_summary']['percent'])}</th>
|
||||
</tr>
|
||||
%endfor
|
||||
</table>
|
||||
|
||||
@@ -26,9 +26,9 @@
|
||||
<%include file="navigation.html" />
|
||||
<section class="content-wrapper">
|
||||
${self.body()}
|
||||
<%block name="bodyextra"/>
|
||||
</section>
|
||||
|
||||
<%block name="bodyextra"/>
|
||||
<%include file="footer.html" />
|
||||
|
||||
<%static:js group='application'/>
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
if(json.success) {
|
||||
location.href="${reverse('dashboard')}";
|
||||
}else{
|
||||
$('#register_message).html("<p><font color='red'>" + json.error + "</font></p>")
|
||||
$('#register_message').html("<p><font color='red'>" + json.error + "</font></p>");
|
||||
}
|
||||
});
|
||||
})(this)
|
||||
|
||||
@@ -18,99 +18,98 @@
|
||||
<script type="text/javascript" src="${static.url('js/vendor/flot/jquery.flot.stack.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/vendor/flot/jquery.flot.symbol.js')}"></script>
|
||||
<script>
|
||||
${profile_graphs.body(grade_summary, "grade-detail-graph")}
|
||||
${profile_graphs.body(grade_summary, course.grade_cutoffs, "grade-detail-graph")}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
var loc=true; // Activate on clicks? Not if already clicked.
|
||||
var lang=true;
|
||||
$(function() {
|
||||
$("#change_location").click(function() {
|
||||
$(this).hide();
|
||||
$("#change_location").click(function() {
|
||||
$(this).hide();
|
||||
|
||||
log_event("profile", {"type":"location_show", "old":$("#location_sub").text()});
|
||||
log_event("profile", {"type":"location_show", "old":$("#location_sub").text()});
|
||||
|
||||
if(loc) {
|
||||
$("#description").html('<div>'+
|
||||
"Preferred format is city, state, country (so for us, "+
|
||||
""Cambridge, Massachusetts, USA"), but give "+
|
||||
"as much or as little detail as you want. </div>");
|
||||
if(loc) {
|
||||
$("#description").html('<div>'+
|
||||
"Preferred format is city, state, country (so for us, "+
|
||||
""Cambridge, Massachusetts, USA"), but give "+
|
||||
"as much or as little detail as you want. </div>");
|
||||
|
||||
loc=false;
|
||||
loc=false;
|
||||
|
||||
$("#location_sub").html('<form>'+'<input id="id_loc_text" type="text" name="loc_text" />'+
|
||||
'<input type="submit" id="change_loc_button" value="Save" />'+'</form>');
|
||||
$("#location_sub").html('<form>'+'<input id="id_loc_text" type="text" name="loc_text" />'+
|
||||
'<input type="submit" id="change_loc_button" value="Save" />'+'</form>');
|
||||
|
||||
$("#change_loc_button").click(function() {
|
||||
$("#change_location").show();
|
||||
$("#change_loc_button").click(function() {
|
||||
$("#change_location").show();
|
||||
|
||||
postJSON('/change_setting', {'location':$("#id_loc_text").attr("value")}, function(json) {
|
||||
$("#location_sub").text(json.location);
|
||||
loc=true;
|
||||
$("#description").html("");
|
||||
log_event("profile", {"type":"location_change", "new":json.location});
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
$('#change_password').click(function(){
|
||||
$('.modal').trigger('click');
|
||||
log_event("profile", {"type":"password_show"});
|
||||
});
|
||||
|
||||
$('#pwd_reset_button').click(function() {
|
||||
$.postWithPrefix('/password_reset/',{ "csrfmiddlewaretoken" : "${ csrf }",
|
||||
"email" : $('#id_email').val()}, function(data){
|
||||
$("#password_reset_complete_link").click();
|
||||
log_event("profile", {"type":"password_send"});
|
||||
});
|
||||
postJSON('/change_setting', {'location':$("#id_loc_text").attr("value")}, function(json) {
|
||||
$("#location_sub").text(json.location);
|
||||
loc=true;
|
||||
$("#description").html("");
|
||||
log_event("profile", {"type":"location_change", "new":json.location});
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
$("#change_email_form").submit(function(){
|
||||
var new_email = $('#new_email_field').val();
|
||||
var new_password = $('#new_email_password').val();
|
||||
|
||||
postJSON('/change_email',{"new_email":new_email,
|
||||
"password":new_password},
|
||||
function(data){
|
||||
if(data.success){
|
||||
$("#change_email").html("<h1>Please verify your new email</h1><p>You'll receive a confirmation in your in-box. Please click the link in the email to confirm the email change.</p>");
|
||||
} else {
|
||||
$("#change_email_error").html(data.error);
|
||||
}
|
||||
});
|
||||
log_event("profile", {"type":"email_change_request",
|
||||
"old_email":"${email}",
|
||||
"new_email":new_email});
|
||||
return false;
|
||||
$('#change_password').click(function(){
|
||||
$('.modal').trigger('click');
|
||||
log_event("profile", {"type":"password_show"});
|
||||
});
|
||||
|
||||
$("#change_name_form").submit(function(){
|
||||
var new_name = $('#new_name_field').val();
|
||||
var rationale = $('#name_rationale_field').val();
|
||||
|
||||
postJSON('/change_name',{"new_name":new_name,
|
||||
"rationale":rationale},
|
||||
function(data){
|
||||
if(data.success){
|
||||
$("#apply_name_change").html("<h1>Your request has been submitted.</h1><p>We'll send you an e-mail when approve the change or need further information. Please allow for up to a week for us to process your request.</p>");
|
||||
} else {
|
||||
$("#change_name_error").html(data.error);
|
||||
}
|
||||
});
|
||||
log_event("profile", {"type":"name_change_request",
|
||||
"new_name":new_name,
|
||||
"rationale":rationale});
|
||||
return false;
|
||||
$('#pwd_reset_button').click(function() {
|
||||
$.postWithPrefix('/password_reset/',{ "csrfmiddlewaretoken" : "${ csrf }",
|
||||
"email" : $('#id_email').val()}, function(data){
|
||||
$("#password_reset_complete_link").click();
|
||||
log_event("profile", {"type":"password_send"});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
$("#change_email_form").submit(function(){
|
||||
var new_email = $('#new_email_field').val();
|
||||
var new_password = $('#new_email_password').val();
|
||||
|
||||
postJSON('/change_email',{"new_email":new_email,
|
||||
"password":new_password},
|
||||
function(data){
|
||||
if(data.success){
|
||||
$("#change_email").html("<h1>Please verify your new email</h1><p>You'll receive a confirmation in your in-box. Please click the link in the email to confirm the email change.</p>");
|
||||
} else {
|
||||
$("#change_email_error").html(data.error);
|
||||
}
|
||||
});
|
||||
log_event("profile", {"type":"email_change_request",
|
||||
"old_email":"${email}",
|
||||
"new_email":new_email});
|
||||
return false;
|
||||
});
|
||||
|
||||
$("#change_name_form").submit(function(){
|
||||
var new_name = $('#new_name_field').val();
|
||||
var rationale = $('#name_rationale_field').val();
|
||||
|
||||
postJSON('/change_name',{"new_name":new_name,
|
||||
"rationale":rationale},
|
||||
function(data){
|
||||
if(data.success){
|
||||
$("#apply_name_change").html("<h1>Your request has been submitted.</h1><p>We'll send you an e-mail when approve the change or need further information. Please allow for up to a week for us to process your request.</p>");
|
||||
} else {
|
||||
$("#change_name_error").html(data.error);
|
||||
}
|
||||
});
|
||||
log_event("profile", {"type":"name_change_request",
|
||||
"new_name":new_name,
|
||||
"rationale":rationale});
|
||||
return false;
|
||||
});
|
||||
|
||||
});
|
||||
</script>
|
||||
</%block>
|
||||
|
||||
|
||||
<%include file="course_navigation.html" args="active_page='profile'" />
|
||||
|
||||
<section class="container">
|
||||
@@ -139,19 +138,26 @@ $(function() {
|
||||
%>
|
||||
|
||||
<h3><a href="${reverse('courseware_section', kwargs={'course_id' : course.id, 'chapter' : chapter['url_name'], 'section' : section['url_name']})}">
|
||||
${ section['display_name'] }</a> ${"({0:.3n}/{1:.3n}) {2}".format( float(earned), float(total), percentageString )}</h3>
|
||||
${section['format']}
|
||||
%if 'due' in section and section['due']!="":
|
||||
due ${section['due']}
|
||||
%endif
|
||||
${ section['display_name'] }</a><span> ${"({0:.3n}/{1:.3n}) {2}".format( float(earned), float(total), percentageString )}</span></h3>
|
||||
<p>
|
||||
${section['format']}
|
||||
|
||||
%if 'due' in section and section['due']!="":
|
||||
<em>
|
||||
due ${section['due']}
|
||||
</em>
|
||||
%endif
|
||||
</p>
|
||||
|
||||
%if len(section['scores']) > 0:
|
||||
<ol class="scores">
|
||||
${ "Problem Scores: " if section['graded'] else "Practice Scores: "}
|
||||
%for score in section['scores']:
|
||||
<li class="score">${"{0:.3n}/{1:.3n}".format(float(score.earned),float(score.possible))}</li>
|
||||
%endfor
|
||||
</ol>
|
||||
<section class="scores">
|
||||
<h3> ${ "Problem Scores: " if section['graded'] else "Practice Scores: "} </h3>
|
||||
<ol>
|
||||
%for score in section['scores']:
|
||||
<li>${"{0:.3n}/{1:.3n}".format(float(score.earned),float(score.possible))}</li>
|
||||
%endfor
|
||||
</ol>
|
||||
</section>
|
||||
%endif
|
||||
|
||||
</li> <!--End section-->
|
||||
@@ -202,7 +208,7 @@ $(function() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div id="password_reset_complete" class="leanModal_box">
|
||||
<div id="password_reset_complete" class="modal">
|
||||
<a href="#password_reset_complete" rel="leanModal" id="password_reset_complete_link"></a>
|
||||
<h1>Password Reset Email Sent</h1>
|
||||
<p>
|
||||
@@ -210,83 +216,78 @@ $(function() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div id="apply_name_change" class="leanModal_box">
|
||||
<h1>Apply to change your name</h1>
|
||||
<form id="change_name_form">
|
||||
<div id="change_name_error"> </div>
|
||||
<fieldset>
|
||||
<p>To uphold the credibility of <span class="edx">edX</span> certificates, name changes must go through an approval process. A member of the course staff will review your request, and if approved, update your information. Please allow up to a week for your request to be processed. Thank you.</p>
|
||||
<ul>
|
||||
<li>
|
||||
<label>Enter your desired full name, as it will appear on the <span class="edx">edX</span> Certificate: </label>
|
||||
<input id="new_name_field" value="" type="text" />
|
||||
</li>
|
||||
<li>
|
||||
<label>Reason for name change:</label>
|
||||
<textarea id="name_rationale_field" value=""></textarea>
|
||||
</li>
|
||||
<li>
|
||||
<input type="submit" id="submit">
|
||||
</li>
|
||||
</ul>
|
||||
</fieldset>
|
||||
</form>
|
||||
<div id="apply_name_change" class="modal">
|
||||
<div class="inner-wrapper">
|
||||
<header>
|
||||
<h2>Apply to change your name</h2>
|
||||
<hr />
|
||||
</header>
|
||||
<form id="change_name_form">
|
||||
<div id="change_name_error"> </div>
|
||||
<fieldset>
|
||||
<p>To uphold the credibility of <span class="edx">edX</span> certificates, name changes must go through an approval process. A member of the course staff will review your request, and if approved, update your information. Please allow up to a week for your request to be processed. Thank you.</p>
|
||||
<label>Enter your desired full name, as it will appear on the <span class="edx">edX</span> Certificate: </label>
|
||||
<input id="new_name_field" value="" type="text" />
|
||||
<label>Reason for name change:</label>
|
||||
<textarea id="name_rationale_field" value=""></textarea>
|
||||
<input type="submit" id="submit">
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="change_email" class="leanModal_box">
|
||||
<h1>Change e-mail</h1>
|
||||
<div id="change_email" class="modal">
|
||||
<div class="inner-wrapper">
|
||||
<header>
|
||||
<h2>Change e-mail</h2>
|
||||
<hr />
|
||||
</header>
|
||||
<div id="apply_name_change_error"></div>
|
||||
<form id="change_email_form">
|
||||
<div id="change_email_error"> </div>
|
||||
<fieldset>
|
||||
<ul>
|
||||
<li>
|
||||
<label> Please enter your new email address: </label>
|
||||
<input id="new_email_field" type="email" value="" />
|
||||
</li>
|
||||
<label> Please enter your new email address: </label>
|
||||
<input id="new_email_field" type="email" value="" />
|
||||
<label> Please confirm your password: </label>
|
||||
<input id="new_email_password" value="" type="password" />
|
||||
<p>We will send a confirmation to both ${email} and your new e-mail as part of the process.</p>
|
||||
<input type="submit" id="submit_email_change" />
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<li>
|
||||
<label> Please confirm your password: </label>
|
||||
<input id="new_email_password" value="" type="password" />
|
||||
</li>
|
||||
<li>
|
||||
<p>We will send a confirmation to both ${email} and your new e-mail as part of the process.</p>
|
||||
<input type="submit" id="submit_email_change" />
|
||||
</li>
|
||||
</ul>
|
||||
<div id="deactivate-account" class="modal">
|
||||
<div class="inner-wrapper">
|
||||
<header>
|
||||
<h2>Deactivate <span class="edx">edX</span> Account</h2>
|
||||
<hr />
|
||||
</header>
|
||||
<p>Once you deactivate you’re MIT<em>x</em> account you will no longer recieve updates and new class announcements from MIT<em>x</em>.</p>
|
||||
<p>If you’d like to still get updates and new class announcements you can just <a href="#unenroll" rel="leanModal">unenroll</a> and keep your account active.</p>
|
||||
|
||||
<form id="unenroll_form">
|
||||
<div id="unenroll_error"> </div>
|
||||
<fieldset>
|
||||
<input type="submit" id="" value="Yes, I don't want an edX account or hear about any new classes or updates to edX" />
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="deactivate-account" class="leanModal_box">
|
||||
<h1>Deactivate <span class="edx">edX</span> Account</h1>
|
||||
<p>Once you deactivate you’re MIT<em>x</em> account you will no longer recieve updates and new class announcements from MIT<em>x</em>.</p>
|
||||
<p>If you’d like to still get updates and new class announcements you can just <a href="#unenroll" rel="leanModal">unenroll</a> and keep your account active.</p>
|
||||
<div id="unenroll" class="modal">
|
||||
<div class="inner-wrapper">
|
||||
<header>
|
||||
<h2>Unenroll from 6.002x</h2>
|
||||
<hr />
|
||||
</header>
|
||||
<p>Please note: you will still receive updates and new class announcements from ed<em>X</em>. If you don’t wish to receive any more updates or announcements <a href="#deactivate-account" rel="leanModal">deactivate your account</a>.</p>
|
||||
|
||||
<form id="unenroll_form">
|
||||
<div id="unenroll_error"> </div>
|
||||
<fieldset>
|
||||
<ul>
|
||||
<li>
|
||||
<input type="submit" id="" value="Yes, I don't want an edX account or hear about any new classes or updates to edX" />
|
||||
</li>
|
||||
</ul>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div id="unenroll" class="leanModal_box">
|
||||
<h1>Unenroll from 6.002x</h1>
|
||||
<p>Please note: you will still receive updates and new class announcements from ed<em>X</em>. If you don’t wish to receive any more updates or announcements <a href="#deactivate-account" rel="leanModal">deactivate your account</a>.</p>
|
||||
|
||||
<form id="unenroll_form">
|
||||
<div id="unenroll_error"> </div>
|
||||
<fieldset>
|
||||
<ul>
|
||||
<li>
|
||||
<input type="submit" id="" value="Yes, I want to unenroll from 6.002x but still hear about any new classes or updates to edX" />
|
||||
</li>
|
||||
</ul>
|
||||
</fieldset>
|
||||
</form>
|
||||
<form id="unenroll_form">
|
||||
<div id="unenroll_error"> </div>
|
||||
<fieldset>
|
||||
<input type="submit" id="" value="Yes, I want to unenroll from 6.002x but still hear about any new classes or updates to edX" />
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<%page args="grade_summary, graph_div_id, **kwargs"/>
|
||||
<%page args="grade_summary, grade_cutoffs, graph_div_id, **kwargs"/>
|
||||
<%!
|
||||
import json
|
||||
import math
|
||||
%>
|
||||
|
||||
$(function () {
|
||||
@@ -89,8 +90,16 @@ $(function () {
|
||||
ticks += [ [overviewBarX, "Total"] ]
|
||||
tickIndex += 1 + sectionSpacer
|
||||
|
||||
totalScore = grade_summary['percent']
|
||||
totalScore = math.floor(grade_summary['percent'] * 100) / 100 #We floor it to the nearest percent, 80.9 won't show up like a 90 (an A)
|
||||
detail_tooltips['Dropped Scores'] = dropped_score_tooltips
|
||||
|
||||
|
||||
## ----------------------------- Grade cutoffs ------------------------- ##
|
||||
|
||||
grade_cutoff_ticks = [ [1, "100%"], [0, "0%"] ]
|
||||
for grade in ['A', 'B', 'C']:
|
||||
percent = grade_cutoffs[grade]
|
||||
grade_cutoff_ticks.append( [ percent, "{0} {1:.0%}".format(grade, percent) ] )
|
||||
%>
|
||||
|
||||
var series = ${ json.dumps( series ) };
|
||||
@@ -98,6 +107,7 @@ $(function () {
|
||||
var bottomTicks = ${ json.dumps(bottomTicks) };
|
||||
var detail_tooltips = ${ json.dumps(detail_tooltips) };
|
||||
var droppedScores = ${ json.dumps(droppedScores) };
|
||||
var grade_cutoff_ticks = ${ json.dumps(grade_cutoff_ticks) }
|
||||
|
||||
//Alwasy be sure that one series has the xaxis set to 2, or the second xaxis labels won't show up
|
||||
series.push( {label: 'Dropped Scores', data: droppedScores, points: {symbol: "cross", show: true, radius: 3}, bars: {show: false}, color: "#333"} );
|
||||
@@ -107,10 +117,10 @@ $(function () {
|
||||
lines: {show: false, steps: false },
|
||||
bars: {show: true, barWidth: 0.8, align: 'center', lineWidth: 0, fill: .8 },},
|
||||
xaxis: {tickLength: 0, min: 0.0, max: ${tickIndex - sectionSpacer}, ticks: ticks, labelAngle: 90},
|
||||
yaxis: {ticks: [[1, "100%"], [0.87, "A 87%"], [0.7, "B 70%"], [0.6, "C 60%"], [0, "0%"]], min: 0.0, max: 1.0, labelWidth: 50},
|
||||
yaxis: {ticks: grade_cutoff_ticks, min: 0.0, max: 1.0, labelWidth: 50},
|
||||
grid: { hoverable: true, clickable: true, borderWidth: 1,
|
||||
markings: [ {yaxis: {from: 0.87, to: 1 }, color: "#ddd"}, {yaxis: {from: 0.7, to: 0.87 }, color: "#e9e9e9"},
|
||||
{yaxis: {from: 0.6, to: 0.7 }, color: "#f3f3f3"}, ] },
|
||||
markings: [ {yaxis: {from: ${grade_cutoffs['A']}, to: 1 }, color: "#ddd"}, {yaxis: {from: ${grade_cutoffs['B']}, to: ${grade_cutoffs['A']} }, color: "#e9e9e9"},
|
||||
{yaxis: {from: ${grade_cutoffs['C']}, to: ${grade_cutoffs['B']} }, color: "#f3f3f3"}, ] },
|
||||
legend: {show: false},
|
||||
};
|
||||
|
||||
|
||||
@@ -71,9 +71,9 @@
|
||||
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
<%block name="wiki_head"/>
|
||||
|
||||
|
||||
</%block>
|
||||
|
||||
<%block name="bodyextra">
|
||||
@@ -86,7 +86,7 @@
|
||||
<div class="wiki-wrapper">
|
||||
<%block name="wiki_panel">
|
||||
<div aria-label="Wiki Navigation" id="wiki_panel">
|
||||
<h2>Course Wiki</h2>
|
||||
<h2>Course Wiki</h2>
|
||||
<ul class="action">
|
||||
<li>
|
||||
<h3>
|
||||
@@ -101,12 +101,12 @@
|
||||
|
||||
<div id="wiki_create_form">
|
||||
<%
|
||||
baseURL = wiki_reverse("wiki_create", course=course, kwargs={"article_path" : namespace + "/" })
|
||||
baseURL = wiki_reverse("wiki_create", course=course, kwargs={"article_path" : namespace + "/" })
|
||||
%>
|
||||
<form method="GET" onsubmit="this.action='${baseURL}' + this.wiki_article_name.value.replace(/([^a-zA-Z0-9\-])/g, '');">
|
||||
<div>
|
||||
<label for="id_wiki_article_name">Title of article</label>
|
||||
<input type="text" name="wiki_article_name" id="id_wiki_article_name" /><br/>
|
||||
<input type="text" name="wiki_article_name" id="id_wiki_article_name" />
|
||||
</div>
|
||||
<ul>
|
||||
<li>
|
||||
@@ -130,31 +130,31 @@
|
||||
</%block>
|
||||
|
||||
<section class="wiki-body">
|
||||
%if wiki_article is not UNDEFINED:
|
||||
<header>
|
||||
%if wiki_article.locked:
|
||||
<p><strong>This article has been locked</strong></p>
|
||||
%endif
|
||||
<p>Last modified: ${wiki_article.modified_on.strftime("%b %d, %Y, %I:%M %p")}</p>
|
||||
%endif
|
||||
|
||||
%if wiki_article is not UNDEFINED:
|
||||
<ul>
|
||||
|
||||
<li>
|
||||
<a href="${ wiki_reverse('wiki_view', wiki_article, course)}" class="view">View</a>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a href="${ wiki_reverse('wiki_edit', wiki_article, course)}" class="edit">Edit</a>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a href="${ wiki_reverse('wiki_history', wiki_article, course)}" class="history">History</a>
|
||||
</li>
|
||||
</ul>
|
||||
</header>
|
||||
%if wiki_article is not UNDEFINED:
|
||||
<header>
|
||||
%if wiki_article.locked:
|
||||
<p><strong>This article has been locked</strong></p>
|
||||
%endif
|
||||
<p>Last modified: ${wiki_article.modified_on.strftime("%b %d, %Y, %I:%M %p")}</p>
|
||||
%endif
|
||||
|
||||
%if wiki_article is not UNDEFINED:
|
||||
<ul>
|
||||
|
||||
<li>
|
||||
<a href="${ wiki_reverse('wiki_view', wiki_article, course)}" class="view">View</a>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a href="${ wiki_reverse('wiki_edit', wiki_article, course)}" class="edit">Edit</a>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a href="${ wiki_reverse('wiki_history', wiki_article, course)}" class="history">History</a>
|
||||
</li>
|
||||
</ul>
|
||||
</header>
|
||||
%endif
|
||||
|
||||
<%block name="wiki_page_title"/>
|
||||
<%block name="wiki_body"/>
|
||||
|
||||
@@ -65,12 +65,10 @@ ${"Edit " + wiki_title + " - " if wiki_title is not UNDEFINED else ""}MITx 6.002
|
||||
</div>
|
||||
${wiki_form}
|
||||
%if create_article:
|
||||
<input type="submit" id="submit_edit" value="Create article" /></td>
|
||||
<input type="submit" id="submit_edit" value="Create article" />
|
||||
%else:
|
||||
<input type="submit" id="submit_edit" name="edit" value="Save Changes" />
|
||||
<input type="submit" id="submit_delete" name="delete" value="Delete article" />
|
||||
%endif
|
||||
</form>
|
||||
|
||||
<%include file="simplewiki_instructions.html"/>
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ Displaying all articles
|
||||
<li><h3><a href="${wiki_reverse("wiki_view", article, course)}">${article.title} ${'(Deleted)' if article_deleted else ''}</a></h3></li>
|
||||
%endfor
|
||||
|
||||
%if not wiki_search_results:
|
||||
%if not wiki_search_results:
|
||||
No articles matching <b>${wiki_search_query if wiki_search_query is not UNDEFINED else ""} </b>!
|
||||
%endif
|
||||
</ul>
|
||||
|
||||
@@ -2,9 +2,13 @@ ${module_content}
|
||||
%if edit_link:
|
||||
<div><a href="${edit_link}">Edit</a></div>
|
||||
% endif
|
||||
|
||||
<div class="staff_info">
|
||||
<a href="javascript:void(0)" onclick="javascript:$('#${element_id}_debug').toggle()">Staff Debug Info</a>
|
||||
<span style="display:none" id="${element_id}_debug">
|
||||
definition = <pre>${definition | h}</pre>
|
||||
metadata = ${metadata | h}
|
||||
</span>
|
||||
</div>
|
||||
%if render_histogram:
|
||||
<div id="histogram_${element_id}" class="histogram" data-histogram="${histogram}"></div>
|
||||
|
||||
@@ -89,17 +89,6 @@ $("#open_close_accordion a").click(function(){
|
||||
</nav>
|
||||
|
||||
<img id="bookpage" src="${ settings.BOOK_URL }p${ "%03i"%(page) }.png">
|
||||
|
||||
<nav class="bottom-nav">
|
||||
<ul>
|
||||
<li class="last">
|
||||
<a href="javascript:prev_page()">Previous page</a>
|
||||
</li>
|
||||
<li class="next">
|
||||
<a href="javascript:next_page()">Next page</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</section>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -107,7 +107,6 @@ if settings.COURSEWARE_ENABLED:
|
||||
|
||||
# TODO: These views need to be updated before they work
|
||||
# url(r'^calculate$', 'util.views.calculate'),
|
||||
# url(r'^gradebook$', 'courseware.views.gradebook'),
|
||||
# TODO: We should probably remove the circuit package. I believe it was only used in the old way of saving wiki circuits for the wiki
|
||||
# url(r'^edit_circuit/(?P<circuit>[^/]*)$', 'circuit.views.edit_circuit'),
|
||||
# url(r'^save_circuit/(?P<circuit>[^/]*)$', 'circuit.views.save_circuit'),
|
||||
@@ -119,7 +118,7 @@ if settings.COURSEWARE_ENABLED:
|
||||
#About the course
|
||||
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/about$',
|
||||
'courseware.views.course_about', name="about_course"),
|
||||
|
||||
|
||||
#Inside the course
|
||||
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/info$',
|
||||
'courseware.views.course_info', name="info"),
|
||||
@@ -143,6 +142,10 @@ if settings.COURSEWARE_ENABLED:
|
||||
# discussion
|
||||
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/discussion/',
|
||||
include('django_comment_client.urls')),
|
||||
|
||||
# For the instructor
|
||||
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/gradebook$',
|
||||
'courseware.views.gradebook'),
|
||||
)
|
||||
|
||||
# Multicourse wiki
|
||||
|
||||
32
rakefile
32
rakefile
@@ -83,13 +83,20 @@ end
|
||||
task :pylint => "pylint_#{system}"
|
||||
end
|
||||
|
||||
$failed_tests = 0
|
||||
|
||||
def run_tests(system, report_dir)
|
||||
def run_tests(system, report_dir, stop_on_failure=true)
|
||||
ENV['NOSE_XUNIT_FILE'] = File.join(report_dir, "nosetests.xml")
|
||||
ENV['NOSE_COVER_HTML_DIR'] = File.join(report_dir, "cover")
|
||||
sh(django_admin(system, :test, 'test', *Dir["#{system}/djangoapps/*"].each))
|
||||
sh(django_admin(system, :test, 'test', *Dir["#{system}/djangoapps/*"].each)) do |ok, res|
|
||||
if !ok and stop_on_failure
|
||||
abort "Test failed!"
|
||||
end
|
||||
$failed_tests += 1 unless ok
|
||||
end
|
||||
end
|
||||
|
||||
TEST_TASKS = []
|
||||
|
||||
[:lms, :cms].each do |system|
|
||||
report_dir = File.join(REPORT_DIR, system.to_s)
|
||||
@@ -97,15 +104,16 @@ end
|
||||
|
||||
# Per System tasks
|
||||
desc "Run all django tests on our djangoapps for the #{system}"
|
||||
task "test_#{system}" => ["clean_test_files", "#{system}:collectstatic:test", "fasttest_#{system}"]
|
||||
task "test_#{system}", [:stop_on_failure] => ["clean_test_files", "#{system}:collectstatic:test", "fasttest_#{system}"]
|
||||
|
||||
# Have a way to run the tests without running collectstatic -- useful when debugging without
|
||||
# messing with static files.
|
||||
task "fasttest_#{system}" => [report_dir, :predjango] do
|
||||
run_tests(system, report_dir)
|
||||
task "fasttest_#{system}", [:stop_on_failure] => [report_dir, :predjango] do |t, args|
|
||||
args.with_defaults(:stop_on_failure => 'true')
|
||||
run_tests(system, report_dir, args.stop_on_failure)
|
||||
end
|
||||
|
||||
task :test => "test_#{system}"
|
||||
TEST_TASKS << "test_#{system}"
|
||||
|
||||
desc <<-desc
|
||||
Start the #{system} locally with the specified environment (defaults to dev).
|
||||
@@ -142,7 +150,17 @@ Dir["common/lib/*"].each do |lib|
|
||||
ENV['NOSE_XUNIT_FILE'] = File.join(report_dir, "nosetests.xml")
|
||||
sh("nosetests #{lib} --cover-erase --with-xunit --with-xcoverage --cover-html --cover-inclusive --cover-package #{File.basename(lib)} --cover-html-dir #{File.join(report_dir, "cover")}")
|
||||
end
|
||||
task :test => task_name
|
||||
TEST_TASKS << task_name
|
||||
end
|
||||
|
||||
task :test do
|
||||
TEST_TASKS.each do |task|
|
||||
Rake::Task[task].invoke(false)
|
||||
end
|
||||
|
||||
if $failed_tests > 0
|
||||
abort "Tests failed!"
|
||||
end
|
||||
end
|
||||
|
||||
task :runserver => :lms
|
||||
|
||||
Reference in New Issue
Block a user