Commit 2fb02f1a by Rocky Duan

Merge branch 'master' of github.com:MITx/mitx into merge

Conflicts:
	lms/urls.py
parents 272c7b8b 165ffe0a
...@@ -90,10 +90,12 @@ def add_histogram(get_html, module): ...@@ -90,10 +90,12 @@ def add_histogram(get_html, module):
# TODO (ichuang): Remove after fall 2012 LMS migration done # TODO (ichuang): Remove after fall 2012 LMS migration done
if settings.MITX_FEATURES.get('ENABLE_LMS_MIGRATION'): 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 osfs = module.system.filestore
if filename is not None and osfs.exists(filename): 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] data_dir = osfs.root_path.rsplit('/')[-1]
edit_link = "https://github.com/MITx/%s/tree/master/%s" % (data_dir,filepath) edit_link = "https://github.com/MITx/%s/tree/master/%s" % (data_dir,filepath)
else: else:
......
...@@ -204,7 +204,7 @@ def extract_choices(element): ...@@ -204,7 +204,7 @@ def extract_choices(element):
raise Exception("[courseware.capa.inputtypes.extract_choices] \ raise Exception("[courseware.capa.inputtypes.extract_choices] \
Expected a <choice> tag; got %s instead" Expected a <choice> tag; got %s instead"
% choice.tag) % 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)) choices.append((choice.get("name"), choice_text))
......
...@@ -800,6 +800,12 @@ class CodeResponse(LoncapaResponse): ...@@ -800,6 +800,12 @@ class CodeResponse(LoncapaResponse):
''' '''
Grade student code using an external queueing server, called 'xqueue' 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 External requests are only submitted for student submission grading
(i.e. and not for getting reference answers) (i.e. and not for getting reference answers)
''' '''
...@@ -873,15 +879,16 @@ class CodeResponse(LoncapaResponse): ...@@ -873,15 +879,16 @@ class CodeResponse(LoncapaResponse):
'edX_cmd': 'get_score', 'edX_cmd': 'get_score',
'edX_tests': self.tests, 'edX_tests': self.tests,
'processor': self.code, 'processor': self.code,
'edX_student_response': unicode(submission), # unicode on File object returns its filename
} }
# Submit request # 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, (error, msg) = qinterface.send_to_queue(header=xheader,
body=json.dumps(contents), body=json.dumps(contents),
file_to_upload=submission) file_to_upload=submission)
else: else:
contents.update({'edX_student_response': submission})
(error, msg) = qinterface.send_to_queue(header=xheader, (error, msg) = qinterface.send_to_queue(header=xheader,
body=json.dumps(contents)) body=json.dumps(contents))
......
...@@ -39,5 +39,18 @@ def convert_files_to_filenames(answers): ...@@ -39,5 +39,18 @@ def convert_files_to_filenames(answers):
''' '''
new_answers = dict() new_answers = dict()
for answer_id in answers.keys(): 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 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): ...@@ -103,7 +103,9 @@ class ABTestDescriptor(RawDescriptor, XmlDescriptor):
experiment = xml_object.get('experiment') experiment = xml_object.get('experiment')
if experiment is None: 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 = { definition = {
'data': { 'data': {
...@@ -127,7 +129,9 @@ class ABTestDescriptor(RawDescriptor, XmlDescriptor): ...@@ -127,7 +129,9 @@ class ABTestDescriptor(RawDescriptor, XmlDescriptor):
definition['data']['group_content'][name] = child_content_urls definition['data']['group_content'][name] = child_content_urls
definition['children'].extend(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: if default_portion < 0:
raise InvalidDefinitionError("ABTest portions must add up to less than or equal to 1") raise InvalidDefinitionError("ABTest portions must add up to less than or equal to 1")
......
...@@ -119,9 +119,9 @@ class CapaModule(XModule): ...@@ -119,9 +119,9 @@ class CapaModule(XModule):
if self.show_answer == "": if self.show_answer == "":
self.show_answer = "closed" self.show_answer = "closed"
if instance_state != None: if instance_state is not None:
instance_state = json.loads(instance_state) 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.attempts = instance_state['attempts']
self.name = only_one(dom2.xpath('/problem/@name')) self.name = only_one(dom2.xpath('/problem/@name'))
...@@ -130,7 +130,7 @@ class CapaModule(XModule): ...@@ -130,7 +130,7 @@ class CapaModule(XModule):
if weight_string: if weight_string:
self.weight = float(weight_string) self.weight = float(weight_string)
else: else:
self.weight = 1 self.weight = None
if self.rerandomize == 'never': if self.rerandomize == 'never':
seed = 1 seed = 1
...@@ -563,6 +563,11 @@ class CapaDescriptor(RawDescriptor): ...@@ -563,6 +563,11 @@ class CapaDescriptor(RawDescriptor):
module_class = CapaModule 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] # VS[compat]
# TODO (cpennington): Delete this method once all fall 2012 course are being # TODO (cpennington): Delete this method once all fall 2012 course are being
# edited in the cms # edited in the cms
...@@ -572,8 +577,3 @@ class CapaDescriptor(RawDescriptor): ...@@ -572,8 +577,3 @@ class CapaDescriptor(RawDescriptor):
'problems/' + path[8:], 'problems/' + path[8:],
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 ...@@ -3,6 +3,7 @@ import time
import dateutil.parser import dateutil.parser
import logging import logging
from xmodule.util.decorators import lazyproperty
from xmodule.graders import load_grading_policy from xmodule.graders import load_grading_policy
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.seq_module import SequenceDescriptor, SequenceModule from xmodule.seq_module import SequenceDescriptor, SequenceModule
...@@ -12,14 +13,10 @@ log = logging.getLogger(__name__) ...@@ -12,14 +13,10 @@ log = logging.getLogger(__name__)
class CourseDescriptor(SequenceDescriptor): class CourseDescriptor(SequenceDescriptor):
module_class = SequenceModule module_class = SequenceModule
metadata_attributes = SequenceDescriptor.metadata_attributes + ('org', 'course')
def __init__(self, system, definition=None, **kwargs): def __init__(self, system, definition=None, **kwargs):
super(CourseDescriptor, self).__init__(system, definition, **kwargs) super(CourseDescriptor, self).__init__(system, definition, **kwargs)
self._grader = None
self._grade_cutoffs = None
msg = None msg = None
try: try:
self.start = time.strptime(self.metadata["start"], "%Y-%m-%dT%H:%M") self.start = time.strptime(self.metadata["start"], "%Y-%m-%dT%H:%M")
...@@ -42,17 +39,14 @@ class CourseDescriptor(SequenceDescriptor): ...@@ -42,17 +39,14 @@ class CourseDescriptor(SequenceDescriptor):
@property @property
def grader(self): def grader(self):
self.__load_grading_policy() return self.__grading_policy['GRADER']
return self._grader
@property @property
def grade_cutoffs(self): def grade_cutoffs(self):
self.__load_grading_policy() return self.__grading_policy['GRADE_CUTOFFS']
return self._grade_cutoffs
def __load_grading_policy(self): @lazyproperty
if not self._grader or not self._grade_cutoffs: def __grading_policy(self):
policy_string = "" policy_string = ""
try: try:
...@@ -63,8 +57,61 @@ class CourseDescriptor(SequenceDescriptor): ...@@ -63,8 +57,61 @@ class CourseDescriptor(SequenceDescriptor):
grading_policy = load_grading_policy(policy_string) grading_policy = load_grading_policy(policy_string)
self._grader = grading_policy['GRADER'] return grading_policy
self._grade_cutoffs = grading_policy['GRADE_CUTOFFS']
@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 @staticmethod
......
...@@ -207,7 +207,7 @@ div.video { ...@@ -207,7 +207,7 @@ div.video {
h3 { h3 {
color: #999; color: #999;
float: left; float: left;
font-size: 12px; font-size: em(14);
font-weight: normal; font-weight: normal;
letter-spacing: 1px; letter-spacing: 1px;
padding: 0 lh(.25) 0 lh(.5); padding: 0 lh(.25) 0 lh(.5);
...@@ -221,6 +221,7 @@ div.video { ...@@ -221,6 +221,7 @@ div.video {
margin-bottom: 0; margin-bottom: 0;
padding: 0 lh(.5) 0 0; padding: 0 lh(.5) 0 0;
line-height: 46px; line-height: 46px;
color: #fff;
} }
&:hover, &:active, &:focus { &:hover, &:active, &:focus {
......
import sys import hashlib
import logging import logging
import random
import string
import sys
from pkg_resources import resource_string from pkg_resources import resource_string
from lxml import etree from lxml import etree
...@@ -35,7 +38,8 @@ class ErrorDescriptor(EditingDescriptor): ...@@ -35,7 +38,8 @@ class ErrorDescriptor(EditingDescriptor):
error_msg='Error not available'): error_msg='Error not available'):
'''Create an instance of this descriptor from the supplied data. '''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 Takes an extra, optional, parameter--the error that caused an
issue. (should be a string, or convert usefully into one). issue. (should be a string, or convert usefully into one).
...@@ -45,6 +49,13 @@ class ErrorDescriptor(EditingDescriptor): ...@@ -45,6 +49,13 @@ class ErrorDescriptor(EditingDescriptor):
definition = {'data': inner} definition = {'data': inner}
inner['error_msg'] = str(error_msg) 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: try:
# If this is already an error tag, don't want to re-wrap it. # If this is already an error tag, don't want to re-wrap it.
xml_obj = etree.fromstring(xml_data) xml_obj = etree.fromstring(xml_data)
...@@ -63,7 +74,7 @@ class ErrorDescriptor(EditingDescriptor): ...@@ -63,7 +74,7 @@ class ErrorDescriptor(EditingDescriptor):
inner['contents'] = xml_data inner['contents'] = xml_data
# TODO (vshnayder): Do we need a unique slug here? Just pick a random # TODO (vshnayder): Do we need a unique slug here? Just pick a random
# 64-bit num? # 64-bit num?
location = ['i4x', org, course, 'error', 'slug'] location = ['i4x', org, course, 'error', url_name]
metadata = {} # stays in the xml_data metadata = {} # stays in the xml_data
return cls(system, definition, location=location, metadata=metadata) return cls(system, definition, location=location, metadata=metadata)
......
...@@ -13,6 +13,7 @@ from .html_checker import check_html ...@@ -13,6 +13,7 @@ from .html_checker import check_html
log = logging.getLogger("mitx.courseware") log = logging.getLogger("mitx.courseware")
class HtmlModule(XModule): class HtmlModule(XModule):
def get_html(self): def get_html(self):
return self.html return self.html
...@@ -36,18 +37,19 @@ class HtmlDescriptor(XmlDescriptor, EditingDescriptor): ...@@ -36,18 +37,19 @@ class HtmlDescriptor(XmlDescriptor, EditingDescriptor):
# are being edited in the cms # are being edited in the cms
@classmethod @classmethod
def backcompat_paths(cls, path): def backcompat_paths(cls, path):
origpath = path
if path.endswith('.html.xml'): 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 = [] candidates = []
while os.sep in path: while os.sep in path:
candidates.append(path) candidates.append(path)
_, _, path = path.partition(os.sep) _, _, path = path.partition(os.sep)
# also look for .html versions instead of .xml # also look for .html versions instead of .xml
if origpath.endswith('.xml'): nc = []
candidates.append(origpath[:-4] + '.html') for candidate in candidates:
return 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 # NOTE: html descriptors are special. We do not want to parse and
# export them ourselves, because that can break things (e.g. lxml # export them ourselves, because that can break things (e.g. lxml
...@@ -69,7 +71,7 @@ class HtmlDescriptor(XmlDescriptor, EditingDescriptor): ...@@ -69,7 +71,7 @@ class HtmlDescriptor(XmlDescriptor, EditingDescriptor):
if filename is None: if filename is None:
definition_xml = copy.deepcopy(xml_object) definition_xml = copy.deepcopy(xml_object)
cls.clean_metadata_from_xml(definition_xml) cls.clean_metadata_from_xml(definition_xml)
return {'data' : stringify_children(definition_xml)} return {'data': stringify_children(definition_xml)}
else: else:
filepath = cls._format_filepath(xml_object.tag, filename) filepath = cls._format_filepath(xml_object.tag, filename)
...@@ -80,7 +82,7 @@ class HtmlDescriptor(XmlDescriptor, EditingDescriptor): ...@@ -80,7 +82,7 @@ class HtmlDescriptor(XmlDescriptor, EditingDescriptor):
# online and has imported all current (fall 2012) courses from xml # online and has imported all current (fall 2012) courses from xml
if not system.resources_fs.exists(filepath): if not system.resources_fs.exists(filepath):
candidates = cls.backcompat_paths(filepath) candidates = cls.backcompat_paths(filepath)
#log.debug("candidates = {0}".format(candidates)) log.debug("candidates = {0}".format(candidates))
for candidate in candidates: for candidate in candidates:
if system.resources_fs.exists(candidate): if system.resources_fs.exists(candidate):
filepath = candidate filepath = candidate
...@@ -95,7 +97,7 @@ class HtmlDescriptor(XmlDescriptor, EditingDescriptor): ...@@ -95,7 +97,7 @@ class HtmlDescriptor(XmlDescriptor, EditingDescriptor):
log.warning(msg) log.warning(msg)
system.error_tracker("Warning: " + msg) system.error_tracker("Warning: " + msg)
definition = {'data' : html} definition = {'data': html}
# TODO (ichuang): remove this after migration # TODO (ichuang): remove this after migration
# for Fall 2012 LMS migration: keep filename (and unmangled filename) # for Fall 2012 LMS migration: keep filename (and unmangled filename)
...@@ -109,17 +111,11 @@ class HtmlDescriptor(XmlDescriptor, EditingDescriptor): ...@@ -109,17 +111,11 @@ class HtmlDescriptor(XmlDescriptor, EditingDescriptor):
# add more info and re-raise # add more info and re-raise
raise Exception(msg), None, sys.exc_info()[2] 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. # TODO (vshnayder): make export put things in the right places.
def definition_to_xml(self, resource_fs): def definition_to_xml(self, resource_fs):
'''If the contents are valid xml, write them to filename.xml. Otherwise, '''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. string to filename.html.
''' '''
try: try:
...@@ -138,4 +134,3 @@ class HtmlDescriptor(XmlDescriptor, EditingDescriptor): ...@@ -138,4 +134,3 @@ class HtmlDescriptor(XmlDescriptor, EditingDescriptor):
elt = etree.Element('html') elt = etree.Element('html')
elt.set("filename", self.url_name) elt.set("filename", self.url_name)
return elt return elt
...@@ -188,21 +188,26 @@ class XMLModuleStore(ModuleStoreBase): ...@@ -188,21 +188,26 @@ class XMLModuleStore(ModuleStoreBase):
course_file = StringIO(clean_out_mako_templating(course_file.read())) course_file = StringIO(clean_out_mako_templating(course_file.read()))
course_data = etree.parse(course_file).getroot() course_data = etree.parse(course_file).getroot()
org = course_data.get('org') org = course_data.get('org')
if org is None: 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)) "Using default 'edx'".format(dir=course_dir))
log.error(msg)
tracker(msg)
org = 'edx' org = 'edx'
course = course_data.get('course') course = course_data.get('course')
if course is None: 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( " Using default '{default}'".format(
dir=course_dir, dir=course_dir,
default=course_dir default=course_dir
)) ))
log.error(msg)
tracker(msg)
course = course_dir course = course_dir
system = ImportSystem(self, org, course, course_dir, tracker) system = ImportSystem(self, org, course, course_dir, tracker)
......
...@@ -122,16 +122,3 @@ class SequenceDescriptor(MakoModuleDescriptor, XmlDescriptor): ...@@ -122,16 +122,3 @@ class SequenceDescriptor(MakoModuleDescriptor, XmlDescriptor):
etree.fromstring(child.export_to_xml(resource_fs))) etree.fromstring(child.export_to_xml(resource_fs)))
return xml_object 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 ...@@ -15,6 +15,7 @@ import xmodule
import capa.calc as calc import capa.calc as calc
import capa.capa_problem as lcp import capa.capa_problem as lcp
from capa.correctmap import CorrectMap from capa.correctmap import CorrectMap
from capa.util import convert_files_to_filenames
from xmodule import graders, x_module from xmodule import graders, x_module
from xmodule.x_module import ModuleSystem from xmodule.x_module import ModuleSystem
from xmodule.graders import Score, aggregate_scores from xmodule.graders import Score, aggregate_scores
...@@ -31,7 +32,7 @@ i4xs = ModuleSystem( ...@@ -31,7 +32,7 @@ i4xs = ModuleSystem(
user=Mock(), user=Mock(),
filestore=fs.osfs.OSFS(os.path.dirname(os.path.realpath(__file__))), filestore=fs.osfs.OSFS(os.path.dirname(os.path.realpath(__file__))),
debug=True, debug=True,
xqueue_callback_url='/', xqueue={'interface':None, 'callback_url':'/', 'default_queuename': 'testqueue'},
is_staff=False is_staff=False
) )
...@@ -278,7 +279,6 @@ class StringResponseWithHintTest(unittest.TestCase): ...@@ -278,7 +279,6 @@ class StringResponseWithHintTest(unittest.TestCase):
class CodeResponseTest(unittest.TestCase): class CodeResponseTest(unittest.TestCase):
''' '''
Test CodeResponse Test CodeResponse
''' '''
def test_update_score(self): def test_update_score(self):
problem_file = os.path.dirname(__file__) + "/test_files/coderesponse.xml" problem_file = os.path.dirname(__file__) + "/test_files/coderesponse.xml"
...@@ -328,6 +328,17 @@ class CodeResponseTest(unittest.TestCase): ...@@ -328,6 +328,17 @@ class CodeResponseTest(unittest.TestCase):
else: else:
self.assertTrue(test_lcp.correct_map.is_queued(answer_ids[j])) # Should be queued, message undelivered 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): class ChoiceResponseTest(unittest.TestCase):
......
from xmodule.modulestore.xml import XMLModuleStore import unittest
from nose.tools import assert_equals
from nose import SkipTest
from tempfile import mkdtemp
from fs.osfs import OSFS 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 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)
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 check_export_roundtrip(data_dir):
class RoundTripTestCase(unittest.TestCase):
'''Check that our test courses roundtrip properly'''
def check_export_roundtrip(self, data_dir, course_dir):
print "Starting import" print "Starting import"
initial_import = XMLModuleStore('org', 'course', data_dir, eager=True) initial_import = XMLModuleStore(data_dir, eager=True, course_dirs=[course_dir])
initial_course = initial_import.course
courses = initial_import.get_courses()
self.assertEquals(len(courses), 1)
initial_course = courses[0]
print "Starting export" print "Starting export"
export_dir = mkdtemp() export_dir = mkdtemp()
print "export_dir: {0}".format(export_dir)
fs = OSFS(export_dir) fs = OSFS(export_dir)
xml = initial_course.export_to_xml(fs) export_course_dir = 'export'
with fs.open('course.xml', 'w') as course_xml: 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) course_xml.write(xml)
print "Starting second import" print "Starting second import"
second_import = XMLModuleStore('org', 'course', export_dir, eager=True) 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" print "Checking key equality"
assert_equals(initial_import.modules.keys(), second_import.modules.keys()) self.assertEquals(sorted(initial_import.modules.keys()),
sorted(second_import.modules.keys()))
print "Checking module equality" print "Checking module equality"
for location in initial_import.modules.keys(): for location in initial_import.modules.keys():
print "Checking", location print "Checking", location
assert_equals(initial_import.modules[location], second_import.modules[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_toy_roundtrip(): def test_full_roundtrip(self):
dir = "" self.check_export_roundtrip(DATA_DIR, "full")
# TODO: add paths and make this run.
raise SkipTest()
check_export_roundtrip(dir)
...@@ -5,6 +5,7 @@ from fs.memoryfs import MemoryFS ...@@ -5,6 +5,7 @@ from fs.memoryfs import MemoryFS
from lxml import etree from lxml import etree
from xmodule.x_module import XMLParsingSystem, XModuleDescriptor from xmodule.x_module import XMLParsingSystem, XModuleDescriptor
from xmodule.xml_module import is_pointer_tag
from xmodule.errortracker import make_error_tracker from xmodule.errortracker import make_error_tracker
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.modulestore.exceptions import ItemNotFoundError from xmodule.modulestore.exceptions import ItemNotFoundError
...@@ -46,22 +47,17 @@ class DummySystem(XMLParsingSystem): ...@@ -46,22 +47,17 @@ class DummySystem(XMLParsingSystem):
raise Exception("Shouldn't be called") raise Exception("Shouldn't be called")
class ImportTestCase(unittest.TestCase): class ImportTestCase(unittest.TestCase):
'''Make sure module imports work properly, including for malformed inputs''' '''Make sure module imports work properly, including for malformed inputs'''
@staticmethod @staticmethod
def get_system(): def get_system():
'''Get a dummy system''' '''Get a dummy system'''
return DummySystem() return DummySystem()
def test_fallback(self): 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>''' bad_xml = '''<sequential display_name="oops"><video url="hi"></sequential>'''
system = self.get_system() system = self.get_system()
descriptor = XModuleDescriptor.load_from_xml(bad_xml, system, 'org', 'course', descriptor = XModuleDescriptor.load_from_xml(bad_xml, system, 'org', 'course',
...@@ -70,6 +66,22 @@ class ImportTestCase(unittest.TestCase): ...@@ -70,6 +66,22 @@ class ImportTestCase(unittest.TestCase):
self.assertEqual(descriptor.__class__.__name__, self.assertEqual(descriptor.__class__.__name__,
'ErrorDescriptor') '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): def test_reimport(self):
'''Make sure an already-exported error xml tag loads properly''' '''Make sure an already-exported error xml tag loads properly'''
...@@ -111,30 +123,65 @@ class ImportTestCase(unittest.TestCase): ...@@ -111,30 +123,65 @@ class ImportTestCase(unittest.TestCase):
xml_out = etree.fromstring(xml_str_out) xml_out = etree.fromstring(xml_str_out)
self.assertEqual(xml_out.tag, 'sequential') self.assertEqual(xml_out.tag, 'sequential')
def test_metadata_inherit(self): def test_metadata_import_export(self):
"""Make sure metadata inherits properly""" """Two checks:
- unknown metadata is preserved across import-export
- inherited metadata doesn't leak to children.
"""
system = self.get_system() system = self.get_system()
v = "1 hour" v = '1 hour'
start_xml = '''<course graceperiod="{grace}" url_name="test1" display_name="myseq"> 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"> <chapter url="hi" url_name="ch" display_name="CH">
<html url_name="h" display_name="H">Two houses, ...</html></chapter> <html url_name="h" display_name="H">Two houses, ...</html>
</course>'''.format(grace=v) </chapter>
</course>'''.format(grace=v, org=org, course=course, url_name=url_name)
descriptor = XModuleDescriptor.load_from_xml(start_xml, system, descriptor = XModuleDescriptor.load_from_xml(start_xml, system,
'org', 'course') org, course)
print "Errors: {0}".format(system.errorlog.errors)
print descriptor, descriptor.metadata print descriptor, descriptor.metadata
self.assertEqual(descriptor.metadata['graceperiod'], v) 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] child = descriptor.get_children()[0]
self.assertEqual(child.metadata['graceperiod'], v) 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() resource_fs = MemoryFS()
exported_xml = descriptor.export_to_xml(resource_fs) exported_xml = descriptor.export_to_xml(resource_fs)
# Check that the exported xml is just a pointer
print "Exported xml:", exported_xml print "Exported xml:", exported_xml
root = etree.fromstring(exported_xml) pointer = etree.fromstring(exported_xml)
chapter_tag = root[0] self.assertTrue(is_pointer_tag(pointer))
self.assertEqual(chapter_tag.tag, 'chapter') # but it's a special case course pointer
self.assertFalse('graceperiod' in chapter_tag.attrib) 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)
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
\ No newline at end of file
...@@ -6,6 +6,7 @@ from fs.errors import ResourceNotFoundError ...@@ -6,6 +6,7 @@ from fs.errors import ResourceNotFoundError
from functools import partial from functools import partial
from lxml import etree from lxml import etree
from lxml.etree import XMLSyntaxError from lxml.etree import XMLSyntaxError
from pprint import pprint
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.errortracker import exc_info_to_str from xmodule.errortracker import exc_info_to_str
...@@ -550,9 +551,9 @@ class XModuleDescriptor(Plugin, HTMLSnippet): ...@@ -550,9 +551,9 @@ class XModuleDescriptor(Plugin, HTMLSnippet):
if not eq: if not eq:
for attr in self.equality_attributes: for attr in self.equality_attributes:
print(getattr(self, attr, None), pprint((getattr(self, attr, None),
getattr(other, attr, None), getattr(other, attr, None),
getattr(self, attr, None) == getattr(other, attr, None)) getattr(self, attr, None) == getattr(other, attr, None)))
return eq return eq
...@@ -643,7 +644,7 @@ class ModuleSystem(object): ...@@ -643,7 +644,7 @@ class ModuleSystem(object):
user=None, user=None,
filestore=None, filestore=None,
debug=False, debug=False,
xqueue = None, xqueue=None,
is_staff=False): is_staff=False):
''' '''
Create a closure around the system environment. Create a closure around the system environment.
......
...@@ -11,22 +11,44 @@ import sys ...@@ -11,22 +11,44 @@ import sys
log = logging.getLogger(__name__) 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): 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 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. the value to store in the xml.
""" """
def __new__(_cls, metadata_key, def __new__(_cls, from_xml=lambda x: x,
to_metadata=lambda x: x, to_xml=lambda x: x):
from_metadata=lambda x: x): return _AttrMapBase.__new__(_cls, from_xml, to_xml)
return _AttrMapBase.__new__(_cls, metadata_key, to_metadata, from_metadata)
class XmlDescriptor(XModuleDescriptor): class XmlDescriptor(XModuleDescriptor):
...@@ -39,19 +61,28 @@ class XmlDescriptor(XModuleDescriptor): ...@@ -39,19 +61,28 @@ class XmlDescriptor(XModuleDescriptor):
# The attributes will be removed from the definition xml passed # The attributes will be removed from the definition xml passed
# to definition_from_xml, and from the xml returned by definition_to_xml # 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', metadata_attributes = ('format', 'graceperiod', 'showanswer', 'rerandomize',
'start', 'due', 'graded', 'display_name', 'url_name', 'hide_from_toc', '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. # VS[compat] Remove once unused.
'name', 'slug') '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 # A dictionary mapping xml attribute names AttrMaps that describe how
# to import and export them # to import and export them
xml_attribute_map = { xml_attribute_map = {
# type conversion: want True/False in python, "true"/"false" in xml # type conversion: want True/False in python, "true"/"false" in xml
'graded': AttrMap('graded', 'graded': AttrMap(lambda val: val == 'true',
lambda val: val == 'true',
lambda val: str(val).lower()), lambda val: str(val).lower()),
} }
...@@ -102,11 +133,31 @@ class XmlDescriptor(XModuleDescriptor): ...@@ -102,11 +133,31 @@ class XmlDescriptor(XModuleDescriptor):
return etree.parse(file_object).getroot() return etree.parse(file_object).getroot()
@classmethod @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): def load_definition(cls, xml_object, system, location):
'''Load a descriptor definition from the specified xml_object. '''Load a descriptor definition from the specified xml_object.
Subclasses should not need to override this except in special Subclasses should not need to override this except in special
cases (e.g. html module)''' 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') filename = xml_object.get('filename')
if filename is None: if filename is None:
definition_xml = copy.deepcopy(xml_object) definition_xml = copy.deepcopy(xml_object)
...@@ -120,22 +171,14 @@ class XmlDescriptor(XModuleDescriptor): ...@@ -120,22 +171,14 @@ class XmlDescriptor(XModuleDescriptor):
# again in the correct format. This should go away once the CMS is # again in the correct format. This should go away once the CMS is
# online and has imported all current (fall 2012) courses from xml # online and has imported all current (fall 2012) courses from xml
if not system.resources_fs.exists(filepath) and hasattr( if not system.resources_fs.exists(filepath) and hasattr(
cls, cls, 'backcompat_paths'):
'backcompat_paths'):
candidates = cls.backcompat_paths(filepath) candidates = cls.backcompat_paths(filepath)
for candidate in candidates: for candidate in candidates:
if system.resources_fs.exists(candidate): if system.resources_fs.exists(candidate):
filepath = candidate filepath = candidate
break break
try: definition_xml = cls.load_file(filepath, system.resources_fs, location)
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]
cls.clean_metadata_from_xml(definition_xml) cls.clean_metadata_from_xml(definition_xml)
definition = cls.definition_from_xml(definition_xml, system) definition = cls.definition_from_xml(definition_xml, system)
...@@ -146,6 +189,28 @@ class XmlDescriptor(XModuleDescriptor): ...@@ -146,6 +189,28 @@ class XmlDescriptor(XModuleDescriptor):
return definition 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 @classmethod
def from_xml(cls, xml_data, system, org=None, course=None): def from_xml(cls, xml_data, system, org=None, course=None):
...@@ -160,26 +225,27 @@ class XmlDescriptor(XModuleDescriptor): ...@@ -160,26 +225,27 @@ class XmlDescriptor(XModuleDescriptor):
url identifiers url identifiers
""" """
xml_object = etree.fromstring(xml_data) xml_object = etree.fromstring(xml_data)
# VS[compat] -- just have the url_name lookup once translation is done # VS[compat] -- just have the url_name lookup, once translation is done
slug = xml_object.get('url_name', xml_object.get('slug')) url_name = xml_object.get('url_name', xml_object.get('slug'))
location = Location('i4x', org, course, xml_object.tag, slug) location = Location('i4x', org, course, xml_object.tag, url_name)
# 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
def load_metadata(): definition = cls.load_definition(definition_xml, system, location)
metadata = {} # VS[compat] -- make Ike's github preview links work in both old and
for attr in cls.metadata_attributes: # new file layouts
val = xml_object.get(attr) if is_pointer_tag(xml_object):
if val is not None: # new style -- contents actually at filepath
# VS[compat]. Remove after all key translations done definition['filename'] = [filepath, filepath]
attr = cls._translate(attr)
attr_map = cls.xml_attribute_map.get(attr, AttrMap(attr)) metadata = cls.load_metadata(definition_xml)
metadata[attr_map.metadata_key] = attr_map.to_metadata(val)
return metadata
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'))
return cls( return cls(
system, system,
definition, definition,
...@@ -193,20 +259,6 @@ class XmlDescriptor(XModuleDescriptor): ...@@ -193,20 +259,6 @@ class XmlDescriptor(XModuleDescriptor):
name=name, name=name,
ext=cls.filename_extension) 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): def export_to_xml(self, resource_fs):
""" """
Returns an xml string representing this module, and all modules Returns an xml string representing this module, and all modules
...@@ -227,42 +279,39 @@ class XmlDescriptor(XModuleDescriptor): ...@@ -227,42 +279,39 @@ class XmlDescriptor(XModuleDescriptor):
xml_object = self.definition_to_xml(resource_fs) xml_object = self.definition_to_xml(resource_fs)
self.__class__.clean_metadata_from_xml(xml_object) 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 xml_object.tag = self.category
# Write it to a file if necessary def val_for_xml(attr):
if self.split_to_file(xml_object): """Get the value for this attribute that we want to store.
# Put this object in its own file (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))
# Write the definition to a file
filepath = self.__class__._format_filepath(self.category, self.url_name) filepath = self.__class__._format_filepath(self.category, self.url_name)
resource_fs.makedir(os.path.dirname(filepath), allow_recreate=True) resource_fs.makedir(os.path.dirname(filepath), allow_recreate=True)
with resource_fs.open(filepath, 'w') as file: with resource_fs.open(filepath, 'w') as file:
file.write(etree.tostring(xml_object, pretty_print=True)) 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 = ''
xml_object.set('filename', self.url_name) # And return just a pointer with the category and filename.
record_object = etree.Element(self.category)
# Add the metadata record_object.set('url_name', self.url_name)
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
if (metadata_key not in self.metadata or
metadata_key in self._inherited_metadata):
continue
val = attr_map.from_metadata(self.metadata[metadata_key]) # Special case for course pointers:
xml_object.set(attr, val) 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)
# Now we just have to make it beautiful return etree.tostring(record_object, pretty_print=True)
return etree.tostring(xml_object, pretty_print=True)
def definition_to_xml(self, resource_fs): def definition_to_xml(self, resource_fs):
""" """
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
<chapter name="Overview"> <chapter name="Overview">
<video name="Welcome" youtube="0.75:izygArpw-Qo,1.0:p2Q6BrNhdh8,1.25:1EeWXzPdhSA,1.50:rABDYkeK0x8"/> <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"> <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"/> <video name="S0V1: Video Resources" youtube="0.75:EuzkdzfR0i8,1.0:1bK-WdDi6Qw,1.25:0v1VzoDVUTM,1.50:Bxk_-ZJb240"/>
</videosequence> </videosequence>
<section name="Lecture 2"> <section name="Lecture 2">
...@@ -15,7 +15,7 @@ ...@@ -15,7 +15,7 @@
<chapter name="Chapter 2"> <chapter name="Chapter 2">
<section name="Problem Set 1"> <section name="Problem Set 1">
<sequential> <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> </sequential>
</section> </section>
<video name="Lost Video" youtube="1.0:TBvX7HzxexQ"/> <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 ...@@ -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. 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 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
\ No newline at end of file
...@@ -52,7 +52,7 @@ def certificate_request(request): ...@@ -52,7 +52,7 @@ def certificate_request(request):
return return_error(survey_response['error']) return return_error(survey_response['error'])
grade = None grade = None
student_gradesheet = grades.grade_sheet(request.user) student_gradesheet = grades.grade(request.user, request, course)
grade = student_gradesheet['grade'] grade = student_gradesheet['grade']
if not grade: if not grade:
...@@ -65,7 +65,7 @@ def certificate_request(request): ...@@ -65,7 +65,7 @@ def certificate_request(request):
else: else:
#This is not a POST, we should render the page with the form #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']) certificate_state = certificate_state_for_student(request.user, grade_sheet['grade'])
if certificate_state['state'] != "requestable": if certificate_state['state'] != "requestable":
......
...@@ -46,12 +46,15 @@ def check_course(course_id, course_must_be_open=True, course_required=True): ...@@ -46,12 +46,15 @@ def check_course(course_id, course_must_be_open=True, course_required=True):
def course_image_url(course): 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): 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: Valid keys:
- overview - overview
- title - title
...@@ -70,18 +73,23 @@ def get_course_about_section(course, section_key): ...@@ -70,18 +73,23 @@ def get_course_about_section(course, section_key):
- more_info - more_info
""" """
# Many of these are stored as html files instead of some semantic markup. This can change without effecting # Many of these are stored as html files instead of some semantic
# this interface when we find a good format for defining so many snippets of text/html. # 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 # TODO: Remove number, instructors from this list
if section_key in ['short_description', 'description', 'key_dates', 'video', 'course_staff_short', 'course_staff_extended', if section_key in ['short_description', 'description', 'key_dates', 'video',
'requirements', 'syllabus', 'textbook', 'faq', 'more_info', 'number', 'instructors', 'overview', 'course_staff_short', 'course_staff_extended',
'requirements', 'syllabus', 'textbook', 'faq', 'more_info',
'number', 'instructors', 'overview',
'effort', 'end_date', 'prerequisites']: 'effort', 'end_date', 'prerequisites']:
try: try:
with course.system.resources_fs.open(path("about") / section_key + ".html") as htmlFile: 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: 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 return None
elif section_key == "title": elif section_key == "title":
return course.metadata.get('display_name', course.url_name) return course.metadata.get('display_name', course.url_name)
...@@ -95,7 +103,9 @@ def get_course_about_section(course, section_key): ...@@ -95,7 +103,9 @@ def get_course_about_section(course, section_key):
def get_course_info_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: Valid keys:
- handouts - handouts
- guest_handouts - guest_handouts
...@@ -103,43 +113,51 @@ def get_course_info_section(course, section_key): ...@@ -103,43 +113,51 @@ def get_course_info_section(course, section_key):
- guest_updates - guest_updates
""" """
# Many of these are stored as html files instead of some semantic markup. This can change without effecting # Many of these are stored as html files instead of some semantic
# this interface when we find a good format for defining so many snippets of text/html. # 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']: if section_key in ['handouts', 'guest_handouts', 'updates', 'guest_updates']:
try: try:
with course.system.resources_fs.open(path("info") / section_key + ".html") as htmlFile: 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: 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 !" return "! Info section missing !"
raise KeyError("Invalid about key " + str(section_key)) raise KeyError("Invalid about key " + str(section_key))
def course_staff_group_name(course): 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 coursename = course
else: else:
coursename = course.metadata.get('data_dir','UnknownCourseName') # should be a CourseDescriptor, so grab its location.course:
if not coursename: # Fall 2012: not all course.xml have metadata correct yet coursename = course.location.course
coursename = course.metadata.get('course','')
return 'staff_%s' % coursename 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. 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. 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: if user is None or (not user.is_authenticated()) or course is None:
return False return False
if user.is_staff: if user.is_staff:
return True 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) 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: if staff_group in user_groups:
return True return True
return False return False
...@@ -154,7 +172,8 @@ def get_courses_by_university(user): ...@@ -154,7 +172,8 @@ def get_courses_by_university(user):
Returns dict of lists of courses available, keyed by course.org (ie university). Returns dict of lists of courses available, keyed by course.org (ie university).
Courses are sorted by course.number. 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. # TODO: Clean up how 'error' is done.
# filter out any courses that errored. # filter out any courses that errored.
...@@ -163,7 +182,7 @@ def get_courses_by_university(user): ...@@ -163,7 +182,7 @@ def get_courses_by_university(user):
courses = sorted(courses, key=lambda course: course.number) courses = sorted(courses, key=lambda course: course.number)
universities = defaultdict(list) universities = defaultdict(list)
for course in courses: 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): if not has_access_to_course(user,course):
continue continue
universities[course.org].append(course) universities[course.org].append(course)
......
...@@ -3,26 +3,135 @@ import logging ...@@ -3,26 +3,135 @@ import logging
from django.conf import settings from django.conf import settings
from models import StudentModuleCache
from module_render import get_module, get_instance_module
from xmodule import graders from xmodule import graders
from xmodule.graders import Score from xmodule.graders import Score
from models import StudentModule from models import StudentModule
_log = logging.getLogger("mitx.courseware") log = logging.getLogger("mitx.courseware")
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 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))
def grade_sheet(student, course, grader, student_module_cache): 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
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
""" """
This pulls a summary of all problems in the course. It returns a dictionary
with two datastructures:
- courseware_summary is a summary of all sections with problems in the letter_grade = None
course. It is organized as an array of chapters, each containing an array of for possible_grade in ['A', 'B', 'C']:
sections, each containing an array of scores. This contains information for if percentage >= grade_cutoffs[possible_grade]:
graded and ungraded problems, and is good for displaying a course summary letter_grade = possible_grade
with due dates, etc. break
- grade_summary is the output from the course grader. More information on return letter_grade
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: Arguments:
student: A User object for the student to grade student: A User object for the student to grade
...@@ -30,49 +139,24 @@ def grade_sheet(student, course, grader, student_module_cache): ...@@ -30,49 +139,24 @@ def grade_sheet(student, course, grader, student_module_cache):
student_module_cache: A StudentModuleCache initialized with all student_module_cache: A StudentModuleCache initialized with all
instance_modules for the student instance_modules for the student
""" """
totaled_scores = {}
chapters = [] chapters = []
for c in course.get_children(): for c in course.get_children():
sections = [] sections = []
for s in c.get_children(): 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) graded = s.metadata.get('graded', False)
scores = [] scores = []
for module in yield_descendents(s): for module in yield_module_descendents(s):
(correct, total) = get_score(student, module, student_module_cache) (correct, total) = get_score(student, module, student_module_cache)
if correct is None and total is None: if correct is None and total is None:
continue 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, scores.append(Score(correct, total, graded,
module.metadata.get('display_name'))) module.metadata.get('display_name')))
section_total, graded_total = graders.aggregate_scores( section_total, graded_total = graders.aggregate_scores(
scores, s.metadata.get('display_name')) scores, s.metadata.get('display_name'))
#Add the graded total to totaled_scores
format = s.metadata.get('format', "") 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({ sections.append({
'display_name': s.display_name, 'display_name': s.display_name,
'url_name': s.url_name, 'url_name': s.url_name,
...@@ -88,13 +172,10 @@ def grade_sheet(student, course, grader, student_module_cache): ...@@ -88,13 +172,10 @@ def grade_sheet(student, course, grader, student_module_cache):
'url_name': c.url_name, 'url_name': c.url_name,
'sections': sections}) 'sections': sections})
grade_summary = grader.grade(totaled_scores) return chapters
return {'courseware_summary': chapters,
'grade_summary': grade_summary}
def get_score(user, problem, cache): def get_score(user, problem, student_module_cache):
""" """
Return the score for a user on a problem Return the score for a user on a problem
...@@ -105,17 +186,18 @@ def get_score(user, problem, cache): ...@@ -105,17 +186,18 @@ def get_score(user, problem, cache):
correct = 0.0 correct = 0.0
# If the ID is not in the cache, add the item # If the ID is not in the cache, add the item
instance_module = cache.lookup(problem.category, problem.id) instance_module = get_instance_module(user, problem, student_module_cache)
if instance_module is None: # instance_module = student_module_cache.lookup(problem.category, problem.id)
instance_module = StudentModule(module_type=problem.category, # if instance_module is None:
module_state_key=problem.id, # instance_module = StudentModule(module_type=problem.category,
student=user, # module_state_key=problem.id,
state=None, # student=user,
grade=0, # state=None,
max_grade=problem.max_score(), # grade=0,
done='i') # max_grade=problem.max_score(),
cache.append(instance_module) # done='i')
instance_module.save() # cache.append(instance_module)
# instance_module.save()
# If this problem is ungraded/ungradable, bail # If this problem is ungraded/ungradable, bail
if instance_module.max_grade is None: if instance_module.max_grade is None:
...@@ -126,8 +208,11 @@ def get_score(user, problem, cache): ...@@ -126,8 +208,11 @@ def get_score(user, problem, cache):
if correct is not None and total is not None: if correct is not None and total is not None:
#Now we re-weight the problem, if specified #Now we re-weight the problem, if specified
weight = getattr(problem, 'weight', 1) weight = getattr(problem, 'weight', None)
if weight != 1: 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 correct = correct * weight / total
total = weight total = weight
......
...@@ -78,8 +78,8 @@ class Command(BaseCommand): ...@@ -78,8 +78,8 @@ class Command(BaseCommand):
# TODO (cpennington): Get coursename in a legitimate way # TODO (cpennington): Get coursename in a legitimate way
course_location = 'i4x://edx/6002xs12/course/6.002_Spring_2012' course_location = 'i4x://edx/6002xs12/course/6.002_Spring_2012'
student_module_cache = StudentModuleCache(sample_user, modulestore().get_item(course_location)) 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) course = get_module(sample_user, None, course_location, student_module_cache)
to_run = [ to_run = [
#TODO (vshnayder) : make check_rendering work (use module_render.py), #TODO (vshnayder) : make check_rendering work (use module_render.py),
......
...@@ -67,17 +67,19 @@ class StudentModuleCache(object): ...@@ -67,17 +67,19 @@ class StudentModuleCache(object):
""" """
A cache of StudentModules for a specific student 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 Find any StudentModule objects that are needed by any child modules of the
supplied descriptor. Avoids making multiple queries to the database supplied descriptor, or caches only the StudentModule objects specifically
for every descriptor in descriptors. Avoids making multiple queries to the
database.
descriptor: An XModuleDescriptor Arguments
depth is the number of levels of descendent modules to load StudentModules for, in addition to user: The user for which to fetch maching StudentModules
the supplied descriptor. If depth is None, load all descendent StudentModules descriptors: An array of XModuleDescriptors.
''' '''
if user.is_authenticated(): 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 # This works around a limitation in sqlite3 on the number of parameters
# that can be put into a single query # that can be put into a single query
...@@ -92,26 +94,51 @@ class StudentModuleCache(object): ...@@ -92,26 +94,51 @@ class StudentModuleCache(object):
else: else:
self.cache = [] 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 descriptor: An XModuleDescriptor
depth is the number of levels of descendent modules to load StudentModules for, in addition to 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 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
keys = [descriptor.location.url()] should be cached
"""
shared_state_key = getattr(descriptor, 'shared_state_key', None) def get_child_descriptors(descriptor, depth, descriptor_filter):
if shared_state_key is not None: if descriptor_filter(descriptor):
keys.append(shared_state_key) descriptors = [descriptor]
else:
descriptors = []
if depth is None or depth > 0: if depth is None or depth > 0:
new_depth = depth - 1 if depth is not None else depth new_depth = depth - 1 if depth is not None else depth
for child in descriptor.get_children(): for child in descriptor.get_children():
keys.extend(self._get_module_state_keys(child, new_depth)) 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):
'''
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 return keys
......
...@@ -51,8 +51,8 @@ def toc_for_course(user, request, course, active_chapter, active_section): ...@@ -51,8 +51,8 @@ def toc_for_course(user, request, course, active_chapter, active_section):
chapters with name 'hidden' are skipped. chapters with name 'hidden' are skipped.
''' '''
student_module_cache = StudentModuleCache(user, course, depth=2) student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(user, course, depth=2)
(course, _, _, _) = get_module(user, request, course.location, student_module_cache) course = get_module(user, request, course.location, student_module_cache)
chapters = list() chapters = list()
for chapter in course.get_display_items(): for chapter in course.get_display_items():
...@@ -121,16 +121,13 @@ def get_module(user, request, location, student_module_cache, position=None): ...@@ -121,16 +121,13 @@ def get_module(user, request, location, student_module_cache, position=None):
- position : extra information from URL for user-specified - position : extra information from URL for user-specified
position within module position within module
Returns: Returns: xmodule instance
- 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
''' '''
descriptor = modulestore().get_item(location) descriptor = modulestore().get_item(location)
#TODO Only check the cache if this module can possibly have state
if user.is_authenticated():
instance_module = student_module_cache.lookup(descriptor.category, instance_module = student_module_cache.lookup(descriptor.category,
descriptor.location.url()) descriptor.location.url())
...@@ -140,6 +137,10 @@ def get_module(user, request, location, student_module_cache, position=None): ...@@ -140,6 +137,10 @@ def get_module(user, request, location, student_module_cache, position=None):
shared_state_key) shared_state_key)
else: else:
shared_module = None shared_module = None
else:
instance_module = None
shared_module = None
instance_state = instance_module.state if instance_module is not None else {} 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 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): ...@@ -163,9 +164,8 @@ def get_module(user, request, location, student_module_cache, position=None):
'default_queuename': xqueue_default_queuename.replace(' ','_') } 'default_queuename': xqueue_default_queuename.replace(' ','_') }
def _get_module(location): def _get_module(location):
(module, _, _, _) = get_module(user, request, location, return get_module(user, request, location,
student_module_cache, position) student_module_cache, position)
return module
# TODO (cpennington): When modules are shared between courses, the static # TODO (cpennington): When modules are shared between courses, the static
# prefix is going to have to be specific to the module, not the directory # prefix is going to have to be specific to the module, not the directory
...@@ -198,21 +198,46 @@ def get_module(user, request, location, student_module_cache, position=None): ...@@ -198,21 +198,46 @@ def get_module(user, request, location, student_module_cache, position=None):
if has_staff_access_to_course(user, module.location.course): if has_staff_access_to_course(user, module.location.course):
module.get_html = add_histogram(module.get_html, module) module.get_html = add_histogram(module.get_html, module)
# If StudentModule for this instance wasn't already in the database, return module
# and this isn't a guest user, create it.
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(): if user.is_authenticated():
instance_module = student_module_cache.lookup(module.category,
module.location.url())
if not instance_module: if not instance_module:
instance_module = StudentModule( instance_module = StudentModule(
student=user, student=user,
module_type=descriptor.category, module_type=module.category,
module_state_key=module.id, module_state_key=module.id,
state=module.get_instance_state(), state=module.get_instance_state(),
max_grade=module.max_score()) max_grade=module.max_score())
instance_module.save() 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) student_module_cache.append(instance_module)
if not shared_module and shared_state_key is not None:
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( shared_module = StudentModule(
student=user, student=user,
module_type=descriptor.category, module_type=descriptor.category,
...@@ -220,9 +245,12 @@ def get_module(user, request, location, student_module_cache, position=None): ...@@ -220,9 +245,12 @@ def get_module(user, request, location, student_module_cache, position=None):
state=module.get_shared_state()) state=module.get_shared_state())
shared_module.save() shared_module.save()
student_module_cache.append(shared_module) student_module_cache.append(shared_module)
else:
shared_module = None
return (module, instance_module, shared_module, descriptor.category) return shared_module
else:
return None
@csrf_exempt @csrf_exempt
def xqueue_callback(request, userid, id, dispatch): def xqueue_callback(request, userid, id, dispatch):
...@@ -240,12 +268,13 @@ def xqueue_callback(request, userid, id, dispatch): ...@@ -240,12 +268,13 @@ def xqueue_callback(request, userid, id, dispatch):
# Retrieve target StudentModule # Retrieve target StudentModule
user = User.objects.get(id=userid) user = User.objects.get(id=userid)
student_module_cache = StudentModuleCache(user, modulestore().get_item(id)) student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(user, modulestore().get_item(id))
instance, instance_module, shared_module, module_type = get_module(request.user, request, id, student_module_cache) instance = get_module(user, request, id, student_module_cache)
instance_module = get_instance_module(user, instance, student_module_cache)
if instance_module is None: if instance_module is None:
log.debug("Couldn't find module '%s' for user '%s'", log.debug("Couldn't find module '%s' for user '%s'",
id, request.user) id, user)
raise Http404 raise Http404
oldgrade = instance_module.grade oldgrade = instance_module.grade
...@@ -285,15 +314,17 @@ def modx_dispatch(request, dispatch=None, id=None): ...@@ -285,15 +314,17 @@ def modx_dispatch(request, dispatch=None, id=None):
- id -- the module id. Used to look up the XModule instance - id -- the module id. Used to look up the XModule instance
''' '''
# ''' (fix emacs broken parsing) # ''' (fix emacs broken parsing)
# Check for submitted files # Check for submitted files
p = request.POST.copy() p = request.POST.copy()
if request.FILES: if request.FILES:
for inputfile_id in request.FILES.keys(): for inputfile_id in request.FILES.keys():
p[inputfile_id] = request.FILES[inputfile_id] p[inputfile_id] = request.FILES[inputfile_id]
student_module_cache = StudentModuleCache(request.user, modulestore().get_item(id)) student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(request.user, modulestore().get_item(id))
instance, instance_module, shared_module, module_type = get_module(request.user, request, id, student_module_cache) 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) # Don't track state for anonymous users (who don't have student modules)
if instance_module is not None: if instance_module is not None:
......
...@@ -135,10 +135,25 @@ class ActivateLoginTestCase(TestCase): ...@@ -135,10 +135,25 @@ class ActivateLoginTestCase(TestCase):
class PageLoader(ActivateLoginTestCase): class PageLoader(ActivateLoginTestCase):
''' Base class that adds a function to load all pages in a modulestore ''' ''' 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): def check_pages_load(self, course_name, data_dir, modstore):
print "Checking course {0} in {1}".format(course_name, data_dir) print "Checking course {0} in {1}".format(course_name, data_dir)
import_from_xml(modstore, data_dir, [course_name]) 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 n = 0
num_bad = 0 num_bad = 0
all_ok = True all_ok = True
......
...@@ -48,7 +48,6 @@ log = logging.getLogger("mitx.courseware") ...@@ -48,7 +48,6 @@ log = logging.getLogger("mitx.courseware")
template_imports = {'urllib': urllib} template_imports = {'urllib': urllib}
def user_groups(user): def user_groups(user):
if not user.is_authenticated(): if not user.is_authenticated():
return [] return []
...@@ -59,6 +58,8 @@ def user_groups(user): ...@@ -59,6 +58,8 @@ def user_groups(user):
# Kill caching on dev machines -- we switch groups a lot # Kill caching on dev machines -- we switch groups a lot
group_names = cache.get(key) group_names = cache.get(key)
if settings.DEBUG:
group_names = None
if group_names is None: if group_names is None:
group_names = [u.name for u in UserTestGroup.objects.filter(users=user)] group_names = [u.name for u in UserTestGroup.objects.filter(users=user)]
...@@ -86,14 +87,13 @@ def gradebook(request, course_id): ...@@ -86,14 +87,13 @@ def gradebook(request, course_id):
student_objects = User.objects.all()[:100] student_objects = User.objects.all()[:100]
student_info = [] student_info = []
#TODO: Only select students who are in the course
for student in student_objects: for student in student_objects:
student_module_cache = StudentModuleCache(student, course)
course, _, _, _ = get_module(request.user, request, course.location, student_module_cache)
student_info.append({ student_info.append({
'username': student.username, 'username': student.username,
'id': student.id, 'id': student.id,
'email': student.email, '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 'realname': UserProfile.objects.get(user=student).name
}) })
...@@ -116,8 +116,11 @@ def profile(request, course_id, student_id=None): ...@@ -116,8 +116,11 @@ def profile(request, course_id, student_id=None):
user_info = UserProfile.objects.get(user=student) user_info = UserProfile.objects.get(user=student)
student_module_cache = StudentModuleCache(request.user, course) student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(request.user, course)
course_module, _, _, _ = get_module(request.user, request, course.location, student_module_cache) 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, context = {'name': user_info.name,
'username': student.username, 'username': student.username,
...@@ -125,9 +128,11 @@ def profile(request, course_id, student_id=None): ...@@ -125,9 +128,11 @@ def profile(request, course_id, student_id=None):
'language': user_info.language, 'language': user_info.language,
'email': student.email, 'email': student.email,
'course': course, '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) return render_to_response('profile.html', context)
...@@ -198,9 +203,10 @@ def index(request, course_id, chapter=None, section=None, ...@@ -198,9 +203,10 @@ def index(request, course_id, chapter=None, section=None,
if look_for_module: if look_for_module:
section_descriptor = get_section(course, chapter, section) section_descriptor = get_section(course, chapter, section)
if section_descriptor is not None: if section_descriptor is not None:
student_module_cache = StudentModuleCache(request.user, student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(
request.user,
section_descriptor) section_descriptor)
module, _, _, _ = get_module(request.user, request, module = get_module(request.user, request,
section_descriptor.location, section_descriptor.location,
student_module_cache) student_module_cache)
context['content'] = module.get_html() context['content'] = module.get_html()
......
...@@ -158,6 +158,9 @@ COURSE_SETTINGS = {'6.002x_Fall_2012': {'number' : '6.002x', ...@@ -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 ################################## ############################### XModule Store ##################################
MODULESTORE = { MODULESTORE = {
......
...@@ -16,3 +16,8 @@ MITX_FEATURES['ENABLE_TEXTBOOK'] = False ...@@ -16,3 +16,8 @@ MITX_FEATURES['ENABLE_TEXTBOOK'] = False
MITX_FEATURES['ENABLE_DISCUSSION'] = 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 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') ])
...@@ -20,7 +20,7 @@ div.info-wrapper { ...@@ -20,7 +20,7 @@ div.info-wrapper {
> li { > li {
@extend .clearfix; @extend .clearfix;
border-bottom: 1px solid #e3e3e3; border-bottom: 1px solid lighten($border-color, 10%);
margin-bottom: lh(); margin-bottom: lh();
padding-bottom: lh(.5); padding-bottom: lh(.5);
list-style-type: disk; list-style-type: disk;
...@@ -76,42 +76,29 @@ div.info-wrapper { ...@@ -76,42 +76,29 @@ div.info-wrapper {
h1 { h1 {
@extend .bottom-border; @extend .bottom-border;
padding: lh(.5) lh(.5); padding: lh(.5) lh(.5);
}
header {
// h1 {
// font-weight: 100;
// font-style: italic;
// }
p {
color: #666;
font-size: 12px;
margin-bottom: 0; margin-bottom: 0;
margin-top: 4px;
}
} }
ol { ol {
background: none; background: none;
list-style: none; list-style: none;
padding-left: 0; padding-left: 0;
margin: 0;
li { li {
@extend .clearfix; @extend .clearfix;
background: none; background: none;
border-bottom: 1px solid #d3d3d3; border-bottom: 1px solid $border-color;
@include box-shadow(0 1px 0 #eee);
@include box-sizing(border-box); @include box-sizing(border-box);
padding: em(7) lh(.75); padding: em(7) lh(.75);
position: relative; position: relative;
font-size: 1em;
&.expandable, &.expandable,
&.collapsable { &.collapsable {
h4 { h4 {
font-style: $body-font-size;
font-weight: normal; font-weight: normal;
font-size: 1em;
padding-left: 18px; padding-left: 18px;
} }
} }
...@@ -122,16 +109,12 @@ div.info-wrapper { ...@@ -122,16 +109,12 @@ div.info-wrapper {
li { li {
border-bottom: 0; border-bottom: 0;
border-top: 1px solid #d3d3d3; border-top: 1px solid $border-color;
@include box-shadow(inset 0 1px 0 #eee); @include box-shadow(inset 0 1px 0 #eee);
padding-left: lh(1.5); font-size: 1em;
} }
} }
&:hover {
background-color: #e9e9e9;
}
div.hitarea { div.hitarea {
background-image: url('../images/treeview-default.gif'); background-image: url('../images/treeview-default.gif');
display: block; display: block;
...@@ -159,14 +142,12 @@ div.info-wrapper { ...@@ -159,14 +142,12 @@ div.info-wrapper {
h3 { h3 {
border-bottom: 0; border-bottom: 0;
@include box-shadow(none); @include box-shadow(none);
color: #999; color: #aaa;
font-size: $body-font-size; font-size: 1em;
font-weight: bold; margin-bottom: em(6);
text-transform: uppercase;
} }
p { p {
font-size: $body-font-size;
letter-spacing: 0; letter-spacing: 0;
margin: 0; margin: 0;
text-transform: none; text-transform: none;
...@@ -191,14 +172,8 @@ div.info-wrapper { ...@@ -191,14 +172,8 @@ div.info-wrapper {
} }
a { a {
color: lighten($text-color, 10%);
@include inline-block(); @include inline-block();
text-decoration: none; line-height: lh();
@include transition();
&:hover {
color: $mit-red;
}
} }
} }
} }
......
...@@ -10,38 +10,26 @@ div.profile-wrapper { ...@@ -10,38 +10,26 @@ div.profile-wrapper {
header { header {
@extend .bottom-border; @extend .bottom-border;
margin: 0 ; margin: 0;
padding: lh(.5) lh(); padding: lh(.5);
h1 { h1 {
font-size: 18px;
margin: 0; margin: 0;
padding-right: 30px; padding-right: 30px;
} }
a {
color: #999;
font-size: 12px;
position: absolute;
right: lh(.5);
text-transform: uppercase;
top: 13px;
&:hover {
color: #555;
}
}
} }
ul { ul {
list-style: none; list-style: none;
padding: 0;
margin: 0;
li { li {
border-bottom: 1px solid #d3d3d3; border-bottom: 1px solid #d3d3d3;
@include box-shadow(0 1px 0 #eee); @include box-shadow(0 1px 0 #eee);
color: lighten($text-color, 10%); color: lighten($text-color, 10%);
display: block; display: block;
padding: 7px lh(); padding: lh(.5) 0 lh(.5) lh(.5);
position: relative; position: relative;
text-decoration: none; text-decoration: none;
@include transition(); @include transition();
...@@ -144,11 +132,14 @@ div.profile-wrapper { ...@@ -144,11 +132,14 @@ div.profile-wrapper {
@extend .content; @extend .content;
header { header {
@extend h1.top-header;
@extend .clearfix; @extend .clearfix;
@extend h1.top-header;
margin-bottom: lh();
h1 { h1 {
float: left; float: left;
font-size: 1em;
font-weight: 100;
margin: 0; margin: 0;
} }
} }
...@@ -162,6 +153,7 @@ div.profile-wrapper { ...@@ -162,6 +153,7 @@ div.profile-wrapper {
border-top: 1px solid #e3e3e3; border-top: 1px solid #e3e3e3;
list-style: none; list-style: none;
margin-top: lh(); margin-top: lh();
padding-left: 0;
> li { > li {
@extend .clearfix; @extend .clearfix;
...@@ -178,9 +170,11 @@ div.profile-wrapper { ...@@ -178,9 +170,11 @@ div.profile-wrapper {
border-right: 1px dashed #ddd; border-right: 1px dashed #ddd;
@include box-sizing(border-box); @include box-sizing(border-box);
display: table-cell; display: table-cell;
letter-spacing: 0;
margin: 0; margin: 0;
padding: 0; padding: 0;
padding-right: flex-gutter(9); padding-right: flex-gutter(9);
text-transform: none;
width: flex-grid(2, 9); width: flex-grid(2, 9);
} }
...@@ -203,13 +197,37 @@ div.profile-wrapper { ...@@ -203,13 +197,37 @@ div.profile-wrapper {
h3 { h3 {
color: #666; color: #666;
span {
color: #999;
font-size: em(14);
font-weight: 100;
}
}
p {
color: #999;
font-size: em(14);
}
section.scores {
margin: lh(.5) 0;
h3 {
font-size: em(14);
@include inline-block;
} }
ol { ol {
list-style: none; list-style: none;
margin: 0;
padding: 0;
@include inline-block;
li { li {
display: inline-block; @include inline-block;
font-size: em(14);
font-weight: normal;
padding-right: 1em; padding-right: 1em;
} }
} }
...@@ -218,4 +236,5 @@ div.profile-wrapper { ...@@ -218,4 +236,5 @@ div.profile-wrapper {
} }
} }
} }
}
} }
...@@ -7,7 +7,7 @@ div.book-wrapper { ...@@ -7,7 +7,7 @@ div.book-wrapper {
@include box-sizing(border-box); @include box-sizing(border-box);
ul#booknav { ul#booknav {
font-size: 12px; font-size: $body-font-size;
a { a {
color: #000; color: #000;
...@@ -39,8 +39,7 @@ div.book-wrapper { ...@@ -39,8 +39,7 @@ div.book-wrapper {
} }
> li { > li {
border-bottom: 1px solid #d3d3d3; border-bottom: 1px solid $border-color;
@include box-shadow(0 1px 0 #eee);
padding: 7px 7px 7px 30px; padding: 7px 7px 7px 30px;
} }
} }
...@@ -48,9 +47,11 @@ div.book-wrapper { ...@@ -48,9 +47,11 @@ div.book-wrapper {
section.book { section.book {
@extend .content; @extend .content;
padding-right: 0;
padding-bottom: 0;
padding-top: 0;
nav { nav {
@extend .topbar;
@extend .clearfix; @extend .clearfix;
a { a {
...@@ -62,32 +63,57 @@ div.book-wrapper { ...@@ -62,32 +63,57 @@ div.book-wrapper {
@extend .clearfix; @extend .clearfix;
li { 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 { &.last {
display: block; left: 0;
float: left;
a { a {
border-left: 0; background-image: url('../images/textbook/textbook-left.png');
border-right: 1px solid darken(#f6efd4, 20%);
@include box-shadow(inset -1px 0 0 lighten(#f6efd4, 5%));
} }
} }
&.next { &.next {
display: block; right: 0;
float: right;
}
&:hover { a {
background: none; background-image: url('../images/textbook/textbook-right.png');
} }
} }
}
} }
&.bottom-nav { &.bottom-nav {
border-bottom: 0; border-bottom: 0;
border-top: 1px solid #EDDFAA;
margin-bottom: -(lh()); margin-bottom: -(lh());
margin-top: lh(); margin-top: lh();
} }
...@@ -95,9 +121,10 @@ div.book-wrapper { ...@@ -95,9 +121,10 @@ div.book-wrapper {
section.page { section.page {
text-align: center; text-align: center;
position: relative;
border: 1px solid $border-color;
img { img {
border: 1px solid $border-color;
max-width: 100%; max-width: 100%;
} }
} }
......
body { body, h1, h2, h3, h4, h5, h6, p, p a:link, p a:visited, a {
font-family: $sans-serif;
}
h1, h2, h3, h4, h5, h6 {
font-family: $sans-serif; font-family: $sans-serif;
} }
...@@ -19,3 +15,9 @@ table { ...@@ -19,3 +15,9 @@ table {
table-layout: fixed; table-layout: fixed;
} }
} }
form {
label {
display: block;
}
}
h1.top-header { h1.top-header {
border-bottom: 1px solid #e3e3e3; border-bottom: 1px solid #e3e3e3;
text-align: left; text-align: left;
font-size: 24px; font-size: em(24);
font-weight: 100; font-weight: 100;
padding-bottom: lh(); padding-bottom: lh();
} }
...@@ -51,7 +51,6 @@ h1.top-header { ...@@ -51,7 +51,6 @@ h1.top-header {
.sidebar { .sidebar {
border-right: 1px solid #C8C8C8; border-right: 1px solid #C8C8C8;
@include box-shadow(inset -1px 0 0 #e6e6e6);
@include box-sizing(border-box); @include box-sizing(border-box);
display: table-cell; display: table-cell;
font-family: $sans-serif; font-family: $sans-serif;
...@@ -75,7 +74,7 @@ h1.top-header { ...@@ -75,7 +74,7 @@ h1.top-header {
} }
.bottom-border { .bottom-border {
border-bottom: 1px solid #d3d3d3; border-bottom: 1px solid $border-color;
} }
@media print { @media print {
......
...@@ -4,78 +4,70 @@ div#wiki_panel { ...@@ -4,78 +4,70 @@ div#wiki_panel {
h2 { h2 {
@extend .bottom-border; @extend .bottom-border;
font-size: 18px;
margin: 0 ; margin: 0 ;
padding: lh(.5) lh(); padding: lh(.5) lh() lh(.5) 0;
} color: #000;
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%;
} }
ul { ul {
li { padding-left: 0;
@include box-shadow(inset 0 1px 0 0 #eee); margin: 0;
border-top: 1px solid #d3d3d3;
&:hover {
background: #efefef;
@include background-image(linear-gradient(-90deg, rgb(245,245,245), rgb(225,225,225)));
}
&:first-child { li {
border: none; @extend .bottom-border;
}
&.search { &.search {
padding: 10px lh(); padding: 10px lh() 10px 0;
label { label {
display: none; display: none;
} }
} }
&.create-article {
h3 {
}
}
a { a {
color: #666; color: #666;
font-size: 14px; 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 { div#wiki_create_form {
@extend .clearfix; @extend .clearfix;
background: #dadada; padding: lh(.5) lh() lh(.5) 0;
border-bottom: 1px solid #d3d3d3;
padding: 15px; label {
font-family: $sans-serif;
margin-bottom: lh(.5);
}
input[type="text"] { input[type="text"] {
@include box-sizing(border-box); @include box-sizing(border-box);
display: block; display: block;
margin-bottom: 6px;
width: 100%; width: 100%;
margin-bottom: lh(.5);
} }
ul { ul {
list-style: none; list-style: none;
margin: 0;
li { li {
float: left; float: left;
border-bottom: 0;
&#cancel { &#cancel {
float: right; float: right;
......
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; }
/*
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 @@ ...@@ -299,3 +299,7 @@
} }
} }
} }
.leanModal_box {
@extend .modal;
}
...@@ -8,6 +8,7 @@ ...@@ -8,6 +8,7 @@
</%block> </%block>
<%block name="headextra"> <%block name="headextra">
<%static:css group='course'/>
<style type="text/css"> <style type="text/css">
.grade_a {color:green;} .grade_a {color:green;}
...@@ -19,7 +20,8 @@ ...@@ -19,7 +20,8 @@
</%block> </%block>
<%include file="navigation.html" args="active_page=''" /> <%include file="course_navigation.html" args="active_page=''" />
<section class="container"> <section class="container">
<div class="gradebook-wrapper"> <div class="gradebook-wrapper">
<section class="gradebook-content"> <section class="gradebook-content">
...@@ -28,7 +30,7 @@ ...@@ -28,7 +30,7 @@
%if len(students) > 0: %if len(students) > 0:
<table> <table>
<% <%
templateSummary = students[0]['grade_info']['grade_summary'] templateSummary = students[0]['grade_summary']
%> %>
...@@ -42,15 +44,15 @@ ...@@ -42,15 +44,15 @@
<%def name="percent_data(percentage)"> <%def name="percent_data(percentage)">
<% <%
data_class = "grade_none" letter_grade = 'None'
if percentage > .87: if percentage > 0:
data_class = "grade_a" letter_grade = 'F'
elif percentage > .70: for grade in ['A', 'B', 'C']:
data_class = "grade_b" if percentage >= course.grade_cutoffs[grade]:
elif percentage > .6: letter_grade = grade
data_class = "grade_c" break
elif percentage > 0:
data_class = "grade_f" data_class = "grade_" + letter_grade
%> %>
<td class="${data_class}">${ "{0:.0%}".format( percentage ) }</td> <td class="${data_class}">${ "{0:.0%}".format( percentage ) }</td>
</%def> </%def>
...@@ -58,10 +60,10 @@ ...@@ -58,10 +60,10 @@
%for student in students: %for student in students:
<tr> <tr>
<td><a href="/profile/${student['id']}/">${student['username']}</a></td> <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'] )} ${percent_data( section['percent'] )}
%endfor %endfor
<th>${percent_data( student['grade_info']['grade_summary']['percent'])}</th> <th>${percent_data( student['grade_summary']['percent'])}</th>
</tr> </tr>
%endfor %endfor
</table> </table>
......
...@@ -26,9 +26,9 @@ ...@@ -26,9 +26,9 @@
<%include file="navigation.html" /> <%include file="navigation.html" />
<section class="content-wrapper"> <section class="content-wrapper">
${self.body()} ${self.body()}
<%block name="bodyextra"/>
</section> </section>
<%block name="bodyextra"/>
<%include file="footer.html" /> <%include file="footer.html" />
<%static:js group='application'/> <%static:js group='application'/>
......
...@@ -20,7 +20,7 @@ ...@@ -20,7 +20,7 @@
if(json.success) { if(json.success) {
location.href="${reverse('dashboard')}"; location.href="${reverse('dashboard')}";
}else{ }else{
$('#register_message).html("<p><font color='red'>" + json.error + "</font></p>") $('#register_message').html("<p><font color='red'>" + json.error + "</font></p>");
} }
}); });
})(this) })(this)
......
...@@ -18,7 +18,7 @@ ...@@ -18,7 +18,7 @@
<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.stack.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/flot/jquery.flot.symbol.js')}"></script> <script type="text/javascript" src="${static.url('js/vendor/flot/jquery.flot.symbol.js')}"></script>
<script> <script>
${profile_graphs.body(grade_summary, "grade-detail-graph")} ${profile_graphs.body(grade_summary, course.grade_cutoffs, "grade-detail-graph")}
</script> </script>
<script> <script>
...@@ -110,7 +110,6 @@ $(function() { ...@@ -110,7 +110,6 @@ $(function() {
</script> </script>
</%block> </%block>
<%include file="course_navigation.html" args="active_page='profile'" /> <%include file="course_navigation.html" args="active_page='profile'" />
<section class="container"> <section class="container">
...@@ -139,19 +138,26 @@ $(function() { ...@@ -139,19 +138,26 @@ $(function() {
%> %>
<h3><a href="${reverse('courseware_section', kwargs={'course_id' : course.id, 'chapter' : chapter['url_name'], 'section' : section['url_name']})}"> <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['display_name'] }</a><span> ${"({0:.3n}/{1:.3n}) {2}".format( float(earned), float(total), percentageString )}</span></h3>
<p>
${section['format']} ${section['format']}
%if 'due' in section and section['due']!="": %if 'due' in section and section['due']!="":
<em>
due ${section['due']} due ${section['due']}
</em>
%endif %endif
</p>
%if len(section['scores']) > 0: %if len(section['scores']) > 0:
<ol class="scores"> <section class="scores">
${ "Problem Scores: " if section['graded'] else "Practice Scores: "} <h3> ${ "Problem Scores: " if section['graded'] else "Practice Scores: "} </h3>
<ol>
%for score in section['scores']: %for score in section['scores']:
<li class="score">${"{0:.3n}/{1:.3n}".format(float(score.earned),float(score.possible))}</li> <li>${"{0:.3n}/{1:.3n}".format(float(score.earned),float(score.possible))}</li>
%endfor %endfor
</ol> </ol>
</section>
%endif %endif
</li> <!--End section--> </li> <!--End section-->
...@@ -202,7 +208,7 @@ $(function() { ...@@ -202,7 +208,7 @@ $(function() {
</div> </div>
</section> </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> <a href="#password_reset_complete" rel="leanModal" id="password_reset_complete_link"></a>
<h1>Password Reset Email Sent</h1> <h1>Password Reset Email Sent</h1>
<p> <p>
...@@ -210,83 +216,78 @@ $(function() { ...@@ -210,83 +216,78 @@ $(function() {
</p> </p>
</div> </div>
<div id="apply_name_change" class="leanModal_box"> <div id="apply_name_change" class="modal">
<h1>Apply to change your name</h1> <div class="inner-wrapper">
<header>
<h2>Apply to change your name</h2>
<hr />
</header>
<form id="change_name_form"> <form id="change_name_form">
<div id="change_name_error"> </div> <div id="change_name_error"> </div>
<fieldset> <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> <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> <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" /> <input id="new_name_field" value="" type="text" />
</li>
<li>
<label>Reason for name change:</label> <label>Reason for name change:</label>
<textarea id="name_rationale_field" value=""></textarea> <textarea id="name_rationale_field" value=""></textarea>
</li>
<li>
<input type="submit" id="submit"> <input type="submit" id="submit">
</li>
</ul>
</fieldset> </fieldset>
</form> </form>
</div>
</div> </div>
<div id="change_email" class="leanModal_box"> <div id="change_email" class="modal">
<h1>Change e-mail</h1> <div class="inner-wrapper">
<header>
<h2>Change e-mail</h2>
<hr />
</header>
<div id="apply_name_change_error"></div> <div id="apply_name_change_error"></div>
<form id="change_email_form"> <form id="change_email_form">
<div id="change_email_error"> </div> <div id="change_email_error"> </div>
<fieldset> <fieldset>
<ul>
<li>
<label> Please enter your new email address: </label> <label> Please enter your new email address: </label>
<input id="new_email_field" type="email" value="" /> <input id="new_email_field" type="email" value="" />
</li>
<li>
<label> Please confirm your password: </label> <label> Please confirm your password: </label>
<input id="new_email_password" value="" type="password" /> <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> <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" /> <input type="submit" id="submit_email_change" />
</li>
</ul>
</fieldset> </fieldset>
</form> </form>
</div>
</div> </div>
<div id="deactivate-account" class="leanModal_box"> <div id="deactivate-account" class="modal">
<h1>Deactivate <span class="edx">edX</span> Account</h1> <div class="inner-wrapper">
<header>
<h2>Deactivate <span class="edx">edX</span> Account</h2>
<hr />
</header>
<p>Once you deactivate you&rsquo;re MIT<em>x</em> account you will no longer recieve updates and new class announcements from MIT<em>x</em>.</p> <p>Once you deactivate you&rsquo;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&rsquo;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> <p>If you&rsquo;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"> <form id="unenroll_form">
<div id="unenroll_error"> </div> <div id="unenroll_error"> </div>
<fieldset> <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" /> <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> </fieldset>
</form> </form>
</div>
</div> </div>
<div id="unenroll" class="leanModal_box"> <div id="unenroll" class="modal">
<h1>Unenroll from 6.002x</h1> <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&rsquo;t wish to receive any more updates or announcements <a href="#deactivate-account" rel="leanModal">deactivate your account</a>.</p> <p>Please note: you will still receive updates and new class announcements from ed<em>X</em>. If you don&rsquo;t wish to receive any more updates or announcements <a href="#deactivate-account" rel="leanModal">deactivate your account</a>.</p>
<form id="unenroll_form"> <form id="unenroll_form">
<div id="unenroll_error"> </div> <div id="unenroll_error"> </div>
<fieldset> <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" /> <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> </fieldset>
</form> </form>
</div>
</div> </div>
<%page args="grade_summary, graph_div_id, **kwargs"/> <%page args="grade_summary, grade_cutoffs, graph_div_id, **kwargs"/>
<%! <%!
import json import json
import math
%> %>
$(function () { $(function () {
...@@ -89,8 +90,16 @@ $(function () { ...@@ -89,8 +90,16 @@ $(function () {
ticks += [ [overviewBarX, "Total"] ] ticks += [ [overviewBarX, "Total"] ]
tickIndex += 1 + sectionSpacer 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 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 ) }; var series = ${ json.dumps( series ) };
...@@ -98,6 +107,7 @@ $(function () { ...@@ -98,6 +107,7 @@ $(function () {
var bottomTicks = ${ json.dumps(bottomTicks) }; var bottomTicks = ${ json.dumps(bottomTicks) };
var detail_tooltips = ${ json.dumps(detail_tooltips) }; var detail_tooltips = ${ json.dumps(detail_tooltips) };
var droppedScores = ${ json.dumps(droppedScores) }; 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 //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"} ); series.push( {label: 'Dropped Scores', data: droppedScores, points: {symbol: "cross", show: true, radius: 3}, bars: {show: false}, color: "#333"} );
...@@ -107,10 +117,10 @@ $(function () { ...@@ -107,10 +117,10 @@ $(function () {
lines: {show: false, steps: false }, lines: {show: false, steps: false },
bars: {show: true, barWidth: 0.8, align: 'center', lineWidth: 0, fill: .8 },}, 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}, 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, 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"}, markings: [ {yaxis: {from: ${grade_cutoffs['A']}, to: 1 }, color: "#ddd"}, {yaxis: {from: ${grade_cutoffs['B']}, to: ${grade_cutoffs['A']} }, color: "#e9e9e9"},
{yaxis: {from: 0.6, to: 0.7 }, color: "#f3f3f3"}, ] }, {yaxis: {from: ${grade_cutoffs['C']}, to: ${grade_cutoffs['B']} }, color: "#f3f3f3"}, ] },
legend: {show: false}, legend: {show: false},
}; };
......
...@@ -106,7 +106,7 @@ ...@@ -106,7 +106,7 @@
<form method="GET" onsubmit="this.action='${baseURL}' + this.wiki_article_name.value.replace(/([^a-zA-Z0-9\-])/g, '');"> <form method="GET" onsubmit="this.action='${baseURL}' + this.wiki_article_name.value.replace(/([^a-zA-Z0-9\-])/g, '');">
<div> <div>
<label for="id_wiki_article_name">Title of article</label> <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> </div>
<ul> <ul>
<li> <li>
......
...@@ -65,12 +65,10 @@ ${"Edit " + wiki_title + " - " if wiki_title is not UNDEFINED else ""}MITx 6.002 ...@@ -65,12 +65,10 @@ ${"Edit " + wiki_title + " - " if wiki_title is not UNDEFINED else ""}MITx 6.002
</div> </div>
${wiki_form} ${wiki_form}
%if create_article: %if create_article:
<input type="submit" id="submit_edit" value="Create article" /></td> <input type="submit" id="submit_edit" value="Create article" />
%else: %else:
<input type="submit" id="submit_edit" name="edit" value="Save Changes" /> <input type="submit" id="submit_edit" name="edit" value="Save Changes" />
<input type="submit" id="submit_delete" name="delete" value="Delete article" /> <input type="submit" id="submit_delete" name="delete" value="Delete article" />
%endif
</form>
<%include file="simplewiki_instructions.html"/> <%include file="simplewiki_instructions.html"/>
......
...@@ -2,9 +2,13 @@ ${module_content} ...@@ -2,9 +2,13 @@ ${module_content}
%if edit_link: %if edit_link:
<div><a href="${edit_link}">Edit</a></div> <div><a href="${edit_link}">Edit</a></div>
% endif % endif
<div class="staff_info"> <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> definition = <pre>${definition | h}</pre>
metadata = ${metadata | h} metadata = ${metadata | h}
</span>
</div> </div>
%if render_histogram: %if render_histogram:
<div id="histogram_${element_id}" class="histogram" data-histogram="${histogram}"></div> <div id="histogram_${element_id}" class="histogram" data-histogram="${histogram}"></div>
......
...@@ -89,17 +89,6 @@ $("#open_close_accordion a").click(function(){ ...@@ -89,17 +89,6 @@ $("#open_close_accordion a").click(function(){
</nav> </nav>
<img id="bookpage" src="${ settings.BOOK_URL }p${ "%03i"%(page) }.png"> <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>
</section> </section>
</div> </div>
......
...@@ -107,7 +107,6 @@ if settings.COURSEWARE_ENABLED: ...@@ -107,7 +107,6 @@ if settings.COURSEWARE_ENABLED:
# TODO: These views need to be updated before they work # TODO: These views need to be updated before they work
# url(r'^calculate$', 'util.views.calculate'), # 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 # 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'^edit_circuit/(?P<circuit>[^/]*)$', 'circuit.views.edit_circuit'),
# url(r'^save_circuit/(?P<circuit>[^/]*)$', 'circuit.views.save_circuit'), # url(r'^save_circuit/(?P<circuit>[^/]*)$', 'circuit.views.save_circuit'),
...@@ -143,6 +142,10 @@ if settings.COURSEWARE_ENABLED: ...@@ -143,6 +142,10 @@ if settings.COURSEWARE_ENABLED:
# discussion # discussion
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/discussion/', url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/discussion/',
include('django_comment_client.urls')), include('django_comment_client.urls')),
# For the instructor
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/gradebook$',
'courseware.views.gradebook'),
) )
# Multicourse wiki # Multicourse wiki
......
...@@ -83,13 +83,20 @@ end ...@@ -83,13 +83,20 @@ end
task :pylint => "pylint_#{system}" task :pylint => "pylint_#{system}"
end 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_XUNIT_FILE'] = File.join(report_dir, "nosetests.xml")
ENV['NOSE_COVER_HTML_DIR'] = File.join(report_dir, "cover") 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 end
TEST_TASKS = []
[:lms, :cms].each do |system| [:lms, :cms].each do |system|
report_dir = File.join(REPORT_DIR, system.to_s) report_dir = File.join(REPORT_DIR, system.to_s)
...@@ -97,15 +104,16 @@ end ...@@ -97,15 +104,16 @@ end
# Per System tasks # Per System tasks
desc "Run all django tests on our djangoapps for the #{system}" 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 # Have a way to run the tests without running collectstatic -- useful when debugging without
# messing with static files. # messing with static files.
task "fasttest_#{system}" => [report_dir, :predjango] do task "fasttest_#{system}", [:stop_on_failure] => [report_dir, :predjango] do |t, args|
run_tests(system, report_dir) args.with_defaults(:stop_on_failure => 'true')
run_tests(system, report_dir, args.stop_on_failure)
end end
task :test => "test_#{system}" TEST_TASKS << "test_#{system}"
desc <<-desc desc <<-desc
Start the #{system} locally with the specified environment (defaults to dev). Start the #{system} locally with the specified environment (defaults to dev).
...@@ -142,7 +150,17 @@ Dir["common/lib/*"].each do |lib| ...@@ -142,7 +150,17 @@ Dir["common/lib/*"].each do |lib|
ENV['NOSE_XUNIT_FILE'] = File.join(report_dir, "nosetests.xml") 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")}") 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 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 end
task :runserver => :lms task :runserver => :lms
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment