Commit bc823d3b by Calen Pennington

Merge branch 'master' into asset-pipeline

Conflicts:
	djangoapps/multicourse/__init__.py
	requirements.txt
	sass/_info.scss
	sass/_textbook.scss
	sass/courseware/_sequence-nav.scss
	sass/courseware/_video.scss
	sass/index/_base.scss
	sass/index/_extends.scss
	sass/index/_footer.scss
	sass/index/_header.scss
	sass/index/_index.scss
	sass/index/_variables.scss
	sass/layout/_calculator.scss
	sass/print.scss
	static/css
	static/css/application.css
	static/css/marketing.css
	templates/main.html
	templates/marketing.html
	templates/sass/index/_base.scss
	templates/sass/index/_extends.scss
	templates/sass/index/_footer.scss
	templates/sass/index/_header.scss
	templates/sass/index/_index.scss
	templates/sass/index/_variables.scss
	templates/sass/marketing/_base.scss
	templates/sass/marketing/_extends.scss
	templates/sass/marketing/_footer.scss
	templates/sass/marketing/_header.scss
	templates/sass/marketing/_index.scss
	templates/sass/marketing/_variables.scss
parents e8c4d1db cc6e0c55
...@@ -4,6 +4,9 @@ ...@@ -4,6 +4,9 @@
*.swp *.swp
*.orig *.orig
*.DS_Store *.DS_Store
:2e_*
:2e#
.AppleDouble
database.sqlite database.sqlite
courseware/static/js/mathjax/* courseware/static/js/mathjax/*
db.newaskbot db.newaskbot
......
...@@ -78,17 +78,23 @@ def evaluator(variables, functions, string, cs=False): ...@@ -78,17 +78,23 @@ def evaluator(variables, functions, string, cs=False):
# log.debug("functions: {0}".format(functions)) # log.debug("functions: {0}".format(functions))
# log.debug("string: {0}".format(string)) # log.debug("string: {0}".format(string))
def lower_dict(d):
return dict([(k.lower(), d[k]) for k in d])
all_variables = copy.copy(default_variables) all_variables = copy.copy(default_variables)
all_variables.update(variables)
all_functions = copy.copy(default_functions) all_functions = copy.copy(default_functions)
if not cs:
all_variables = lower_dict(all_variables)
all_functions = lower_dict(all_functions)
all_variables.update(variables)
all_functions.update(functions) all_functions.update(functions)
if not cs: if not cs:
string_cs = string.lower() string_cs = string.lower()
for v in all_variables.keys(): all_functions = lower_dict(all_functions)
all_variables[v.lower()]=all_variables[v] all_variables = lower_dict(all_variables)
for f in all_functions.keys():
all_functions[f.lower()]=all_functions[f]
CasedLiteral = CaselessLiteral CasedLiteral = CaselessLiteral
else: else:
string_cs = string string_cs = string
......
#
# File: courseware/capa/capa_problem.py
#
'''
Main module which shows problems (of "capa" type).
This is used by capa_module.
'''
import copy import copy
import logging import logging
import math import math
...@@ -10,32 +19,45 @@ import struct ...@@ -10,32 +19,45 @@ import struct
from lxml import etree from lxml import etree
from lxml.etree import Element from lxml.etree import Element
from xml.sax.saxutils import escape, unescape
from mako.template import Template from mako.template import Template
from util import contextualize_text from util import contextualize_text
from inputtypes import textline, schematic import inputtypes
from responsetypes import numericalresponse, formularesponse, customresponse, schematicresponse, StudentInputError from responsetypes import NumericalResponse, FormulaResponse, CustomResponse, SchematicResponse, MultipleChoiceResponse, StudentInputError, TrueFalseResponse, ExternalResponse,ImageResponse,OptionResponse
import calc import calc
import eia import eia
log = logging.getLogger("mitx.courseware") log = logging.getLogger("mitx.courseware")
response_types = {'numericalresponse':numericalresponse, response_types = {'numericalresponse':NumericalResponse,
'formularesponse':formularesponse, 'formularesponse':FormulaResponse,
'customresponse':customresponse, 'customresponse':CustomResponse,
'schematicresponse':schematicresponse} 'schematicresponse':SchematicResponse,
entry_types = ['textline', 'schematic'] 'externalresponse':ExternalResponse,
response_properties = ["responseparam", "answer"] 'multiplechoiceresponse':MultipleChoiceResponse,
'truefalseresponse':TrueFalseResponse,
'imageresponse':ImageResponse,
'optionresponse':OptionResponse,
}
entry_types = ['textline', 'schematic', 'choicegroup','textbox','imageinput','optioninput']
solution_types = ['solution'] # extra things displayed after "show answers" is pressed
response_properties = ["responseparam", "answer"] # these get captured as student responses
# How to convert from original XML to HTML # How to convert from original XML to HTML
# We should do this with xlst later # We should do this with xlst later
html_transforms = {'problem': {'tag':'div'}, html_transforms = {'problem': {'tag':'div'},
"numericalresponse": {'tag':'span'}, "numericalresponse": {'tag':'span'},
"customresponse": {'tag':'span'}, "customresponse": {'tag':'span'},
"externalresponse": {'tag':'span'},
"schematicresponse": {'tag':'span'}, "schematicresponse": {'tag':'span'},
"formularesponse": {'tag':'span'}, "formularesponse": {'tag':'span'},
"text": {'tag':'span'}} "multiplechoiceresponse": {'tag':'span'},
"text": {'tag':'span'},
"math": {'tag':'span'},
}
global_context={'random':random, global_context={'random':random,
'numpy':numpy, 'numpy':numpy,
...@@ -47,30 +69,28 @@ global_context={'random':random, ...@@ -47,30 +69,28 @@ global_context={'random':random,
# These should be removed from HTML output, including all subelements # These should be removed from HTML output, including all subelements
html_problem_semantics = ["responseparam", "answer", "script"] html_problem_semantics = ["responseparam", "answer", "script"]
# These should be removed from HTML output, but keeping subelements # These should be removed from HTML output, but keeping subelements
html_skip = ["numericalresponse", "customresponse", "schematicresponse", "formularesponse", "text"] html_skip = ["numericalresponse", "customresponse", "schematicresponse", "formularesponse", "text","externalresponse"]
# These should be transformed
html_special_response = {"textline":textline.render, # removed in MC
"schematic":schematic.render} ## These should be transformed
#html_special_response = {"textline":textline.render,
# "schematic":schematic.render,
# "textbox":textbox.render,
# "solution":solution.render,
# }
class LoncapaProblem(object): class LoncapaProblem(object):
def __init__(self, filename, id=None, state=None, seed=None): def __init__(self, fileobject, id, state=None, seed=None):
## Initialize class variables from state ## Initialize class variables from state
self.seed = None self.seed = None
self.student_answers = dict() self.student_answers = dict()
self.correct_map = dict() self.correct_map = dict()
self.done = False self.done = False
self.filename = filename self.problem_id = id
if seed != None: if seed != None:
self.seed = seed self.seed = seed
if id:
self.problem_id = id
else:
print "NO ID"
raise Exception("This should never happen (183)")
#self.problem_id = filename
if state: if state:
if 'seed' in state: if 'seed' in state:
self.seed = state['seed'] self.seed = state['seed']
...@@ -81,17 +101,13 @@ class LoncapaProblem(object): ...@@ -81,17 +101,13 @@ class LoncapaProblem(object):
if 'done' in state: if 'done' in state:
self.done = state['done'] self.done = state['done']
# print self.seed
# TODO: Does this deplete the Linux entropy pool? Is this fast enough? # TODO: Does this deplete the Linux entropy pool? Is this fast enough?
if not self.seed: if not self.seed:
self.seed=struct.unpack('i', os.urandom(4))[0] self.seed=struct.unpack('i', os.urandom(4))[0]
# print filename, self.seed, seed
## Parse XML file ## Parse XML file
#log.debug(u"LoncapaProblem() opening file {0}".format(filename)) file_text = fileobject.read()
file_text = open(filename).read() self.fileobject = fileobject # save it, so we can use for debugging information later
# Convert startouttext and endouttext to proper <text></text> # Convert startouttext and endouttext to proper <text></text>
# TODO: Do with XML operations # TODO: Do with XML operations
file_text = re.sub("startouttext\s*/","text",file_text) file_text = re.sub("startouttext\s*/","text",file_text)
...@@ -100,6 +116,9 @@ class LoncapaProblem(object): ...@@ -100,6 +116,9 @@ class LoncapaProblem(object):
self.preprocess_problem(self.tree, correct_map=self.correct_map, answer_map = self.student_answers) self.preprocess_problem(self.tree, correct_map=self.correct_map, answer_map = self.student_answers)
self.context = self.extract_context(self.tree, seed=self.seed) self.context = self.extract_context(self.tree, seed=self.seed)
for response in self.tree.xpath('//'+"|//".join(response_types)):
responder = response_types[response.tag](response, self.context)
responder.preprocess_response()
def get_state(self): def get_state(self):
''' Stored per-user session data neeeded to: ''' Stored per-user session data neeeded to:
...@@ -111,6 +130,9 @@ class LoncapaProblem(object): ...@@ -111,6 +130,9 @@ class LoncapaProblem(object):
'done':self.done} 'done':self.done}
def get_max_score(self): def get_max_score(self):
'''
TODO: multiple points for programming problems.
'''
sum = 0 sum = 0
for et in entry_types: for et in entry_types:
sum = sum + self.tree.xpath('count(//'+et+')') sum = sum + self.tree.xpath('count(//'+et+')')
...@@ -129,41 +151,76 @@ class LoncapaProblem(object): ...@@ -129,41 +151,76 @@ class LoncapaProblem(object):
'total':self.get_max_score()} 'total':self.get_max_score()}
def grade_answers(self, answers): def grade_answers(self, answers):
'''
Grade student responses. Called by capa_module.check_problem.
answers is a dict of all the entries from request.POST, but with the first part
of each key removed (the string before the first "_").
Thus, for example, input_ID123 -> ID123, and input_fromjs_ID123 -> fromjs_ID123
'''
self.student_answers = answers self.student_answers = answers
context=self.extract_context(self.tree) context=self.extract_context(self.tree)
self.correct_map = dict() self.correct_map = dict()
problems_simple = self.extract_problems(self.tree) problems_simple = self.extract_problems(self.tree)
for response in problems_simple: for response in problems_simple:
grader = response_types[response.tag](response, self.context) grader = response_types[response.tag](response, self.context)
results = grader.grade(answers) results = grader.grade(answers) # call the responsetype instance to do the actual grading
self.correct_map.update(results) self.correct_map.update(results)
return self.correct_map return self.correct_map
def get_question_answers(self): def get_question_answers(self):
'''
Make a dict of (id,correct_answer) entries, for all the problems.
Called by "show answers" button JSON request (see capa_module)
'''
context=self.extract_context(self.tree) context=self.extract_context(self.tree)
answer_map = dict() answer_map = dict()
problems_simple = self.extract_problems(self.tree) problems_simple = self.extract_problems(self.tree) # purified (flat) XML tree of just response queries
for response in problems_simple: for response in problems_simple:
responder = response_types[response.tag](response, self.context) responder = response_types[response.tag](response, self.context) # instance of numericalresponse, customresponse,...
results = responder.get_answers() results = responder.get_answers()
answer_map.update(results) answer_map.update(results) # dict of (id,correct_answer)
# example for the following: <textline size="5" correct_answer="saturated" />
for entry in problems_simple.xpath("//"+"|//".join(response_properties+entry_types)): for entry in problems_simple.xpath("//"+"|//".join(response_properties+entry_types)):
answer = entry.get('correct_answer') answer = entry.get('correct_answer') # correct answer, when specified elsewhere, eg in a textline
if answer: if answer:
answer_map[entry.get('id')] = contextualize_text(answer, self.context) answer_map[entry.get('id')] = contextualize_text(answer, self.context)
# include solutions from <solution>...</solution> stanzas
# Tentative merge; we should figure out how we want to handle hints and solutions
for entry in self.tree.xpath("//"+"|//".join(solution_types)):
answer = etree.tostring(entry)
if answer:
answer_map[entry.get('id')] = answer
return answer_map return answer_map
# ======= Private ======== # ======= Private ========
def extract_context(self, tree, seed = struct.unpack('i', os.urandom(4))[0]): # private def extract_context(self, tree, seed = struct.unpack('i', os.urandom(4))[0]): # private
''' Problem XML goes to Python execution context. Runs everything in script tags ''' '''
Extract content of <script>...</script> from the problem.xml file, and exec it in the
context of this problem. Provides ability to randomize problems, and also set
variables for problem answer checking.
Problem XML goes to Python execution context. Runs everything in script tags
'''
random.seed(self.seed) random.seed(self.seed)
context = dict() ### IKE: Why do we need these two lines?
for script in tree.xpath('/problem/script'): context = {'global_context':global_context} # save global context in here also
exec script.text in global_context, context global_context['context'] = context # and put link to local context in the global one
#for script in tree.xpath('/problem/script'):
for script in tree.findall('.//script'):
code = script.text
XMLESC = {"&apos;": "'", "&quot;": '"'}
code = unescape(code,XMLESC)
try:
exec code in global_context, context
except Exception,err:
print "[courseware.capa.capa_problem.extract_context] error %s" % err
print "in doing exec of this code:",code
return context return context
def get_html(self): def get_html(self):
...@@ -175,21 +232,46 @@ class LoncapaProblem(object): ...@@ -175,21 +232,46 @@ class LoncapaProblem(object):
if problemtree.tag in html_problem_semantics: if problemtree.tag in html_problem_semantics:
return return
if problemtree.tag in html_special_response: problemid = problemtree.get('id') # my ID
# used to be
# if problemtree.tag in html_special_response:
if hasattr(inputtypes, problemtree.tag):
# status is currently the answer for the problem ID for the input element,
# but it will turn into a dict containing both the answer and any associated message
# for the problem ID for the input element.
status = "unsubmitted" status = "unsubmitted"
if problemtree.get('id') in self.correct_map: if problemid in self.correct_map:
status = self.correct_map[problemtree.get('id')] status = self.correct_map[problemtree.get('id')]
value = "" value = ""
if self.student_answers and problemtree.get('id') in self.student_answers: if self.student_answers and problemid in self.student_answers:
value = self.student_answers[problemtree.get('id')] value = self.student_answers[problemid]
return html_special_response[problemtree.tag](problemtree, value, status) #TODO #### This code is a hack. It was merged to help bring two branches
#### in sync, but should be replaced. msg should be passed in a
#### response_type
# prepare the response message, if it exists in correct_map
if 'msg' in self.correct_map:
msg = self.correct_map['msg']
elif ('msg_%s' % problemid) in self.correct_map:
msg = self.correct_map['msg_%s' % problemid]
else:
msg = ''
#if settings.DEBUG:
# print "[courseware.capa.capa_problem.extract_html] msg = ",msg
# do the rendering
#render_function = html_special_response[problemtree.tag]
render_function = getattr(inputtypes, problemtree.tag)
return render_function(problemtree, value, status, msg) # render the special response (textline, schematic,...)
tree=Element(problemtree.tag) tree=Element(problemtree.tag)
for item in problemtree: for item in problemtree:
subitems = self.extract_html(item) subitems = self.extract_html(item)
if subitems: if subitems is not None:
for subitem in subitems: for subitem in subitems:
tree.append(subitem) tree.append(subitem)
for (key,value) in problemtree.items(): for (key,value) in problemtree.items():
...@@ -210,11 +292,11 @@ class LoncapaProblem(object): ...@@ -210,11 +292,11 @@ class LoncapaProblem(object):
# TODO: Fix. This loses Element().tail # TODO: Fix. This loses Element().tail
#if problemtree.tag in html_skip: #if problemtree.tag in html_skip:
# return tree # return tree
return [tree] return [tree]
def preprocess_problem(self, tree, correct_map=dict(), answer_map=dict()): # private def preprocess_problem(self, tree, correct_map=dict(), answer_map=dict()): # private
''' Assign IDs to all the responses '''
Assign IDs to all the responses
Assign sub-IDs to all entries (textline, schematic, etc.) Assign sub-IDs to all entries (textline, schematic, etc.)
Annoted correctness and value Annoted correctness and value
In-place transformation In-place transformation
...@@ -228,13 +310,21 @@ class LoncapaProblem(object): ...@@ -228,13 +310,21 @@ class LoncapaProblem(object):
response.attrib['state'] = correct response.attrib['state'] = correct
response_id = response_id + 1 response_id = response_id + 1
answer_id = 1 answer_id = 1
for entry in tree.xpath("|".join(['//'+response.tag+'[@id=$id]//'+x for x in entry_types]), for entry in tree.xpath("|".join(['//'+response.tag+'[@id=$id]//'+x for x in (entry_types + solution_types)]),
id=response_id_str): id=response_id_str):
# assign one answer_id for each entry_type or solution_type
entry.attrib['response_id'] = str(response_id) entry.attrib['response_id'] = str(response_id)
entry.attrib['answer_id'] = str(answer_id) entry.attrib['answer_id'] = str(answer_id)
entry.attrib['id'] = "%s_%i_%i"%(self.problem_id, response_id, answer_id) entry.attrib['id'] = "%s_%i_%i"%(self.problem_id, response_id, answer_id)
answer_id=answer_id+1 answer_id=answer_id+1
# <solution>...</solution> may not be associated with any specific response; give IDs for those separately
# TODO: We should make the namespaces consistent and unique (e.g. %s_problem_%i).
solution_id = 1
for solution in tree.findall('.//solution'):
solution.attrib['id'] = "%s_solution_%i"%(self.problem_id, solution_id)
solution_id += 1
def extract_problems(self, problem_tree): def extract_problems(self, problem_tree):
''' Remove layout from the problem, and give a purified XML tree of just the problems ''' ''' Remove layout from the problem, and give a purified XML tree of just the problems '''
problem_tree=copy.deepcopy(problem_tree) problem_tree=copy.deepcopy(problem_tree)
......
#
# File: courseware/capa/inputtypes.py
#
'''
Module containing the problem elements which render into input objects
- textline
- textbox (change this to textarea?)
- schemmatic
- choicegroup (for multiplechoice: checkbox, radio, or select option)
- imageinput (for clickable image)
- optioninput (for option list)
These are matched by *.html files templates/*.html which are mako templates with the actual html.
Each input type takes the xml tree as 'element', the previous answer as 'value', and the graded status as 'status'
'''
# TODO: rename "state" to "status" for all below
# status is currently the answer for the problem ID for the input element,
# but it will turn into a dict containing both the answer and any associated message for the problem ID for the input element.
import re
import shlex # for splitting quoted strings
from django.conf import settings
from lxml.etree import Element from lxml.etree import Element
from lxml import etree from lxml import etree
from mitxmako.shortcuts import render_to_response, render_to_string from mitxmako.shortcuts import render_to_string
#-----------------------------------------------------------------------------
def optioninput(element, value, status, msg=''):
'''
Select option input type.
Example:
<optioninput options="('Up','Down')" correct="Up"/><text>The location of the sky</text>
'''
eid=element.get('id')
options = element.get('options')
if not options:
raise Exception,"[courseware.capa.inputtypes.optioninput] Missing options specification in " + etree.tostring(element)
oset = shlex.shlex(options[1:-1])
oset.quotes = "'"
oset.whitespace = ","
oset = [x[1:-1] for x in list(oset)]
# osetdict = dict([('option_%s_%s' % (eid,x),oset[x]) for x in range(len(oset)) ]) # make dict with IDs
osetdict = dict([(oset[x],oset[x]) for x in range(len(oset)) ]) # make dict with key,value same
if settings.DEBUG:
print '[courseware.capa.inputtypes.optioninput] osetdict=',osetdict
context={'id':eid,
'value':value,
'state':status,
'msg':msg,
'options':osetdict,
}
html=render_to_string("optioninput.html", context)
return etree.XML(html)
#-----------------------------------------------------------------------------
def choicegroup(element, value, status, msg=''):
'''
Radio button inputs: multiple choice or true/false
TODO: allow order of choices to be randomized, following lon-capa spec. Use "location" attribute,
ie random, top, bottom.
'''
eid=element.get('id')
if element.get('type') == "MultipleChoice":
type="radio"
elif element.get('type') == "TrueFalse":
type="checkbox"
else:
type="radio"
choices={}
for choice in element:
assert choice.tag =="choice", "only <choice> tags should be immediate children of a <choicegroup>"
choices[choice.get("name")] = etree.tostring(choice[0]) # TODO: what if choice[0] has math tags in it?
context={'id':eid, 'value':value, 'state':status, 'type':type, 'choices':choices}
html=render_to_string("choicegroup.html", context)
return etree.XML(html)
def textline(element, value, state, msg=""):
eid=element.get('id')
count = int(eid.split('_')[-2])-1 # HACK
size = element.get('size')
context = {'id':eid, 'value':value, 'state':state, 'count':count, 'size': size}
html=render_to_string("textinput.html", context)
return etree.XML(html)
class textline(object): #-----------------------------------------------------------------------------
@staticmethod
def render(element, value, state): def js_textline(element, value, status, msg=''):
## TODO: Code should follow PEP8 (4 spaces per indentation level)
'''
textline is used for simple one-line inputs, like formularesponse and symbolicresponse.
'''
eid=element.get('id') eid=element.get('id')
count = int(eid.split('_')[-2])-1 # HACK count = int(eid.split('_')[-2])-1 # HACK
size = element.get('size') size = element.get('size')
context = {'id':eid, 'value':value, 'state':state, 'count':count, 'size': size} dojs = element.get('dojs') # dojs is used for client-side javascript display & return
html=render_to_string("textinput.html", context) # when dojs=='math', a <span id=display_eid>`{::}`</span>
# and a hidden textarea with id=input_eid_fromjs will be output
context = {'id':eid, 'value':value, 'state':status, 'count':count, 'size': size,
'dojs':dojs,
'msg':msg,
}
html=render_to_string("jstext.html", context)
return etree.XML(html) return etree.XML(html)
class schematic(object): #-----------------------------------------------------------------------------
@staticmethod ## TODO: Make a wrapper for <codeinput>
def render(element, value, state): def textbox(element, value, status, msg=''):
eid = element.get('id') '''
height = element.get('height') The textbox is used for code input. The message is the return HTML string from
width = element.get('width') evaluating the code, eg error messages, and output from the code tests.
parts = element.get('parts')
analyses = element.get('analyses') TODO: make this use rows and cols attribs, not size
initial_value = element.get('initial_value') '''
submit_analyses = element.get('submit_analyses') eid=element.get('id')
context = { count = int(eid.split('_')[-2])-1 # HACK
'id':eid, size = element.get('size')
'value':value, context = {'id':eid, 'value':value, 'state':status, 'count':count, 'size': size, 'msg':msg}
'initial_value':initial_value, html=render_to_string("textbox.html", context)
'state':state,
'width':width,
'height':height,
'parts':parts,
'analyses':analyses,
'submit_analyses':submit_analyses,
}
html=render_to_string("schematicinput.html", context)
return etree.XML(html) return etree.XML(html)
#-----------------------------------------------------------------------------
def schematic(element, value, status, msg=''):
eid = element.get('id')
height = element.get('height')
width = element.get('width')
parts = element.get('parts')
analyses = element.get('analyses')
initial_value = element.get('initial_value')
submit_analyses = element.get('submit_analyses')
context = {
'id':eid,
'value':value,
'initial_value':initial_value,
'state':state,
'width':width,
'height':height,
'parts':parts,
'analyses':analyses,
'submit_analyses':submit_analyses,
}
html=render_to_string("schematicinput.html", context)
return etree.XML(html)
#-----------------------------------------------------------------------------
### TODO: Move out of inputtypes
def math(element, value, status, msg=''):
'''
This is not really an input type. It is a convention from Lon-CAPA, used for
displaying a math equation.
Examples:
<m display="jsmath">$\displaystyle U(r)=4 U_0 </m>
<m>$r_0$</m>
We convert these to [mathjax]...[/mathjax] and [mathjaxinline]...[/mathjaxinline]
TODO: use shorter tags (but this will require converting problem XML files!)
'''
mathstr = re.sub('\$(.*)\$','[mathjaxinline]\\1[/mathjaxinline]',element.text)
mtag = 'mathjax'
if not '\\displaystyle' in mathstr: mtag += 'inline'
else: mathstr = mathstr.replace('\\displaystyle','')
mathstr = mathstr.replace('mathjaxinline]','%s]'%mtag)
#if '\\displaystyle' in mathstr:
# isinline = False
# mathstr = mathstr.replace('\\displaystyle','')
#else:
# isinline = True
# html=render_to_string("mathstring.html",{'mathstr':mathstr,'isinline':isinline,'tail':element.tail})
html = '<html><html>%s</html><html>%s</html></html>' % (mathstr,element.tail)
xhtml = etree.XML(html)
# xhtml.tail = element.tail # don't forget to include the tail!
return xhtml
#-----------------------------------------------------------------------------
def solution(element, value, status, msg=''):
'''
This is not really an input type. It is just a <span>...</span> which is given an ID,
that is used for displaying an extended answer (a problem "solution") after "show answers"
is pressed. Note that the solution content is NOT sent with the HTML. It is obtained
by a JSON call.
'''
eid=element.get('id')
size = element.get('size')
context = {'id':eid,
'value':value,
'state':status,
'size': size,
'msg':msg,
}
html=render_to_string("solutionspan.html", context)
return etree.XML(html)
#-----------------------------------------------------------------------------
def imageinput(element, value, status, msg=''):
'''
Clickable image as an input field. Element should specify the image source, height, and width, eg
<imageinput src="/static/Physics801/Figures/Skier-conservation of energy.jpg" width="388" height="560" />
TODO: showanswer for imageimput does not work yet - need javascript to put rectangle over acceptable area of image.
'''
eid = element.get('id')
src = element.get('src')
height = element.get('height')
width = element.get('width')
# if value is of the form [x,y] then parse it and send along coordinates of previous answer
m = re.match('\[([0-9]+),([0-9]+)]',value.strip().replace(' ',''))
if m:
(gx,gy) = [int(x)-15 for x in m.groups()]
else:
(gx,gy) = (0,0)
context = {
'id':eid,
'value':value,
'height': height,
'width' : width,
'src':src,
'gx':gx,
'gy':gy,
'state' : status, # to change
'msg': msg, # to change
}
if settings.DEBUG:
print '[courseware.capa.inputtypes.imageinput] context=',context
html=render_to_string("imageinput.html", context)
return etree.XML(html)
#
# File: courseware/capa/responsetypes.py
#
'''
Problem response evaluation. Handles checking of student responses, of a variety of types.
Used by capa_problem.py
'''
# standard library imports
import json import json
import math import math
import numbers import numbers
import numpy import numpy
import random import random
import re
import requests
import scipy import scipy
import traceback import traceback
import copy
import abc
# specific library imports
from calc import evaluator, UndefinedVariable from calc import evaluator, UndefinedVariable
from django.conf import settings from django.conf import settings
from util import contextualize_text from util import contextualize_text
from lxml import etree
from lxml.etree import Element
from lxml.html.soupparser import fromstring as fromstring_bs # uses Beautiful Soup!!! FIXME?
# local imports
import calc import calc
import eia import eia
# TODO: Should be the same object as in capa_problem from util import contextualize_text
global_context={'random':random,
'numpy':numpy,
'math':math,
'scipy':scipy,
'calc':calc,
'eia':eia}
def compare_with_tolerance(v1, v2, tol): def compare_with_tolerance(v1, v2, tol):
''' Compare v1 to v2 with maximum tolerance tol ''' Compare v1 to v2 with maximum tolerance tol
...@@ -34,15 +46,153 @@ def compare_with_tolerance(v1, v2, tol): ...@@ -34,15 +46,153 @@ def compare_with_tolerance(v1, v2, tol):
tolerance = evaluator(dict(),dict(),tol) tolerance = evaluator(dict(),dict(),tol)
return abs(v1-v2) <= tolerance return abs(v1-v2) <= tolerance
class numericalresponse(object): class GenericResponse(object):
__metaclass__=abc.ABCMeta
@abc.abstractmethod
def grade(self, student_answers):
pass
@abc.abstractmethod
def get_answers(self):
pass
#not an abstract method because plenty of responses will not want to preprocess anything, and we should not require that they override this method.
def preprocess_response(self):
pass
#Every response type needs methods "grade" and "get_answers"
#-----------------------------------------------------------------------------
class MultipleChoiceResponse(GenericResponse):
'''
Example:
<multiplechoiceresponse direction="vertical" randomize="yes">
<choicegroup type="MultipleChoice">
<choice location="random" name="1" correct="false"><span>`a+b`<br/></span></choice>
<choice location="random" name="2" correct="true"><span><math>a+b^2</math><br/></span></choice>
<choice location="random" name="3" correct="false"><math>a+b+c</math></choice>
<choice location="bottom" name="4" correct="false"><math>a+b+d</math></choice>
</choicegroup>
</multiplechoiceresponse>
TODO: handle direction and randomize
'''
def __init__(self, xml, context):
self.xml = xml
self.correct_choices = xml.xpath('//*[@id=$id]//choice[@correct="true"]',
id=xml.get('id'))
self.correct_choices = [choice.get('name') for choice in self.correct_choices]
self.context = context
self.answer_field = xml.find('choicegroup') # assumes only ONE choicegroup within this response
self.answer_id = xml.xpath('//*[@id=$id]//choicegroup/@id',
id=xml.get('id'))
if not len(self.answer_id) == 1:
raise Exception("should have exactly one choice group per multiplechoicceresponse")
self.answer_id=self.answer_id[0]
def grade(self, student_answers):
if self.answer_id in student_answers and student_answers[self.answer_id] in self.correct_choices:
return {self.answer_id:'correct'}
else:
return {self.answer_id:'incorrect'}
def get_answers(self):
return {self.answer_id:self.correct_choices}
def preprocess_response(self):
'''
Initialize name attributes in <choice> stanzas in the <choicegroup> in this response.
'''
i=0
for response in self.xml.xpath("choicegroup"):
rtype = response.get('type')
if rtype not in ["MultipleChoice"]:
response.set("type", "MultipleChoice") # force choicegroup to be MultipleChoice if not valid
for choice in list(response):
if choice.get("name") == None:
choice.set("name", "choice_"+str(i))
i+=1
else:
choice.set("name", "choice_"+choice.get("name"))
class TrueFalseResponse(MultipleChoiceResponse):
def preprocess_response(self):
i=0
for response in self.xml.xpath("choicegroup"):
response.set("type", "TrueFalse")
for choice in list(response):
if choice.get("name") == None:
choice.set("name", "choice_"+str(i))
i+=1
else:
choice.set("name", "choice_"+choice.get("name"))
def grade(self, student_answers):
correct = set(self.correct_choices)
answers = set(student_answers.get(self.answer_id, []))
if correct == answers:
return { self.answer_id : 'correct'}
return {self.answer_id : 'incorrect'}
#-----------------------------------------------------------------------------
class OptionResponse(GenericResponse):
'''
Example:
<optionresponse direction="vertical" randomize="yes">
<optioninput options="('Up','Down')" correct="Up"><text>The location of the sky</text></optioninput>
<optioninput options="('Up','Down')" correct="Down"><text>The location of the earth</text></optioninput>
</optionresponse>
TODO: handle direction and randomize
'''
def __init__(self, xml, context):
self.xml = xml
self.answer_fields = xml.findall('optioninput')
if settings.DEBUG:
print '[courseware.capa.responsetypes.OR.init] answer_fields=%s' % (self.answer_fields)
self.context = context
def grade(self, student_answers):
cmap = {}
amap = self.get_answers()
for aid in amap:
if aid in student_answers and student_answers[aid]==amap[aid]:
cmap[aid] = 'correct'
else:
cmap[aid] = 'incorrect'
return cmap
def get_answers(self):
amap = dict([(af.get('id'),af.get('correct')) for af in self.answer_fields])
return amap
#-----------------------------------------------------------------------------
class NumericalResponse(GenericResponse):
def __init__(self, xml, context): def __init__(self, xml, context):
self.xml = xml self.xml = xml
self.correct_answer = contextualize_text(xml.get('answer'), context) self.correct_answer = contextualize_text(xml.get('answer'), context)
self.tolerance_xml = xml.xpath('//*[@id=$id]//responseparam[@type="tolerance"]/@default', try:
id=xml.get('id'))[0] self.tolerance_xml = xml.xpath('//*[@id=$id]//responseparam[@type="tolerance"]/@default',
self.tolerance = contextualize_text(self.tolerance_xml, context) id=xml.get('id'))[0]
self.answer_id = xml.xpath('//*[@id=$id]//textline/@id', self.tolerance = contextualize_text(self.tolerance_xml, context)
id=xml.get('id'))[0] except Exception,err:
self.tolerance = 0
try:
self.answer_id = xml.xpath('//*[@id=$id]//textline/@id',
id=xml.get('id'))[0]
except Exception, err:
self.answer_id = None
def grade(self, student_answers): def grade(self, student_answers):
''' Display HTML for a numeric response ''' ''' Display HTML for a numeric response '''
...@@ -63,7 +213,50 @@ class numericalresponse(object): ...@@ -63,7 +213,50 @@ class numericalresponse(object):
def get_answers(self): def get_answers(self):
return {self.answer_id:self.correct_answer} return {self.answer_id:self.correct_answer}
class customresponse(object): #-----------------------------------------------------------------------------
class CustomResponse(GenericResponse):
'''
Custom response. The python code to be run should be in <answer>...</answer>. Example:
<customresponse>
<startouttext/>
<br/>
Suppose that \(I(t)\) rises from \(0\) to \(I_S\) at a time \(t_0 \neq 0\)
In the space provided below write an algebraic expression for \(I(t)\).
<br/>
<textline size="5" correct_answer="IS*u(t-t0)" />
<endouttext/>
<answer type="loncapa/python">
correct=['correct']
try:
r = str(submission[0])
except ValueError:
correct[0] ='incorrect'
r = '0'
if not(r=="IS*u(t-t0)"):
correct[0] ='incorrect'
</answer>
</customresponse>
Alternatively, the check function can be defined in <script>...</script> Example:
<script type="loncapa/python"><![CDATA[
def sympy_check2():
messages[0] = '%s:%s' % (submission[0],fromjs[0].replace('<','&lt;'))
#messages[0] = str(answers)
correct[0] = 'correct'
]]>
</script>
<customresponse cfn="sympy_check2" type="cs" expect="2.27E-39" dojs="math" size="30" answer="2.27E-39">
<textline size="40" dojs="math" />
<responseparam description="Numerical Tolerance" type="tolerance" default="0.00001" name="tol"/>
</customresponse>
'''
def __init__(self, xml, context): def __init__(self, xml, context):
self.xml = xml self.xml = xml
## CRITICAL TODO: Should cover all entrytypes ## CRITICAL TODO: Should cover all entrytypes
...@@ -72,19 +265,201 @@ class customresponse(object): ...@@ -72,19 +265,201 @@ class customresponse(object):
self.answer_ids = xml.xpath('//*[@id=$id]//textline/@id', self.answer_ids = xml.xpath('//*[@id=$id]//textline/@id',
id=xml.get('id')) id=xml.get('id'))
self.context = context self.context = context
# if <customresponse> has an "expect" attribute then save that
self.expect = xml.get('expect')
self.myid = xml.get('id')
# the <answer>...</answer> stanza should be local to the current <customresponse>. So try looking there first.
self.code = None
answer = None
try:
answer = xml.xpath('//*[@id=$id]//answer',id=xml.get('id'))[0]
except IndexError,err:
# print "xml = ",etree.tostring(xml,pretty_print=True)
# if we have a "cfn" attribute then look for the function specified by cfn, in the problem context
# ie the comparison function is defined in the <script>...</script> stanza instead
cfn = xml.get('cfn')
if cfn:
if settings.DEBUG: print "[courseware.capa.responsetypes] cfn = ",cfn
if cfn in context:
self.code = context[cfn]
else:
print "can't find cfn in context = ",context
if not self.code:
if not answer:
# raise Exception,"[courseware.capa.responsetypes.customresponse] missing code checking script! id=%s" % self.myid
print "[courseware.capa.responsetypes.customresponse] missing code checking script! id=%s" % self.myid
self.code = ''
else:
answer_src = answer.get('src')
if answer_src != None:
self.code = open(settings.DATA_DIR+'src/'+answer_src).read()
else:
self.code = answer.text
def grade(self, student_answers):
'''
student_answers is a dict with everything from request.POST, but with the first part
of each key removed (the string before the first "_").
'''
def getkey2(dict,key,default):
"""utilify function: get dict[key] if key exists, or return default"""
if dict.has_key(key):
return dict[key]
return default
idset = sorted(self.answer_ids) # ordered list of answer id's
submission = [student_answers[k] for k in idset] # ordered list of answers
fromjs = [ getkey2(student_answers,k+'_fromjs',None) for k in idset ] # ordered list of fromjs_XXX responses (if exists)
# if there is only one box, and it's empty, then don't evaluate
if len(idset)==1 and not submission[0]:
return {idset[0]:'no_answer_entered'}
gctxt = self.context['global_context']
correct = ['unknown'] * len(idset)
messages = [''] * len(idset)
# put these in the context of the check function evaluator
# note that this doesn't help the "cfn" version - only the exec version
self.context.update({'xml' : self.xml, # our subtree
'response_id' : self.myid, # my ID
'expect': self.expect, # expected answer (if given as attribute)
'submission':submission, # ordered list of student answers from entry boxes in our subtree
'idset':idset, # ordered list of ID's of all entry boxes in our subtree
'fromjs':fromjs, # ordered list of all javascript inputs in our subtree
'answers':student_answers, # dict of student's responses, with keys being entry box IDs
'correct':correct, # the list to be filled in by the check function
'messages':messages, # the list of messages to be filled in by the check function
'testdat':'hello world',
})
# exec the check function
if type(self.code)==str:
try:
exec self.code in self.context['global_context'], self.context
except Exception,err:
print "oops in customresponse (code) error %s" % err
print "context = ",self.context
print traceback.format_exc()
else: # self.code is not a string; assume its a function
# this is an interface to the Tutor2 check functions
fn = self.code
try:
answer_given = submission[0] if (len(idset)==1) else submission
if fn.func_code.co_argcount>=4: # does it want four arguments (the answers dict, myname)?
ret = fn(self.expect,answer_given,student_answers,self.answer_ids[0])
elif fn.func_code.co_argcount>=3: # does it want a third argument (the answers dict)?
ret = fn(self.expect,answer_given,student_answers)
else:
ret = fn(self.expect,answer_given)
except Exception,err:
print "oops in customresponse (cfn) error %s" % err
# print "context = ",self.context
print traceback.format_exc()
if settings.DEBUG: print "[courseware.capa.responsetypes.customresponse.grade] ret = ",ret
if type(ret)==dict:
correct[0] = 'correct' if ret['ok'] else 'incorrect'
msg = ret['msg']
if 1:
# try to clean up message html
msg = '<html>'+msg+'</html>'
msg = etree.tostring(fromstring_bs(msg),pretty_print=True)
msg = msg.replace('&#13;','')
#msg = re.sub('<html>(.*)</html>','\\1',msg,flags=re.M|re.DOTALL) # python 2.7
msg = re.sub('(?ms)<html>(.*)</html>','\\1',msg)
messages[0] = msg
else:
correct[0] = 'correct' if ret else 'incorrect'
# build map giving "correct"ness of the answer(s)
#correct_map = dict(zip(idset, self.context['correct']))
correct_map = {}
for k in range(len(idset)):
correct_map[idset[k]] = correct[k]
correct_map['msg_%s' % idset[k]] = messages[k]
return correct_map
def get_answers(self):
'''
Give correct answer expected for this response.
capa_problem handles correct_answers from entry objects like textline, and that
is what should be used when this response has multiple entry objects.
but for simplicity, if an "expect" attribute was given by the content author
ie <customresponse expect="foo" ...> then return it now.
'''
if len(self.answer_ids)>1:
return {}
if self.expect:
return {self.answer_ids[0] : self.expect}
return {}
#-----------------------------------------------------------------------------
class ExternalResponse(GenericResponse):
"""
Grade the student's input using an external server.
Typically used by coding problems.
"""
def __init__(self, xml, context):
self.xml = xml
self.answer_ids = xml.xpath('//*[@id=$id]//textbox/@id|//*[@id=$id]//textline/@id',
id=xml.get('id'))
self.context = context
answer = xml.xpath('//*[@id=$id]//answer', answer = xml.xpath('//*[@id=$id]//answer',
id=xml.get('id'))[0] id=xml.get('id'))[0]
answer_src = answer.get('src') answer_src = answer.get('src')
if answer_src != None: if answer_src != None:
self.code = open(settings.DATA_DIR+'src/'+answer_src).read() self.code = open(settings.DATA_DIR+'src/'+answer_src).read()
else: else:
self.code = answer.text self.code = answer.text
self.tests = xml.get('answer')
def grade(self, student_answers): def grade(self, student_answers):
submission = [student_answers[k] for k in sorted(self.answer_ids)] submission = [student_answers[k] for k in sorted(self.answer_ids)]
self.context.update({'submission':submission}) self.context.update({'submission':submission})
exec self.code in global_context, self.context
return zip(sorted(self.answer_ids), self.context['correct']) xmlstr = etree.tostring(self.xml, pretty_print=True)
payload = {'xml': xmlstr,
### Question: Is this correct/what we want? Shouldn't this be a json.dumps?
'LONCAPA_student_response': ''.join(submission),
'LONCAPA_correct_answer': self.tests,
'processor' : self.code,
}
# call external server; TODO: get URL from settings.py
r = requests.post("http://eecs1.mit.edu:8889/pyloncapa",data=payload)
rxml = etree.fromstring(r.text) # response is XML; prase it
ad = rxml.find('awarddetail').text
admap = {'EXACT_ANS':'correct', # TODO: handle other loncapa responses
'WRONG_FORMAT': 'incorrect',
}
self.context['correct'] = ['correct']
if ad in admap:
self.context['correct'][0] = admap[ad]
# self.context['correct'] = ['correct','correct']
correct_map = dict(zip(sorted(self.answer_ids), self.context['correct']))
# TODO: separate message for each answer_id?
correct_map['msg'] = rxml.find('message').text.replace('&nbsp;','&#160;') # store message in correct_map
return correct_map
def get_answers(self): def get_answers(self):
# Since this is explicitly specified in the problem, this will # Since this is explicitly specified in the problem, this will
...@@ -94,16 +469,27 @@ class customresponse(object): ...@@ -94,16 +469,27 @@ class customresponse(object):
class StudentInputError(Exception): class StudentInputError(Exception):
pass pass
class formularesponse(object): #-----------------------------------------------------------------------------
class FormulaResponse(GenericResponse):
def __init__(self, xml, context): def __init__(self, xml, context):
self.xml = xml self.xml = xml
self.correct_answer = contextualize_text(xml.get('answer'), context) self.correct_answer = contextualize_text(xml.get('answer'), context)
self.samples = contextualize_text(xml.get('samples'), context) self.samples = contextualize_text(xml.get('samples'), context)
self.tolerance_xml = xml.xpath('//*[@id=$id]//responseparam[@type="tolerance"]/@default', try:
id=xml.get('id'))[0] self.tolerance_xml = xml.xpath('//*[@id=$id]//responseparam[@type="tolerance"]/@default',
self.tolerance = contextualize_text(self.tolerance_xml, context) id=xml.get('id'))[0]
self.answer_id = xml.xpath('//*[@id=$id]//textline/@id', self.tolerance = contextualize_text(self.tolerance_xml, context)
id=xml.get('id'))[0] except Exception,err:
self.tolerance = 0
try:
self.answer_id = xml.xpath('//*[@id=$id]//textline/@id',
id=xml.get('id'))[0]
except Exception, err:
self.answer_id = None
raise Exception, "[courseware.capa.responsetypes.FormulaResponse] Error: missing answer_id!!"
self.context = context self.context = context
ts = xml.get('type') ts = xml.get('type')
if ts == None: if ts == None:
...@@ -129,7 +515,7 @@ class formularesponse(object): ...@@ -129,7 +515,7 @@ class formularesponse(object):
for i in range(numsamples): for i in range(numsamples):
instructor_variables = self.strip_dict(dict(self.context)) instructor_variables = self.strip_dict(dict(self.context))
student_variables = dict() student_variables = dict()
for var in ranges: for var in ranges: # ranges give numerical ranges for testing
value = random.uniform(*ranges[var]) value = random.uniform(*ranges[var])
instructor_variables[str(var)] = value instructor_variables[str(var)] = value
student_variables[str(var)] = value student_variables[str(var)] = value
...@@ -164,7 +550,9 @@ class formularesponse(object): ...@@ -164,7 +550,9 @@ class formularesponse(object):
def get_answers(self): def get_answers(self):
return {self.answer_id:self.correct_answer} return {self.answer_id:self.correct_answer}
class schematicresponse(object): #-----------------------------------------------------------------------------
class SchematicResponse(GenericResponse):
def __init__(self, xml, context): def __init__(self, xml, context):
self.xml = xml self.xml = xml
self.answer_ids = xml.xpath('//*[@id=$id]//schematic/@id', self.answer_ids = xml.xpath('//*[@id=$id]//schematic/@id',
...@@ -179,6 +567,7 @@ class schematicresponse(object): ...@@ -179,6 +567,7 @@ class schematicresponse(object):
self.code = answer.text self.code = answer.text
def grade(self, student_answers): def grade(self, student_answers):
from capa_problem import global_context
submission = [json.loads(student_answers[k]) for k in sorted(self.answer_ids)] submission = [json.loads(student_answers[k]) for k in sorted(self.answer_ids)]
self.context.update({'submission':submission}) self.context.update({'submission':submission})
exec self.code in global_context, self.context exec self.code in global_context, self.context
...@@ -188,3 +577,64 @@ class schematicresponse(object): ...@@ -188,3 +577,64 @@ class schematicresponse(object):
# Since this is explicitly specified in the problem, this will # Since this is explicitly specified in the problem, this will
# be handled by capa_problem # be handled by capa_problem
return {} return {}
#-----------------------------------------------------------------------------
class ImageResponse(GenericResponse):
"""
Handle student response for image input: the input is a click on an image,
which produces an [x,y] coordinate pair. The click is correct if it falls
within a region specified. This region is nominally a rectangle.
Lon-CAPA requires that each <imageresponse> has a <foilgroup> inside it. That
doesn't make sense to me (Ike). Instead, let's have it such that <imageresponse>
should contain one or more <imageinput> stanzas. Each <imageinput> should specify
a rectangle, given as an attribute, defining the correct answer.
Example:
<imageresponse>
<imageinput src="image1.jpg" width="200" height="100" rectangle="(10,10)-(20,30)" />
<imageinput src="image2.jpg" width="210" height="130" rectangle="(12,12)-(40,60)" />
</imageresponse>
"""
def __init__(self, xml, context):
self.xml = xml
self.context = context
self.ielements = xml.findall('imageinput')
self.answer_ids = [ie.get('id') for ie in self.ielements]
def grade(self, student_answers):
correct_map = {}
expectedset = self.get_answers()
for aid in self.answer_ids: # loop through IDs of <imageinput> fields in our stanza
given = student_answers[aid] # this should be a string of the form '[x,y]'
# parse expected answer
# TODO: Compile regexp on file load
m = re.match('[\(\[]([0-9]+),([0-9]+)[\)\]]-[\(\[]([0-9]+),([0-9]+)[\)\]]',expectedset[aid].strip().replace(' ',''))
if not m:
msg = 'Error in problem specification! cannot parse rectangle in %s' % (etree.tostring(self.ielements[aid],
pretty_print=True))
raise Exception,'[capamodule.capa.responsetypes.imageinput] '+msg
(llx,lly,urx,ury) = [int(x) for x in m.groups()]
# parse given answer
m = re.match('\[([0-9]+),([0-9]+)]',given.strip().replace(' ',''))
if not m:
raise Exception,'[capamodule.capa.responsetypes.imageinput] error grading %s (input=%s)' % (err,aid,given)
(gx,gy) = [int(x) for x in m.groups()]
# answer is correct if (x,y) is within the specified rectangle
if (llx <= gx <= urx) and (lly <= gy <= ury):
correct_map[aid] = 'correct'
else:
correct_map[aid] = 'incorrect'
if settings.DEBUG:
print "[capamodule.capa.responsetypes.imageinput] correct_map=",correct_map
return correct_map
def get_answers(self):
return dict([(ie.get('id'),ie.get('rectangle')) for ie in self.ielements])
import math
import operator
from numpy import eye, array
from pyparsing import Word, alphas, nums, oneOf, Literal
from pyparsing import ZeroOrMore, OneOrMore, StringStart
from pyparsing import StringEnd, Optional, Forward
from pyparsing import CaselessLiteral, Group, StringEnd
from pyparsing import NoMatch, stringEnd
base_units = ['meter', 'gram', 'second', 'ampere', 'kelvin', 'mole', 'cd']
unit_vectors = dict([(base_units[i], eye(len(base_units))[:,i]) for i in range(len(base_units))])
def unit_evaluator(unit_string, units=unit_map):
''' Evaluate an expression. Variables are passed as a dictionary
from string to value. Unary functions are passed as a dictionary
from string to function '''
if string.strip() == "":
return float('nan')
ops = { "^" : operator.pow,
"*" : operator.mul,
"/" : operator.truediv,
}
prefixes={'%':0.01,'k':1e3,'M':1e6,'G':1e9,
'T':1e12,#'P':1e15,'E':1e18,'Z':1e21,'Y':1e24,
'c':1e-2,'m':1e-3,'u':1e-6,
'n':1e-9,'p':1e-12}#,'f':1e-15,'a':1e-18,'z':1e-21,'y':1e-24}
def super_float(text):
''' Like float, but with si extensions. 1k goes to 1000'''
if text[-1] in suffixes:
return float(text[:-1])*suffixes[text[-1]]
else:
return float(text)
def number_parse_action(x): # [ '7' ] -> [ 7 ]
return [super_float("".join(x))]
def exp_parse_action(x): # [ 2 ^ 3 ^ 2 ] -> 512
x = [e for e in x if type(e) == float] # Ignore ^
x.reverse()
x=reduce(lambda a,b:b**a, x)
return x
def parallel(x): # Parallel resistors [ 1 2 ] => 2/3
if len(x) == 1:
return x[0]
if 0 in x:
return float('nan')
x = [1./e for e in x if type(e) == float] # Ignore ^
return 1./sum(x)
def sum_parse_action(x): # [ 1 + 2 - 3 ] -> 0
total = 0.0
op = ops['+']
for e in x:
if e in set('+-'):
op = ops[e]
else:
total=op(total, e)
return total
def prod_parse_action(x): # [ 1 * 2 / 3 ] => 0.66
prod = 1.0
op = ops['*']
for e in x:
if e in set('*/'):
op = ops[e]
else:
prod=op(prod, e)
return prod
def func_parse_action(x):
return [functions[x[0]](x[1])]
number_suffix=reduce(lambda a,b:a|b, map(Literal,suffixes.keys()), NoMatch()) # SI suffixes and percent
(dot,minus,plus,times,div,lpar,rpar,exp)=map(Literal,".-+*/()^")
number_part=Word(nums)
inner_number = ( number_part+Optional("."+number_part) ) | ("."+number_part) # 0.33 or 7 or .34
number=Optional(minus | plus)+ inner_number + \
Optional(CaselessLiteral("E")+Optional("-")+number_part)+ \
Optional(number_suffix) # 0.33k or -17
number=number.setParseAction( number_parse_action ) # Convert to number
# Predefine recursive variables
expr = Forward()
factor = Forward()
def sreduce(f, l):
''' Same as reduce, but handle len 1 and len 0 lists sensibly '''
if len(l)==0:
return NoMatch()
if len(l)==1:
return l[0]
return reduce(f, l)
# Handle variables passed in. E.g. if we have {'R':0.5}, we make the substitution.
# Special case for no variables because of how we understand PyParsing is put together
if len(variables)>0:
varnames = sreduce(lambda x,y:x|y, map(lambda x: CaselessLiteral(x), variables.keys()))
varnames.setParseAction(lambda x:map(lambda y:variables[y], x))
else:
varnames=NoMatch()
# Same thing for functions.
if len(functions)>0:
funcnames = sreduce(lambda x,y:x|y, map(lambda x: CaselessLiteral(x), functions.keys()))
function = funcnames+lpar.suppress()+expr+rpar.suppress()
function.setParseAction(func_parse_action)
else:
function = NoMatch()
atom = number | varnames | lpar+expr+rpar | function
factor << (atom + ZeroOrMore(exp+atom)).setParseAction(exp_parse_action) # 7^6
paritem = factor + ZeroOrMore(Literal('||')+factor) # 5k || 4k
paritem=paritem.setParseAction(parallel)
term = paritem + ZeroOrMore((times|div)+paritem) # 7 * 5 / 4 - 3
term = term.setParseAction(prod_parse_action)
expr << Optional((plus|minus)) + term + ZeroOrMore((plus|minus)+term) # -5 + 4 - 3
expr=expr.setParseAction(sum_parse_action)
return (expr+stringEnd).parseString(string)[0]
if __name__=='__main__':
variables={'R1':2.0, 'R3':4.0}
functions={'sin':math.sin, 'cos':math.cos}
print "X",evaluator(variables, functions, "10000||sin(7+5)-6k")
print "X",evaluator(variables, functions, "13")
print evaluator({'R1': 2.0, 'R3':4.0}, {}, "13")
#
print evaluator({'a': 2.2997471478310274, 'k': 9, 'm': 8, 'x': 0.66009498411213041}, {}, "5")
print evaluator({},{}, "-1")
print evaluator({},{}, "-(7+5)")
print evaluator({},{}, "-0.33")
print evaluator({},{}, "-.33")
print evaluator({},{}, "5+7 QWSEKO")
'''
courseware/content_parser.py
This file interfaces between all courseware modules and the top-level course.xml file for a course.
Does some caching (to be explained).
'''
import hashlib import hashlib
import json
import logging import logging
import os import os
import re import re
...@@ -14,9 +22,11 @@ try: # This lets us do __name__ == ='__main__' ...@@ -14,9 +22,11 @@ try: # This lets us do __name__ == ='__main__'
from student.models import UserProfile from student.models import UserProfile
from student.models import UserTestGroup from student.models import UserTestGroup
from mitxmako.shortcuts import render_to_response, render_to_string from mitxmako.shortcuts import render_to_string
from util.cache import cache from util.cache import cache
from multicourse import multicourse_settings
except: except:
print "Could not import/content_parser"
settings = None settings = None
''' This file will eventually form an abstraction layer between the ''' This file will eventually form an abstraction layer between the
...@@ -97,20 +107,9 @@ def item(l, default="", process=lambda x:x): ...@@ -97,20 +107,9 @@ def item(l, default="", process=lambda x:x):
def id_tag(course): def id_tag(course):
''' Tag all course elements with unique IDs ''' ''' Tag all course elements with unique IDs '''
old_ids = {'video':'youtube',
'problem':'filename',
'sequential':'id',
'html':'filename',
'vertical':'id',
'tab':'id',
'schematic':'id',
'book' : 'id'}
import courseware.modules import courseware.modules
default_ids = courseware.modules.get_default_ids() default_ids = courseware.modules.get_default_ids()
#print default_ids, old_ids
#print default_ids == old_ids
# Tag elements with unique IDs # Tag elements with unique IDs
elements = course.xpath("|".join(['//'+c for c in default_ids])) elements = course.xpath("|".join(['//'+c for c in default_ids]))
for elem in elements: for elem in elements:
...@@ -153,6 +152,9 @@ def propogate_downward_tag(element, attribute_name, parent_attribute = None): ...@@ -153,6 +152,9 @@ def propogate_downward_tag(element, attribute_name, parent_attribute = None):
return return
def user_groups(user): def user_groups(user):
if not user.is_authenticated():
return []
# TODO: Rewrite in Django # TODO: Rewrite in Django
key = 'user_group_names_{user.id}'.format(user=user) key = 'user_group_names_{user.id}'.format(user=user)
cache_expiration = 60 * 60 # one hour cache_expiration = 60 * 60 # one hour
...@@ -177,15 +179,23 @@ def course_xml_process(tree): ...@@ -177,15 +179,23 @@ def course_xml_process(tree):
propogate_downward_tag(tree, "due") propogate_downward_tag(tree, "due")
propogate_downward_tag(tree, "graded") propogate_downward_tag(tree, "graded")
propogate_downward_tag(tree, "graceperiod") propogate_downward_tag(tree, "graceperiod")
propogate_downward_tag(tree, "showanswer")
propogate_downward_tag(tree, "rerandomize")
return tree return tree
def course_file(user): def course_file(user,coursename=None):
''' Given a user, return course.xml''' ''' Given a user, return course.xml'''
#import logging
#log = logging.getLogger("tracking")
#log.info( "DEBUG: cf:"+str(user) )
filename = UserProfile.objects.get(user=user).courseware # user.profile_cache.courseware if user.is_authenticated():
filename = UserProfile.objects.get(user=user).courseware # user.profile_cache.courseware
else:
filename = 'guest_course.xml'
# if a specific course is specified, then use multicourse to get the right path to the course XML directory
if coursename and settings.ENABLE_MULTICOURSE:
xp = multicourse_settings.get_course_xmlpath(coursename)
filename = xp + filename # prefix the filename with the path
groups = user_groups(user) groups = user_groups(user)
options = {'dev_content':settings.DEV_CONTENT, options = {'dev_content':settings.DEV_CONTENT,
'groups' : groups} 'groups' : groups}
...@@ -207,13 +217,24 @@ def course_file(user): ...@@ -207,13 +217,24 @@ def course_file(user):
return tree return tree
def section_file(user, section): def section_file(user, section, coursename=None, dironly=False):
''' Given a user and the name of a section, return that section '''
Given a user and the name of a section, return that section.
This is done specific to each course.
If dironly=True then return the sections directory.
''' '''
filename = section+".xml" filename = section+".xml"
if filename not in os.listdir(settings.DATA_DIR + '/sections/'): # if a specific course is specified, then use multicourse to get the right path to the course XML directory
print filename+" not in "+str(os.listdir(settings.DATA_DIR + '/sections/')) xp = ''
if coursename and settings.ENABLE_MULTICOURSE: xp = multicourse_settings.get_course_xmlpath(coursename)
dirname = settings.DATA_DIR + xp + '/sections/'
if dironly: return dirname
if filename not in os.listdir(dirname):
print filename+" not in "+str(os.listdir(dirname))
return None return None
options = {'dev_content':settings.DEV_CONTENT, options = {'dev_content':settings.DEV_CONTENT,
...@@ -223,7 +244,7 @@ def section_file(user, section): ...@@ -223,7 +244,7 @@ def section_file(user, section):
return tree return tree
def module_xml(user, module, id_tag, module_id): def module_xml(user, module, id_tag, module_id, coursename=None):
''' Get XML for a module based on module and module_id. Assumes ''' Get XML for a module based on module and module_id. Assumes
module occurs once in courseware XML file or hidden section. ''' module occurs once in courseware XML file or hidden section. '''
# Sanitize input # Sanitize input
...@@ -236,14 +257,15 @@ def module_xml(user, module, id_tag, module_id): ...@@ -236,14 +257,15 @@ def module_xml(user, module, id_tag, module_id):
id_tag=id_tag, id_tag=id_tag,
id=module_id) id=module_id)
#result_set=doc.xpathEval(xpath_search) #result_set=doc.xpathEval(xpath_search)
doc = course_file(user) doc = course_file(user,coursename)
section_list = (s[:-4] for s in os.listdir(settings.DATA_DIR+'/sections') if s[-4:]=='.xml') sdirname = section_file(user,'',coursename,True) # get directory where sections information is stored
section_list = (s[:-4] for s in os.listdir(sdirname) if s[-4:]=='.xml')
result_set=doc.xpath(xpath_search) result_set=doc.xpath(xpath_search)
if len(result_set)<1: if len(result_set)<1:
for section in section_list: for section in section_list:
try: try:
s = section_file(user, section) s = section_file(user, section, coursename)
except etree.XMLSyntaxError: except etree.XMLSyntaxError:
ex= sys.exc_info() ex= sys.exc_info()
raise ContentException("Malformed XML in " + section+ "("+str(ex[1].msg)+")") raise ContentException("Malformed XML in " + section+ "("+str(ex[1].msg)+")")
......
GRADER = [
{
'type' : "Homework",
'min_count' : 12,
'drop_count' : 2,
'short_label' : "HW",
'weight' : 0.15,
},
{
'type' : "Lab",
'min_count' : 12,
'drop_count' : 2,
'category' : "Labs",
'weight' : 0.15
},
{
'type' : "Midterm",
'name' : "Midterm Exam",
'short_label' : "Midterm",
'weight' : 0.3,
},
{
'type' : "Final",
'name' : "Final Exam",
'short_label' : "Final",
'weight' : 0.4,
}
]
import abc
import logging
from django.conf import settings
from collections import namedtuple
log = logging.getLogger("mitx.courseware")
# This is a tuple for holding scores, either from problems or sections.
# Section either indicates the name of the problem or the name of the section
Score = namedtuple("Score", "earned possible graded section")
def grader_from_conf(conf):
"""
This creates a CourseGrader from a configuration (such as in course_settings.py).
The conf can simply be an instance of CourseGrader, in which case no work is done.
More commonly, the conf is a list of dictionaries. A WeightedSubsectionsGrader
with AssignmentFormatGrader's or SingleSectionGrader's as subsections will be
generated. Every dictionary should contain the parameters for making either a
AssignmentFormatGrader or SingleSectionGrader, in addition to a 'weight' key.
"""
if isinstance(conf, CourseGrader):
return conf
subgraders = []
for subgraderconf in conf:
subgraderconf = subgraderconf.copy()
weight = subgraderconf.pop("weight", 0)
try:
if 'min_count' in subgraderconf:
#This is an AssignmentFormatGrader
subgrader = AssignmentFormatGrader(**subgraderconf)
subgraders.append( (subgrader, subgrader.category, weight) )
elif 'name' in subgraderconf:
#This is an SingleSectionGrader
subgrader = SingleSectionGrader(**subgraderconf)
subgraders.append( (subgrader, subgrader.category, weight) )
else:
raise ValueError("Configuration has no appropriate grader class.")
except (TypeError, ValueError) as error:
errorString = "Unable to parse grader configuration:\n " + str(subgraderconf) + "\n Error was:\n " + str(error)
log.critical(errorString)
raise ValueError(errorString)
return WeightedSubsectionsGrader( subgraders )
class CourseGrader(object):
"""
A course grader takes the totaled scores for each graded section (that a student has
started) in the course. From these scores, the grader calculates an overall percentage
grade. The grader should also generate information about how that score was calculated,
to be displayed in graphs or charts.
A grader has one required method, grade(), which is passed a grade_sheet. The grade_sheet
contains scores for all graded section that the student has started. If a student has
a score of 0 for that section, it may be missing from the grade_sheet. The grade_sheet
is keyed by section format. Each value is a list of Score namedtuples for each section
that has the matching section format.
The grader outputs a dictionary with the following keys:
- percent: Contaisn a float value, which is the final percentage score for the student.
- section_breakdown: This is a list of dictionaries which provide details on sections
that were graded. These are used for display in a graph or chart. The format for a
section_breakdown dictionary is explained below.
- grade_breakdown: This is a list of dictionaries which provide details on the contributions
of the final percentage grade. This is a higher level breakdown, for when the grade is constructed
of a few very large sections (such as Homeworks, Labs, a Midterm, and a Final). The format for
a grade_breakdown is explained below. This section is optional.
A dictionary in the section_breakdown list has the following keys:
percent: A float percentage for the section.
label: A short string identifying the section. Preferably fixed-length. E.g. "HW 3".
detail: A string explanation of the score. E.g. "Homework 1 - Ohms Law - 83% (5/6)"
category: A string identifying the category. Items with the same category are grouped together
in the display (for example, by color).
prominent: A boolean value indicating that this section should be displayed as more prominent
than other items.
A dictionary in the grade_breakdown list has the following keys:
percent: A float percentage in the breakdown. All percents should add up to the final percentage.
detail: A string explanation of this breakdown. E.g. "Homework - 10% of a possible 15%"
category: A string identifying the category. Items with the same category are grouped together
in the display (for example, by color).
"""
__metaclass__ = abc.ABCMeta
@abc.abstractmethod
def grade(self, grade_sheet):
raise NotImplementedError
class WeightedSubsectionsGrader(CourseGrader):
"""
This grader takes a list of tuples containing (grader, category_name, weight) and computes
a final grade by totalling the contribution of each sub grader and multiplying it by the
given weight. For example, the sections may be
[ (homeworkGrader, "Homework", 0.15), (labGrader, "Labs", 0.15), (midtermGrader, "Midterm", 0.30), (finalGrader, "Final", 0.40) ]
All items in section_breakdown for each subgrader will be combined. A grade_breakdown will be
composed using the score from each grader.
Note that the sum of the weights is not take into consideration. If the weights add up to
a value > 1, the student may end up with a percent > 100%. This allows for sections that
are extra credit.
"""
def __init__(self, sections):
self.sections = sections
def grade(self, grade_sheet):
total_percent = 0.0
section_breakdown = []
grade_breakdown = []
for subgrader, category, weight in self.sections:
subgrade_result = subgrader.grade(grade_sheet)
weightedPercent = subgrade_result['percent'] * weight
section_detail = "{0} = {1:.1%} of a possible {2:.0%}".format(category, weightedPercent, weight)
total_percent += weightedPercent
section_breakdown += subgrade_result['section_breakdown']
grade_breakdown.append( {'percent' : weightedPercent, 'detail' : section_detail, 'category' : category} )
return {'percent' : total_percent,
'section_breakdown' : section_breakdown,
'grade_breakdown' : grade_breakdown}
class SingleSectionGrader(CourseGrader):
"""
This grades a single section with the format 'type' and the name 'name'.
If the name is not appropriate for the short short_label or category, they each may
be specified individually.
"""
def __init__(self, type, name, short_label = None, category = None):
self.type = type
self.name = name
self.short_label = short_label or name
self.category = category or name
def grade(self, grade_sheet):
foundScore = None
if self.type in grade_sheet:
for score in grade_sheet[self.type]:
if score.section == self.name:
foundScore = score
break
if foundScore:
percent = foundScore.earned / float(foundScore.possible)
detail = "{name} - {percent:.0%} ({earned:.3n}/{possible:.3n})".format( name = self.name,
percent = percent,
earned = float(foundScore.earned),
possible = float(foundScore.possible))
else:
percent = 0.0
detail = "{name} - 0% (?/?)".format(name = self.name)
if settings.GENERATE_PROFILE_SCORES:
points_possible = random.randrange(50, 100)
points_earned = random.randrange(40, points_possible)
percent = points_earned / float(points_possible)
detail = "{name} - {percent:.0%} ({earned:.3n}/{possible:.3n})".format( name = self.name,
percent = percent,
earned = float(points_earned),
possible = float(points_possible))
breakdown = [{'percent': percent, 'label': self.short_label, 'detail': detail, 'category': self.category, 'prominent': True}]
return {'percent' : percent,
'section_breakdown' : breakdown,
#No grade_breakdown here
}
class AssignmentFormatGrader(CourseGrader):
"""
Grades all sections matching the format 'type' with an equal weight. A specified
number of lowest scores can be dropped from the calculation. The minimum number of
sections in this format must be specified (even if those sections haven't been
written yet).
min_count defines how many assignments are expected throughout the course. Placeholder
scores (of 0) will be inserted if the number of matching sections in the course is < min_count.
If there number of matching sections in the course is > min_count, min_count will be ignored.
category should be presentable to the user, but may not appear. When the grade breakdown is
displayed, scores from the same category will be similar (for example, by color).
section_type is a string that is the type of a singular section. For example, for Labs it
would be "Lab". This defaults to be the same as category.
short_label is similar to section_type, but shorter. For example, for Homework it would be
"HW".
"""
def __init__(self, type, min_count, drop_count, category = None, section_type = None, short_label = None):
self.type = type
self.min_count = min_count
self.drop_count = drop_count
self.category = category or self.type
self.section_type = section_type or self.type
self.short_label = short_label or self.type
def grade(self, grade_sheet):
def totalWithDrops(breakdown, drop_count):
#create an array of tuples with (index, mark), sorted by mark['percent'] descending
sorted_breakdown = sorted( enumerate(breakdown), key=lambda x: -x[1]['percent'] )
# A list of the indices of the dropped scores
dropped_indices = []
if drop_count > 0:
dropped_indices = [x[0] for x in sorted_breakdown[-drop_count:]]
aggregate_score = 0
for index, mark in enumerate(breakdown):
if index not in dropped_indices:
aggregate_score += mark['percent']
if (len(breakdown) - drop_count > 0):
aggregate_score /= len(breakdown) - drop_count
return aggregate_score, dropped_indices
#Figure the homework scores
scores = grade_sheet.get(self.type, [])
breakdown = []
for i in range( max(self.min_count, len(scores)) ):
if i < len(scores):
percentage = scores[i].earned / float(scores[i].possible)
summary = "{section_type} {index} - {name} - {percent:.0%} ({earned:.3n}/{possible:.3n})".format(index = i+1,
section_type = self.section_type,
name = scores[i].section,
percent = percentage,
earned = float(scores[i].earned),
possible = float(scores[i].possible) )
else:
percentage = 0
summary = "{section_type} {index} Unreleased - 0% (?/?)".format(index = i+1, section_type = self.section_type)
if settings.GENERATE_PROFILE_SCORES:
points_possible = random.randrange(10, 50)
points_earned = random.randrange(5, points_possible)
percentage = points_earned / float(points_possible)
summary = "{section_type} {index} - {name} - {percent:.0%} ({earned:.3n}/{possible:.3n})".format(index = i+1,
section_type = self.section_type,
name = "Randomly Generated",
percent = percentage,
earned = float(points_earned),
possible = float(points_possible) )
short_label = "{short_label} {index:02d}".format(index = i+1, short_label = self.short_label)
breakdown.append( {'percent': percentage, 'label': short_label, 'detail': summary, 'category': self.category} )
total_percent, dropped_indices = totalWithDrops(breakdown, self.drop_count)
for dropped_index in dropped_indices:
breakdown[dropped_index]['mark'] = {'detail': "The lowest {drop_count} {section_type} scores are dropped.".format(drop_count = self.drop_count, section_type=self.section_type) }
total_detail = "{section_type} Average = {percent:.0%}".format(percent = total_percent, section_type = self.section_type)
total_label = "{short_label} Avg".format(short_label = self.short_label)
breakdown.append( {'percent': total_percent, 'label': total_label, 'detail': total_detail, 'category': self.category, 'prominent': True} )
return {'percent' : total_percent,
'section_breakdown' : breakdown,
#No grade_breakdown here
}
import courseware.content_parser as content_parser """
import courseware.modules Course settings module. The settings are based of django.conf. All settings in
import logging courseware.global_course_settings are first applied, and then any settings
in the settings.DATA_DIR/course_settings.py are applied. A setting must be
in ALL_CAPS.
Settings are used by calling
from courseware import course_settings
Note that courseware.course_settings is not a module -- it's an object. So
importing individual settings is not possible:
from courseware.course_settings import GRADER # This won't work.
"""
from lxml import etree
import random import random
import urllib import imp
import logging
import sys
import types
from collections import namedtuple
from django.conf import settings from django.conf import settings
from lxml import etree
from courseware import global_course_settings
from courseware import graders
from courseware.graders import Score
from models import StudentModule from models import StudentModule
from student.models import UserProfile import courseware.content_parser as content_parser
import courseware.modules
log = logging.getLogger("mitx.courseware") _log = logging.getLogger("mitx.courseware")
Score = namedtuple("Score", "earned possible weight graded section") class Settings(object):
def __init__(self):
# update this dict from global settings (but only for ALL_CAPS settings)
for setting in dir(global_course_settings):
if setting == setting.upper():
setattr(self, setting, getattr(global_course_settings, setting))
data_dir = settings.DATA_DIR
fp = None
try:
fp, pathname, description = imp.find_module("course_settings", [data_dir])
mod = imp.load_module("course_settings", fp, pathname, description)
except Exception as e:
_log.exception("Unable to import course settings file from " + data_dir + ". Error: " + str(e))
mod = types.ModuleType('course_settings')
finally:
if fp:
fp.close()
for setting in dir(mod):
if setting == setting.upper():
setting_value = getattr(mod, setting)
setattr(self, setting, setting_value)
# Here is where we should parse any configurations, so that we can fail early
self.GRADER = graders.grader_from_conf(self.GRADER)
def get_grade(user, problem, cache): course_settings = Settings()
## HACK: assumes max score is fixed per problem
id = problem.get('id')
correct = 0
# If the ID is not in the cache, add the item
if id not in cache:
module = StudentModule(module_type = 'problem', # TODO: Move into StudentModule.__init__?
module_id = id,
student = user,
state = None,
grade = 0,
max_grade = None,
done = 'i')
cache[id] = module
# Grab the # correct from cache
if id in cache:
response = cache[id]
if response.grade!=None:
correct=response.grade
# Grab max grade from cache, or if it doesn't exist, compute and save to DB
if id in cache and response.max_grade != None:
total = response.max_grade
else:
total=courseware.modules.capa_module.Module(etree.tostring(problem), "id").max_score()
response.max_grade = total
response.save()
return (correct, total)
def grade_sheet(student):
def grade_sheet(student,coursename=None):
""" """
This pulls a summary of all problems in the course. It returns a dictionary with two datastructures: This pulls a summary of all problems in the course. It returns a dictionary with two datastructures:
...@@ -54,11 +75,9 @@ def grade_sheet(student): ...@@ -54,11 +75,9 @@ def grade_sheet(student):
each containing an array of sections, each containing an array of scores. This contains information for graded and ungraded 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. problems, and is good for displaying a course summary with due dates, etc.
- grade_summary is a summary of how the final grade breaks down. It is an array of "sections". Each section can either be - grade_summary is the output from the course grader. More information on the format is in the docstring for CourseGrader.
a conglomerate of scores (like labs or homeworks) which has subscores and a totalscore, or a section can be all from one assignment
(such as a midterm or final) and only has a totalscore. Each section has a weight that shows how it contributes to the total grade.
""" """
dom=content_parser.course_file(student) dom=content_parser.course_file(student,coursename)
course = dom.xpath('//course/@name')[0] course = dom.xpath('//course/@name')[0]
xmlChapters = dom.xpath('//course[@name=$course]/chapter', course=course) xmlChapters = dom.xpath('//course[@name=$course]/chapter', course=course)
...@@ -68,7 +87,6 @@ def grade_sheet(student): ...@@ -68,7 +87,6 @@ def grade_sheet(student):
response_by_id[response.module_id] = response response_by_id[response.module_id] = response
totaled_scores = {} totaled_scores = {}
chapters=[] chapters=[]
for c in xmlChapters: for c in xmlChapters:
...@@ -85,33 +103,29 @@ def grade_sheet(student): ...@@ -85,33 +103,29 @@ def grade_sheet(student):
scores=[] scores=[]
if len(problems)>0: if len(problems)>0:
for p in problems: for p in problems:
(correct,total) = get_grade(student, p, response_by_id) (correct,total) = get_score(student, p, response_by_id, coursename=coursename)
# id = p.get('id')
# correct = 0
# if id in response_by_id:
# response = response_by_id[id]
# if response.grade!=None:
# correct=response.grade
# total=courseware.modules.capa_module.Module(etree.tostring(p), "id").max_score() # TODO: Add state. Not useful now, but maybe someday problems will have randomized max scores?
# print correct, total
if settings.GENERATE_PROFILE_SCORES: if settings.GENERATE_PROFILE_SCORES:
if total > 1: if total > 1:
correct = random.randrange( max(total-2, 1) , total + 1 ) correct = random.randrange( max(total-2, 1) , total + 1 )
else: else:
correct = total correct = total
scores.append( Score(int(correct),total, float(p.get("weight", total)), graded, p.get("name")) )
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, p.get("name")) )
section_total, graded_total = aggregate_scores(scores) section_total, graded_total = aggregate_scores(scores, s.get("name"))
#Add the graded total to totaled_scores #Add the graded total to totaled_scores
format = s.get('format') if s.get('format') else "" format = s.get('format', "")
subtitle = s.get('subtitle') if s.get('subtitle') else format subtitle = s.get('subtitle', format)
if format and graded_total[1] > 0: if format and graded_total[1] > 0:
format_scores = totaled_scores.get(format, []) format_scores = totaled_scores.get(format, [])
format_scores.append( graded_total ) format_scores.append( graded_total )
totaled_scores[ format ] = format_scores totaled_scores[ format ] = format_scores
score={'section':s.get("name"), section_score={'section':s.get("name"),
'scores':scores, 'scores':scores,
'section_total' : section_total, 'section_total' : section_total,
'format' : format, 'format' : format,
...@@ -119,154 +133,79 @@ def grade_sheet(student): ...@@ -119,154 +133,79 @@ def grade_sheet(student):
'due' : s.get("due") or "", 'due' : s.get("due") or "",
'graded' : graded, 'graded' : graded,
} }
sections.append(score) sections.append(section_score)
chapters.append({'course':course, chapters.append({'course':course,
'chapter' : c.get("name"), 'chapter' : c.get("name"),
'sections' : sections,}) 'sections' : sections,})
grade_summary = grade_summary_6002x(totaled_scores)
return {'courseware_summary' : chapters, #all assessments as they appear in the course definition grader = course_settings.GRADER
'grade_summary' : grade_summary, #graded assessments only grade_summary = grader.grade(totaled_scores)
}
return {'courseware_summary' : chapters,
'grade_summary' : grade_summary}
def aggregate_scores(scores): def aggregate_scores(scores, section_name = "summary"):
scores = filter( lambda score: score.possible > 0, scores ) total_correct_graded = sum(score.earned for score in scores if score.graded)
total_possible_graded = sum(score.possible for score in scores if score.graded)
total_correct_graded = sum((score.earned*1.0/score.possible)*score.weight for score in scores if score.graded) total_correct = sum(score.earned for score in scores)
total_possible_graded = sum(score.weight for score in scores if score.graded) total_possible = sum(score.possible for score in scores)
total_correct = sum((score.earned*1.0/score.possible)*score.weight for score in scores)
total_possible = sum(score.weight for score in scores)
#regardless of whether or not it is graded #regardless of whether or not it is graded
all_total = Score(total_correct, all_total = Score(total_correct,
total_possible, total_possible,
1,
False, False,
"summary") section_name)
#selecting only graded things #selecting only graded things
graded_total = Score(total_correct_graded, graded_total = Score(total_correct_graded,
total_possible_graded, total_possible_graded,
1,
True, True,
"summary") section_name)
return all_total, graded_total return all_total, graded_total
def grade_summary_6002x(totaled_scores):
"""
This function takes the a dictionary of (graded) section scores, and applies the course grading rules to create
the grade_summary. For 6.002x this means homeworks and labs all have equal weight, with the lowest 2 of each
being dropped. There is one midterm and one final.
"""
def totalWithDrops(scores, drop_count):
#Note that this key will sort the list descending
sorted_scores = sorted( enumerate(scores), key=lambda x: -x[1]['percentage'] )
# A list of the indices of the dropped scores
dropped_indices = [score[0] for score in sorted_scores[-drop_count:]]
aggregate_score = 0
for index, score in enumerate(scores):
if index not in dropped_indices:
aggregate_score += score['percentage']
aggregate_score /= len(scores) - drop_count
return aggregate_score, dropped_indices
#Figure the homework scores
homework_scores = totaled_scores['Homework'] if 'Homework' in totaled_scores else []
homework_percentages = []
for i in range(12):
if i < len(homework_scores):
percentage = homework_scores[i].earned / float(homework_scores[i].possible)
summary = "Homework {0} - {1} - {2:.0%} ({3:g}/{4:g})".format( i + 1, homework_scores[i].section , percentage, homework_scores[i].earned, homework_scores[i].possible )
else:
percentage = 0
summary = "Unreleased Homework {0} - 0% (?/?)".format(i + 1)
if settings.GENERATE_PROFILE_SCORES:
points_possible = random.randrange(10, 50)
points_earned = random.randrange(5, points_possible)
percentage = points_earned / float(points_possible)
summary = "Random Homework - {0:.0%} ({1:g}/{2:g})".format( percentage, points_earned, points_possible )
label = "HW {0:02d}".format(i + 1)
homework_percentages.append( {'percentage': percentage, 'summary': summary, 'label' : label} )
homework_total, homework_dropped_indices = totalWithDrops(homework_percentages, 2)
#Figure the lab scores
lab_scores = totaled_scores['Lab'] if 'Lab' in totaled_scores else []
lab_percentages = []
for i in range(12):
if i < len(lab_scores):
percentage = lab_scores[i].earned / float(lab_scores[i].possible)
summary = "Lab {0} - {1} - {2:.0%} ({3:g}/{4:g})".format( i + 1, lab_scores[i].section , percentage, lab_scores[i].earned, lab_scores[i].possible )
else:
percentage = 0
summary = "Unreleased Lab {0} - 0% (?/?)".format(i + 1)
if settings.GENERATE_PROFILE_SCORES:
points_possible = random.randrange(10, 50)
points_earned = random.randrange(5, points_possible)
percentage = points_earned / float(points_possible)
summary = "Random Lab - {0:.0%} ({1:g}/{2:g})".format( percentage, points_earned, points_possible )
label = "Lab {0:02d}".format(i + 1)
lab_percentages.append( {'percentage': percentage, 'summary': summary, 'label' : label} )
lab_total, lab_dropped_indices = totalWithDrops(lab_percentages, 2)
#TODO: Pull this data about the midterm and final from the databse. It should be exactly similar to above, but we aren't sure how exams will be done yet.
#This is a hack, but I have no intention of having this function be useful for anything but 6.002x anyway, so I don't want to make it pretty. def get_score(user, problem, cache, coursename=None):
midterm_score = totaled_scores['Midterm'][0] if 'Midterm' in totaled_scores else Score('?', '?', '?', True, "?") ## HACK: assumes max score is fixed per problem
midterm_percentage = midterm_score.earned * 1.0 / midterm_score.possible if 'Midterm' in totaled_scores else 0 id = problem.get('id')
correct = 0.0
final_score = totaled_scores['Final'][0] if 'Final' in totaled_scores else Score('?', '?', '?', True, "?")
final_percentage = final_score.earned * 1.0 / final_score.possible if 'Final' in totaled_scores else 0
if settings.GENERATE_PROFILE_SCORES: # If the ID is not in the cache, add the item
midterm_score = Score(random.randrange(50, 150), 150, 150, True, "?") if id not in cache:
midterm_percentage = midterm_score.earned / float(midterm_score.possible) module = StudentModule(module_type = 'problem', # TODO: Move into StudentModule.__init__?
module_id = id,
student = user,
state = None,
grade = 0,
max_grade = None,
done = 'i')
cache[id] = module
# Grab the # correct from cache
if id in cache:
response = cache[id]
if response.grade!=None:
correct=float(response.grade)
final_score = Score(random.randrange(100, 300), 300, 300, True, "?") # Grab max grade from cache, or if it doesn't exist, compute and save to DB
final_percentage = final_score.earned / float(final_score.possible) if id in cache and response.max_grade != None:
total = response.max_grade
else:
## HACK 1: We shouldn't specifically reference capa_module
## HACK 2: Backwards-compatibility: This should be written when a grade is saved, and removed from the system
from module_render import I4xSystem
system = I4xSystem(None, None, None, coursename=coursename)
total=float(courseware.modules.capa_module.Module(system, etree.tostring(problem), "id").max_score())
response.max_grade = total
response.save()
#Now we re-weight the problem, if specified
grade_summary = [ weight = problem.get("weight", None)
{ if weight:
'category': 'Homework', weight = float(weight)
'subscores' : homework_percentages, correct = correct * weight / total
'dropped_indices' : homework_dropped_indices, total = weight
'totalscore' : homework_total,
'totalscore_summary' : "Homework Average - {0:.0%}".format(homework_total), return (correct, total)
'totallabel' : 'HW Avg',
'weight' : 0.15,
},
{
'category': 'Labs',
'subscores' : lab_percentages,
'dropped_indices' : lab_dropped_indices,
'totalscore' : lab_total,
'totalscore_summary' : "Lab Average - {0:.0%}".format(lab_total),
'totallabel' : 'Lab Avg',
'weight' : 0.15,
},
{
'category': 'Midterm',
'totalscore' : midterm_percentage,
'totalscore_summary' : "Midterm - {0:.0%} ({1}/{2})".format(midterm_percentage, midterm_score.earned, midterm_score.possible),
'totallabel' : 'Midterm',
'weight' : 0.30,
},
{
'category': 'Final',
'totalscore' : final_percentage,
'totalscore_summary' : "Final - {0:.0%} ({1}/{2})".format(final_percentage, final_score.earned, final_score.possible),
'totallabel' : 'Final',
'weight' : 0.40,
}
]
return grade_summary
...@@ -6,9 +6,9 @@ from django.core.management.base import BaseCommand ...@@ -6,9 +6,9 @@ from django.core.management.base import BaseCommand
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import User from django.contrib.auth.models import User
from mitx.courseware.content_parser import course_file from courseware.content_parser import course_file
import mitx.courseware.module_render import courseware.module_render
import mitx.courseware.modules import courseware.modules
class Command(BaseCommand): class Command(BaseCommand):
help = "Does basic validity tests on course.xml." help = "Does basic validity tests on course.xml."
...@@ -25,15 +25,15 @@ class Command(BaseCommand): ...@@ -25,15 +25,15 @@ class Command(BaseCommand):
check = False check = False
print "Confirming all modules render. Nothing should print during this step. " print "Confirming all modules render. Nothing should print during this step. "
for module in course.xpath('//problem|//html|//video|//vertical|//sequential|/tab'): for module in course.xpath('//problem|//html|//video|//vertical|//sequential|/tab'):
module_class=mitx.courseware.modules.modx_modules[module.tag] module_class = courseware.modules.modx_modules[module.tag]
# TODO: Abstract this out in render_module.py # TODO: Abstract this out in render_module.py
try: try:
instance=module_class(etree.tostring(module), module_class(etree.tostring(module),
module.get('id'), module.get('id'),
ajax_url='', ajax_url='',
state=None, state=None,
track_function = lambda x,y,z:None, track_function = lambda x,y,z:None,
render_function = lambda x: {'content':'','destroy_js':'','init_js':'','type':'video'}) render_function = lambda x: {'content':'','destroy_js':'','init_js':'','type':'video'})
except: except:
print "==============> Error in ", etree.tostring(module) print "==============> Error in ", etree.tostring(module)
check = False check = False
......
import StringIO
import json
import logging import logging
import os
import sys
import sys
import urllib
import uuid
from lxml import etree from lxml import etree
from django.conf import settings
from django.contrib.auth.models import User
from django.core.context_processors import csrf
from django.db import connection
from django.http import Http404 from django.http import Http404
from django.http import HttpResponse from django.http import HttpResponse
from django.shortcuts import redirect from django.shortcuts import redirect
from django.template import Context from django.template import Context
from django.template import Context, loader from django.template import Context, loader
from mitxmako.shortcuts import render_to_response, render_to_string
from fs.osfs import OSFS
from django.conf import settings
from mitxmako.shortcuts import render_to_string
from models import StudentModule from models import StudentModule
from student.models import UserProfile
import track.views import track.views
import courseware.content_parser as content_parser
import courseware.modules import courseware.modules
log = logging.getLogger("mitx.courseware") log = logging.getLogger("mitx.courseware")
class I4xSystem(object):
'''
This is an abstraction such that x_modules can function independent
of the courseware (e.g. import into other types of courseware, LMS,
or if we want to have a sandbox server for user-contributed content)
'''
def __init__(self, ajax_url, track_function, render_function, filestore=None):
self.ajax_url = ajax_url
self.track_function = track_function
if not filestore:
self.filestore = OSFS(settings.DATA_DIR)
self.render_function = render_function
self.exception404 = Http404
def __repr__(self):
return repr(self.__dict__)
def __str__(self):
return str(self.__dict__)
def object_cache(cache, user, module_type, module_id): def object_cache(cache, user, module_type, module_id):
# We don't look up on user -- all queries include user # We don't look up on user -- all queries include user
# Additional lookup would require a DB hit the way Django # Additional lookup would require a DB hit the way Django
...@@ -51,60 +60,19 @@ def make_track_function(request): ...@@ -51,60 +60,19 @@ def make_track_function(request):
return track.views.server_track(request, event_type, event, page='x_module') return track.views.server_track(request, event_type, event, page='x_module')
return f return f
def modx_dispatch(request, module=None, dispatch=None, id=None):
''' Generic view for extensions. '''
if not request.user.is_authenticated():
return redirect('/')
# Grab the student information for the module from the database
s = StudentModule.objects.filter(student=request.user,
module_id=id)
#s = StudentModule.get_with_caching(request.user, id)
if len(s) == 0 or s is None:
log.debug("Couldnt find module for user and id " + str(module) + " " + str(request.user) + " "+ str(id))
raise Http404
s = s[0]
oldgrade = s.grade
oldstate = s.state
dispatch=dispatch.split('?')[0]
ajax_url = '/modx/'+module+'/'+id+'/'
# Grab the XML corresponding to the request from course.xml
xml = content_parser.module_xml(request.user, module, 'id', id)
# Create the module
instance=courseware.modules.get_module_class(module)(xml,
id,
ajax_url=ajax_url,
state=oldstate,
track_function = make_track_function(request),
render_function = None)
# Let the module handle the AJAX
ajax_return=instance.handle_ajax(dispatch, request.POST)
# Save the state back to the database
s.state=instance.get_state()
if instance.get_score():
s.grade=instance.get_score()['score']
if s.grade != oldgrade or s.state != oldstate:
s.save()
# Return whatever the module wanted to return to the client/caller
return HttpResponse(ajax_return)
def grade_histogram(module_id): def grade_histogram(module_id):
''' Print out a histogram of grades on a given problem. ''' Print out a histogram of grades on a given problem.
Part of staff member debug info. Part of staff member debug info.
''' '''
from django.db import connection, transaction from django.db import connection
cursor = connection.cursor() cursor = connection.cursor()
cursor.execute("select courseware_studentmodule.grade,COUNT(courseware_studentmodule.student_id) from courseware_studentmodule where courseware_studentmodule.module_id=%s group by courseware_studentmodule.grade", [module_id]) cursor.execute("select courseware_studentmodule.grade,COUNT(courseware_studentmodule.student_id) from courseware_studentmodule where courseware_studentmodule.module_id=%s group by courseware_studentmodule.grade", [module_id])
grades = list(cursor.fetchall()) grades = list(cursor.fetchall())
print grades
grades.sort(key=lambda x:x[0]) # Probably not necessary grades.sort(key=lambda x:x[0]) # Probably not necessary
if (len(grades) == 1 and grades[0][0] == None):
return []
return grades return grades
def render_x_module(user, request, xml_module, module_object_preload): def render_x_module(user, request, xml_module, module_object_preload):
...@@ -125,31 +93,51 @@ def render_x_module(user, request, xml_module, module_object_preload): ...@@ -125,31 +93,51 @@ def render_x_module(user, request, xml_module, module_object_preload):
else: else:
state = smod.state state = smod.state
# get coursename if stored
if 'coursename' in request.session: coursename = request.session['coursename']
else: coursename = None
# Create a new instance # Create a new instance
ajax_url = '/modx/'+module_type+'/'+module_id+'/' ajax_url = settings.MITX_ROOT_URL + '/modx/'+module_type+'/'+module_id+'/'
instance=module_class(etree.tostring(xml_module), system = I4xSystem(track_function = make_track_function(request),
render_function = lambda x: render_module(user, request, x, module_object_preload),
ajax_url = ajax_url,
filestore = None
)
instance=module_class(system,
etree.tostring(xml_module),
module_id, module_id,
ajax_url=ajax_url, state=state)
state=state,
track_function = make_track_function(request),
render_function = lambda x: render_module(user, request, x, module_object_preload))
# If instance wasn't already in the database, create it # If instance wasn't already in the database, and this
if not smod: # isn't a guest user, create it
if not smod and user.is_authenticated():
smod=StudentModule(student=user, smod=StudentModule(student=user,
module_type = module_type, module_type = module_type,
module_id=module_id, module_id=module_id,
state=instance.get_state()) state=instance.get_state())
smod.save() smod.save()
module_object_preload.append(smod) module_object_preload.append(smod)
# Grab content # Grab content
content = instance.get_html() content = instance.get_html()
init_js = instance.get_init_js()
destory_js = instance.get_destroy_js()
# special extra information about each problem, only for users who are staff
if user.is_staff: if user.is_staff:
histogram = grade_histogram(module_id)
render_histogram = len(histogram) > 0
content=content+render_to_string("staff_problem_info.html", {'xml':etree.tostring(xml_module), content=content+render_to_string("staff_problem_info.html", {'xml':etree.tostring(xml_module),
'histogram':grade_histogram(module_id)}) 'module_id' : module_id,
'render_histogram' : render_histogram})
if render_histogram:
init_js = init_js+render_to_string("staff_problem_histogram.js", {'histogram' : histogram,
'module_id' : module_id})
content = {'content':content, content = {'content':content,
"destroy_js":instance.get_destroy_js(), "destroy_js":destory_js,
'init_js':instance.get_init_js(), 'init_js':init_js,
'type':module_type} 'type':module_type}
return content return content
......
...@@ -16,13 +16,12 @@ import traceback ...@@ -16,13 +16,12 @@ import traceback
from lxml import etree from lxml import etree
## TODO: Abstract out from Django ## TODO: Abstract out from Django
from django.conf import settings from mitxmako.shortcuts import render_to_string
from mitxmako.shortcuts import render_to_response, render_to_string
from django.http import Http404
from x_module import XModule from x_module import XModule
from courseware.capa.capa_problem import LoncapaProblem, StudentInputError from courseware.capa.capa_problem import LoncapaProblem, StudentInputError
import courseware.content_parser as content_parser import courseware.content_parser as content_parser
from multicourse import multicourse_settings
log = logging.getLogger("mitx.courseware") log = logging.getLogger("mitx.courseware")
...@@ -92,10 +91,13 @@ class Module(XModule): ...@@ -92,10 +91,13 @@ class Module(XModule):
# User submitted a problem, and hasn't reset. We don't want # User submitted a problem, and hasn't reset. We don't want
# more submissions. # more submissions.
if self.lcp.done and self.rerandomize == "always": if self.lcp.done and self.rerandomize == "always":
#print "!"
check_button = False check_button = False
save_button = False save_button = False
# Only show the reset button if pressing it will show different values
if self.rerandomize != 'always':
reset_button = False
# User hasn't submitted an answer yet -- we don't want resets # User hasn't submitted an answer yet -- we don't want resets
if not self.lcp.done: if not self.lcp.done:
reset_button = False reset_button = False
...@@ -114,25 +116,26 @@ class Module(XModule): ...@@ -114,25 +116,26 @@ class Module(XModule):
if len(explain) == 0: if len(explain) == 0:
explain = False explain = False
html=render_to_string('problem.html', context = {'problem' : content,
{'problem' : content, 'id' : self.item_id,
'id' : self.item_id, 'check_button' : check_button,
'check_button' : check_button, 'reset_button' : reset_button,
'reset_button' : reset_button, 'save_button' : save_button,
'save_button' : save_button, 'answer_available' : self.answer_available(),
'answer_available' : self.answer_available(), 'ajax_url' : self.ajax_url,
'ajax_url' : self.ajax_url, 'attempts_used': self.attempts,
'attempts_used': self.attempts, 'attempts_allowed': self.max_attempts,
'attempts_allowed': self.max_attempts, 'explain': explain,
'explain': explain }
})
html=render_to_string('problem.html', context)
if encapsulate: if encapsulate:
html = '<div id="main_{id}">'.format(id=self.item_id)+html+"</div>" html = '<div id="main_{id}">'.format(id=self.item_id)+html+"</div>"
return html return html
def __init__(self, xml, item_id, ajax_url=None, track_url=None, state=None, track_function=None, render_function = None, meta = None): def __init__(self, system, xml, item_id, state=None):
XModule.__init__(self, xml, item_id, ajax_url, track_url, state, track_function, render_function) XModule.__init__(self, system, xml, item_id, state)
self.attempts = 0 self.attempts = 0
self.max_attempts = None self.max_attempts = None
...@@ -185,17 +188,24 @@ class Module(XModule): ...@@ -185,17 +188,24 @@ class Module(XModule):
if state!=None and 'attempts' in state: if state!=None and 'attempts' in state:
self.attempts=state['attempts'] self.attempts=state['attempts']
self.filename=content_parser.item(dom2.xpath('/problem/@filename')) self.filename="problems/"+content_parser.item(dom2.xpath('/problem/@filename'))+".xml"
filename=settings.DATA_DIR+"/problems/"+self.filename+".xml"
self.name=content_parser.item(dom2.xpath('/problem/@name')) self.name=content_parser.item(dom2.xpath('/problem/@name'))
self.weight=content_parser.item(dom2.xpath('/problem/@weight')) self.weight=content_parser.item(dom2.xpath('/problem/@weight'))
if self.rerandomize == 'never': if self.rerandomize == 'never':
seed = 1 seed = 1
else: else:
seed = None seed = None
self.lcp=LoncapaProblem(filename, self.item_id, state, seed = seed) try:
fp = self.filestore.open(self.filename)
except Exception,err:
print '[courseware.capa.capa_module.Module.init] error %s: cannot open file %s' % (err,self.filename)
raise Exception,err
self.lcp=LoncapaProblem(fp, self.item_id, state, seed = seed)
def handle_ajax(self, dispatch, get): def handle_ajax(self, dispatch, get):
'''
This is called by courseware.module_render, to handle an AJAX call. "get" is request.POST
'''
if dispatch=='problem_get': if dispatch=='problem_get':
response = self.get_problem(get) response = self.get_problem(get)
elif False: #self.close_date > elif False: #self.close_date >
...@@ -241,17 +251,23 @@ class Module(XModule): ...@@ -241,17 +251,23 @@ class Module(XModule):
return True return True
if self.show_answer == 'closed' and not self.closed(): if self.show_answer == 'closed' and not self.closed():
return False return False
print "aa", self.show_answer if self.show_answer == 'always':
raise Http404 return True
raise self.system.exception404 #TODO: Not 404
def get_answer(self, get): def get_answer(self, get):
'''
For the "show answer" button.
TODO: show answer events should be logged here, not just in the problem.js
'''
if not self.answer_available(): if not self.answer_available():
raise Http404 raise self.system.exception404
else: else:
return json.dumps(self.lcp.get_question_answers(), answers = self.lcp.get_question_answers()
return json.dumps(answers,
cls=ComplexEncoder) cls=ComplexEncoder)
# Figure out if we should move these to capa_problem? # Figure out if we should move these to capa_problem?
def get_problem(self, get): def get_problem(self, get):
''' Same as get_problem_html -- if we want to reconfirm we ''' Same as get_problem_html -- if we want to reconfirm we
...@@ -265,66 +281,56 @@ class Module(XModule): ...@@ -265,66 +281,56 @@ class Module(XModule):
event_info['state'] = self.lcp.get_state() event_info['state'] = self.lcp.get_state()
event_info['filename'] = self.filename event_info['filename'] = self.filename
# make a dict of all the student responses ("answers").
answers=dict() answers=dict()
# input_resistor_1 ==> resistor_1 # input_resistor_1 ==> resistor_1
for key in get: for key in get:
answers['_'.join(key.split('_')[1:])]=get[key] answers['_'.join(key.split('_')[1:])]=get[key]
# print "XXX", answers, get
event_info['answers']=answers event_info['answers']=answers
# Too late. Cannot submit # Too late. Cannot submit
if self.closed(): if self.closed():
event_info['failure']='closed' event_info['failure']='closed'
self.tracker('save_problem_check_fail', event_info) self.tracker('save_problem_check_fail', event_info)
print "cp" raise self.system.exception404
raise Http404
# Problem submitted. Student should reset before checking # Problem submitted. Student should reset before checking
# again. # again.
if self.lcp.done and self.rerandomize == "always": if self.lcp.done and self.rerandomize == "always":
event_info['failure']='unreset' event_info['failure']='unreset'
self.tracker('save_problem_check_fail', event_info) self.tracker('save_problem_check_fail', event_info)
print "cpdr" raise self.system.exception404
raise Http404
try: try:
old_state = self.lcp.get_state() old_state = self.lcp.get_state()
lcp_id = self.lcp.problem_id lcp_id = self.lcp.problem_id
filename = self.lcp.filename
correct_map = self.lcp.grade_answers(answers) correct_map = self.lcp.grade_answers(answers)
except StudentInputError as inst: except StudentInputError as inst:
self.lcp = LoncapaProblem(filename, id=lcp_id, state=old_state) self.lcp = LoncapaProblem(self.filestore.open(self.filename), id=lcp_id, state=old_state)
traceback.print_exc() traceback.print_exc()
# print {'error':sys.exc_info(),
# 'answers':answers,
# 'seed':self.lcp.seed,
# 'filename':self.lcp.filename}
return json.dumps({'success':inst.message}) return json.dumps({'success':inst.message})
except: except:
self.lcp = LoncapaProblem(filename, id=lcp_id, state=old_state) self.lcp = LoncapaProblem(self.filestore.open(self.filename), id=lcp_id, state=old_state)
traceback.print_exc() traceback.print_exc()
raise Exception,"error in capa_module"
return json.dumps({'success':'Unknown Error'}) return json.dumps({'success':'Unknown Error'})
self.attempts = self.attempts + 1 self.attempts = self.attempts + 1
self.lcp.done=True self.lcp.done=True
success = 'correct' success = 'correct'
for i in correct_map: for i in correct_map:
if correct_map[i]!='correct': if correct_map[i]!='correct':
success = 'incorrect' success = 'incorrect'
js=json.dumps({'correct_map' : correct_map,
'success' : success})
event_info['correct_map']=correct_map event_info['correct_map']=correct_map
event_info['success']=success event_info['success']=success
self.tracker('save_problem_check', event_info) self.tracker('save_problem_check', event_info)
return js return json.dumps({'success': success,
'contents': self.get_problem_html(encapsulate=False)})
def save_problem(self, get): def save_problem(self, get):
event_info = dict() event_info = dict()
...@@ -382,8 +388,7 @@ class Module(XModule): ...@@ -382,8 +388,7 @@ class Module(XModule):
self.lcp.questions=dict() # Detailed info about questions in problem instance. TODO: Should be by id and not lid. self.lcp.questions=dict() # Detailed info about questions in problem instance. TODO: Should be by id and not lid.
self.lcp.seed=None self.lcp.seed=None
filename=settings.DATA_DIR+"problems/"+self.filename+".xml" self.lcp=LoncapaProblem(self.filestore.open(self.filename), self.item_id, self.lcp.get_state())
self.lcp=LoncapaProblem(filename, self.item_id, self.lcp.get_state())
event_info['new_state']=self.lcp.get_state() event_info['new_state']=self.lcp.get_state()
self.tracker('reset_problem', event_info) self.tracker('reset_problem', event_info)
......
import json import json
## TODO: Abstract out from Django
from django.conf import settings
from mitxmako.shortcuts import render_to_response, render_to_string from mitxmako.shortcuts import render_to_response, render_to_string
from x_module import XModule from x_module import XModule
...@@ -24,13 +22,13 @@ class Module(XModule): ...@@ -24,13 +22,13 @@ class Module(XModule):
textlist=[i for i in textlist if type(i)==str] textlist=[i for i in textlist if type(i)==str]
return "".join(textlist) return "".join(textlist)
try: try:
filename=settings.DATA_DIR+"html/"+self.filename filename="html/"+self.filename
return open(filename).read() return self.filestore.open(filename).read()
except: # For backwards compatibility. TODO: Remove except: # For backwards compatibility. TODO: Remove
return render_to_string(self.filename, {'id': self.item_id}) return render_to_string(self.filename, {'id': self.item_id})
def __init__(self, xml, item_id, ajax_url=None, track_url=None, state=None, track_function=None, render_function = None): def __init__(self, system, xml, item_id, state=None):
XModule.__init__(self, xml, item_id, ajax_url, track_url, state, track_function, render_function) XModule.__init__(self, system, xml, item_id, state)
xmltree=etree.fromstring(xml) xmltree=etree.fromstring(xml)
self.filename = None self.filename = None
filename_l=xmltree.xpath("/html/@filename") filename_l=xmltree.xpath("/html/@filename")
......
...@@ -19,6 +19,6 @@ class Module(XModule): ...@@ -19,6 +19,6 @@ class Module(XModule):
def get_html(self): def get_html(self):
return '<input type="hidden" class="schematic" name="{item_id}" height="480" width="640">'.format(item_id=self.item_id) return '<input type="hidden" class="schematic" name="{item_id}" height="480" width="640">'.format(item_id=self.item_id)
def __init__(self, xml, item_id, ajax_url=None, track_url=None, state=None, render_function = None): def __init__(self, system, xml, item_id, state=None):
XModule.__init__(self, xml, item_id, ajax_url, track_url, state, render_function) XModule.__init__(self, system, xml, item_id, state)
...@@ -2,10 +2,7 @@ import json ...@@ -2,10 +2,7 @@ import json
from lxml import etree from lxml import etree
## TODO: Abstract out from Django from mitxmako.shortcuts import render_to_string
from django.http import Http404
from django.conf import settings
from mitxmako.shortcuts import render_to_response, render_to_string
from x_module import XModule from x_module import XModule
...@@ -38,12 +35,10 @@ class Module(XModule): ...@@ -38,12 +35,10 @@ class Module(XModule):
return self.destroy_js return self.destroy_js
def handle_ajax(self, dispatch, get): def handle_ajax(self, dispatch, get):
print "GET", get
print "DISPATCH", dispatch
if dispatch=='goto_position': if dispatch=='goto_position':
self.position = int(get['position']) self.position = int(get['position'])
return json.dumps({'success':True}) return json.dumps({'success':True})
raise Http404() raise self.system.exception404
def render(self): def render(self):
if self.rendered: if self.rendered:
...@@ -107,14 +102,13 @@ class Module(XModule): ...@@ -107,14 +102,13 @@ class Module(XModule):
self.rendered = True self.rendered = True
def __init__(self, system, xml, item_id, state=None):
def __init__(self, xml, item_id, ajax_url=None, track_url=None, state=None, track_function=None, render_function = None): XModule.__init__(self, system, xml, item_id, state)
XModule.__init__(self, xml, item_id, ajax_url, track_url, state, track_function, render_function) self.xmltree = etree.fromstring(xml)
self.xmltree=etree.fromstring(xml)
self.position = 1 self.position = 1
if state!=None: if state != None:
state = json.loads(state) state = json.loads(state)
if 'position' in state: self.position = int(state['position']) if 'position' in state: self.position = int(state['position'])
......
...@@ -14,16 +14,16 @@ class Module(XModule): ...@@ -14,16 +14,16 @@ class Module(XModule):
@classmethod @classmethod
def get_xml_tags(c): def get_xml_tags(c):
## TODO: Abstract out from filesystem
tags = os.listdir(settings.DATA_DIR+'/custom_tags') tags = os.listdir(settings.DATA_DIR+'/custom_tags')
return tags return tags
def get_html(self): def get_html(self):
return self.html return self.html
def __init__(self, xml, item_id, ajax_url=None, track_url=None, state=None, track_function=None, render_function = None): def __init__(self, system, xml, item_id, state=None):
XModule.__init__(self, xml, item_id, ajax_url, track_url, state, track_function, render_function) XModule.__init__(self, system, xml, item_id, state)
xmltree = etree.fromstring(xml) xmltree = etree.fromstring(xml)
filename = xmltree.tag filename = xmltree.tag
params = dict(xmltree.items()) params = dict(xmltree.items())
# print params
self.html = render_to_string(filename, params, namespace = 'custom_tags') self.html = render_to_string(filename, params, namespace = 'custom_tags')
import json import json
## TODO: Abstract out from Django
from django.conf import settings
from mitxmako.shortcuts import render_to_response, render_to_string from mitxmako.shortcuts import render_to_response, render_to_string
from x_module import XModule from x_module import XModule
...@@ -26,8 +24,9 @@ class Module(XModule): ...@@ -26,8 +24,9 @@ class Module(XModule):
def get_destroy_js(self): def get_destroy_js(self):
return self.destroy_js_text return self.destroy_js_text
def __init__(self, xml, item_id, ajax_url=None, track_url=None, state=None, track_function=None, render_function = None):
XModule.__init__(self, xml, item_id, ajax_url, track_url, state, track_function, render_function) def __init__(self, system, xml, item_id, state=None):
XModule.__init__(self, system, xml, item_id, state)
xmltree=etree.fromstring(xml) xmltree=etree.fromstring(xml)
self.contents=[(e.get("name"),self.render_function(e)) \ self.contents=[(e.get("name"),self.render_function(e)) \
for e in xmltree] for e in xmltree]
......
...@@ -3,8 +3,6 @@ import logging ...@@ -3,8 +3,6 @@ import logging
from lxml import etree from lxml import etree
## TODO: Abstract out from Django
from django.conf import settings
from mitxmako.shortcuts import render_to_response, render_to_string from mitxmako.shortcuts import render_to_response, render_to_string
from x_module import XModule from x_module import XModule
...@@ -42,7 +40,8 @@ class Module(XModule): ...@@ -42,7 +40,8 @@ class Module(XModule):
return render_to_string('video.html',{'streams':self.video_list(), return render_to_string('video.html',{'streams':self.video_list(),
'id':self.item_id, 'id':self.item_id,
'position':self.position, 'position':self.position,
'name':self.name}) 'name':self.name,
'annotations':self.annotations})
def get_init_js(self): def get_init_js(self):
'''JavaScript code to be run when problem is shown. Be aware '''JavaScript code to be run when problem is shown. Be aware
...@@ -52,19 +51,23 @@ class Module(XModule): ...@@ -52,19 +51,23 @@ class Module(XModule):
log.debug(u"INIT POSITION {0}".format(self.position)) log.debug(u"INIT POSITION {0}".format(self.position))
return render_to_string('video_init.js',{'streams':self.video_list(), return render_to_string('video_init.js',{'streams':self.video_list(),
'id':self.item_id, 'id':self.item_id,
'position':self.position}) 'position':self.position})+self.annotations_init
def get_destroy_js(self): def get_destroy_js(self):
return "videoDestroy(\"{0}\");".format(self.item_id) return "videoDestroy(\"{0}\");".format(self.item_id)+self.annotations_destroy
def __init__(self, xml, item_id, ajax_url=None, track_url=None, state=None, track_function=None, render_function = None): def __init__(self, system, xml, item_id, state=None):
XModule.__init__(self, xml, item_id, ajax_url, track_url, state, track_function, render_function) XModule.__init__(self, system, xml, item_id, state)
self.youtube = etree.XML(xml).get('youtube') xmltree=etree.fromstring(xml)
self.name = etree.XML(xml).get('name') self.youtube = xmltree.get('youtube')
self.name = xmltree.get('name')
self.position = 0 self.position = 0
if state != None: if state != None:
state = json.loads(state) state = json.loads(state)
if 'position' in state: if 'position' in state:
self.position = int(float(state['position'])) self.position = int(float(state['position']))
#log.debug("POSITION IN STATE")
#log.debug(u"LOAD POSITION {0}".format(self.position)) self.annotations=[(e.get("name"),self.render_function(e)) \
for e in xmltree]
self.annotations_init="".join([e[1]['init_js'] for e in self.annotations if 'init_js' in e[1]])
self.annotations_destroy="".join([e[1]['destroy_js'] for e in self.annotations if 'destroy_js' in e[1]])
...@@ -45,13 +45,17 @@ class XModule(object): ...@@ -45,13 +45,17 @@ class XModule(object):
get is a dictionary-like object ''' get is a dictionary-like object '''
return "" return ""
def __init__(self, xml, item_id, ajax_url=None, track_url=None, state=None, track_function=None, render_function = None): def __init__(self, system, xml, item_id, track_url=None, state=None):
''' In most cases, you must pass state or xml''' ''' In most cases, you must pass state or xml'''
self.xml = xml self.xml = xml
self.item_id = item_id self.item_id = item_id
self.ajax_url = ajax_url
self.track_url = track_url
self.state = state self.state = state
self.tracker = track_function
self.render_function = render_function
if system:
## These are temporary; we really should go
## through self.system.
self.ajax_url = system.ajax_url
self.tracker = system.track_function
self.filestore = system.filestore
self.render_function = system.render_function
self.system = system
<problem>
<text><p>
Two skiers are on frictionless black diamond ski slopes.
Hello</p></text>
<imageresponse max="1" loncapaid="11">
<imageinput src="/static/Physics801/Figures/Skier-conservation of energy.jpg" width="560" height="388" rectangle="(490,11)-(556,98)"/>
<text>Click on the image where the top skier will stop momentarily if the top skier starts from rest.</text>
<imageinput src="/static/Physics801/Figures/Skier-conservation of energy.jpg" width="560" height="388" rectangle="(242,202)-(296,276)"/>
<text>Click on the image where the lower skier will stop momentarily if the lower skier starts from rest.</text>
<hintgroup showoncorrect="no">
<text><p>Use conservation of energy.</p></text>
</hintgroup>
</imageresponse>
</problem>
\ No newline at end of file
<problem>
<multiplechoiceresponse>
<choicegroup>
<choice correct="false" >
<startouttext />This is foil One.<endouttext />
</choice>
<choice correct="false" >
<startouttext />This is foil Two.<endouttext />
</choice>
<choice correct="true" >
<startouttext />This is foil Three.<endouttext />
</choice>
<choice correct="false">
<startouttext />This is foil Four.<endouttext />
</choice>
<choice correct="false">
<startouttext />This is foil Five.<endouttext />
</choice>
</choicegroup>
</multiplechoiceresponse>
</problem>
<problem>
<multiplechoiceresponse>
<choicegroup>
<choice correct="false" name="foil1">
<startouttext />This is foil One.<endouttext />
</choice>
<choice correct="false" name="foil2">
<startouttext />This is foil Two.<endouttext />
</choice>
<choice correct="true" name="foil3">
<startouttext />This is foil Three.<endouttext />
</choice>
<choice correct="false" name="foil4">
<startouttext />This is foil Four.<endouttext />
</choice>
<choice correct="false" name="foil5">
<startouttext />This is foil Five.<endouttext />
</choice>
</choicegroup>
</multiplechoiceresponse>
</problem>
<problem>
<text>
<p>
Why do bicycles benefit from having larger wheels when going up a bump as shown in the picture? <br/>
Assume that for both bicycles:<br/>
1.) The tires have equal air pressure.<br/>
2.) The bicycles never leave the contact with the bump.<br/>
3.) The bicycles have the same mass. The bicycle tires (regardless of size) have the same mass.<br/>
</p>
</text>
<optionresponse texlayout="horizontal" max="10" randomize="yes">
<ul>
<li>
<text>
<p>The bicycles with larger wheels have more time to go over the bump. This decreases the magnitude of the force needed to lift the bicycle.</p>
</text>
<optioninput name="Foil1" location="random" options="('True','False')" correct="True">
</optioninput>
</li>
<li>
<text>
<p>The bicycles with larger wheels always have a smaller vertical displacement regardless of speed.</p>
</text>
<optioninput name="Foil2" location="random" options="('True','False')" correct="False">
</optioninput>
</li>
<li>
<text>
<p>The bicycles with larger wheels experience a force backward with less magnitude for the same amount of time.</p>
</text>
<optioninput name="Foil3" location="random" options="('True','False')" correct="False">
</optioninput>
</li>
<li>
<text>
<p>The bicycles with larger wheels experience a force backward with less magnitude for a greater amount of time.</p>
</text>
<optioninput name="Foil4" location="random" options="('True','False')" correct="True">
</optioninput>
</li>
<li>
<text>
<p>The bicycles with larger wheels have more kinetic energy turned into gravitational potential energy.</p>
</text>
<optioninput name="Foil5" location="random" options="('True','False')" correct="False">
</optioninput>
</li>
<li>
<text>
<p>The bicycles with larger wheels have more rotational kinetic energy, so the horizontal velocity of the biker changes less.</p>
</text>
<optioninput name="Foil6" location="random" options="('True','False')" correct="False">
</optioninput>
</li>
</ul>
<hintgroup showoncorrect="no">
<text>
<br/>
<br/>
</text>
</hintgroup>
</optionresponse>
</problem>
<problem>
<truefalseresponse max="10" randomize="yes">
<choicegroup>
<choice location="random" correct="true" name="foil1">
<startouttext />This is foil One.<endouttext />
</choice>
<choice location="random" correct="true" name="foil2">
<startouttext />This is foil Two.<endouttext />
</choice>
<choice location="random" correct="false" name="foil3">
<startouttext />This is foil Three.<endouttext />
</choice>
<choice location="random" correct="false" name="foil4">
<startouttext />This is foil Four.<endouttext />
</choice>
<choice location="random" correct="false" name="foil5">
<startouttext />This is foil Five.<endouttext />
</choice>
</choicegroup>
</truefalseresponse>
</problem>
import unittest import unittest
import os
import numpy import numpy
import courseware.modules import courseware.modules
import courseware.capa.calc as calc import courseware.capa.calc as calc
from grades import Score, aggregate_scores import courseware.capa.capa_problem as lcp
import courseware.graders as graders
from courseware.graders import Score, CourseGrader, WeightedSubsectionsGrader, SingleSectionGrader, AssignmentFormatGrader
from courseware.grades import aggregate_scores
class ModelsTest(unittest.TestCase): class ModelsTest(unittest.TestCase):
def setUp(self): def setUp(self):
...@@ -33,6 +37,11 @@ class ModelsTest(unittest.TestCase): ...@@ -33,6 +37,11 @@ class ModelsTest(unittest.TestCase):
self.assertTrue(abs(calc.evaluator(variables, functions, "k*T/q-0.025"))<0.001) self.assertTrue(abs(calc.evaluator(variables, functions, "k*T/q-0.025"))<0.001)
self.assertTrue(abs(calc.evaluator(variables, functions, "e^(j*pi)")+1)<0.00001) self.assertTrue(abs(calc.evaluator(variables, functions, "e^(j*pi)")+1)<0.00001)
self.assertTrue(abs(calc.evaluator(variables, functions, "j||1")-0.5-0.5j)<0.00001) self.assertTrue(abs(calc.evaluator(variables, functions, "j||1")-0.5-0.5j)<0.00001)
variables['t'] = 1.0
self.assertTrue(abs(calc.evaluator(variables, functions, "t")-1.0)<0.00001)
self.assertTrue(abs(calc.evaluator(variables, functions, "T")-1.0)<0.00001)
self.assertTrue(abs(calc.evaluator(variables, functions, "t", cs=True)-1.0)<0.00001)
self.assertTrue(abs(calc.evaluator(variables, functions, "T", cs=True)-298)<0.2)
exception_happened = False exception_happened = False
try: try:
calc.evaluator({},{}, "5+7 QWSEKO") calc.evaluator({},{}, "5+7 QWSEKO")
...@@ -54,42 +63,284 @@ class ModelsTest(unittest.TestCase): ...@@ -54,42 +63,284 @@ class ModelsTest(unittest.TestCase):
exception_happened = True exception_happened = True
self.assertTrue(exception_happened) self.assertTrue(exception_happened)
class GraderTest(unittest.TestCase): #-----------------------------------------------------------------------------
# tests of capa_problem inputtypes
class MultiChoiceTest(unittest.TestCase):
def test_MC_grade(self):
multichoice_file = os.path.dirname(__file__)+"/test_files/multichoice.xml"
test_lcp = lcp.LoncapaProblem(open(multichoice_file), '1')
correct_answers = {'1_2_1':'choice_foil3'}
self.assertEquals(test_lcp.grade_answers(correct_answers)['1_2_1'], 'correct')
false_answers = {'1_2_1':'choice_foil2'}
self.assertEquals(test_lcp.grade_answers(false_answers)['1_2_1'], 'incorrect')
def test_MC_bare_grades(self):
multichoice_file = os.path.dirname(__file__)+"/test_files/multi_bare.xml"
test_lcp = lcp.LoncapaProblem(open(multichoice_file), '1')
correct_answers = {'1_2_1':'choice_2'}
self.assertEquals(test_lcp.grade_answers(correct_answers)['1_2_1'], 'correct')
false_answers = {'1_2_1':'choice_1'}
self.assertEquals(test_lcp.grade_answers(false_answers)['1_2_1'], 'incorrect')
def test_TF_grade(self):
truefalse_file = os.getcwd()+"/djangoapps/courseware/test_files/truefalse.xml"
test_lcp = lcp.LoncapaProblem(open(truefalse_file), '1')
correct_answers = {'1_2_1':['choice_foil2', 'choice_foil1']}
self.assertEquals(test_lcp.grade_answers(correct_answers)['1_2_1'], 'correct')
false_answers = {'1_2_1':['choice_foil1']}
self.assertEquals(test_lcp.grade_answers(false_answers)['1_2_1'], 'incorrect')
false_answers = {'1_2_1':['choice_foil1', 'choice_foil3']}
self.assertEquals(test_lcp.grade_answers(false_answers)['1_2_1'], 'incorrect')
false_answers = {'1_2_1':['choice_foil3']}
self.assertEquals(test_lcp.grade_answers(false_answers)['1_2_1'], 'incorrect')
false_answers = {'1_2_1':['choice_foil1', 'choice_foil2', 'choice_foil3']}
self.assertEquals(test_lcp.grade_answers(false_answers)['1_2_1'], 'incorrect')
class ImageResponseTest(unittest.TestCase):
def test_ir_grade(self):
imageresponse_file = os.path.dirname(__file__)+"/test_files/imageresponse.xml"
test_lcp = lcp.LoncapaProblem(open(imageresponse_file), '1')
correct_answers = {'1_2_1':'(490,11)-(556,98)',
'1_2_2':'(242,202)-(296,276)'}
test_answers = {'1_2_1':'[500,20]',
'1_2_2':'[250,300]',
}
self.assertEquals(test_lcp.grade_answers(test_answers)['1_2_1'], 'correct')
self.assertEquals(test_lcp.grade_answers(test_answers)['1_2_2'], 'incorrect')
class OptionResponseTest(unittest.TestCase):
'''
Run this with
python manage.py test courseware.OptionResponseTest
'''
def test_or_grade(self):
optionresponse_file = os.path.dirname(__file__)+"/test_files/optionresponse.xml"
test_lcp = lcp.LoncapaProblem(open(optionresponse_file), '1')
correct_answers = {'1_2_1':'True',
'1_2_2':'False'}
test_answers = {'1_2_1':'True',
'1_2_2':'True',
}
self.assertEquals(test_lcp.grade_answers(test_answers)['1_2_1'], 'correct')
self.assertEquals(test_lcp.grade_answers(test_answers)['1_2_2'], 'incorrect')
#-----------------------------------------------------------------------------
# Grading tests
class GradesheetTest(unittest.TestCase):
def test_weighted_grading(self): def test_weighted_grading(self):
scores = [] scores = []
Score.__sub__=lambda me, other: (me.earned - other.earned) + (me.possible - other.possible) Score.__sub__=lambda me, other: (me.earned - other.earned) + (me.possible - other.possible)
all, graded = aggregate_scores(scores) all, graded = aggregate_scores(scores)
self.assertEqual(all, Score(earned=0, possible=0, weight=1, graded=False, section="summary")) self.assertEqual(all, Score(earned=0, possible=0, graded=False, section="summary"))
self.assertEqual(graded, Score(earned=0, possible=0, weight=1, graded=True, section="summary")) self.assertEqual(graded, Score(earned=0, possible=0, graded=True, section="summary"))
scores.append(Score(earned=0, possible=5, weight=1, graded=False, section="summary")) scores.append(Score(earned=0, possible=5, graded=False, section="summary"))
all, graded = aggregate_scores(scores) all, graded = aggregate_scores(scores)
self.assertEqual(all, Score(earned=0, possible=1, weight=1, graded=False, section="summary")) self.assertEqual(all, Score(earned=0, possible=5, graded=False, section="summary"))
self.assertEqual(graded, Score(earned=0, possible=0, weight=1, graded=True, section="summary")) self.assertEqual(graded, Score(earned=0, possible=0, graded=True, section="summary"))
scores.append(Score(earned=3, possible=5, weight=1, graded=True, section="summary")) scores.append(Score(earned=3, possible=5, graded=True, section="summary"))
all, graded = aggregate_scores(scores) all, graded = aggregate_scores(scores)
self.assertAlmostEqual(all, Score(earned=3.0/5, possible=2, weight=1, graded=False, section="summary")) self.assertAlmostEqual(all, Score(earned=3, possible=10, graded=False, section="summary"))
self.assertAlmostEqual(graded, Score(earned=3.0/5, possible=1, weight=1, graded=True, section="summary")) self.assertAlmostEqual(graded, Score(earned=3, possible=5, graded=True, section="summary"))
scores.append(Score(earned=2, possible=5, weight=2, graded=True, section="summary")) scores.append(Score(earned=2, possible=5, graded=True, section="summary"))
all, graded = aggregate_scores(scores) all, graded = aggregate_scores(scores)
self.assertAlmostEqual(all, Score(earned=7.0/5, possible=4, weight=1, graded=False, section="summary")) self.assertAlmostEqual(all, Score(earned=5, possible=15, graded=False, section="summary"))
self.assertAlmostEqual(graded, Score(earned=7.0/5, possible=3, weight=1, graded=True, section="summary")) self.assertAlmostEqual(graded, Score(earned=5, possible=10, graded=True, section="summary"))
scores.append(Score(earned=2, possible=5, weight=0, graded=True, section="summary")) class GraderTest(unittest.TestCase):
all, graded = aggregate_scores(scores)
self.assertAlmostEqual(all, Score(earned=7.0/5, possible=4, weight=1, graded=False, section="summary"))
self.assertAlmostEqual(graded, Score(earned=7.0/5, possible=3, weight=1, graded=True, section="summary"))
scores.append(Score(earned=2, possible=5, weight=3, graded=False, section="summary")) empty_gradesheet = {
all, graded = aggregate_scores(scores) }
self.assertAlmostEqual(all, Score(earned=13.0/5, possible=7, weight=1, graded=False, section="summary"))
self.assertAlmostEqual(graded, Score(earned=7.0/5, possible=3, weight=1, graded=True, section="summary")) incomplete_gradesheet = {
'Homework': [],
'Lab': [],
'Midterm' : [],
}
test_gradesheet = {
'Homework': [Score(earned=2, possible=20.0, graded=True, section='hw1'),
Score(earned=16, possible=16.0, graded=True, section='hw2')],
#The dropped scores should be from the assignments that don't exist yet
'Lab': [Score(earned=1, possible=2.0, graded=True, section='lab1'), #Dropped
Score(earned=1, possible=1.0, graded=True, section='lab2'),
Score(earned=1, possible=1.0, graded=True, section='lab3'),
Score(earned=5, possible=25.0, graded=True, section='lab4'), #Dropped
Score(earned=3, possible=4.0, graded=True, section='lab5'), #Dropped
Score(earned=6, possible=7.0, graded=True, section='lab6'),
Score(earned=5, possible=6.0, graded=True, section='lab7')],
'Midterm' : [Score(earned=50.5, possible=100, graded=True, section="Midterm Exam"),],
}
def test_SingleSectionGrader(self):
midtermGrader = graders.SingleSectionGrader("Midterm", "Midterm Exam")
lab4Grader = graders.SingleSectionGrader("Lab", "lab4")
badLabGrader = graders.SingleSectionGrader("Lab", "lab42")
for graded in [midtermGrader.grade(self.empty_gradesheet),
midtermGrader.grade(self.incomplete_gradesheet),
badLabGrader.grade(self.test_gradesheet)]:
self.assertEqual( len(graded['section_breakdown']), 1 )
self.assertEqual( graded['percent'], 0.0 )
graded = midtermGrader.grade(self.test_gradesheet)
self.assertAlmostEqual( graded['percent'], 0.505 )
self.assertEqual( len(graded['section_breakdown']), 1 )
graded = lab4Grader.grade(self.test_gradesheet)
self.assertAlmostEqual( graded['percent'], 0.2 )
self.assertEqual( len(graded['section_breakdown']), 1 )
def test_AssignmentFormatGrader(self):
homeworkGrader = graders.AssignmentFormatGrader("Homework", 12, 2)
noDropGrader = graders.AssignmentFormatGrader("Homework", 12, 0)
#Even though the minimum number is 3, this should grade correctly when 7 assignments are found
overflowGrader = graders.AssignmentFormatGrader("Lab", 3, 2)
labGrader = graders.AssignmentFormatGrader("Lab", 7, 3)
#Test the grading of an empty gradesheet
for graded in [ homeworkGrader.grade(self.empty_gradesheet),
noDropGrader.grade(self.empty_gradesheet),
homeworkGrader.grade(self.incomplete_gradesheet),
noDropGrader.grade(self.incomplete_gradesheet) ]:
self.assertAlmostEqual( graded['percent'], 0.0 )
#Make sure the breakdown includes 12 sections, plus one summary
self.assertEqual( len(graded['section_breakdown']), 12 + 1 )
graded = homeworkGrader.grade(self.test_gradesheet)
self.assertAlmostEqual( graded['percent'], 0.11 ) # 100% + 10% / 10 assignments
self.assertEqual( len(graded['section_breakdown']), 12 + 1 )
graded = noDropGrader.grade(self.test_gradesheet)
self.assertAlmostEqual( graded['percent'], 0.0916666666666666 ) # 100% + 10% / 12 assignments
self.assertEqual( len(graded['section_breakdown']), 12 + 1 )
graded = overflowGrader.grade(self.test_gradesheet)
self.assertAlmostEqual( graded['percent'], 0.8880952380952382 ) # 100% + 10% / 5 assignments
self.assertEqual( len(graded['section_breakdown']), 7 + 1 )
graded = labGrader.grade(self.test_gradesheet)
self.assertAlmostEqual( graded['percent'], 0.9226190476190477 )
self.assertEqual( len(graded['section_breakdown']), 7 + 1 )
def test_WeightedSubsectionsGrader(self):
#First, a few sub graders
homeworkGrader = graders.AssignmentFormatGrader("Homework", 12, 2)
labGrader = graders.AssignmentFormatGrader("Lab", 7, 3)
midtermGrader = graders.SingleSectionGrader("Midterm", "Midterm Exam")
weightedGrader = graders.WeightedSubsectionsGrader( [(homeworkGrader, homeworkGrader.category, 0.25), (labGrader, labGrader.category, 0.25),
(midtermGrader, midtermGrader.category, 0.5)] )
overOneWeightsGrader = graders.WeightedSubsectionsGrader( [(homeworkGrader, homeworkGrader.category, 0.5), (labGrader, labGrader.category, 0.5),
(midtermGrader, midtermGrader.category, 0.5)] )
#The midterm should have all weight on this one
zeroWeightsGrader = graders.WeightedSubsectionsGrader( [(homeworkGrader, homeworkGrader.category, 0.0), (labGrader, labGrader.category, 0.0),
(midtermGrader, midtermGrader.category, 0.5)] )
#This should always have a final percent of zero
allZeroWeightsGrader = graders.WeightedSubsectionsGrader( [(homeworkGrader, homeworkGrader.category, 0.0), (labGrader, labGrader.category, 0.0),
(midtermGrader, midtermGrader.category, 0.0)] )
emptyGrader = graders.WeightedSubsectionsGrader( [] )
graded = weightedGrader.grade(self.test_gradesheet)
self.assertAlmostEqual( graded['percent'], 0.5106547619047619 )
self.assertEqual( len(graded['section_breakdown']), (12 + 1) + (7+1) + 1 )
self.assertEqual( len(graded['grade_breakdown']), 3 )
graded = overOneWeightsGrader.grade(self.test_gradesheet)
self.assertAlmostEqual( graded['percent'], 0.7688095238095238 )
self.assertEqual( len(graded['section_breakdown']), (12 + 1) + (7+1) + 1 )
self.assertEqual( len(graded['grade_breakdown']), 3 )
graded = zeroWeightsGrader.grade(self.test_gradesheet)
self.assertAlmostEqual( graded['percent'], 0.2525 )
self.assertEqual( len(graded['section_breakdown']), (12 + 1) + (7+1) + 1 )
self.assertEqual( len(graded['grade_breakdown']), 3 )
graded = allZeroWeightsGrader.grade(self.test_gradesheet)
self.assertAlmostEqual( graded['percent'], 0.0 )
self.assertEqual( len(graded['section_breakdown']), (12 + 1) + (7+1) + 1 )
self.assertEqual( len(graded['grade_breakdown']), 3 )
for graded in [ weightedGrader.grade(self.empty_gradesheet),
weightedGrader.grade(self.incomplete_gradesheet),
zeroWeightsGrader.grade(self.empty_gradesheet),
allZeroWeightsGrader.grade(self.empty_gradesheet)]:
self.assertAlmostEqual( graded['percent'], 0.0 )
self.assertEqual( len(graded['section_breakdown']), (12 + 1) + (7+1) + 1 )
self.assertEqual( len(graded['grade_breakdown']), 3 )
graded = emptyGrader.grade(self.test_gradesheet)
self.assertAlmostEqual( graded['percent'], 0.0 )
self.assertEqual( len(graded['section_breakdown']), 0 )
self.assertEqual( len(graded['grade_breakdown']), 0 )
def test_graderFromConf(self):
#Confs always produce a graders.WeightedSubsectionsGrader, so we test this by repeating the test
#in test_graders.WeightedSubsectionsGrader, but generate the graders with confs.
weightedGrader = graders.grader_from_conf([
{
'type' : "Homework",
'min_count' : 12,
'drop_count' : 2,
'short_label' : "HW",
'weight' : 0.25,
},
{
'type' : "Lab",
'min_count' : 7,
'drop_count' : 3,
'category' : "Labs",
'weight' : 0.25
},
{
'type' : "Midterm",
'name' : "Midterm Exam",
'short_label' : "Midterm",
'weight' : 0.5,
},
])
emptyGrader = graders.grader_from_conf([])
graded = weightedGrader.grade(self.test_gradesheet)
self.assertAlmostEqual( graded['percent'], 0.5106547619047619 )
self.assertEqual( len(graded['section_breakdown']), (12 + 1) + (7+1) + 1 )
self.assertEqual( len(graded['grade_breakdown']), 3 )
graded = emptyGrader.grade(self.test_gradesheet)
self.assertAlmostEqual( graded['percent'], 0.0 )
self.assertEqual( len(graded['section_breakdown']), 0 )
self.assertEqual( len(graded['grade_breakdown']), 0 )
#Test that graders can also be used instead of lists of dictionaries
homeworkGrader = graders.AssignmentFormatGrader("Homework", 12, 2)
homeworkGrader2 = graders.grader_from_conf(homeworkGrader)
graded = homeworkGrader2.grade(self.test_gradesheet)
self.assertAlmostEqual( graded['percent'], 0.11 )
self.assertEqual( len(graded['section_breakdown']), 12 + 1 )
#TODO: How do we test failure cases? The parser only logs an error when it can't parse something. Maybe it should throw exceptions?
scores.append(Score(earned=2, possible=5, weight=.5, graded=True, section="summary"))
all, graded = aggregate_scores(scores)
self.assertAlmostEqual(all, Score(earned=14.0/5, possible=7.5, weight=1, graded=False, section="summary"))
self.assertAlmostEqual(graded, Score(earned=8.0/5, possible=3.5, weight=1, graded=True, section="summary"))
import json
import logging import logging
import os
import random
import sys
import StringIO
import urllib import urllib
import uuid
from django.conf import settings from django.conf import settings
from django.core.context_processors import csrf from django.core.context_processors import csrf
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.http import HttpResponse, Http404 from django.contrib.auth.decorators import login_required
from django.http import Http404, HttpResponse
from django.shortcuts import redirect from django.shortcuts import redirect
from django.template import Context, loader
from mitxmako.shortcuts import render_to_response, render_to_string from mitxmako.shortcuts import render_to_response, render_to_string
#from django.views.decorators.csrf import ensure_csrf_cookie #from django.views.decorators.csrf import ensure_csrf_cookie
from django.db import connection
from django.views.decorators.cache import cache_control from django.views.decorators.cache import cache_control
from lxml import etree from lxml import etree
from module_render import render_module, modx_dispatch from module_render import render_module, make_track_function, I4xSystem
from models import StudentModule from models import StudentModule
from student.models import UserProfile from student.models import UserProfile
from multicourse import multicourse_settings
import courseware.content_parser as content_parser import courseware.content_parser as content_parser
import courseware.modules.capa_module import courseware.modules
import courseware.grades as grades import courseware.grades as grades
...@@ -40,22 +34,26 @@ template_imports={'urllib':urllib} ...@@ -40,22 +34,26 @@ template_imports={'urllib':urllib}
def gradebook(request): def gradebook(request):
if 'course_admin' not in content_parser.user_groups(request.user): if 'course_admin' not in content_parser.user_groups(request.user):
raise Http404 raise Http404
# TODO: This should be abstracted out. We repeat this logic many times.
if 'coursename' in request.session: coursename = request.session['coursename']
else: coursename = None
student_objects = User.objects.all()[:100] student_objects = User.objects.all()[:100]
student_info = [{'username' :s.username, student_info = [{'username' :s.username,
'id' : s.id, 'id' : s.id,
'email': s.email, 'email': s.email,
'grade_info' : grades.grade_sheet(s), 'grade_info' : grades.grade_sheet(s,coursename),
'realname' : UserProfile.objects.get(user = s).name 'realname' : UserProfile.objects.get(user = s).name
} for s in student_objects] } for s in student_objects]
return render_to_response('gradebook.html',{'students':student_info}) return render_to_response('gradebook.html',{'students':student_info})
@login_required
@cache_control(no_cache=True, no_store=True, must_revalidate=True) @cache_control(no_cache=True, no_store=True, must_revalidate=True)
def profile(request, student_id = None): def profile(request, student_id = None):
''' User profile. Show username, location, etc, as well as grades . ''' User profile. Show username, location, etc, as well as grades .
We need to allow the user to change some of these settings .''' We need to allow the user to change some of these settings .'''
if not request.user.is_authenticated():
return redirect('/')
if student_id == None: if student_id == None:
student = request.user student = request.user
...@@ -67,6 +65,9 @@ def profile(request, student_id = None): ...@@ -67,6 +65,9 @@ def profile(request, student_id = None):
user_info = UserProfile.objects.get(user=student) # request.user.profile_cache # user_info = UserProfile.objects.get(user=student) # request.user.profile_cache #
if 'coursename' in request.session: coursename = request.session['coursename']
else: coursename = None
context={'name':user_info.name, context={'name':user_info.name,
'username':student.username, 'username':student.username,
'location':user_info.location, 'location':user_info.location,
...@@ -75,7 +76,7 @@ def profile(request, student_id = None): ...@@ -75,7 +76,7 @@ def profile(request, student_id = None):
'format_url_params' : content_parser.format_url_params, 'format_url_params' : content_parser.format_url_params,
'csrf':csrf(request)['csrf_token'] 'csrf':csrf(request)['csrf_token']
} }
context.update(grades.grade_sheet(student)) context.update(grades.grade_sheet(student,coursename))
return render_to_response('profile.html', context) return render_to_response('profile.html', context)
...@@ -84,8 +85,8 @@ def render_accordion(request,course,chapter,section): ...@@ -84,8 +85,8 @@ def render_accordion(request,course,chapter,section):
parameter. Returns (initialization_javascript, content)''' parameter. Returns (initialization_javascript, content)'''
if not course: if not course:
course = "6.002 Spring 2012" course = "6.002 Spring 2012"
toc=content_parser.toc_from_xml(content_parser.course_file(request.user), chapter, section) toc=content_parser.toc_from_xml(content_parser.course_file(request.user,course), chapter, section)
active_chapter=1 active_chapter=1
for i in range(len(toc)): for i in range(len(toc)):
if toc[i]['active']: if toc[i]['active']:
...@@ -96,19 +97,21 @@ def render_accordion(request,course,chapter,section): ...@@ -96,19 +97,21 @@ def render_accordion(request,course,chapter,section):
['format_url_params',content_parser.format_url_params], ['format_url_params',content_parser.format_url_params],
['csrf',csrf(request)['csrf_token']]] + \ ['csrf',csrf(request)['csrf_token']]] + \
template_imports.items()) template_imports.items())
return {'init_js':render_to_string('accordion_init.js',context), return render_to_string('accordion.html',context)
'content':render_to_string('accordion.html',context)}
@cache_control(no_cache=True, no_store=True, must_revalidate=True) @cache_control(no_cache=True, no_store=True, must_revalidate=True)
def render_section(request, section): def render_section(request, section):
''' TODO: Consolidate with index ''' TODO: Consolidate with index
''' '''
user = request.user user = request.user
if not settings.COURSEWARE_ENABLED or not user.is_authenticated(): if not settings.COURSEWARE_ENABLED:
return redirect('/') return redirect('/')
if 'coursename' in request.session: coursename = request.session['coursename']
else: coursename = None
# try: # try:
dom = content_parser.section_file(user, section) dom = content_parser.section_file(user, section, coursename)
#except: #except:
# raise Http404 # raise Http404
...@@ -116,16 +119,19 @@ def render_section(request, section): ...@@ -116,16 +119,19 @@ def render_section(request, section):
module_ids = dom.xpath("//@id") module_ids = dom.xpath("//@id")
module_object_preload = list(StudentModule.objects.filter(student=user, if user.is_authenticated():
module_id__in=module_ids)) module_object_preload = list(StudentModule.objects.filter(student=user,
module_id__in=module_ids))
else:
module_object_preload = []
module=render_module(user, request, dom, module_object_preload) module=render_module(user, request, dom, module_object_preload)
if 'init_js' not in module: if 'init_js' not in module:
module['init_js']='' module['init_js']=''
context={'init':accordion['init_js']+module['init_js'], context={'init':module['init_js'],
'accordion':accordion['content'], 'accordion':accordion,
'content':module['content'], 'content':module['content'],
'csrf':csrf(request)['csrf_token']} 'csrf':csrf(request)['csrf_token']}
...@@ -134,13 +140,21 @@ def render_section(request, section): ...@@ -134,13 +140,21 @@ def render_section(request, section):
@cache_control(no_cache=True, no_store=True, must_revalidate=True) @cache_control(no_cache=True, no_store=True, must_revalidate=True)
def index(request, course="6.002 Spring 2012", chapter="Using the System", section="Hints"): def index(request, course=None, chapter="Using the System", section="Hints"):
''' Displays courseware accordion, and any associated content. ''' Displays courseware accordion, and any associated content.
''' '''
user = request.user user = request.user
if not settings.COURSEWARE_ENABLED or not user.is_authenticated(): if not settings.COURSEWARE_ENABLED:
return redirect('/') return redirect('/')
if course==None:
if not settings.ENABLE_MULTICOURSE:
course = "6.002 Spring 2012"
elif 'coursename' in request.session:
course = request.session['coursename']
else:
course = settings.COURSE_DEFAULT
# Fixes URLs -- we don't get funny encoding characters from spaces # Fixes URLs -- we don't get funny encoding characters from spaces
# so they remain readable # so they remain readable
## TODO: Properly replace underscores ## TODO: Properly replace underscores
...@@ -148,16 +162,18 @@ def index(request, course="6.002 Spring 2012", chapter="Using the System", secti ...@@ -148,16 +162,18 @@ def index(request, course="6.002 Spring 2012", chapter="Using the System", secti
chapter=chapter.replace("_"," ") chapter=chapter.replace("_"," ")
section=section.replace("_"," ") section=section.replace("_"," ")
# HACK: Force course to 6.002 for now # use multicourse module to determine if "course" is valid
# Without this, URLs break #if course!=settings.COURSE_NAME.replace('_',' '):
if course!="6.002 Spring 2012": if not multicourse_settings.is_valid_course(course):
return redirect('/') return redirect('/')
#import logging #import logging
#log = logging.getLogger("mitx") #log = logging.getLogger("mitx")
#log.info( "DEBUG: "+str(user) ) #log.info( "DEBUG: "+str(user) )
dom = content_parser.course_file(user) request.session['coursename'] = course # keep track of current course being viewed in django's request.session
dom = content_parser.course_file(user,course) # also pass course to it, for course-specific XML path
dom_module = dom.xpath("//course[@name=$course]/chapter[@name=$chapter]//section[@name=$section]/*[1]", dom_module = dom.xpath("//course[@name=$course]/chapter[@name=$chapter]//section[@name=$section]/*[1]",
course=course, chapter=chapter, section=section) course=course, chapter=chapter, section=section)
if len(dom_module) == 0: if len(dom_module) == 0:
...@@ -170,8 +186,11 @@ def index(request, course="6.002 Spring 2012", chapter="Using the System", secti ...@@ -170,8 +186,11 @@ def index(request, course="6.002 Spring 2012", chapter="Using the System", secti
module_ids = dom.xpath("//course[@name=$course]/chapter[@name=$chapter]//section[@name=$section]//@id", module_ids = dom.xpath("//course[@name=$course]/chapter[@name=$chapter]//section[@name=$section]//@id",
course=course, chapter=chapter, section=section) course=course, chapter=chapter, section=section)
module_object_preload = list(StudentModule.objects.filter(student=user, if user.is_authenticated():
module_id__in=module_ids)) module_object_preload = list(StudentModule.objects.filter(student=user,
module_id__in=module_ids))
else:
module_object_preload = []
module=render_module(user, request, module, module_object_preload) module=render_module(user, request, module, module_object_preload)
...@@ -179,10 +198,156 @@ def index(request, course="6.002 Spring 2012", chapter="Using the System", secti ...@@ -179,10 +198,156 @@ def index(request, course="6.002 Spring 2012", chapter="Using the System", secti
if 'init_js' not in module: if 'init_js' not in module:
module['init_js']='' module['init_js']=''
context={'init':accordion['init_js']+module['init_js'], context={'init':module['init_js'],
'accordion':accordion['content'], 'accordion':accordion,
'content':module['content'], 'content':module['content'],
'COURSE_TITLE':multicourse_settings.get_course_title(course),
'csrf':csrf(request)['csrf_token']} 'csrf':csrf(request)['csrf_token']}
result = render_to_response('courseware.html', context) result = render_to_response('courseware.html', context)
return result return result
def modx_dispatch(request, module=None, dispatch=None, id=None):
''' Generic view for extensions. '''
if not request.user.is_authenticated():
return redirect('/')
# Grab the student information for the module from the database
s = StudentModule.objects.filter(student=request.user,
module_id=id)
#s = StudentModule.get_with_caching(request.user, id)
if len(s) == 0 or s is None:
log.debug("Couldnt find module for user and id " + str(module) + " " + str(request.user) + " "+ str(id))
raise Http404
s = s[0]
oldgrade = s.grade
oldstate = s.state
dispatch=dispatch.split('?')[0]
ajax_url = settings.MITX_ROOT_URL + '/modx/'+module+'/'+id+'/'
# get coursename if stored
if 'coursename' in request.session: coursename = request.session['coursename']
else: coursename = None
# Grab the XML corresponding to the request from course.xml
xml = content_parser.module_xml(request.user, module, 'id', id, coursename)
# Create the module
system = I4xSystem(track_function = make_track_function(request),
render_function = None,
ajax_url = ajax_url,
filestore = None
)
instance=courseware.modules.get_module_class(module)(system,
xml,
id,
state=oldstate)
# Let the module handle the AJAX
ajax_return=instance.handle_ajax(dispatch, request.POST)
# Save the state back to the database
s.state=instance.get_state()
if instance.get_score():
s.grade=instance.get_score()['score']
if s.grade != oldgrade or s.state != oldstate:
s.save()
# Return whatever the module wanted to return to the client/caller
return HttpResponse(ajax_return)
def quickedit(request, id=None):
'''
quick-edit capa problem.
Maybe this should be moved into capa/views.py
Or this should take a "module" argument, and the quickedit moved into capa_module.
'''
print "WARNING: UNDEPLOYABLE CODE. FOR DEV USE ONLY."
print "In deployed use, this will only edit on one server"
print "We need a setting to disable for production where there is"
print "a load balanacer"
if not request.user.is_staff():
return redirect('/')
# get coursename if stored
if 'coursename' in request.session: coursename = request.session['coursename']
else: coursename = None
def get_lcp(coursename,id):
# Grab the XML corresponding to the request from course.xml
module = 'problem'
xml = content_parser.module_xml(request.user, module, 'id', id, coursename)
ajax_url = settings.MITX_ROOT_URL + '/modx/'+module+'/'+id+'/'
# Create the module (instance of capa_module.Module)
system = I4xSystem(track_function = make_track_function(request),
render_function = None,
ajax_url = ajax_url,
filestore = None,
coursename = coursename,
role = 'staff' if request.user.is_staff else 'student', # TODO: generalize this
)
instance=courseware.modules.get_module_class(module)(system,
xml,
id,
state=None)
lcp = instance.lcp
pxml = lcp.tree
pxmls = etree.tostring(pxml,pretty_print=True)
return instance, pxmls
instance, pxmls = get_lcp(coursename,id)
# if there was a POST, then process it
msg = ''
if 'qesubmit' in request.POST:
action = request.POST['qesubmit']
if "Revert" in action:
msg = "Reverted to original"
elif action=='Change Problem':
key = 'quickedit_%s' % id
if not key in request.POST:
msg = "oops, missing code key=%s" % key
else:
newcode = request.POST[key]
# see if code changed
if str(newcode)==str(pxmls) or '<?xml version="1.0"?>\n'+str(newcode)==str(pxmls):
msg = "No changes"
else:
# check new code
isok = False
try:
newxml = etree.fromstring(newcode)
isok = True
except Exception,err:
msg = "Failed to change problem: XML error \"<font color=red>%s</font>\"" % err
if isok:
filename = instance.lcp.fileobject.name
fp = open(filename,'w') # TODO - replace with filestore call?
fp.write(newcode)
fp.close()
msg = "<font color=green>Problem changed!</font> (<tt>%s</tt>)" % filename
instance, pxmls = get_lcp(coursename,id)
lcp = instance.lcp
# get the rendered problem HTML
phtml = instance.get_problem_html()
context = {'id':id,
'msg' : msg,
'lcp' : lcp,
'filename' : lcp.fileobject.name,
'pxmls' : pxmls,
'phtml' : phtml,
'init_js':instance.get_init_js(),
}
result = render_to_response('quickedit.html', context)
return result
# multicourse/multicourse_settings.py
#
# central module for providing fixed settings (course name, number, title)
# for multiple courses. Loads this information from django.conf.settings
#
# Allows backward compatibility with settings configurations without
# multiple courses specified.
#
# The central piece of configuration data is the dict COURSE_SETTINGS, with
# keys being the COURSE_NAME (spaces ok), and the value being a dict of
# parameter,value pairs. The required parameters are:
#
# - number : course number (used in the simplewiki pages)
# - title : humanized descriptive course title
#
# Optional parameters:
#
# - xmlpath : path (relative to data directory) for this course (defaults to "")
#
# If COURSE_SETTINGS does not exist, then fallback to 6.002_Spring_2012 default,
# for now.
from django.conf import settings
#-----------------------------------------------------------------------------
# load course settings
if hasattr(settings,'COURSE_SETTINGS'): # in the future, this could be replaced by reading an XML file
COURSE_SETTINGS = settings.COURSE_SETTINGS
elif hasattr(settings,'COURSE_NAME'): # backward compatibility
COURSE_SETTINGS = {settings.COURSE_NAME: {'number': settings.COURSE_NUMBER,
'title': settings.COURSE_TITLE,
},
}
else: # default to 6.002_Spring_2012
COURSE_SETTINGS = {'6.002_Spring_2012': {'number': '6.002x',
'title': 'Circuits and Electronics',
},
}
#-----------------------------------------------------------------------------
# wrapper functions around course settings
def get_course_settings(coursename):
if not coursename:
if hasattr(settings,'COURSE_DEFAULT'):
coursename = settings.COURSE_DEFAULT
else:
coursename = '6.002_Spring_2012'
if coursename in COURSE_SETTINGS: return COURSE_SETTINGS[coursename]
coursename = coursename.replace(' ','_')
if coursename in COURSE_SETTINGS: return COURSE_SETTINGS[coursename]
return None
def is_valid_course(coursename):
return not (get_course_settings==None)
def get_course_property(coursename,property):
cs = get_course_settings(coursename)
if not cs: return '' # raise exception instead?
if property in cs: return cs[property]
return '' # default
def get_course_xmlpath(coursename):
return get_course_property(coursename,'xmlpath')
def get_course_title(coursename):
return get_course_property(coursename,'title')
def get_course_number(coursename):
return get_course_property(coursename,'number')
# multicourse/views.py
...@@ -9,7 +9,7 @@ from django.db.models import signals ...@@ -9,7 +9,7 @@ from django.db.models import signals
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from markdown import markdown from markdown import markdown
from settings import * from wiki_settings import *
from util.cache import cache from util.cache import cache
......
...@@ -3,7 +3,7 @@ from django.conf import settings ...@@ -3,7 +3,7 @@ from django.conf import settings
from django.template.defaultfilters import stringfilter from django.template.defaultfilters import stringfilter
from django.utils.http import urlquote as django_urlquote from django.utils.http import urlquote as django_urlquote
from simplewiki.settings import * from simplewiki.wiki_settings import *
register = template.Library() register = template.Library()
......
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import types from django.conf import settings as settings
from django.conf import settings
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.core.context_processors import csrf from django.core.context_processors import csrf
from django.core.urlresolvers import get_callable
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.db.models import Q from django.db.models import Q
from django.http import Http404, HttpResponse, HttpResponseRedirect, HttpResponseServerError, HttpResponseForbidden, HttpResponseNotAllowed from django.http import HttpResponse, HttpResponseRedirect
from django.http import HttpResponse
from django.shortcuts import get_object_or_404
from django.shortcuts import redirect
from django.template import Context
from django.template import RequestContext, Context, loader
from django.utils import simplejson from django.utils import simplejson
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from mitxmako.shortcuts import render_to_response, render_to_string from mitxmako.shortcuts import render_to_response
from mako.lookup import TemplateLookup
from mako.template import Template from multicourse import multicourse_settings
import mitxmako.middleware
from models import * # TODO: Clean up from models import Revision, Article, CreateArticleForm, RevisionFormWithTitle, RevisionForm
from settings import * import wiki_settings
def view(request, wiki_url): def view(request, wiki_url):
if not request.user.is_authenticated():
return redirect('/')
(article, path, err) = fetch_from_url(request, wiki_url) (article, path, err) = fetch_from_url(request, wiki_url)
if err: if err:
return err return err
if 'coursename' in request.session: coursename = request.session['coursename']
else: coursename = None
course_number = multicourse_settings.get_course_number(coursename)
perm_err = check_permissions(request, article, check_read=True, check_deleted=True) perm_err = check_permissions(request, article, check_read=True, check_deleted=True)
if perm_err: if perm_err:
return perm_err return perm_err
...@@ -39,15 +32,12 @@ def view(request, wiki_url): ...@@ -39,15 +32,12 @@ def view(request, wiki_url):
'wiki_write': article.can_write_l(request.user), 'wiki_write': article.can_write_l(request.user),
'wiki_attachments_write': article.can_attach(request.user), 'wiki_attachments_write': article.can_attach(request.user),
'wiki_current_revision_deleted' : not (article.current_revision.deleted == 0), 'wiki_current_revision_deleted' : not (article.current_revision.deleted == 0),
'wiki_title' : article.title + " - MITX 6.002x Wiki" 'wiki_title' : article.title + " - MITX %s Wiki" % course_number
} }
d.update(csrf(request)) d.update(csrf(request))
return render_to_response('simplewiki_view.html', d) return render_to_response('simplewiki_view.html', d)
def view_revision(request, revision_number, wiki_url, revision=None): def view_revision(request, revision_number, wiki_url, revision=None):
if not request.user.is_authenticated():
return redirect('/')
(article, path, err) = fetch_from_url(request, wiki_url) (article, path, err) = fetch_from_url(request, wiki_url)
if err: if err:
return err return err
...@@ -76,8 +66,6 @@ def view_revision(request, revision_number, wiki_url, revision=None): ...@@ -76,8 +66,6 @@ def view_revision(request, revision_number, wiki_url, revision=None):
def root_redirect(request): def root_redirect(request):
if not request.user.is_authenticated():
return redirect('/')
try: try:
root = Article.get_root() root = Article.get_root()
except: except:
...@@ -87,8 +75,6 @@ def root_redirect(request): ...@@ -87,8 +75,6 @@ def root_redirect(request):
return HttpResponseRedirect(reverse('wiki_view', args=(root.get_url()))) return HttpResponseRedirect(reverse('wiki_view', args=(root.get_url())))
def create(request, wiki_url): def create(request, wiki_url):
if not request.user.is_authenticated():
return redirect('/')
url_path = get_url_path(wiki_url) url_path = get_url_path(wiki_url)
...@@ -161,9 +147,6 @@ def create(request, wiki_url): ...@@ -161,9 +147,6 @@ def create(request, wiki_url):
return render_to_response('simplewiki_edit.html', d) return render_to_response('simplewiki_edit.html', d)
def edit(request, wiki_url): def edit(request, wiki_url):
if not request.user.is_authenticated():
return redirect('/')
(article, path, err) = fetch_from_url(request, wiki_url) (article, path, err) = fetch_from_url(request, wiki_url)
if err: if err:
return err return err
...@@ -173,7 +156,7 @@ def edit(request, wiki_url): ...@@ -173,7 +156,7 @@ def edit(request, wiki_url):
if perm_err: if perm_err:
return perm_err return perm_err
if WIKI_ALLOW_TITLE_EDIT: if wiki_settings.WIKI_ALLOW_TITLE_EDIT:
EditForm = RevisionFormWithTitle EditForm = RevisionFormWithTitle
else: else:
EditForm = RevisionForm EditForm = RevisionForm
...@@ -195,7 +178,7 @@ def edit(request, wiki_url): ...@@ -195,7 +178,7 @@ def edit(request, wiki_url):
if not request.user.is_anonymous(): if not request.user.is_anonymous():
new_revision.revision_user = request.user new_revision.revision_user = request.user
new_revision.save() new_revision.save()
if WIKI_ALLOW_TITLE_EDIT: if wiki_settings.WIKI_ALLOW_TITLE_EDIT:
new_revision.article.title = f.cleaned_data['title'] new_revision.article.title = f.cleaned_data['title']
new_revision.article.save() new_revision.article.save()
return HttpResponseRedirect(reverse('wiki_view', args=(article.get_url(),))) return HttpResponseRedirect(reverse('wiki_view', args=(article.get_url(),)))
...@@ -215,9 +198,6 @@ def edit(request, wiki_url): ...@@ -215,9 +198,6 @@ def edit(request, wiki_url):
return render_to_response('simplewiki_edit.html', d) return render_to_response('simplewiki_edit.html', d)
def history(request, wiki_url, page=1): def history(request, wiki_url, page=1):
if not request.user.is_authenticated():
return redirect('/')
(article, path, err) = fetch_from_url(request, wiki_url) (article, path, err) = fetch_from_url(request, wiki_url)
if err: if err:
return err return err
...@@ -302,9 +282,6 @@ def history(request, wiki_url, page=1): ...@@ -302,9 +282,6 @@ def history(request, wiki_url, page=1):
def revision_feed(request, page=1): def revision_feed(request, page=1):
if not request.user.is_superuser:
return redirect('/')
page_size = 10 page_size = 10
try: try:
...@@ -332,8 +309,6 @@ def revision_feed(request, page=1): ...@@ -332,8 +309,6 @@ def revision_feed(request, page=1):
return render_to_response('simplewiki_revision_feed.html', d) return render_to_response('simplewiki_revision_feed.html', d)
def search_articles(request): def search_articles(request):
if not request.user.is_authenticated():
return redirect('/')
# blampe: We should check for the presence of other popular django search # blampe: We should check for the presence of other popular django search
# apps and use those if possible. Only fall back on this as a last resort. # apps and use those if possible. Only fall back on this as a last resort.
# Adding some context to results (eg where matches were) would also be nice. # Adding some context to results (eg where matches were) would also be nice.
...@@ -380,9 +355,6 @@ def search_articles(request): ...@@ -380,9 +355,6 @@ def search_articles(request):
def search_add_related(request, wiki_url): def search_add_related(request, wiki_url):
if not request.user.is_authenticated():
return redirect('/')
(article, path, err) = fetch_from_url(request, wiki_url) (article, path, err) = fetch_from_url(request, wiki_url)
if err: if err:
return err return err
...@@ -435,9 +407,6 @@ def add_related(request, wiki_url): ...@@ -435,9 +407,6 @@ def add_related(request, wiki_url):
return HttpResponseRedirect(reverse('wiki_view', args=(article.get_url(),))) return HttpResponseRedirect(reverse('wiki_view', args=(article.get_url(),)))
def remove_related(request, wiki_url, related_id): def remove_related(request, wiki_url, related_id):
if not request.user.is_authenticated():
return redirect('/')
(article, path, err) = fetch_from_url(request, wiki_url) (article, path, err) = fetch_from_url(request, wiki_url)
if err: if err:
return err return err
...@@ -457,8 +426,6 @@ def remove_related(request, wiki_url, related_id): ...@@ -457,8 +426,6 @@ def remove_related(request, wiki_url, related_id):
return HttpResponseRedirect(reverse('wiki_view', args=(article.get_url(),))) return HttpResponseRedirect(reverse('wiki_view', args=(article.get_url(),)))
def random_article(request): def random_article(request):
if not request.user.is_authenticated():
return redirect('/')
from random import randint from random import randint
num_arts = Article.objects.count() num_arts = Article.objects.count()
article = Article.objects.all()[randint(0, num_arts-1)] article = Article.objects.all()[randint(0, num_arts-1)]
...@@ -470,8 +437,6 @@ def encode_err(request, url): ...@@ -470,8 +437,6 @@ def encode_err(request, url):
return render_to_response('simplewiki_error.html', d) return render_to_response('simplewiki_error.html', d)
def not_found(request, wiki_url): def not_found(request, wiki_url):
if not request.user.is_authenticated():
return redirect('/')
"""Generate a NOT FOUND message for some URL""" """Generate a NOT FOUND message for some URL"""
d = {'wiki_err_notfound': True, d = {'wiki_err_notfound': True,
'wiki_url': wiki_url} 'wiki_url': wiki_url}
...@@ -543,17 +508,22 @@ def check_permissions(request, article, check_read=False, check_write=False, che ...@@ -543,17 +508,22 @@ def check_permissions(request, article, check_read=False, check_write=False, che
# LOGIN PROTECTION # # LOGIN PROTECTION #
#################### ####################
if WIKI_REQUIRE_LOGIN_VIEW: if wiki_settings.WIKI_REQUIRE_LOGIN_VIEW:
view = login_required(view) view = login_required(view)
history = login_required(history) history = login_required(history)
# search_related = login_required(search_related) search_articles = login_required(search_articles)
# wiki_encode_err = login_required(wiki_encode_err) root_redirect = login_required(root_redirect)
revision_feed = login_required(revision_feed)
if WIKI_REQUIRE_LOGIN_EDIT: random_article = login_required(random_article)
search_add_related = login_required(search_add_related)
not_found = login_required(not_found)
view_revision = login_required(view_revision)
if wiki_settings.WIKI_REQUIRE_LOGIN_EDIT:
create = login_required(create) create = login_required(create)
edit = login_required(edit) edit = login_required(edit)
add_related = login_required(add_related) add_related = login_required(add_related)
remove_related = login_required(remove_related) remove_related = login_required(remove_related)
if WIKI_CONTEXT_PREPROCESSORS: if wiki_settings.WIKI_CONTEXT_PREPROCESSORS:
settings.TEMPLATE_CONTEXT_PROCESSORS = settings.TEMPLATE_CONTEXT_PROCESSORS + WIKI_CONTEXT_PREPROCESSORS settings.TEMPLATE_CONTEXT_PROCESSORS += wiki_settings.WIKI_CONTEXT_PREPROCESSORS
...@@ -3,14 +3,21 @@ import os ...@@ -3,14 +3,21 @@ import os
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.core.servers.basehttp import FileWrapper from django.core.servers.basehttp import FileWrapper
from django.db.models.fields.files import FieldFile from django.db.models.fields.files import FieldFile
from django.http import HttpResponse, HttpResponseRedirect, HttpResponseForbidden, Http404 from django.http import HttpResponse, HttpResponseForbidden, Http404
from django.template import loader, Context from django.template import loader, Context
from settings import * # TODO: Clean up from models import ArticleAttachment, get_attachment_filepath
from models import Article, ArticleAttachment, get_attachment_filepath from views import check_permissions, fetch_from_url
from views import not_found, check_permissions, get_url_path, fetch_from_url
from wiki_settings import (
from simplewiki.settings import WIKI_ALLOW_ANON_ATTACHMENTS WIKI_ALLOW_ANON_ATTACHMENTS,
WIKI_ALLOW_ATTACHMENTS,
WIKI_ATTACHMENTS_MAX,
WIKI_ATTACHMENTS_ROOT,
WIKI_ATTACHMENTS_ALLOWED_EXTENSIONS,
WIKI_REQUIRE_LOGIN_VIEW,
WIKI_REQUIRE_LOGIN_EDIT,
)
def add_attachment(request, wiki_url): def add_attachment(request, wiki_url):
......
"""
User authentication backend for ssl (no pw required)
"""
from django.conf import settings
from django.contrib import auth
from django.contrib.auth.models import User, check_password
from django.contrib.auth.backends import ModelBackend
from django.contrib.auth.middleware import RemoteUserMiddleware
from django.core.exceptions import ImproperlyConfigured
import os, string, re
from random import choice
from student.models import UserProfile
#-----------------------------------------------------------------------------
def ssl_dn_extract_info(dn):
'''
Extract username, email address (may be anyuser@anydomain.com) and full name
from the SSL DN string. Return (user,email,fullname) if successful, and None
otherwise.
'''
ss = re.search('/emailAddress=(.*)@([^/]+)',dn)
if ss:
user = ss.group(1)
email = "%s@%s" % (user,ss.group(2))
else:
return None
ss = re.search('/CN=([^/]+)/',dn)
if ss:
fullname = ss.group(1)
else:
return None
return (user,email,fullname)
def check_nginx_proxy(request):
'''
Check for keys in the HTTP header (META) to se if we are behind an ngix reverse proxy.
If so, get user info from the SSL DN string and return that, as (user,email,fullname)
'''
m = request.META
if m.has_key('HTTP_X_REAL_IP'): # we're behind a nginx reverse proxy, which has already done ssl auth
if not m.has_key('HTTP_SSL_CLIENT_S_DN'):
return None
dn = m['HTTP_SSL_CLIENT_S_DN']
return ssl_dn_extract_info(dn)
return None
#-----------------------------------------------------------------------------
def get_ssl_username(request):
x = check_nginx_proxy(request)
if x:
return x[0]
env = request._req.subprocess_env
if env.has_key('SSL_CLIENT_S_DN_Email'):
email = env['SSL_CLIENT_S_DN_Email']
user = email[:email.index('@')]
return user
return None
#-----------------------------------------------------------------------------
class NginxProxyHeaderMiddleware(RemoteUserMiddleware):
'''
Django "middleware" function for extracting user information from HTTP request.
'''
# this field is generated by nginx's reverse proxy
header = 'HTTP_SSL_CLIENT_S_DN' # specify the request.META field to use
def process_request(self, request):
# AuthenticationMiddleware is required so that request.user exists.
if not hasattr(request, 'user'):
raise ImproperlyConfigured(
"The Django remote user auth middleware requires the"
" authentication middleware to be installed. Edit your"
" MIDDLEWARE_CLASSES setting to insert"
" 'django.contrib.auth.middleware.AuthenticationMiddleware'"
" before the RemoteUserMiddleware class.")
#raise ImproperlyConfigured('[ProxyHeaderMiddleware] request.META=%s' % repr(request.META))
try:
username = request.META[self.header] # try the nginx META key first
except KeyError:
try:
env = request._req.subprocess_env # else try the direct apache2 SSL key
if env.has_key('SSL_CLIENT_S_DN'):
username = env['SSL_CLIENT_S_DN']
else:
raise ImproperlyConfigured('no ssl key, env=%s' % repr(env))
username = ''
except:
# If specified header doesn't exist then return (leaving
# request.user set to AnonymousUser by the
# AuthenticationMiddleware).
return
# If the user is already authenticated and that user is the user we are
# getting passed in the headers, then the correct user is already
# persisted in the session and we don't need to continue.
#raise ImproperlyConfigured('[ProxyHeaderMiddleware] username=%s' % username)
if request.user.is_authenticated():
if request.user.username == self.clean_username(username, request):
#raise ImproperlyConfigured('%s already authenticated (%s)' % (username,request.user.username))
return
# We are seeing this user for the first time in this session, attempt
# to authenticate the user.
#raise ImproperlyConfigured('calling auth.authenticate, remote_user=%s' % username)
user = auth.authenticate(remote_user=username)
if user:
# User is valid. Set request.user and persist user in the session
# by logging the user in.
request.user = user
if settings.DEBUG: print "[ssl_auth.ssl_auth.NginxProxyHeaderMiddleware] logging in user=%s" % user
auth.login(request, user)
def clean_username(self,username,request):
'''
username is the SSL DN string - extract the actual username from it and return
'''
info = ssl_dn_extract_info(username)
if not info:
return None
(username,email,fullname) = info
return username
#-----------------------------------------------------------------------------
class SSLLoginBackend(ModelBackend):
'''
Django authentication back-end which auto-logs-in a user based on having
already authenticated with an MIT certificate (SSL).
'''
def authenticate(self, username=None, password=None, remote_user=None):
# remote_user is from the SSL_DN string. It will be non-empty only when
# the user has already passed the server authentication, which means
# matching with the certificate authority.
if not remote_user:
# no remote_user, so check username (but don't auto-create user)
if not username:
return None
return None # pass on to another authenticator backend
#raise ImproperlyConfigured("in SSLLoginBackend, username=%s, remote_user=%s" % (username,remote_user))
try:
user = User.objects.get(username=username) # if user already exists don't create it
return user
except User.DoesNotExist:
return None
return None
#raise ImproperlyConfigured("in SSLLoginBackend, username=%s, remote_user=%s" % (username,remote_user))
#if not os.environ.has_key('HTTPS'):
# return None
#if not os.environ.get('HTTPS')=='on': # only use this back-end if HTTPS on
# return None
def GenPasswd(length=8, chars=string.letters + string.digits):
return ''.join([choice(chars) for i in range(length)])
# convert remote_user to user, email, fullname
info = ssl_dn_extract_info(remote_user)
#raise ImproperlyConfigured("[SSLLoginBackend] looking up %s" % repr(info))
if not info:
#raise ImproperlyConfigured("[SSLLoginBackend] remote_user=%s, info=%s" % (remote_user,info))
return None
(username,email,fullname) = info
try:
user = User.objects.get(username=username) # if user already exists don't create it
except User.DoesNotExist:
raise "User does not exist. Not creating user; potential schema consistency issues"
#raise ImproperlyConfigured("[SSLLoginBackend] creating %s" % repr(info))
user = User(username=username, password=GenPasswd()) # create new User
user.is_staff = False
user.is_superuser = False
# get first, last name from fullname
name = fullname
if not name.count(' '):
user.first_name = " "
user.last_name = name
mn = ''
else:
user.first_name = name[:name.find(' ')]
ml = name[name.find(' '):].strip()
if ml.count(' '):
user.last_name = ml[ml.rfind(' '):]
mn = ml[:ml.rfind(' ')]
else:
user.last_name = ml
mn = ''
# set email
user.email = email
# cleanup last name
user.last_name = user.last_name.strip()
# save
user.save()
# auto-create user profile
up = UserProfile(user=user)
up.name = fullname
up.save()
#tui = user.get_profile()
#tui.middle_name = mn
#tui.role = 'Misc'
#tui.section = None # no section assigned at first
#tui.save()
# return None
return user
def get_user(self, user_id):
#if not os.environ.has_key('HTTPS'):
# return None
#if not os.environ.get('HTTPS')=='on': # only use this back-end if HTTPS on
# return None
try:
return User.objects.get(pk=user_id)
except User.DoesNotExist:
return None
#-----------------------------------------------------------------------------
# OLD!
class AutoLoginBackend:
def authenticate(self, username=None, password=None):
raise ImproperlyConfigured("in AutoLoginBackend, username=%s" % username)
if not os.environ.has_key('HTTPS'):
return None
if not os.environ.get('HTTPS')=='on':# only use this back-end if HTTPS on
return None
def GenPasswd(length=8, chars=string.letters + string.digits):
return ''.join([choice(chars) for i in range(length)])
try:
user = User.objects.get(username=username)
except User.DoesNotExist:
user = User(username=username, password=GenPasswd())
user.is_staff = False
user.is_superuser = False
# get first, last name
name = os.environ.get('SSL_CLIENT_S_DN_CN').strip()
if not name.count(' '):
user.first_name = " "
user.last_name = name
mn = ''
else:
user.first_name = name[:name.find(' ')]
ml = name[name.find(' '):].strip()
if ml.count(' '):
user.last_name = ml[ml.rfind(' '):]
mn = ml[:ml.rfind(' ')]
else:
user.last_name = ml
mn = ''
# get email
user.email = os.environ.get('SSL_CLIENT_S_DN_Email')
# save
user.save()
tui = user.get_profile()
tui.middle_name = mn
tui.role = 'Misc'
tui.section = None# no section assigned at first
tui.save()
# return None
return user
def get_user(self, user_id):
if not os.environ.has_key('HTTPS'):
return None
if not os.environ.get('HTTPS')=='on':# only use this back-end if HTTPS on
return None
try:
return User.objects.get(pk=user_id)
except User.DoesNotExist:
return None
# Create your views here. from django.contrib.auth.decorators import login_required
import os from mitxmako.shortcuts import render_to_response
from django.conf import settings
from django.http import Http404
from django.shortcuts import redirect
from mitxmako.shortcuts import render_to_response, render_to_string
@login_required
def index(request, page=0): def index(request, page=0):
if not request.user.is_authenticated():
return redirect('/')
return render_to_response('staticbook.html',{'page':int(page)}) return render_to_response('staticbook.html',{'page':int(page)})
def index_shifted(request, page): def index_shifted(request, page):
......
...@@ -10,14 +10,14 @@ from django.conf import settings ...@@ -10,14 +10,14 @@ from django.conf import settings
from django.contrib.auth import logout, authenticate, login from django.contrib.auth import logout, authenticate, login
from django.contrib.auth.forms import PasswordResetForm from django.contrib.auth.forms import PasswordResetForm
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.auth.decorators import login_required
from django.core.context_processors import csrf from django.core.context_processors import csrf
from django.core.mail import send_mail from django.core.mail import send_mail
from django.core.validators import validate_email, validate_slug, ValidationError from django.core.validators import validate_email, validate_slug, ValidationError
from django.db import connection from django.db import IntegrityError
from django.http import HttpResponse, Http404 from django.http import HttpResponse, Http404
from django.shortcuts import redirect from django.shortcuts import redirect
from mitxmako.shortcuts import render_to_response, render_to_string from mitxmako.shortcuts import render_to_response, render_to_string
from mako import exceptions
from django_future.csrf import ensure_csrf_cookie from django_future.csrf import ensure_csrf_cookie
...@@ -93,12 +93,11 @@ def logout_user(request): ...@@ -93,12 +93,11 @@ def logout_user(request):
logout(request) logout(request)
return redirect('/') return redirect('/')
@login_required
@ensure_csrf_cookie @ensure_csrf_cookie
def change_setting(request): def change_setting(request):
''' JSON call to change a profile setting: Right now, location and language ''' JSON call to change a profile setting: Right now, location and language
''' '''
if not request.user.is_authenticated():
return redirect('/')
up = UserProfile.objects.get(user=request.user) #request.user.profile_cache up = UserProfile.objects.get(user=request.user) #request.user.profile_cache
if 'location' in request.POST: if 'location' in request.POST:
up.location=request.POST['location'] up.location=request.POST['location']
...@@ -162,15 +161,6 @@ def create_account(request, post_override=None): ...@@ -162,15 +161,6 @@ def create_account(request, post_override=None):
# Confirm username and e-mail are unique. TODO: This should be in a transaction
if len(User.objects.filter(username=post_vars['username']))>0:
js['value']="An account with this username already exists."
return HttpResponse(json.dumps(js))
if len(User.objects.filter(email=post_vars['email']))>0:
js['value']="An account with this e-mail already exists."
return HttpResponse(json.dumps(js))
u=User(username=post_vars['username'], u=User(username=post_vars['username'],
email=post_vars['email'], email=post_vars['email'],
is_active=False) is_active=False)
...@@ -178,7 +168,20 @@ def create_account(request, post_override=None): ...@@ -178,7 +168,20 @@ def create_account(request, post_override=None):
r=Registration() r=Registration()
# TODO: Rearrange so that if part of the process fails, the whole process fails. # TODO: Rearrange so that if part of the process fails, the whole process fails.
# Right now, we can have e.g. no registration e-mail sent out and a zombie account # Right now, we can have e.g. no registration e-mail sent out and a zombie account
u.save() try:
u.save()
except IntegrityError:
# Figure out the cause of the integrity error
if len(User.objects.filter(username=post_vars['username']))>0:
js['value']="An account with this username already exists."
return HttpResponse(json.dumps(js))
if len(User.objects.filter(email=post_vars['email']))>0:
js['value']="An account with this e-mail already exists."
return HttpResponse(json.dumps(js))
raise
r.register(u) r.register(u)
up = UserProfile(user=u) up = UserProfile(user=u)
......
...@@ -8,7 +8,8 @@ Common traits: ...@@ -8,7 +8,8 @@ Common traits:
""" """
import json import json
from common import * from envs.logsettings import get_logger_config
from envs.common import *
############################### ALWAYS THE SAME ################################ ############################### ALWAYS THE SAME ################################
DEBUG = False DEBUG = False
...@@ -24,7 +25,6 @@ with open(ENV_ROOT / "env.json") as env_file: ...@@ -24,7 +25,6 @@ with open(ENV_ROOT / "env.json") as env_file:
ENV_TOKENS = json.load(env_file) ENV_TOKENS = json.load(env_file)
SITE_NAME = ENV_TOKENS['SITE_NAME'] SITE_NAME = ENV_TOKENS['SITE_NAME']
CSRF_COOKIE_DOMAIN = ENV_TOKENS['CSRF_COOKIE_DOMAIN']
BOOK_URL = ENV_TOKENS['BOOK_URL'] BOOK_URL = ENV_TOKENS['BOOK_URL']
MEDIA_URL = ENV_TOKENS['MEDIA_URL'] MEDIA_URL = ENV_TOKENS['MEDIA_URL']
...@@ -32,10 +32,10 @@ LOG_DIR = ENV_TOKENS['LOG_DIR'] ...@@ -32,10 +32,10 @@ LOG_DIR = ENV_TOKENS['LOG_DIR']
CACHES = ENV_TOKENS['CACHES'] CACHES = ENV_TOKENS['CACHES']
LOGGING = logsettings.get_logger_config(LOG_DIR, LOGGING = get_logger_config(LOG_DIR,
logging_env=ENV_TOKENS['LOGGING_ENV'], logging_env=ENV_TOKENS['LOGGING_ENV'],
syslog_addr=(ENV_TOKENS['SYSLOG_SERVER'], 514), syslog_addr=(ENV_TOKENS['SYSLOG_SERVER'], 514),
debug=False) debug=False)
############################## SECURE AUTH ITEMS ############################### ############################## SECURE AUTH ITEMS ###############################
# Secret things: passwords, access keys, etc. # Secret things: passwords, access keys, etc.
...@@ -47,4 +47,4 @@ SECRET_KEY = AUTH_TOKENS['SECRET_KEY'] ...@@ -47,4 +47,4 @@ SECRET_KEY = AUTH_TOKENS['SECRET_KEY']
AWS_ACCESS_KEY_ID = AUTH_TOKENS["AWS_ACCESS_KEY_ID"] AWS_ACCESS_KEY_ID = AUTH_TOKENS["AWS_ACCESS_KEY_ID"]
AWS_SECRET_ACCESS_KEY = AUTH_TOKENS["AWS_SECRET_ACCESS_KEY"] AWS_SECRET_ACCESS_KEY = AUTH_TOKENS["AWS_SECRET_ACCESS_KEY"]
DATABASES = AUTH_TOKENS['DATABASES'] DATABASES = AUTH_TOKENS['DATABASES']
\ No newline at end of file
...@@ -24,8 +24,7 @@ import tempfile ...@@ -24,8 +24,7 @@ import tempfile
import djcelery import djcelery
from path import path from path import path
from askbotsettings import * # this is where LIVESETTINGS_OPTIONS comes from from envs.askbotsettings import * # this is where LIVESETTINGS_OPTIONS comes from
import logsettings
################################### FEATURES ################################### ################################### FEATURES ###################################
COURSEWARE_ENABLED = True COURSEWARE_ENABLED = True
...@@ -81,6 +80,7 @@ TEMPLATE_DIRS = ( ...@@ -81,6 +80,7 @@ TEMPLATE_DIRS = (
TEMPLATE_CONTEXT_PROCESSORS = ( TEMPLATE_CONTEXT_PROCESSORS = (
'django.core.context_processors.request', 'django.core.context_processors.request',
'askbot.context.application_settings', 'askbot.context.application_settings',
'django.contrib.messages.context_processors.messages',
#'django.core.context_processors.i18n', #'django.core.context_processors.i18n',
'askbot.user_messages.context_processors.user_messages',#must be before auth 'askbot.user_messages.context_processors.user_messages',#must be before auth
'django.core.context_processors.auth', #this is required for admin 'django.core.context_processors.auth', #this is required for admin
...@@ -113,7 +113,6 @@ TEMPLATE_DEBUG = False ...@@ -113,7 +113,6 @@ TEMPLATE_DEBUG = False
# Site info # Site info
SITE_ID = 1 SITE_ID = 1
SITE_NAME = "localhost:8000" SITE_NAME = "localhost:8000"
CSRF_COOKIE_DOMAIN = '127.0.0.1'
HTTPS = 'on' HTTPS = 'on'
ROOT_URLCONF = 'mitx.urls' ROOT_URLCONF = 'mitx.urls'
IGNORABLE_404_ENDS = ('favicon.ico') IGNORABLE_404_ENDS = ('favicon.ico')
...@@ -134,7 +133,7 @@ STATIC_ROOT = ENV_ROOT / "staticfiles" # We don't run collectstatic -- this is t ...@@ -134,7 +133,7 @@ STATIC_ROOT = ENV_ROOT / "staticfiles" # We don't run collectstatic -- this is t
# FIXME: We should iterate through the courses we have, adding the static # FIXME: We should iterate through the courses we have, adding the static
# contents for each of them. (Right now we just use symlinks.) # contents for each of them. (Right now we just use symlinks.)
STATICFILES_DIRS = ( STATICFILES_DIRS = [
PROJECT_ROOT / "static", PROJECT_ROOT / "static",
ASKBOT_ROOT / "askbot" / "skins", ASKBOT_ROOT / "askbot" / "skins",
("circuits", DATA_DIR / "images"), ("circuits", DATA_DIR / "images"),
...@@ -143,7 +142,7 @@ STATICFILES_DIRS = ( ...@@ -143,7 +142,7 @@ STATICFILES_DIRS = (
# This is how you would use the textbook images locally # This is how you would use the textbook images locally
# ("book", ENV_ROOT / "book_images") # ("book", ENV_ROOT / "book_images")
) ]
# Locale/Internationalization # Locale/Internationalization
TIME_ZONE = 'America/New_York' # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name TIME_ZONE = 'America/New_York' # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
...@@ -151,6 +150,9 @@ LANGUAGE_CODE = 'en' # http://www.i18nguy.com/unicode/language-identifiers.html ...@@ -151,6 +150,9 @@ LANGUAGE_CODE = 'en' # http://www.i18nguy.com/unicode/language-identifiers.html
USE_I18N = True USE_I18N = True
USE_L10N = True USE_L10N = True
# Messages
MESSAGE_STORAGE = 'django.contrib.messages.storage.session.SessionStorage'
#################################### AWS ####################################### #################################### AWS #######################################
# S3BotoStorage insists on a timeout for uploaded assets. We should make it # S3BotoStorage insists on a timeout for uploaded assets. We should make it
# permanent instead, but rather than trying to figure out exactly where that # permanent instead, but rather than trying to figure out exactly where that
...@@ -179,8 +181,8 @@ CELERY_ALWAYS_EAGER = True ...@@ -179,8 +181,8 @@ CELERY_ALWAYS_EAGER = True
djcelery.setup_loader() djcelery.setup_loader()
################################# SIMPLEWIKI ################################### ################################# SIMPLEWIKI ###################################
WIKI_REQUIRE_LOGIN_EDIT = True SIMPLE_WIKI_REQUIRE_LOGIN_EDIT = True
WIKI_REQUIRE_LOGIN_VIEW = True SIMPLE_WIKI_REQUIRE_LOGIN_VIEW = False
################################# Middleware ################################### ################################# Middleware ###################################
# List of finder classes that know how to find static files in # List of finder classes that know how to find static files in
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
These are debug machines used for content creators, so they're kind of a cross These are debug machines used for content creators, so they're kind of a cross
between dev machines and AWS machines. between dev machines and AWS machines.
""" """
from aws import * from envs.aws import *
DEBUG = True DEBUG = True
TEMPLATE_DEBUG = True TEMPLATE_DEBUG = True
......
...@@ -7,16 +7,17 @@ sessions. Assumes structure: ...@@ -7,16 +7,17 @@ sessions. Assumes structure:
/mitx # The location of this repo /mitx # The location of this repo
/log # Where we're going to write log files /log # Where we're going to write log files
""" """
from common import * from envs.common import *
from envs.logsettings import get_logger_config
DEBUG = True DEBUG = True
PIPELINE = True PIPELINE = True
TEMPLATE_DEBUG = True TEMPLATE_DEBUG = True
LOGGING = logsettings.get_logger_config(ENV_ROOT / "log", LOGGING = get_logger_config(ENV_ROOT / "log",
logging_env="dev", logging_env="dev",
tracking_filename="tracking.log", tracking_filename="tracking.log",
debug=True) debug=True)
DATABASES = { DATABASES = {
'default': { 'default': {
...@@ -74,7 +75,8 @@ DEBUG_TOOLBAR_PANELS = ( ...@@ -74,7 +75,8 @@ DEBUG_TOOLBAR_PANELS = (
############################ FILE UPLOADS (ASKBOT) ############################# ############################ FILE UPLOADS (ASKBOT) #############################
DEFAULT_FILE_STORAGE = 'django.core.files.storage.FileSystemStorage' DEFAULT_FILE_STORAGE = 'django.core.files.storage.FileSystemStorage'
MEDIA_ROOT = ENV_ROOT / "uploads" MEDIA_ROOT = ENV_ROOT / "uploads"
MEDIA_URL = "/discussion/upfiles/" MEDIA_URL = "/static/uploads/"
STATICFILES_DIRS.append(("uploads", MEDIA_ROOT))
FILE_UPLOAD_TEMP_DIR = ENV_ROOT / "uploads" FILE_UPLOAD_TEMP_DIR = ENV_ROOT / "uploads"
FILE_UPLOAD_HANDLERS = ( FILE_UPLOAD_HANDLERS = (
'django.core.files.uploadhandler.MemoryFileUploadHandler', 'django.core.files.uploadhandler.MemoryFileUploadHandler',
......
...@@ -13,7 +13,7 @@ Dir structure: ...@@ -13,7 +13,7 @@ Dir structure:
/log # Where we're going to write log files /log # Where we're going to write log files
""" """
from dev import * from envs.dev import *
DATABASES = { DATABASES = {
'default': { 'default': {
......
"""
This config file runs the simplest dev environment using sqlite, and db-based
sessions. Assumes structure:
/envroot/
/db # This is where it'll write the database file
/mitx # The location of this repo
/log # Where we're going to write log files
"""
from envs.common import *
from envs.logsettings import get_logger_config
STATIC_GRAB = True
LOGGING = get_logger_config(ENV_ROOT / "log",
logging_env="dev",
tracking_filename="tracking.log",
debug=False)
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': ENV_ROOT / "db" / "mitx.db",
}
}
CACHES = {
# This is the cache used for most things. Askbot will not work without a
# functioning cache -- it relies on caching to load its settings in places.
# In staging/prod envs, the sessions also live here.
'default': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
'LOCATION': 'mitx_loc_mem_cache'
},
# The general cache is what you get if you use our util.cache. It's used for
# things like caching the course.xml file for different A/B test groups.
# We set it to be a DummyCache to force reloading of course.xml in dev.
# In staging environments, we would grab VERSION from data uploaded by the
# push process.
'general': {
'BACKEND': 'django.core.cache.backends.dummy.DummyCache',
'KEY_PREFIX': 'general',
'VERSION': 4,
}
}
# Dummy secret key for dev
SECRET_KEY = '85920908f28904ed733fe576320db18cabd7b6cd'
############################ FILE UPLOADS (ASKBOT) #############################
DEFAULT_FILE_STORAGE = 'django.core.files.storage.FileSystemStorage'
MEDIA_ROOT = ENV_ROOT / "uploads"
MEDIA_URL = "/discussion/upfiles/"
FILE_UPLOAD_TEMP_DIR = ENV_ROOT / "uploads"
FILE_UPLOAD_HANDLERS = (
'django.core.files.uploadhandler.MemoryFileUploadHandler',
'django.core.files.uploadhandler.TemporaryFileUploadHandler',
)
"""
This config file runs the simplest dev environment using sqlite, and db-based
sessions. Assumes structure:
/envroot/
/db # This is where it'll write the database file
/mitx # The location of this repo
/log # Where we're going to write log files
"""
from envs.common import *
from envs.logsettings import get_logger_config
import os
INSTALLED_APPS = [
app
for app
in INSTALLED_APPS
if not app.startswith('askbot')
]
# Nose Test Runner
INSTALLED_APPS += ['django_nose']
NOSE_ARGS = ['--cover-erase', '--with-xunit', '--with-xcoverage', '--cover-html', '--cover-inclusive']
for app in os.listdir(PROJECT_ROOT / 'djangoapps'):
NOSE_ARGS += ['--cover-package', app]
TEST_RUNNER = 'django_nose.NoseTestSuiteRunner'
# Local Directories
TEST_ROOT = path("test_root")
COURSES_ROOT = TEST_ROOT / "data"
DATA_DIR = COURSES_ROOT
MAKO_TEMPLATES['course'] = [DATA_DIR]
MAKO_TEMPLATES['sections'] = [DATA_DIR / 'sections']
MAKO_TEMPLATES['custom_tags'] = [DATA_DIR / 'custom_tags']
MAKO_TEMPLATES['main'] = [PROJECT_ROOT / 'templates',
DATA_DIR / 'info',
DATA_DIR / 'problems']
LOGGING = get_logger_config(TEST_ROOT / "log",
logging_env="dev",
tracking_filename="tracking.log",
debug=True)
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': PROJECT_ROOT / "db" / "mitx.db",
}
}
CACHES = {
# This is the cache used for most things. Askbot will not work without a
# functioning cache -- it relies on caching to load its settings in places.
# In staging/prod envs, the sessions also live here.
'default': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
'LOCATION': 'mitx_loc_mem_cache'
},
# The general cache is what you get if you use our util.cache. It's used for
# things like caching the course.xml file for different A/B test groups.
# We set it to be a DummyCache to force reloading of course.xml in dev.
# In staging environments, we would grab VERSION from data uploaded by the
# push process.
'general': {
'BACKEND': 'django.core.cache.backends.dummy.DummyCache',
'KEY_PREFIX': 'general',
'VERSION': 4,
}
}
# Dummy secret key for dev
SECRET_KEY = '85920908f28904ed733fe576320db18cabd7b6cd'
############################ FILE UPLOADS (ASKBOT) #############################
DEFAULT_FILE_STORAGE = 'django.core.files.storage.FileSystemStorage'
MEDIA_ROOT = PROJECT_ROOT / "uploads"
MEDIA_URL = "/static/uploads/"
STATICFILES_DIRS.append(("uploads", MEDIA_ROOT))
FILE_UPLOAD_TEMP_DIR = PROJECT_ROOT / "uploads"
FILE_UPLOAD_HANDLERS = (
'django.core.files.uploadhandler.MemoryFileUploadHandler',
'django.core.files.uploadhandler.TemporaryFileUploadHandler',
)
#! /usr/bin/env python
import sys
import json
import random
import copy
from collections import defaultdict
from argparse import ArgumentParser, FileType
from datetime import datetime
def generate_user(user_number):
return {
"pk": user_number,
"model": "auth.user",
"fields": {
"status": "w",
"last_name": "Last",
"gold": 0,
"is_staff": False,
"user_permissions": [],
"interesting_tags": "",
"email_key": None,
"date_joined": "2012-04-26 11:36:39",
"first_name": "",
"email_isvalid": False,
"avatar_type": "n",
"website": "",
"is_superuser": False,
"date_of_birth": None,
"last_login": "2012-04-26 11:36:48",
"location": "",
"new_response_count": 0,
"email": "user{num}@example.com".format(num=user_number),
"username": "user{num}".format(num=user_number),
"is_active": True,
"consecutive_days_visit_count": 0,
"email_tag_filter_strategy": 1,
"groups": [],
"password": "sha1$90e6f$562a1d783a0c47ce06ebf96b8c58123a0671bbf0",
"silver": 0,
"bronze": 0,
"questions_per_page": 10,
"about": "",
"show_country": True,
"country": "",
"display_tag_filter_strategy": 0,
"seen_response_count": 0,
"real_name": "",
"ignored_tags": "",
"reputation": 1,
"gravatar": "366d981a10116969c568a18ee090f44c",
"last_seen": "2012-04-26 11:36:39"
}
}
def parse_args(args=sys.argv[1:]):
parser = ArgumentParser()
parser.add_argument('-d', '--data', type=FileType('r'), default=sys.stdin)
parser.add_argument('-o', '--output', type=FileType('w'), default=sys.stdout)
parser.add_argument('count', type=int)
return parser.parse_args(args)
def main(args=sys.argv[1:]):
args = parse_args(args)
data = json.load(args.data)
unique_students = set(entry['fields']['student'] for entry in data)
if args.count > len(unique_students) * 0.1:
raise Exception("Can't be sufficiently anonymous selecting {count} of {unique} students".format(
count=args.count, unique=len(unique_students)))
by_problems = defaultdict(list)
for entry in data:
by_problems[entry['fields']['module_id']].append(entry)
out_data = []
out_pk = 1
for name, answers in by_problems.items():
for student_id in xrange(args.count):
sample = random.choice(answers)
data = copy.deepcopy(sample)
data["fields"]["student"] = student_id + 1
data["fields"]["created"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
data["fields"]["modified"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
data["pk"] = out_pk
out_pk += 1
out_data.append(data)
for student_id in xrange(args.count):
out_data.append(generate_user(student_id))
json.dump(out_data, args.output, indent=2)
if __name__ == "__main__":
sys.exit(main())
#!/usr/bin/python
from loncapa_check import *
#!/usr/bin/python
#
# File: mitx/lib/loncapa/loncapa_check.py
#
# Python functions which duplicate the standard comparison functions available to LON-CAPA problems.
# Used in translating LON-CAPA problems to i4x problem specification language.
import random
def lc_random(lower,upper,stepsize):
'''
like random.randrange but lower and upper can be non-integer
'''
nstep = int((upper-lower)/(1.0*stepsize))
choices = [lower+x*stepsize for x in range(nstep)]
return random.choice(choices)
...@@ -27,12 +27,16 @@ def render_to_string(template_name, dictionary, context=None, namespace='main'): ...@@ -27,12 +27,16 @@ def render_to_string(template_name, dictionary, context=None, namespace='main'):
# collapse context_instance to a single dictionary for mako # collapse context_instance to a single dictionary for mako
context_dictionary = {} context_dictionary = {}
context_instance['settings'] = settings context_instance['settings'] = settings
context_instance['MITX_ROOT_URL'] = settings.MITX_ROOT_URL
for d in mitxmako.middleware.requestcontext: for d in mitxmako.middleware.requestcontext:
context_dictionary.update(d) context_dictionary.update(d)
for d in context_instance: for d in context_instance:
context_dictionary.update(d) context_dictionary.update(d)
if context: if context:
context_dictionary.update(context) context_dictionary.update(context)
## HACK
## We should remove this, and possible set COURSE_TITLE in the middleware from the session.
if 'COURSE_TITLE' not in context_dictionary: context_dictionary['COURSE_TITLE'] = ''
# fetch and render template # fetch and render template
template = middleware.lookup[namespace].get_template(template_name) template = middleware.lookup[namespace].get_template(template_name)
return template.render(**context_dictionary) return template.render(**context_dictionary)
......
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# File: formula.py
# Date: 04-May-12
# Author: I. Chuang <ichuang@mit.edu>
#
# flexible python representation of a symbolic mathematical formula.
# Acceptes Presentation MathML, Content MathML (and could also do OpenMath)
# Provides sympy representation.
import os, sys, string, re
import operator
import sympy
from sympy.printing.latex import LatexPrinter
from sympy.printing.str import StrPrinter
from sympy import latex, sympify
from sympy.physics.quantum.qubit import *
from sympy.physics.quantum.state import *
# from sympy import exp, pi, I
# from sympy.core.operations import LatticeOp
# import sympy.physics.quantum.qubit
import urllib
from xml.sax.saxutils import escape, unescape
import sympy
import unicodedata
from lxml import etree
#import subprocess
import requests
from copy import deepcopy
print "[lib.sympy_check.formula] Warning: Dark code. Needs review before enabling in prod."
os.environ['PYTHONIOENCODING'] = 'utf-8'
#-----------------------------------------------------------------------------
class dot(sympy.operations.LatticeOp): # my dot product
zero = sympy.Symbol('dotzero')
identity = sympy.Symbol('dotidentity')
#class dot(sympy.Mul): # my dot product
# is_Mul = False
def _print_dot(self,expr):
return '{((%s) \cdot (%s))}' % (expr.args[0],expr.args[1])
LatexPrinter._print_dot = _print_dot
#-----------------------------------------------------------------------------
# unit vectors (for 8.02)
def _print_hat(self,expr): return '\\hat{%s}' % str(expr.args[0]).lower()
LatexPrinter._print_hat = _print_hat
StrPrinter._print_hat = _print_hat
#-----------------------------------------------------------------------------
# helper routines
def to_latex(x):
if x==None: return ''
# LatexPrinter._print_dot = _print_dot
xs = latex(x)
xs = xs.replace(r'\XI','XI') # workaround for strange greek
#return '<math>%s{}{}</math>' % (xs[1:-1])
if xs[0]=='$':
return '[mathjax]%s[/mathjax]<br>' % (xs[1:-1]) # for sympy v6
return '[mathjax]%s[/mathjax]<br>' % (xs) # for sympy v7
def my_evalf(expr,chop=False):
if type(expr)==list:
try:
return [x.evalf(chop=chop) for x in expr]
except:
return expr
try:
return expr.evalf(chop=chop)
except:
return expr
#-----------------------------------------------------------------------------
# my version of sympify to import expression into sympy
def my_sympify(expr,normphase=False,matrix=False,abcsym=False,do_qubit=False,symtab=None):
# make all lowercase real?
if symtab:
varset = symtab
else:
varset = {'p':sympy.Symbol('p'),
'g':sympy.Symbol('g'),
'e':sympy.E, # for exp
'i':sympy.I, # lowercase i is also sqrt(-1)
'Q':sympy.Symbol('Q'), # otherwise it is a sympy "ask key"
#'X':sympy.sympify('Matrix([[0,1],[1,0]])'),
#'Y':sympy.sympify('Matrix([[0,-I],[I,0]])'),
#'Z':sympy.sympify('Matrix([[1,0],[0,-1]])'),
'ZZ':sympy.Symbol('ZZ'), # otherwise it is the PythonIntegerRing
'XI':sympy.Symbol('XI'), # otherwise it is the capital \XI
'hat':sympy.Function('hat'), # for unit vectors (8.02)
}
if do_qubit: # turn qubit(...) into Qubit instance
varset.update({'qubit':sympy.physics.quantum.qubit.Qubit,
'Ket':sympy.physics.quantum.state.Ket,
'dot':dot,
'bit':sympy.Function('bit'),
})
if abcsym: # consider all lowercase letters as real symbols, in the parsing
for letter in string.lowercase:
if letter in varset: # exclude those already done
continue
varset.update({letter:sympy.Symbol(letter,real=True)})
sexpr = sympify(expr,locals=varset)
if normphase: # remove overall phase if sexpr is a list
if type(sexpr)==list:
if sexpr[0].is_number:
ophase = sympy.sympify('exp(-I*arg(%s))' % sexpr[0])
sexpr = [ sympy.Mul(x,ophase) for x in sexpr ]
def to_matrix(x): # if x is a list of lists, and is rectangular, then return Matrix(x)
if not type(x)==list:
return x
for row in x:
if (not type(row)==list):
return x
rdim = len(x[0])
for row in x:
if not len(row)==rdim:
return x
return sympy.Matrix(x)
if matrix:
sexpr = to_matrix(sexpr)
return sexpr
#-----------------------------------------------------------------------------
# class for symbolic mathematical formulas
class formula(object):
'''
Representation of a mathematical formula object. Accepts mathml math expression for constructing,
and can produce sympy translation. The formula may or may not include an assignment (=).
'''
def __init__(self,expr,asciimath=''):
self.expr = expr.strip()
self.asciimath = asciimath
self.the_cmathml = None
self.the_sympy = None
def is_presentation_mathml(self):
return '<mstyle' in self.expr
def is_mathml(self):
return '<math ' in self.expr
def fix_greek_in_mathml(self,xml):
def gettag(x):
return re.sub('{http://[^}]+}','',x.tag)
for k in xml:
tag = gettag(k)
if tag=='mi' or tag=='ci':
usym = unicode(k.text)
try:
udata = unicodedata.name(usym)
except Exception,err:
udata = None
#print "usym = %s, udata=%s" % (usym,udata)
if udata: # eg "GREEK SMALL LETTER BETA"
if 'GREEK' in udata:
usym = udata.split(' ')[-1]
if 'SMALL' in udata: usym = usym.lower()
#print "greek: ",usym
k.text = usym
self.fix_greek_in_mathml(k)
return xml
def preprocess_pmathml(self,xml):
'''
Pre-process presentation MathML from ASCIIMathML to make it more acceptable for SnuggleTeX, and also
to accomodate some sympy conventions (eg hat(i) for \hat{i}).
'''
if type(xml)==str or type(xml)==unicode:
xml = etree.fromstring(xml) # TODO: wrap in try
xml = self.fix_greek_in_mathml(xml) # convert greek utf letters to greek spelled out in ascii
def gettag(x):
return re.sub('{http://[^}]+}','',x.tag)
# f and g are processed as functions by asciimathml, eg "f-2" turns into "<mrow><mi>f</mi><mo>-</mo></mrow><mn>2</mn>"
# this is really terrible for turning into cmathml.
# undo this here.
def fix_pmathml(xml):
for k in xml:
tag = gettag(k)
if tag=='mrow':
if len(k)==2:
if gettag(k[0])=='mi' and k[0].text in ['f','g'] and gettag(k[1])=='mo':
idx = xml.index(k)
xml.insert(idx,deepcopy(k[0])) # drop the <mrow> container
xml.insert(idx+1,deepcopy(k[1]))
xml.remove(k)
fix_pmathml(k)
fix_pmathml(xml)
# hat i is turned into <mover><mi>i</mi><mo>^</mo></mover> ; mangle this into <mi>hat(f)</mi>
# hat i also somtimes turned into <mover><mrow> <mi>j</mi> </mrow><mo>^</mo></mover>
def fix_hat(xml):
for k in xml:
tag = gettag(k)
if tag=='mover':
if len(k)==2:
if gettag(k[0])=='mi' and gettag(k[1])=='mo' and str(k[1].text)=='^':
newk = etree.Element('mi')
newk.text = 'hat(%s)' % k[0].text
xml.replace(k,newk)
if gettag(k[0])=='mrow' and gettag(k[0][0])=='mi' and gettag(k[1])=='mo' and str(k[1].text)=='^':
newk = etree.Element('mi')
newk.text = 'hat(%s)' % k[0][0].text
xml.replace(k,newk)
fix_hat(k)
fix_hat(xml)
self.xml = xml
return self.xml
def get_content_mathml(self):
if self.the_cmathml: return self.the_cmathml
# pre-process the presentation mathml before sending it to snuggletex to convert to content mathml
xml = self.preprocess_pmathml(self.expr)
pmathml = etree.tostring(xml,pretty_print=True)
self.the_pmathml = pmathml
# convert to cmathml
self.the_cmathml = self.GetContentMathML(self.asciimath,pmathml)
return self.the_cmathml
cmathml = property(get_content_mathml,None,None,'content MathML representation')
def make_sympy(self,xml=None):
'''
Return sympy expression for the math formula
'''
if self.the_sympy: return self.the_sympy
if xml==None: # root
if not self.is_mathml():
return my_sympify(self.expr)
if self.is_presentation_mathml():
xml = etree.fromstring(str(self.cmathml))
xml = self.fix_greek_in_mathml(xml)
self.the_sympy = self.make_sympy(xml[0])
else:
xml = etree.fromstring(self.expr)
xml = self.fix_greek_in_mathml(xml)
self.the_sympy = self.make_sympy(xml[0])
return self.the_sympy
def gettag(x):
return re.sub('{http://[^}]+}','',x.tag)
# simple math
def op_divide(*args):
if not len(args)==2:
raise Exception,'divide given wrong number of arguments!'
# print "divide: arg0=%s, arg1=%s" % (args[0],args[1])
return sympy.Mul(args[0],sympy.Pow(args[1],-1))
def op_plus(*args): return sum(args)
def op_times(*args): return reduce(operator.mul,args)
def op_minus(*args):
if len(args)==1:
return -args[0]
if not len(args)==2:
raise Exception,'minus given wrong number of arguments!'
#return sympy.Add(args[0],-args[1])
return args[0]-args[1]
opdict = {'plus': op_plus,
'divide' : operator.div,
'times' : op_times,
'minus' : op_minus,
#'plus': sympy.Add,
#'divide' : op_divide,
#'times' : sympy.Mul,
'minus' : op_minus,
'root' : sympy.sqrt,
'power' : sympy.Pow,
'sin': sympy.sin,
'cos': sympy.cos,
}
# simple sumbols
nums1dict = {'pi': sympy.pi,
}
def parsePresentationMathMLSymbol(xml):
'''
Parse <msub>, <msup>, <mi>, and <mn>
'''
tag = gettag(xml)
if tag=='mn': return xml.text
elif tag=='mi': return xml.text
elif tag=='msub': return '_'.join([parsePresentationMathMLSymbol(y) for y in xml])
elif tag=='msup': return '^'.join([parsePresentationMathMLSymbol(y) for y in xml])
raise Exception,'[parsePresentationMathMLSymbol] unknown tag %s' % tag
# parser tree for content MathML
tag = gettag(xml)
print "tag = ",tag
# first do compound objects
if tag=='apply': # apply operator
opstr = gettag(xml[0])
if opstr in opdict:
op = opdict[opstr]
args = [ self.make_sympy(x) for x in xml[1:]]
return op(*args)
else:
raise Exception,'[formula]: unknown operator tag %s' % (opstr)
elif tag=='list': # square bracket list
if gettag(xml[0])=='matrix':
return self.make_sympy(xml[0])
else:
return [ self.make_sympy(x) for x in xml ]
elif tag=='matrix':
return sympy.Matrix([ self.make_sympy(x) for x in xml ])
elif tag=='vector':
return [ self.make_sympy(x) for x in xml ]
# atoms are below
elif tag=='cn': # number
return sympy.sympify(xml.text)
return float(xml.text)
elif tag=='ci': # variable (symbol)
if len(xml)>0 and (gettag(xml[0])=='msub' or gettag(xml[0])=='msup'):
usym = parsePresentationMathMLSymbol(xml[0])
sym = sympy.Symbol(str(usym))
else:
usym = unicode(xml.text)
if 'hat' in usym:
sym = my_sympify(usym)
else:
sym = sympy.Symbol(str(usym))
return sym
else: # unknown tag
raise Exception,'[formula] unknown tag %s' % tag
sympy = property(make_sympy,None,None,'sympy representation')
def GetContentMathML(self,asciimath,mathml):
# URL = 'http://192.168.1.2:8080/snuggletex-webapp-1.2.2/ASCIIMathMLUpConversionDemo'
URL = 'http://127.0.0.1:8080/snuggletex-webapp-1.2.2/ASCIIMathMLUpConversionDemo'
if 1:
payload = {'asciiMathInput':asciimath,
'asciiMathML':mathml,
#'asciiMathML':unicode(mathml).encode('utf-8'),
}
headers = {'User-Agent':"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.13) Gecko/20080311 Firefox/2.0.0.13"}
r = requests.post(URL,data=payload,headers=headers)
r.encoding = 'utf-8'
ret = r.text
#print "encoding: ",r.encoding
# return ret
mode = 0
cmathml = []
for k in ret.split('\n'):
if 'conversion to Content MathML' in k:
mode = 1
continue
if mode==1:
if '<h3>Maxima Input Form</h3>' in k:
mode = 0
continue
cmathml.append(k)
# return '\n'.join(cmathml)
cmathml = '\n'.join(cmathml[2:])
cmathml = '<math xmlns="http://www.w3.org/1998/Math/MathML">\n' + unescape(cmathml) + '\n</math>'
# print cmathml
#return unicode(cmathml)
return cmathml
#-----------------------------------------------------------------------------
def test1():
xmlstr = '''
<math xmlns="http://www.w3.org/1998/Math/MathML">
<apply>
<plus/>
<cn>1</cn>
<cn>2</cn>
</apply>
</math>
'''
return formula(xmlstr)
def test2():
xmlstr = u'''
<math xmlns="http://www.w3.org/1998/Math/MathML">
<apply>
<plus/>
<cn>1</cn>
<apply>
<times/>
<cn>2</cn>
<ci>α</ci>
</apply>
</apply>
</math>
'''
return formula(xmlstr)
def test3():
xmlstr = '''
<math xmlns="http://www.w3.org/1998/Math/MathML">
<apply>
<divide/>
<cn>1</cn>
<apply>
<plus/>
<cn>2</cn>
<ci>γ</ci>
</apply>
</apply>
</math>
'''
return formula(xmlstr)
def test4():
xmlstr = u'''
<math xmlns="http://www.w3.org/1998/Math/MathML">
<mstyle displaystyle="true">
<mn>1</mn>
<mo>+</mo>
<mfrac>
<mn>2</mn>
<mi>α</mi>
</mfrac>
</mstyle>
</math>
'''
return formula(xmlstr)
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# File: sympy_check2.py
# Date: 02-May-12
# Author: I. Chuang <ichuang@mit.edu>
#
# Use sympy to check for expression equality
#
# Takes in math expressions given as Presentation MathML (from ASCIIMathML), converts to Content MathML using SnuggleTeX
import os, sys, string, re
import traceback
from formula import *
#-----------------------------------------------------------------------------
# check function interface
def sympy_check(expect,ans,adict={},symtab=None,extra_options=None):
options = {'__MATRIX__':False,'__ABC__':False,'__LOWER__':False}
if extra_options: options.update(extra_options)
for op in options: # find options in expect string
if op in expect:
expect = expect.replace(op,'')
options[op] = True
expect = expect.replace('__OR__','__or__') # backwards compatibility
if options['__LOWER__']:
expect = expect.lower()
ans = ans.lower()
try:
ret = check(expect,ans,
matrix=options['__MATRIX__'],
abcsym=options['__ABC__'],
symtab=symtab,
)
except Exception, err:
return {'ok': False,
'msg': 'Error %s<br/>Failed in evaluating check(%s,%s)' % (err,expect,ans)
}
return ret
#-----------------------------------------------------------------------------
# pretty generic checking function
def check(expect,given,numerical=False,matrix=False,normphase=False,abcsym=False,do_qubit=True,symtab=None,dosimplify=False):
"""
Returns dict with
'ok': True if check is good, False otherwise
'msg': response message (in HTML)
"expect" may have multiple possible acceptable answers, separated by "__OR__"
"""
if "__or__" in expect: # if multiple acceptable answers
eset = expect.split('__or__') # then see if any match
for eone in eset:
ret = check(eone,given,numerical,matrix,normphase,abcsym,do_qubit,symtab,dosimplify)
if ret['ok']:
return ret
return ret
flags = {}
if "__autonorm__" in expect:
flags['autonorm']=True
expect = expect.replace('__autonorm__','')
matrix = True
threshold = 1.0e-3
if "__threshold__" in expect:
(expect,st) = expect.split('__threshold__')
threshold = float(st)
numerical=True
if str(given)=='' and not (str(expect)==''):
return {'ok': False, 'msg': ''}
try:
xgiven = my_sympify(given,normphase,matrix,do_qubit=do_qubit,abcsym=abcsym,symtab=symtab)
except Exception,err:
return {'ok': False,'msg': 'Error %s<br/> in evaluating your expression "%s"' % (err,given)}
try:
xexpect = my_sympify(expect,normphase,matrix,do_qubit=do_qubit,abcsym=abcsym,symtab=symtab)
except Exception,err:
return {'ok': False,'msg': 'Error %s<br/> in evaluating OUR expression "%s"' % (err,expect)}
if 'autonorm' in flags: # normalize trace of matrices
try:
xgiven /= xgiven.trace()
except Exception, err:
return {'ok': False,'msg': 'Error %s<br/> in normalizing trace of your expression %s' % (err,to_latex(xgiven))}
try:
xexpect /= xexpect.trace()
except Exception, err:
return {'ok': False,'msg': 'Error %s<br/> in normalizing trace of OUR expression %s' % (err,to_latex(xexpect))}
msg = 'Your expression was evaluated as ' + to_latex(xgiven)
# msg += '<br/>Expected ' + to_latex(xexpect)
# msg += "<br/>flags=%s" % flags
if matrix and numerical:
xgiven = my_evalf(xgiven,chop=True)
dm = my_evalf(sympy.Matrix(xexpect)-sympy.Matrix(xgiven),chop=True)
msg += " = " + to_latex(xgiven)
if abs(dm.vec().norm().evalf())<threshold:
return {'ok': True,'msg': msg}
else:
pass
#msg += "dm = " + to_latex(dm) + " diff = " + str(abs(dm.vec().norm().evalf()))
#msg += "expect = " + to_latex(xexpect)
elif dosimplify:
if (sympy.simplify(xexpect)==sympy.simplify(xgiven)):
return {'ok': True,'msg': msg}
elif numerical:
if (abs((xexpect-xgiven).evalf(chop=True))<threshold):
return {'ok': True,'msg': msg}
elif (xexpect==xgiven):
return {'ok': True,'msg': msg}
#msg += "<p/>expect='%s', given='%s'" % (expect,given) # debugging
# msg += "<p/> dot test " + to_latex(dot(sympy.Symbol('x'),sympy.Symbol('y')))
return {'ok': False,'msg': msg }
#-----------------------------------------------------------------------------
# Check function interface, which takes pmathml input
def sympy_check2(expect,ans,adict={},abname=''):
msg = ''
# msg += '<p/>abname=%s' % abname
# msg += '<p/>adict=%s' % (repr(adict).replace('<','&lt;'))
threshold = 1.0e-3
DEBUG = True
# parse expected answer
try:
fexpect = my_sympify(str(expect))
except Exception,err:
msg += '<p>Error %s in parsing OUR expected answer "%s"</p>' % (err,expect)
return {'ok':False,'msg':msg}
# if expected answer is a number, try parsing provided answer as a number also
try:
fans = my_sympify(str(ans))
except Exception,err:
fans = None
if fexpect.is_number and fans and fans.is_number:
if abs(abs(fans-fexpect)/fexpect)<threshold:
return {'ok':True,'msg':msg}
else:
msg += '<p>You entered: %s</p>' % to_latex(fans)
return {'ok':False,'msg':msg}
if fexpect==fans:
msg += '<p>You entered: %s</p>' % to_latex(fans)
return {'ok':True,'msg':msg}
# convert mathml answer to formula
mmlbox = abname+'_fromjs'
if mmlbox in adict:
mmlans = adict[mmlbox]
f = formula(mmlans)
# get sympy representation of the formula
# if DEBUG: msg += '<p/> mmlans=%s' % repr(mmlans).replace('<','&lt;')
try:
fsym = f.sympy
msg += '<p>You entered: %s</p>' % to_latex(f.sympy)
except Exception,err:
msg += "<p>Error %s in converting to sympy</p>" % str(err).replace('<','&lt;')
if DEBUG: msg += "<p><pre>%s</pre></p>" % traceback.format_exc()
return {'ok':False,'msg':msg}
# compare with expected
if fexpect.is_number:
if fsym.is_number:
if abs(abs(fsym-fexpect)/fexpect)<threshold:
return {'ok':True,'msg':msg}
return {'ok':False,'msg':msg}
msg += "<p>Expecting a numerical answer!</p>"
msg += "<p>given = %s</p>" % repr(ans)
msg += "<p>fsym = %s</p>" % repr(fsym)
# msg += "<p>cmathml = <pre>%s</pre></p>" % str(f.cmathml).replace('<','&lt;')
return {'ok':False,'msg':msg}
if fexpect==fsym:
return {'ok':True,'msg':msg}
if type(fexpect)==list:
try:
xgiven = my_evalf(fsym,chop=True)
dm = my_evalf(sympy.Matrix(fexpect)-sympy.Matrix(xgiven),chop=True)
if abs(dm.vec().norm().evalf())<threshold:
return {'ok': True,'msg': msg}
except Exception,err:
msg += "<p>Error %s in comparing expected (a list) and your answer</p>" % str(err).replace('<','&lt;')
if DEBUG: msg += "<p/><pre>%s</pre>" % traceback.format_exc()
return {'ok':False,'msg':msg}
#diff = (fexpect-fsym).simplify()
#fsym = fsym.simplify()
#fexpect = fexpect.simplify()
try:
diff = (fexpect-fsym)
except Exception,err:
diff = None
if DEBUG:
msg += "<p>Got: %s</p>" % repr(fsym)
# msg += "<p/>Got: %s" % str([type(x) for x in fsym.atoms()]).replace('<','&lt;')
msg += "<p>Expecting: %s</p>" % repr(fexpect).replace('**','^').replace('hat(I)','hat(i)')
# msg += "<p/>Expecting: %s" % str([type(x) for x in fexpect.atoms()]).replace('<','&lt;')
if diff:
msg += "<p>Difference: %s</p>" % to_latex(diff)
return {'ok':False,'msg':msg,'ex':fexpect,'got':fsym}
def sctest1():
x = "1/2*(1+(k_e* Q* q)/(m *g *h^2))"
y = '''
<math xmlns="http://www.w3.org/1998/Math/MathML">
<mstyle displaystyle="true">
<mfrac>
<mn>1</mn>
<mn>2</mn>
</mfrac>
<mrow>
<mo>(</mo>
<mn>1</mn>
<mo>+</mo>
<mfrac>
<mrow>
<msub>
<mi>k</mi>
<mi>e</mi>
</msub>
<mo>⋅</mo>
<mi>Q</mi>
<mo>⋅</mo>
<mi>q</mi>
</mrow>
<mrow>
<mi>m</mi>
<mo>⋅</mo>
<mrow>
<mi>g</mi>
<mo>⋅</mo>
</mrow>
<msup>
<mi>h</mi>
<mn>2</mn>
</msup>
</mrow>
</mfrac>
<mo>)</mo>
</mrow>
</mstyle>
</math>
'''.strip()
z = "1/2(1+(k_e* Q* q)/(m *g *h^2))"
r = sympy_check2(x,z,{'a':z,'a_fromjs':y},'a')
return r
...@@ -3,7 +3,6 @@ import json ...@@ -3,7 +3,6 @@ import json
import sys import sys
from django.conf import settings from django.conf import settings
from django.conf import settings
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.context_processors import csrf from django.core.context_processors import csrf
from django.core.mail import send_mail from django.core.mail import send_mail
...@@ -60,7 +59,10 @@ def send_feedback(request): ...@@ -60,7 +59,10 @@ def send_feedback(request):
def info(request): def info(request):
''' Info page (link from main header) ''' ''' Info page (link from main header) '''
if not request.user.is_authenticated():
return redirect('/')
return render_to_response("info.html", {}) return render_to_response("info.html", {})
def mitxhome(request):
''' Home page (link from main header). List of courses. '''
if settings.ENABLE_MULTICOURSE:
return render_to_response("mitxhome.html", {})
return info(request)
...@@ -4,14 +4,15 @@ require 'tempfile' ...@@ -4,14 +4,15 @@ require 'tempfile'
# Build Constants # Build Constants
REPO_ROOT = File.dirname(__FILE__) REPO_ROOT = File.dirname(__FILE__)
BUILD_DIR = File.join(REPO_ROOT, "build") BUILD_DIR = File.join(REPO_ROOT, "build")
REPORT_DIR = File.join(REPO_ROOT, "reports")
# Packaging constants # Packaging constants
DEPLOY_DIR = "/opt/wwc" DEPLOY_DIR = "/opt/wwc"
PACKAGE_NAME = "mitx" PACKAGE_NAME = "mitx"
LINK_PATH = "/opt/wwc/mitx" LINK_PATH = "/opt/wwc/mitx"
VERSION = "0.1" PKG_VERSION = "0.1"
COMMIT = (ENV["GIT_COMMIT"] || `git rev-parse HEAD`).chomp()[0, 10] COMMIT = (ENV["GIT_COMMIT"] || `git rev-parse HEAD`).chomp()[0, 10]
BRANCH = (ENV["GIT_BRANCH"] || `git symbolic-ref -q HEAD`).chomp().gsub('refs/heads/', '').gsub('origin/', '').gsub('/', '_').downcase() BRANCH = (ENV["GIT_BRANCH"] || `git symbolic-ref -q HEAD`).chomp().gsub('refs/heads/', '').gsub('origin/', '')
BUILD_NUMBER = (ENV["BUILD_NUMBER"] || "dev").chomp() BUILD_NUMBER = (ENV["BUILD_NUMBER"] || "dev").chomp()
if BRANCH == "master" if BRANCH == "master"
...@@ -19,14 +20,41 @@ if BRANCH == "master" ...@@ -19,14 +20,41 @@ if BRANCH == "master"
else else
DEPLOY_NAME = "#{PACKAGE_NAME}-#{BRANCH}-#{BUILD_NUMBER}-#{COMMIT}" DEPLOY_NAME = "#{PACKAGE_NAME}-#{BRANCH}-#{BUILD_NUMBER}-#{COMMIT}"
end end
INSTALL_DIR_PATH = File.join(DEPLOY_DIR, DEPLOY_NAME)
PACKAGE_REPO = "packages@gp.mitx.mit.edu:/opt/pkgrepo.incoming" PACKAGE_REPO = "packages@gp.mitx.mit.edu:/opt/pkgrepo.incoming"
NORMALIZED_DEPLOY_NAME = DEPLOY_NAME.downcase().gsub(/[_\/]/, '-')
INSTALL_DIR_PATH = File.join(DEPLOY_DIR, NORMALIZED_DEPLOY_NAME)
# Set up the clean and clobber tasks # Set up the clean and clobber tasks
CLOBBER.include('build') CLOBBER.include(BUILD_DIR, REPORT_DIR, 'cover*', '.coverage')
CLEAN.include("#{BUILD_DIR}/*.deb", "#{BUILD_DIR}/util") CLEAN.include("#{BUILD_DIR}/*.deb", "#{BUILD_DIR}/util")
def select_executable(*cmds)
cmds.find_all{ |cmd| system("which #{cmd} > /dev/null 2>&1") }[0] || fail("No executables found from #{cmds.join(', ')}")
end
task :default => [:pep8, :pylint, :test]
directory REPORT_DIR
task :pep8 => REPORT_DIR do
sh("pep8 --ignore=E501 djangoapps | tee #{REPORT_DIR}/pep8.report")
end
task :pylint => REPORT_DIR do
Dir.chdir("djangoapps") do
Dir["*"].each do |app|
sh("pylint -f parseable #{app} | tee #{REPORT_DIR}/#{app}.pylint.report")
end
end
end
task :test => REPORT_DIR do
ENV['NOSE_XUNIT_FILE'] = File.join(REPORT_DIR, "nosetests.xml")
django_admin = ENV['DJANGO_ADMIN_PATH'] || select_executable('django-admin.py', 'django-admin')
sh("#{django_admin} test --settings=envs.test --pythonpath=. $(ls djangoapps)")
end
task :package do task :package do
FileUtils.mkdir_p(BUILD_DIR) FileUtils.mkdir_p(BUILD_DIR)
...@@ -53,10 +81,15 @@ task :package do ...@@ -53,10 +81,15 @@ task :package do
args = ["fakeroot", "fpm", "-s", "dir", "-t", "deb", args = ["fakeroot", "fpm", "-s", "dir", "-t", "deb",
"--after-install=#{postinstall.path}", "--after-install=#{postinstall.path}",
"--prefix=#{INSTALL_DIR_PATH}", "--prefix=#{INSTALL_DIR_PATH}",
"--exclude=build",
"--exclude=rakefile",
"--exclude=.git",
"--exclude=**/*.pyc",
"--exclude=reports",
"-C", "#{REPO_ROOT}", "-C", "#{REPO_ROOT}",
"--provides=#{PACKAGE_NAME}", "--provides=#{PACKAGE_NAME}",
"--name=#{DEPLOY_NAME}", "--name=#{NORMALIZED_DEPLOY_NAME}",
"--version=#{VERSION}", "--version=#{PKG_VERSION}",
"-a", "all", "-a", "all",
"."] "."]
system(*args) || raise("fpm failed to build the .deb") system(*args) || raise("fpm failed to build the .deb")
...@@ -64,5 +97,5 @@ task :package do ...@@ -64,5 +97,5 @@ task :package do
end end
task :publish => :package do task :publish => :package do
sh("scp #{BUILD_DIR}/#{DEPLOY_NAME}_#{VERSION}-1_all.deb #{PACKAGE_REPO}") sh("scp #{BUILD_DIR}/#{NORMALIZED_DEPLOY_NAME}_#{PKG_VERSION}*.deb #{PACKAGE_REPO}")
end end
...@@ -2,10 +2,10 @@ section.help.main-content { ...@@ -2,10 +2,10 @@ section.help.main-content {
padding: lh(); padding: lh();
h1 { h1 {
margin-top: 0; border-bottom: 1px solid #ddd;
margin-bottom: lh(); margin-bottom: lh();
margin-top: 0;
padding-bottom: lh(); padding-bottom: lh();
border-bottom: 1px solid #ddd;
} }
p { p {
...@@ -17,9 +17,9 @@ section.help.main-content { ...@@ -17,9 +17,9 @@ section.help.main-content {
} }
section.self-help { section.self-help {
float: left;
margin-bottom: lh(); margin-bottom: lh();
margin-right: flex-gutter(); margin-right: flex-gutter();
float: left;
width: flex-grid(6); width: flex-grid(6);
ul { ul {
...@@ -36,17 +36,17 @@ section.help.main-content { ...@@ -36,17 +36,17 @@ section.help.main-content {
width: flex-grid(6); width: flex-grid(6);
dl { dl {
margin-bottom: lh();
display: block; display: block;
margin-bottom: lh();
dd { dd {
margin-bottom: lh(); margin-bottom: lh();
} }
dt { dt {
font-weight: bold;
float: left;
clear: left; clear: left;
float: left;
font-weight: bold;
width: flex-grid(2, 6); width: flex-grid(2, 6);
} }
} }
......
...@@ -16,28 +16,28 @@ div.info-wrapper { ...@@ -16,28 +16,28 @@ div.info-wrapper {
list-style: none; list-style: none;
> li { > li {
padding-bottom: lh(.5);
margin-bottom: lh(.5);
@extend .clearfix; @extend .clearfix;
border-bottom: 1px solid #e3e3e3; border-bottom: 1px solid #e3e3e3;
margin-bottom: lh(.5);
padding-bottom: lh(.5);
&:first-child { &:first-child {
padding: lh(.5);
margin: 0 (-(lh(.5))) lh();
background: $cream; background: $cream;
border-bottom: 1px solid darken($cream, 10%); border-bottom: 1px solid darken($cream, 10%);
margin: 0 (-(lh(.5))) lh();
padding: lh(.5);
} }
h2 { h2 {
float: left; float: left;
width: flex-grid(2, 9);
margin: 0 flex-gutter() 0 0; margin: 0 flex-gutter() 0 0;
width: flex-grid(2, 9);
} }
section.update-description { section.update-description {
float: left; float: left;
width: flex-grid(7, 9);
margin-bottom: 0; margin-bottom: 0;
width: flex-grid(7, 9);
li { li {
margin-bottom: lh(.5); margin-bottom: lh(.5);
...@@ -55,9 +55,9 @@ div.info-wrapper { ...@@ -55,9 +55,9 @@ div.info-wrapper {
section.handouts { section.handouts {
@extend .sidebar; @extend .sidebar;
border-left: 1px solid #d3d3d3;
@include border-radius(0 4px 4px 0); @include border-radius(0 4px 4px 0);
border-right: 0; border-right: 0;
border-left: 1px solid #d3d3d3;
header { header {
@extend .bottom-border; @extend .bottom-border;
...@@ -69,32 +69,32 @@ div.info-wrapper { ...@@ -69,32 +69,32 @@ div.info-wrapper {
} }
p { p {
color: #666;
font-size: 12px;
margin-bottom: 0; margin-bottom: 0;
margin-top: 4px; margin-top: 4px;
font-size: 12px;
color: #666;
} }
} }
ol { ol {
list-style: none;
background: none; background: none;
list-style: none;
li { li {
@include box-shadow(0 1px 0 #eee); @extend .clearfix;
background: none;
border-bottom: 1px solid #d3d3d3; border-bottom: 1px solid #d3d3d3;
@include box-shadow(0 1px 0 #eee);
@include box-sizing(border-box); @include box-sizing(border-box);
@extend .clearfix;
padding: 7px lh(.75); padding: 7px lh(.75);
background: none;
position: relative; position: relative;
&.expandable, &.expandable,
&.collapsable { &.collapsable {
h4 { h4 {
padding-left: 18px;
font-style: $body-font-size; font-style: $body-font-size;
font-weight: normal; font-weight: normal;
padding-left: 18px;
} }
} }
...@@ -103,10 +103,10 @@ div.info-wrapper { ...@@ -103,10 +103,10 @@ div.info-wrapper {
margin: 7px (-(lh(.75))) 0; margin: 7px (-(lh(.75))) 0;
li { li {
padding-left: 18px + lh(.75);
@include box-shadow(inset 0 1px 0 #eee);
border-top: 1px solid #d3d3d3;
border-bottom: 0; border-bottom: 0;
border-top: 1px solid #d3d3d3;
@include box-shadow(inset 0 1px 0 #eee);
padding-left: 18px + lh(.75);
} }
} }
...@@ -116,13 +116,13 @@ div.info-wrapper { ...@@ -116,13 +116,13 @@ div.info-wrapper {
div.hitarea { div.hitarea {
background-image: url('../images/treeview-default.gif'); background-image: url('../images/treeview-default.gif');
width: 100%;
height: 100%;
max-height: 20px;
display: block; display: block;
position: absolute; height: 100%;
left: lh(.75); left: lh(.75);
margin-left: 0; margin-left: 0;
max-height: 20px;
position: absolute;
width: 100%;
&:hover { &:hover {
opacity: 0.6; opacity: 0.6;
...@@ -140,27 +140,27 @@ div.info-wrapper { ...@@ -140,27 +140,27 @@ div.info-wrapper {
h3 { h3 {
border-bottom: 0; border-bottom: 0;
text-transform: uppercase;
font-weight: bold;
color: #999;
@include box-shadow(none); @include box-shadow(none);
color: #999;
font-size: 12px; font-size: 12px;
font-weight: bold;
text-transform: uppercase;
} }
p { p {
font-size: $body-font-size;
letter-spacing: 0;
margin: 0; margin: 0;
text-transform: none; text-transform: none;
letter-spacing: 0;
font-size: $body-font-size;
a { a {
padding-right: 8px; padding-right: 8px;
&:before { &:before {
color: #ccc;
content: "•"; content: "•";
@include inline-block(); @include inline-block();
padding-right: 8px; padding-right: 8px;
color: #ccc;
} }
&:first-child { &:first-child {
...@@ -173,10 +173,10 @@ div.info-wrapper { ...@@ -173,10 +173,10 @@ div.info-wrapper {
} }
a { a {
@include transition();
color: lighten($text-color, 10%); color: lighten($text-color, 10%);
text-decoration: none;
@include inline-block(); @include inline-block();
text-decoration: none;
@include transition();
&:hover { &:hover {
color: $mit-red; color: $mit-red;
......
...@@ -4,14 +4,14 @@ div.profile-wrapper { ...@@ -4,14 +4,14 @@ div.profile-wrapper {
section.user-info { section.user-info {
@extend .sidebar; @extend .sidebar;
@include border-radius(0px 4px 4px 0);
border-left: 1px solid #d3d3d3; border-left: 1px solid #d3d3d3;
@include border-radius(0px 4px 4px 0);
border-right: 0; border-right: 0;
header { header {
padding: lh(.5) lh();
margin: 0 ;
@extend .bottom-border; @extend .bottom-border;
margin: 0 ;
padding: lh(.5) lh();
h1 { h1 {
font-size: 18px; font-size: 18px;
...@@ -20,12 +20,12 @@ div.profile-wrapper { ...@@ -20,12 +20,12 @@ div.profile-wrapper {
} }
a { a {
color: #999;
font-size: 12px;
position: absolute; position: absolute;
top: 13px;
right: lh(.5); right: lh(.5);
text-transform: uppercase; text-transform: uppercase;
font-size: 12px; top: 13px;
color: #999;
&:hover { &:hover {
color: #555; color: #555;
...@@ -37,14 +37,14 @@ div.profile-wrapper { ...@@ -37,14 +37,14 @@ div.profile-wrapper {
list-style: none; list-style: none;
li { li {
@include transition(); border-bottom: 1px solid #d3d3d3;
@include box-shadow(0 1px 0 #eee);
color: lighten($text-color, 10%); color: lighten($text-color, 10%);
display: block; display: block;
text-decoration: none;
@include box-shadow(0 1px 0 #eee);
padding: 7px lh(); padding: 7px lh();
border-bottom: 1px solid #d3d3d3;
position: relative; position: relative;
text-decoration: none;
@include transition();
div#location_sub, div#language_sub { div#location_sub, div#language_sub {
font-weight: bold; font-weight: bold;
...@@ -57,9 +57,9 @@ div.profile-wrapper { ...@@ -57,9 +57,9 @@ div.profile-wrapper {
input { input {
&[type="text"] { &[type="text"] {
@include box-sizing(border-box);
margin: lh(.5) 0; margin: lh(.5) 0;
width: 100%; width: 100%;
@include box-sizing(border-box);
} }
&[type="input"]{ &[type="input"]{
...@@ -80,12 +80,12 @@ div.profile-wrapper { ...@@ -80,12 +80,12 @@ div.profile-wrapper {
a.edit-email, a.edit-email,
a.name-edit, a.name-edit,
a.email-edit { a.email-edit {
color: #999;
font-size: 12px;
position: absolute; position: absolute;
top: 9px;
right: lh(.5); right: lh(.5);
text-transform: uppercase; text-transform: uppercase;
font-size: 12px; top: 9px;
color: #999;
&:hover { &:hover {
color: #555; color: #555;
...@@ -93,10 +93,10 @@ div.profile-wrapper { ...@@ -93,10 +93,10 @@ div.profile-wrapper {
} }
p { p {
color: #999;
font-size: 12px; font-size: 12px;
margin-bottom: 0; margin-bottom: 0;
margin-top: 4px; margin-top: 4px;
color: #999;
} }
a.deactivate { a.deactivate {
...@@ -132,10 +132,10 @@ div.profile-wrapper { ...@@ -132,10 +132,10 @@ div.profile-wrapper {
padding: 7px lh(); padding: 7px lh();
h2 { h2 {
margin-top: 0; font-size: $body-font-size;
font-weight: bold; font-weight: bold;
margin-top: 0;
text-transform: uppercase; text-transform: uppercase;
font-size: $body-font-size;
} }
} }
} }
...@@ -148,14 +148,14 @@ div.profile-wrapper { ...@@ -148,14 +148,14 @@ div.profile-wrapper {
@extend .clearfix; @extend .clearfix;
h1 { h1 {
margin: 0;
float: left; float: left;
margin: 0;
} }
} }
div#grade-detail-graph { div#grade-detail-graph {
width: 100%;
min-height: 300px; min-height: 300px;
width: 100%;
} }
> ol { > ol {
......
...@@ -3,8 +3,8 @@ div.book-wrapper { ...@@ -3,8 +3,8 @@ div.book-wrapper {
section.book-sidebar { section.book-sidebar {
@extend .sidebar; @extend .sidebar;
@include box-sizing(border-box);
@extend .tran; @extend .tran;
@include box-sizing(border-box);
ul#booknav { ul#booknav {
font-size: 12px; font-size: 12px;
...@@ -22,14 +22,14 @@ div.book-wrapper { ...@@ -22,14 +22,14 @@ div.book-wrapper {
padding-left: 30px; padding-left: 30px;
div.hitarea { div.hitarea {
margin-left: -22px;
background-image: url('../images/treeview-default.gif'); background-image: url('../images/treeview-default.gif');
margin-left: -22px;
position: relative; position: relative;
top: 4px; top: 4px;
&:hover { &:hover {
opacity: 0.6;
filter: alpha(opacity=60); filter: alpha(opacity=60);
opacity: 0.6;
} }
} }
...@@ -63,13 +63,13 @@ div.book-wrapper { ...@@ -63,13 +63,13 @@ div.book-wrapper {
li { li {
&.last { &.last {
float: left;
display: block; display: block;
float: left;
a { a {
@include box-shadow(inset -1px 0 0 lighten(#f6efd4, 5%));
border-right: 1px solid darken(#f6efd4, 20%);
border-left: 0; border-left: 0;
border-right: 1px solid darken(#f6efd4, 20%);
@include box-shadow(inset -1px 0 0 lighten(#f6efd4, 5%));
} }
} }
...@@ -81,10 +81,10 @@ div.book-wrapper { ...@@ -81,10 +81,10 @@ div.book-wrapper {
} }
&.bottom-nav { &.bottom-nav {
margin-top: lh();
margin-bottom: -(lh());
border-bottom: 0; border-bottom: 0;
border-top: 1px solid #EDDFAA; border-top: 1px solid #EDDFAA;
margin-bottom: -(lh());
margin-top: lh();
} }
} }
...@@ -110,18 +110,18 @@ div.book-wrapper { ...@@ -110,18 +110,18 @@ div.book-wrapper {
} }
h2 { h2 {
padding: 0;
visibility: hidden; visibility: hidden;
width: 10px; width: 10px;
padding: 0;
} }
} }
ul#booknav { ul#booknav {
max-height: 100px;
overflow: hidden;
padding: 0;
visibility: hidden; visibility: hidden;
width: 10px; width: 10px;
padding: 0;
overflow: hidden;
max-height: 100px;
} }
} }
......
...@@ -35,10 +35,10 @@ img { ...@@ -35,10 +35,10 @@ img {
} }
#{$all-text-inputs}, textarea { #{$all-text-inputs}, textarea {
@include box-shadow(0 -1px 0 #fff);
@include linear-gradient(#eee, #fff);
border: 1px solid #999; border: 1px solid #999;
@include box-shadow(0 -1px 0 #fff);
font: $body-font-size $body-font-family; font: $body-font-size $body-font-family;
@include linear-gradient(#eee, #fff);
padding: 4px; padding: 4px;
&:focus { &:focus {
...@@ -63,7 +63,7 @@ a { ...@@ -63,7 +63,7 @@ a {
p &, li > &, span > &, .inline { p &, li > &, span > &, .inline {
border-bottom: 1px solid #bbb; border-bottom: 1px solid #bbb;
font-style: italic; // font-style: italic;
} }
&:hover, &:focus { &:hover, &:focus {
......
.clearfix:after { .clearfix:after {
clear: both;
content: "."; content: ".";
display: block; display: block;
height: 0; height: 0;
clear: both;
visibility: hidden; visibility: hidden;
} }
...@@ -40,27 +40,27 @@ h1.top-header { ...@@ -40,27 +40,27 @@ h1.top-header {
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
&:hover, &:focus { &:hover, &:focus {
border: 1px solid darken(#888, 20%);
@include box-shadow(inset 0 1px 0 lighten(#888, 20%), 0 0 3px #ccc); @include box-shadow(inset 0 1px 0 lighten(#888, 20%), 0 0 3px #ccc);
@include linear-gradient(lighten(#888, 10%), darken(#888, 5%)); @include linear-gradient(lighten(#888, 10%), darken(#888, 5%));
border: 1px solid darken(#888, 20%);
} }
} }
.light-button, a.light-button { .light-button, a.light-button {
@include box-shadow(inset 0 1px 0 #fff);
@include linear-gradient(#fff, lighten(#888, 40%));
@include border-radius(3px);
border: 1px solid #ccc; border: 1px solid #ccc;
padding: 4px 8px; @include border-radius(3px);
@include box-shadow(inset 0 1px 0 #fff);
color: #666; color: #666;
cursor: pointer;
font: normal $body-font-size $body-font-family; font: normal $body-font-size $body-font-family;
@include linear-gradient(#fff, lighten(#888, 40%));
padding: 4px 8px;
text-decoration: none; text-decoration: none;
cursor: pointer;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
&:hover, &:focus { &:hover, &:focus {
@include linear-gradient(#fff, lighten(#888, 37%));
border: 1px solid #ccc; border: 1px solid #ccc;
@include linear-gradient(#fff, lighten(#888, 37%));
text-decoration: none; text-decoration: none;
} }
} }
...@@ -70,8 +70,8 @@ h1.top-header { ...@@ -70,8 +70,8 @@ h1.top-header {
color: $mit-red; color: $mit-red;
&:hover { &:hover {
text-decoration: none;
color: darken($mit-red, 20%); color: darken($mit-red, 20%);
text-decoration: none;
} }
} }
} }
...@@ -110,13 +110,13 @@ h1.top-header { ...@@ -110,13 +110,13 @@ h1.top-header {
} }
a { a {
font-style: normal;
border: none; border: none;
font-style: normal;
} }
.bottom-border { .bottom-border {
@include box-shadow(0 1px 0 #eee);
border-bottom: 1px solid #d3d3d3; border-bottom: 1px solid #d3d3d3;
@include box-shadow(0 1px 0 #eee);
} }
@media print { @media print {
...@@ -124,10 +124,10 @@ h1.top-header { ...@@ -124,10 +124,10 @@ h1.top-header {
} }
h3 { h3 {
border: none;
border-bottom: 1px solid #d3d3d3;
@extend .bottom-border; @extend .bottom-border;
background: none; background: none;
border: none;
border-bottom: 1px solid #d3d3d3;
color: #000; color: #000;
font-weight: normal; font-weight: normal;
margin: 0; margin: 0;
...@@ -172,8 +172,8 @@ h1.top-header { ...@@ -172,8 +172,8 @@ h1.top-header {
position: relative; position: relative;
h2 { h2 {
padding-right: 20px;
margin: 0; margin: 0;
padding-right: 20px;
} }
a { a {
...@@ -205,10 +205,10 @@ h1.top-header { ...@@ -205,10 +205,10 @@ h1.top-header {
border-bottom: 1px solid darken($cream, 10%); border-bottom: 1px solid darken($cream, 10%);
@include box-shadow(inset 0 1px 0 #fff, inset 1px 0 0 #fff); @include box-shadow(inset 0 1px 0 #fff, inset 1px 0 0 #fff);
font-size: 12px; font-size: 12px;
height:46px;
line-height: 46px;
margin: (-$body-line-height) (-$body-line-height) $body-line-height; margin: (-$body-line-height) (-$body-line-height) $body-line-height;
text-shadow: 0 1px 0 #fff; text-shadow: 0 1px 0 #fff;
line-height: 46px;
height:46px;
@media print { @media print {
display: none; display: none;
...@@ -242,10 +242,10 @@ h1.top-header { ...@@ -242,10 +242,10 @@ h1.top-header {
} }
p.ie-warning { p.ie-warning {
background: yellow;
display: block !important; display: block !important;
line-height: 1.3em; line-height: 1.3em;
background: yellow; margin-bottom: 0;
padding: lh(); padding: lh();
text-align: left; text-align: left;
margin-bottom: 0;
} }
// Flexible grid
@function flex-grid($columns, $container-columns: $fg-max-columns) {
$width: $columns * $fg-column + ($columns - 1) * $fg-gutter;
$container-width: $container-columns * $fg-column + ($container-columns - 1) * $fg-gutter;
@return percentage($width / $container-width);
}
// Flexible grid gutter
@function flex-gutter($container-columns: $fg-max-columns, $gutter: $fg-gutter) {
$container-width: $container-columns * $fg-column + ($container-columns - 1) * $fg-gutter;
@return percentage($gutter / $container-width);
}
// Percentage of container calculator
@function perc($width, $container-width: $max-width) {
@return percentage($width / $container-width);
}
// Line-height // Line-height
@function lh($amount: 1) { @function lh($amount: 1) {
@return $body-line-height * $amount; @return $body-line-height * $amount;
......
// Variables // Variables
// ---------------------------------------- // // ---------------------------------------- //
// fonts // Type
$body-font-family: "Open Sans", "Lucida Grande", "Lucida Sans Unicode", "Lucida Sans", Geneva, Verdana, sans-serif;; $body-font-family: "Open Sans", "Lucida Grande", "Lucida Sans Unicode", "Lucida Sans", Geneva, Verdana, sans-serif;
$body-font-size: 14px; $body-font-size: 14px;
// grid
$columns: 12;
$column-width: 80px;
$gutter-width: 25px;
$max-width: ($columns * $column-width) + (($columns - 1) * $gutter-width);
$gw-column: perc($column-width);
$gw-gutter: perc($gutter-width);
$body-line-height: golden-ratio($body-font-size, 1); $body-line-height: golden-ratio($body-font-size, 1);
//Flexible grid // Grid
$fg-column: $column-width; $fg-column: 80px;
$fg-gutter: $gutter-width; $fg-gutter: 25px;
$fg-max-columns: $columns; $fg-max-columns: 12;
$fg-max-width: 1400px; $fg-max-width: 1400px;
$fg-min-width: 810px; $fg-min-width: 810px;
// color // Color
$light-gray: #ddd; $light-gray: #ddd;
$dark-gray: #333; $dark-gray: #333;
$mit-red: #993333; $mit-red: #993333;
$cream: #F6EFD4; $cream: #F6EFD4;
$text-color: $dark-gray; $text-color: $dark-gray;
$border-color: $light-gray; $border-color: $light-gray;
// JM MOSFET AMPLIFIER // JM MOSFET AMPLIFIER
div#graph-container { section.tool-wrapper {
@extend .clearfix; @extend .clearfix;
border-top: 1px solid #ddd; background: #073642;
padding-top: lh(1.0); border-bottom: 1px solid darken(#002b36, 10%);
border-top: 1px solid darken(#002b36, 10%);
canvas#graph { @include box-shadow(inset 0 0 0 4px darken(#094959, 2%));
width: flex-grid(4.5, 9); color: #839496;
float: left; display: table;
margin-right: flex-gutter(9); margin: lh() (-(lh())) 0;
}
div#graph-container {
div.graph-controls { background: none;
width: flex-grid(4.5, 9); @include box-sizing(border-box);
float: left; display: table-cell;
padding: lh();
select#musicTypeSelect { vertical-align: top;
display: block; width: flex-grid(4.5, 9) + flex-gutter(9);
margin-bottom: lh();
.ui-widget-content {
background: none;
border: none;
@include border-radius(0);
} }
div#graph-output { canvas {
display: block; width: 100%;
margin-bottom: lh();
} }
div#graph-listen { ul.ui-tabs-nav {
display: block; background: darken(#073642, 2%);
margin-bottom: lh(); border-bottom: 1px solid darken(#073642, 8%);
} @include border-radius(0);
margin: (-(lh())) (-(lh())) 0;
p { padding: 0;
margin-bottom: lh(.5); position: relative;
} width: 110%;
div#label { li {
display: inline-block; background: none;
} border: none;
@include border-radius(0);
input#playButton { color: #fff;
display: block; margin-bottom: 0;
&.ui-tabs-selected {
background-color: #073642;
border-left: 1px solid darken(#073642, 8%);
border-right: 1px solid darken(#073642, 8%);
&:first-child {
border-left: none;
}
a {
color: #eee8d5;
}
}
a {
border: none;
color: #839496;
font: bold 12px $body-font-family;
letter-spacing: 1px;
text-transform: uppercase;
&:hover {
color: #eee8d5;
}
}
}
} }
} }
}
div#schematic-container { div#controlls-container {
@extend .clearfix; @extend .clearfix;
background: darken(#073642, 2%);
canvas { border-right: 1px solid darken(#002b36, 6%);
@include box-shadow(1px 0 0 lighten(#002b36, 6%), inset 0 0 0 4px darken(#094959, 6%));
@include box-sizing(border-box);
display: table-cell;
padding: lh();
vertical-align: top;
width: flex-grid(4.5, 9); width: flex-grid(4.5, 9);
float: left;
margin-right: flex-gutter(9);
}
div.schematic-sliders { div.graph-controls {
width: flex-grid(4.5, 9);
float: left; div.music-wrapper {
@extend .clearfix;
div.slider-label#vs { border-bottom: 1px solid darken(#073642, 10%);
margin-top: lh(2.0); @include box-shadow(0 1px 0 lighten(#073642, 2%));
margin-bottom: lh();
padding: 0 0 lh();
input#playButton {
border-color: darken(#002b36, 6%);
@include button(simple, lighten( #586e75, 5% ));
display: block;
float: right;
font: bold 14px $body-font-family;
&:active {
@include box-shadow(none);
}
&[value="Stop"] {
@include button(simple, darken(#268bd2, 30%));
font: bold 14px $body-font-family;
&:active {
@include box-shadow(none);
}
}
}
}
div.inputs-wrapper {
@extend .clearfix;
border-bottom: 1px solid darken(#073642, 10%);
@include box-shadow(0 1px 0 lighten(#073642, 2%));
@include clearfix;
margin-bottom: lh();
margin-bottom: lh();
padding: 0 0 lh();
}
p {
font-weight: bold;
@include inline-block();
margin: 0;
text-shadow: 0 -1px 0 darken(#073642, 10%);
-webkit-font-smoothing: antialiased;
}
ul {
@include inline-block();
margin-bottom: 0;
li {
@include inline-block();
margin-bottom: 0;
input {
margin-right: 5px;
}
}
}
div#graph-listen {
display: block;
float: left;
margin-bottom: 0;
margin-right: 20px;
margin-top: 8px;
text-align: right;
}
} }
div.slider-label { label {
margin-bottom: lh(0.5); @include border-radius(2px);
color: #fff;
font-weight: bold;
padding: 3px;
-webkit-font-smoothing: antialiased;
} }
div.slider { //MOSFET AMPLIFIER
margin-bottom: lh(1); label[for="vinCheckbox"], label[for="vinRadioButton"]{
color: desaturate(#00bfff, 50%);
} }
}
}
//End JM MOSFET AMPLIFIER
// Labels
div.graph-controls, div#graph-listen {
label { label[for="voutCheckbox"], label[for="voutRadioButton"]{
@include border-radius(2px);
font-weight: bold;
padding: 3px;
}
//MOSFET AMPLIFIER
label[for="vinCheckbox"], label[for="vinRadioButton"]{
color: desaturate(#00bfff, 50%);
}
label[for="voutCheckbox"], label[for="voutRadioButton"]{
color: darken(#ffcf48, 20%); color: darken(#ffcf48, 20%);
} }
label[for="vrCheckbox"], label[for="vrRadioButton"]{
label[for="vrCheckbox"], label[for="vrRadioButton"]{
color: desaturate(#1df914, 40%); color: desaturate(#1df914, 40%);
} }
//RC Filters
label[for="vcCheckbox"], label[for="vcRadioButton"]{ //RC Filters
label[for="vcCheckbox"], label[for="vcRadioButton"]{
color: darken(#ffcf48, 20%); color: darken(#ffcf48, 20%);
} }
//RLC Series
label[for="vlCheckbox"], label[for="vlRadioButton"]{ //RLC Series
label[for="vlCheckbox"], label[for="vlRadioButton"]{
color: desaturate(#d33682, 40%); color: desaturate(#d33682, 40%);
}
div.schematic-sliders {
div.top-sliders {
@extend .clearfix;
border-bottom: 1px solid darken(#073642, 10%);
@include box-shadow(0 1px 0 lighten(#073642, 2%));
margin-bottom: lh();
padding: 0 0 lh();
select#musicTypeSelect {
font: 16px $body-font-family;
@include inline-block();
margin-bottom: 0;
}
p {
font-weight: bold;
@include inline-block();
margin: 0 lh(.5) lh() 0;
text-shadow: 0 -1px 0 darken(#073642, 10%);
-webkit-font-smoothing: antialiased;
}
}
div.slider-label {
font-weight: bold;
margin-bottom: lh(0.5);
text-shadow: 0 -1px 0 darken(#073642, 10%);
-webkit-font-smoothing: antialiased;
}
div.slider {
margin-bottom: lh(1);
&.ui-slider-horizontal {
background: darken(#002b36, 2%);
border: 1px solid darken(#002b36, 8%);
@include box-shadow(none);
height: 0.4em;
}
.ui-slider-handle {
background: lighten( #586e75, 5% ) url('/static/images/amplifier-slider-handle.png') center no-repeat;
border: 1px solid darken(#002b36, 8%);
@include box-shadow(inset 0 1px 0 lighten( #586e75, 20% ));
margin-top: -.3em;
&:hover, &:active {
background-color: lighten( #586e75, 10% );
}
}
}
}
} }
} }
html {
height: 100%;
max-height: 100%;
}
body.courseware {
height: 100%;
max-height: 100%;
}
div.course-wrapper { div.course-wrapper {
@extend .table-wrapper; @extend .table-wrapper;
...@@ -7,9 +17,12 @@ div.course-wrapper { ...@@ -7,9 +17,12 @@ div.course-wrapper {
section.course-content { section.course-content {
@extend .content; @extend .content;
overflow: hidden;
@include border-top-right-radius(4px);
@include border-bottom-right-radius(4px);
h1 { h1 {
@extend .top-header; margin: 0 0 lh();
} }
p { p {
...@@ -159,18 +172,71 @@ div.course-wrapper { ...@@ -159,18 +172,71 @@ div.course-wrapper {
margin-bottom: 15px; margin-bottom: 15px;
padding: 0 0 15px; padding: 0 0 15px;
header {
@extend h1.top-header;
margin-bottom: -16px;
h1 {
margin: 0;
}
h2 {
float: right;
margin-right: 0;
margin-top: 8px;
text-align: right;
padding-right: 0;
}
}
&:last-child { &:last-child {
border-bottom: none; border-bottom: none;
margin-bottom: 0; margin-bottom: 0;
padding-bottom: 0; padding-bottom: 0;
} }
ul {
list-style: disc outside none;
padding-left: 1em;
}
nav.sequence-bottom {
ul {
list-style: none;
padding: 0;
}
}
} }
} }
section.tutorials { section.tutorials {
h2 {
margin-bottom: lh();
}
ul { ul {
list-style: disc outside none; margin: 0;
margin-left: lh(); @include clearfix();
li {
width: flex-grid(3, 9);
float: left;
margin-right: flex-gutter(9);
margin-bottom: lh();
&:nth-child(3n) {
margin-right: 0;
}
&:nth-child(3n+1) {
clear: both;
}
a {
font-weight: bold;
}
}
} }
} }
...@@ -202,6 +268,24 @@ div.course-wrapper { ...@@ -202,6 +268,24 @@ div.course-wrapper {
} }
} }
} }
div.ui-tabs {
border: 0;
@include border-radius(0);
margin: 0;
padding: 0;
.ui-tabs-nav {
background: none;
border: 0;
margin-bottom: lh(.5);
}
.ui-tabs-panel {
@include border-radius(0);
padding: 0;
}
}
} }
&.closed { &.closed {
......
nav.sequence-nav { nav.sequence-nav {
@extend .topbar; @extend .topbar;
@include box-sizing(border-box); border-bottom: 1px solid darken($cream, 20%);
margin-bottom: $body-line-height; margin-bottom: $body-line-height;
position: relative; position: relative;
@include border-top-right-radius(4px);
ol { ol {
border-bottom: 1px solid darken($cream, 20%);
@include box-sizing(border-box); @include box-sizing(border-box);
display: table; display: table;
height: 100%;
padding-right: flex-grid(1, 9); padding-right: flex-grid(1, 9);
width: 100%; width: 100%;
...@@ -61,116 +62,117 @@ nav.sequence-nav { ...@@ -61,116 +62,117 @@ nav.sequence-nav {
display: block; display: block;
height: 17px; height: 17px;
padding: 15px 0 14px; padding: 15px 0 14px;
position: relative;
@include transition(all, .4s, $ease-in-out-quad); @include transition(all, .4s, $ease-in-out-quad);
width: 100%; width: 100%;
// @media screen and (max-width: 800px) { //video
// padding: 12px 8px; &.seq_video_inactive {
// } @extend .inactive;
background-image: url('../images/sequence-nav/video-icon-normal.png');
//video background-position: center;
&.seq_video_inactive { }
@extend .inactive;
background-image: url('../images/sequence-nav/video-icon-normal.png');
background-position: center;
}
&.seq_video_visited {
@extend .visited;
background-image: url('../images/sequence-nav/video-icon-visited.png');
background-position: center;
}
&.seq_video_active { &.seq_video_visited {
@extend .active; @extend .visited;
background-image: url('../images/sequence-nav/video-icon-current.png'); background-image: url('../images/sequence-nav/video-icon-visited.png');
background-position: center; background-position: center;
} }
//other &.seq_video_active {
&.seq_other_inactive { @extend .active;
@extend .inactive; background-image: url('../images/sequence-nav/video-icon-current.png');
background-image: url('../images/sequence-nav/document-icon-normal.png'); background-position: center;
background-position: center; }
}
&.seq_other_visited { //other
@extend .visited; &.seq_other_inactive {
background-image: url('../images/sequence-nav/document-icon-visited.png'); @extend .inactive;
background-position: center; background-image: url('../images/sequence-nav/document-icon-normal.png');
} background-position: center;
}
&.seq_other_active { &.seq_other_visited {
@extend .active; @extend .visited;
background-image: url('../images/sequence-nav/document-icon-current.png'); background-image: url('../images/sequence-nav/document-icon-visited.png');
background-position: center; background-position: center;
} }
//vertical & problems &.seq_other_active {
&.seq_vertical_inactive, &.seq_problem_inactive { @extend .active;
@extend .inactive; background-image: url('../images/sequence-nav/document-icon-current.png');
background-image: url('../images/sequence-nav/list-icon-normal.png'); background-position: center;
background-position: center; }
}
&.seq_vertical_visited, &.seq_problem_visited { //vertical & problems
@extend .visited; &.seq_vertical_inactive, &.seq_problem_inactive {
background-image: url('../images/sequence-nav/list-icon-visited.png'); @extend .inactive;
background-position: center; background-image: url('../images/sequence-nav/list-icon-normal.png');
} background-position: center;
}
&.seq_vertical_active, &.seq_problem_active { &.seq_vertical_visited, &.seq_problem_visited {
@extend .active; @extend .visited;
background-image: url('../images/sequence-nav/list-icon-current.png'); background-image: url('../images/sequence-nav/list-icon-visited.png');
background-position: center; background-position: center;
}
} }
p { &.seq_vertical_active, &.seq_problem_active {
// display: none; @extend .active;
// visibility: hidden; background-image: url('../images/sequence-nav/list-icon-current.png');
background: #333; background-position: center;
color: #fff;
line-height: lh();
margin: 0px 0 0 -5px;
opacity: 0;
padding: 6px;
position: absolute;
text-shadow: 0 -1px 0 #000;
@include transition(all, .6s, $ease-in-out-quart);
white-space: pre-wrap;
z-index: 99;
&.shown {
margin-top: 4px;
opacity: 1;
} }
&:empty { p {
background: none; background: #333;
color: #fff;
display: none;
line-height: lh();
left: 0px;
opacity: 0;
padding: 6px;
position: absolute;
top: 48px;
text-shadow: 0 -1px 0 #000;
@include transition(all, .1s, $ease-in-out-quart);
white-space: pre;
z-index: 99;
&:empty {
background: none;
&::after {
display: none;
}
}
&::after { &::after {
display: none; background: #333;
content: " ";
display: block;
height: 10px;
left: 18px;
position: absolute;
top: -5px;
@include transform(rotate(45deg));
width: 10px;
} }
} }
&::after { &:hover {
background: #333; p {
content: " "; display: block;
display: block; margin-top: 4px;
height: 10px; opacity: 1;
left: 18px; }
position: absolute;
top: -5px;
@include transform(rotate(45deg));
width: 10px;
} }
} }
} }
} }
ul { ul {
margin-right: 1px; list-style: none !important;
height: 100%;
position: absolute; position: absolute;
right: 0; right: 0;
top: 0; top: 0;
...@@ -220,6 +222,7 @@ nav.sequence-nav { ...@@ -220,6 +222,7 @@ nav.sequence-nav {
&.next { &.next {
a { a {
background-image: url('../images/sequence-nav/next-icon.png'); background-image: url('../images/sequence-nav/next-icon.png');
@include border-top-right-radius(4px);
&:hover { &:hover {
background-color: none; background-color: none;
...@@ -232,26 +235,20 @@ nav.sequence-nav { ...@@ -232,26 +235,20 @@ nav.sequence-nav {
section.course-content { section.course-content {
position: relative;
div#seq_content {
margin-bottom: 60px;
}
nav.sequence-bottom { nav.sequence-bottom {
bottom: (-(lh())); margin: lh(2) 0 0;
position: relative; text-align: center;
ul { ul {
@extend .clearfix; @extend .clearfix;
background-color: darken(#F6EFD4, 5%); background-color: darken(#F6EFD4, 5%);
background-color: darken($cream, 5%); background-color: darken($cream, 5%);
border: 1px solid darken(#f6efd4, 20%); border: 1px solid darken(#f6efd4, 20%);
border-bottom: 0; @include border-radius(3px);
@include border-radius(3px 3px 0 0);
@include box-shadow(inset 0 0 0 1px lighten(#f6efd4, 5%)); @include box-shadow(inset 0 0 0 1px lighten(#f6efd4, 5%));
margin: 0 auto; @include inline-block();
overflow: hidden;
width: 106px;
li { li {
float: left; float: left;
...@@ -264,15 +261,13 @@ section.course-content { ...@@ -264,15 +261,13 @@ section.course-content {
background-repeat: no-repeat; background-repeat: no-repeat;
border-bottom: none; border-bottom: none;
display: block; display: block;
display: block; padding: lh(.5) 4px;
padding: lh(.75) 4px;
text-indent: -9999px; text-indent: -9999px;
@include transition(all, .4s, $ease-in-out-quad); @include transition(all, .4s, $ease-in-out-quad);
width: 45px; width: 45px;
&:hover { &:hover {
background-color: darken($cream, 10%); background-color: darken($cream, 10%);
color: darken(#F6EFD4, 60%);
color: darken($cream, 60%); color: darken($cream, 60%);
opacity: .5; opacity: .5;
text-decoration: none; text-decoration: none;
...@@ -288,6 +283,7 @@ section.course-content { ...@@ -288,6 +283,7 @@ section.course-content {
&.prev { &.prev {
a { a {
background-image: url('../images/sequence-nav/previous-icon.png'); background-image: url('../images/sequence-nav/previous-icon.png');
border-right: 1px solid darken(#f6efd4, 20%);
&:hover { &:hover {
background-color: none; background-color: none;
......
...@@ -26,6 +26,7 @@ section.course-index { ...@@ -26,6 +26,7 @@ section.course-index {
} }
&.ui-state-active { &.ui-state-active {
@include background-image(linear-gradient(-90deg, rgb(245,245,245), rgb(225,225,225)));
@extend .active; @extend .active;
} }
} }
...@@ -33,58 +34,99 @@ section.course-index { ...@@ -33,58 +34,99 @@ section.course-index {
ul.ui-accordion-content { ul.ui-accordion-content {
@include border-radius(0); @include border-radius(0);
@include box-shadow( inset -1px 0 0 #e6e6e6); @include box-shadow(inset -1px 0 0 #e6e6e6);
background: #dadada; background: #dadada;
border: none; border: none;
border-bottom: 1px solid #c3c3c3; border-bottom: 1px solid #c3c3c3;
font-size: 12px; font-size: 12px;
margin: 0; margin: 0;
// overflow: visible; padding: 1em 1.5em;
li { li {
background: transparent;
border: 1px solid transparent;
@include border-radius(4px);
margin-bottom: lh(.5);
position: relative; position: relative;
padding: 5px 36px 5px 10px;
&.active {
font-weight: bold;
p.subtitle {
font-weight: normal;
}
// &:after {
// content: " ";
// width: 16px;
// height: 16px;
// position: absolute;
// right: -35px;
// top: 7px;
// display: block;
// background-color: #dadada;
// border-top: 1px solid #c3c3c3;
// border-right: 1px solid #c3c3c3;
// z-index: 99;
// @include transform(rotate(45deg));
// }
}
a { a {
text-decoration: none; text-decoration: none;
margin-bottom: lh(.5);
display: block; display: block;
color: #000; color: #666;
&:hover {
color: #666;
}
p { p {
font-weight: bold;
margin-bottom: 0; margin-bottom: 0;
&.subtitle { span.subtitle {
color: #666; color: #666;
font-weight: normal;
display: block;
} }
} }
} }
&:after {
background: transparent;
border-top: 1px solid rgb(180,180,180);
border-right: 1px solid rgb(180,180,180);
content: "";
display: block;
height: 12px;
margin-top: -6px;
opacity: 0;
position: absolute;
top: 50%;
right: 30px;
@include transform(rotate(45deg));
width: 12px;
}
&:hover {
@include background-image(linear-gradient(-90deg, rgba(245,245,245, 0.4), rgba(230,230,230, 0.4)));
border-color: rgb(200,200,200);
&:after {
opacity: 1;
right: 15px;
@include transition(all, 0.2s, linear);
}
> a p {
color: #333;
}
}
&:active {
@include box-shadow(inset 0 1px 14px 0 rgba(0,0,0, 0.1));
top: 1px;
&:after {
opacity: 1;
right: 15px;
}
}
&.active {
background: rgb(240,240,240);
@include background-image(linear-gradient(-90deg, rgb(245,245,245), rgb(230,230,230)));
border-color: rgb(200,200,200);
font-weight: bold;
> a p {
color: #333;
}
span.subtitle {
font-weight: normal;
}
&:after {
opacity: 1;
right: 15px;
}
}
} }
} }
} }
......
@-moz-document url-prefix() {
a.add-fullscreen {
display: none !important;
}
}
section.course-content { section.course-content {
.dullify {
opacity: .4;
@include transition();
&:hover {
opacity: 1;
}
}
div.video-subtitles { div.video-subtitles {
padding: 6px lh();
margin: 0 (-(lh()));
border-top: 1px solid #e1e1e1;
border-bottom: 1px solid #e1e1e1;
background: #f3f3f3; background: #f3f3f3;
position: relative; border-bottom: 1px solid #e1e1e1;
border-top: 1px solid #e1e1e1;
@include clearfix(); @include clearfix();
display: block;
margin: 0 (-(lh()));
padding: 6px lh();
div.video-wrapper { div.video-wrapper {
float: left; float: left;
width: flex-grid(6, 9);
margin-right: flex-gutter(9); margin-right: flex-gutter(9);
width: flex-grid(6, 9);
div.video-player { div.video-player {
position: relative;
padding-bottom: 56.25%;
padding-top: 30px;
height: 0; height: 0;
overflow: hidden; overflow: hidden;
padding-bottom: 56.25%;
padding-top: 30px;
position: relative;
object { object {
height: 100%;
left: 0;
position: absolute; position: absolute;
top: 0; top: 0;
left: 0;
width: 100%; width: 100%;
height: 100%;
} }
iframe#html5_player { iframe#html5_player {
border: none; border: none;
display: none; display: none;
height: 100%;
left: 0;
position: absolute; position: absolute;
top: 0; top: 0;
left: 0;
width: 100%; width: 100%;
height: 100%;
} }
} }
// ul {
// float: left;
// li {
// margin-top: 5px;
// display: inline-block;
// cursor: pointer;
// border: 0;
// padding: 0;
// div {
// &:empty {
// display: none;
// }
// }
// }
// }
section.video-controls { section.video-controls {
@extend .clearfix; @extend .clearfix;
background: #333; background: #333;
position: relative;
border: 1px solid #000; border: 1px solid #000;
border-top: 0; border-top: 0;
color: #ccc; color: #ccc;
position: relative;
&:hover {
ul, div {
opacity: 1;
}
}
div#slider { div#slider {
@extend .clearfix; @extend .clearfix;
@include border-radius(0);
@include box-shadow(inset 0 1px 0 #eee, 0 1px 0 #555);
background: #c2c2c2; background: #c2c2c2;
border: none; border: none;
border-top: 1px solid #000;
border-bottom: 1px solid #000; border-bottom: 1px solid #000;
@include border-radius(0);
border-top: 1px solid #000;
@include box-shadow(inset 0 1px 0 #eee, 0 1px 0 #555);
height: 7px; height: 7px;
@include transition(height 2.0s ease-in-out); @include transition(height 2.0s ease-in-out);
...@@ -89,80 +92,71 @@ section.course-content { ...@@ -89,80 +92,71 @@ section.course-content {
color: #fff; color: #fff;
font: bold 12px $body-font-family; font: bold 12px $body-font-family;
margin-bottom: 6px; margin-bottom: 6px;
margin-right: 0;
overflow: visible;
padding: 4px; padding: 4px;
text-align: center; text-align: center;
-webkit-font-smoothing: antialiased;
text-shadow: 0 -1px 0 darken($mit-red, 10%); text-shadow: 0 -1px 0 darken($mit-red, 10%);
overflow: visible; -webkit-font-smoothing: antialiased;
&::after { &::after {
background: $mit-red;
border-bottom: 1px solid darken($mit-red, 20%);
border-right: 1px solid darken($mit-red, 20%);
bottom: -5px;
content: " "; content: " ";
width: 7px;
height: 7px;
display: block; display: block;
position: absolute; height: 7px;
bottom: -5px;
left: 50%; left: 50%;
margin-left: -3px; margin-left: -3px;
position: absolute;
@include transform(rotate(45deg)); @include transform(rotate(45deg));
background: $mit-red; width: 7px;
border-right: 1px solid darken($mit-red, 20%);
border-bottom: 1px solid darken($mit-red, 20%);
} }
} }
a.ui-slider-handle { a.ui-slider-handle {
@include border-radius(15px);
@include box-shadow(inset 0 1px 0 lighten($mit-red, 10%));
background: $mit-red url(../images/slider-handle.png) center center no-repeat; background: $mit-red url(../images/slider-handle.png) center center no-repeat;
@include background-size(50%);
border: 1px solid darken($mit-red, 20%); border: 1px solid darken($mit-red, 20%);
@include border-radius(15px);
@include box-shadow(inset 0 1px 0 lighten($mit-red, 10%));
cursor: pointer; cursor: pointer;
height: 15px; height: 15px;
margin-left: -7px; margin-left: -7px;
top: -4px; top: -4px;
width: 15px;
@include transition(height 2.0s ease-in-out, width 2.0s ease-in-out); @include transition(height 2.0s ease-in-out, width 2.0s ease-in-out);
@include background-size(50%); width: 15px;
&:focus, &:hover { &:focus, &:hover {
background-color: lighten($mit-red, 10%); background-color: lighten($mit-red, 10%);
outline: none; outline: none;
} }
} }
&:hover {
height: 14px;
margin-top: -7px;
a.ui-slider-handle {
@include border-radius(20px);
height: 20px;
margin-left: -10px;
top: -4px;
width: 20px;
}
}
} }
ul.vcr { ul.vcr {
@extend .dullify;
float: left; float: left;
list-style: none;
margin-right: lh(); margin-right: lh();
padding: 0;
li { li {
float: left; float: left;
margin-bottom: 0; margin-bottom: 0;
a { a {
@include box-shadow(1px 0 0 #555);
border-bottom: none; border-bottom: none;
border-right: 1px solid #000; border-right: 1px solid #000;
display: block; @include box-shadow(1px 0 0 #555);
cursor: pointer; cursor: pointer;
height: 14px; display: block;
padding: lh(.75); line-height: 46px;
padding: 0 lh(.75);
text-indent: -9999px; text-indent: -9999px;
width: 14px;
@include transition(); @include transition();
width: 14px;
&.play { &.play {
background: url('../images/play-icon.png') center center no-repeat; background: url('../images/play-icon.png') center center no-repeat;
...@@ -179,145 +173,188 @@ section.course-content { ...@@ -179,145 +173,188 @@ section.course-content {
background-color: #444; background-color: #444;
} }
} }
} }
div#vidtime { div#vidtime {
padding-left: lh(.75);
font-weight: bold; font-weight: bold;
line-height: 46px; //height of play pause buttons line-height: 46px; //height of play pause buttons
padding-left: lh(.75);
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
} }
} }
} }
div.secondary-controls { div.secondary-controls {
@extend .dullify;
float: right; float: right;
div.speeds { div.speeds {
border-left: 1px solid #000;
border-right: 1px solid #000;
@include box-shadow(1px 0 0 #555, inset 1px 0 0 #555);
float: left; float: left;
line-height: 0;
padding-right: lh(.25);
margin-right: 0;
@include transition();
cursor: pointer;
-webkit-font-smoothing: antialiased;
h3 { a {
float: left; background: url('/static/images/closed-arrow.png') 10px center no-repeat;
padding: 0 lh(.25) 0 lh(.5); border-left: 1px solid #000;
font-weight: normal; border-right: 1px solid #000;
text-transform: uppercase; @include box-shadow(1px 0 0 #555, inset 1px 0 0 #555);
font-size: 12px; @include clearfix();
letter-spacing: 1px; color: #fff;
color: #999; cursor: pointer;
display: block;
line-height: 46px; //height of play pause buttons line-height: 46px; //height of play pause buttons
} margin-right: 0;
padding-left: 15px;
position: relative;
@include transition();
-webkit-font-smoothing: antialiased;
width: 110px;
p.active { &.open {
@include inline-block(); background: url('/static/images/open-arrow.png') 10px center no-repeat;
padding: 0 lh(.5) 0 0;
margin-bottom: 0;
font-weight: bold;
display: none;
}
// fix for now ol#video_speeds {
ol#video_speeds { display: block;
@include inline-block(); opacity: 1;
}
}
li { h3 {
color: #999;
float: left; float: left;
color: #fff; font-size: 12px;
cursor: pointer; font-weight: normal;
padding: 0 lh(.25); letter-spacing: 1px;
line-height: 46px; //height of play pause buttons padding: 0 lh(.25) 0 lh(.5);
text-transform: uppercase;
}
&.active { p.active {
font-weight: bold; float: left;
} font-weight: bold;
margin-bottom: 0;
padding: 0 lh(.5) 0 0;
}
&:last-child { // fix for now
border-bottom: 0; ol#video_speeds {
margin-top: 0; background-color: #444;
@include box-shadow(none); border: 1px solid #000;
@include box-shadow(inset 1px 0 0 #555, 0 3px 0 #444);
display: none;
left: -1px;
opacity: 0;
position: absolute;
top:0;
@include transition();
width: 100%;
z-index: 10;
li {
border-bottom: 1px solid #000;
@include box-shadow( 0 1px 0 #555);
color: #fff;
cursor: pointer;
padding: 0 lh(.5);
&.active {
font-weight: bold;
}
&:last-child {
border-bottom: 0;
@include box-shadow(none);
margin-top: 0;
}
&:hover {
background-color: #666;
color: #aaa;
}
} }
}
&:hover { &:hover {
background-color: #444; background-color: #444;
} opacity: 1;
} }
} }
}
a.add-fullscreen {
background: url(/static/images/fullscreen.png) center no-repeat;
border-right: 1px solid #000;
@include box-shadow(1px 0 0 #555, inset 1px 0 0 #555);
color: #797979;
display: block;
float: left;
line-height: 46px; //height of play pause buttons
margin-left: 0;
padding: 0 lh(.5);
text-indent: -9999px;
@include transition();
width: 30px;
&:hover { &:hover {
opacity: 1; background-color: #444;
color: #fff;
text-decoration: none;
} }
} }
a.hide-subtitles { a.hide-subtitles {
float: left; background: url('../images/cc.png') center no-repeat;
display: block;
padding: 0 lh(.5);
margin-left: 0;
color: #797979; color: #797979;
line-height: 46px; //height of play pause buttons display: block;
width: 30px; float: left;
text-indent: -9999px;
font-weight: 800; font-weight: 800;
background: url('../images/cc.png') center no-repeat; line-height: 46px; //height of play pause buttons
-webkit-font-smoothing: antialiased; margin-left: 0;
@include transition();
opacity: 1; opacity: 1;
padding: 0 lh(.5);
position: relative; position: relative;
text-indent: -9999px;
&:after { @include transition();
text-indent: 0; -webkit-font-smoothing: antialiased;
position: absolute; width: 30px;
top: 0;
right: -40px;
content: "turn off";
display: block;
width: 70px;
opacity: 0;
visibility: hidden;
@include transition();
}
&:hover { &:hover {
background-color: #444;
color: #fff; color: #fff;
text-decoration: none; text-decoration: none;
background-color: #444;
padding-right: 80px;
background-position: 11px center;
&:after {
right: 0;
opacity: 1;
visibility: visible;
}
} }
&.off { &.off {
opacity: .7; opacity: .7;
&:after {
content: "turn on";
}
} }
} }
} }
} }
&:hover section.video-controls {
ul, div {
opacity: 1;
}
div#slider {
height: 14px;
margin-top: -7px;
a.ui-slider-handle {
@include border-radius(20px);
height: 20px;
margin-left: -10px;
top: -4px;
width: 20px;
}
}
}
} }
ol.subtitles { ol.subtitles {
float: left; float: left;
width: flex-grid(3, 9);
padding-top: 10px;
max-height: 460px; max-height: 460px;
overflow: hidden; overflow: hidden;
padding-top: 10px;
width: flex-grid(3, 9);
li { li {
border: 0; border: 0;
...@@ -354,8 +391,97 @@ section.course-content { ...@@ -354,8 +391,97 @@ section.course-content {
} }
ol.subtitles { ol.subtitles {
width: 0px;
height: 0; height: 0;
width: 0px;
}
}
&.fullscreen {
background: rgba(#000, .95);
border: 0;
bottom: 0;
height: 100%;
left: 0;
margin: 0;
max-height: 100%;
overflow: hidden;
padding: 0;
position: fixed;
top: 0;
width: 100%;
z-index: 999;
&.closed {
ol.subtitles {
height: auto;
right: -(flex-grid(4));
width: auto;
}
}
a.exit {
color: #aaa;
display: none;
font-style: 12px;
left: 20px;
letter-spacing: 1px;
position: absolute;
text-transform: uppercase;
top: 20px;
&::after {
content: "✖";
@include inline-block();
padding-left: 6px;
}
&:hover {
color: $mit-red;
}
}
div.tc-wrapper {
div.video-wrapper {
width: 100%;
}
object#myytplayer, iframe {
bottom: 0;
height: 100%;
left: 0;
overflow: hidden;
position: fixed;
top: 0;
}
section.video-controls {
bottom: 0;
left: 0;
position: absolute;
width: 100%;
z-index: 9999;
}
}
ol.subtitles {
background: rgba(#000, .8);
bottom: 0;
height: 100%;
max-height: 100%;
max-width: flex-grid(3);
padding: lh();
position: fixed;
right: 0;
top: 0;
@include transition();
li {
color: #aaa;
&.current {
color: #fff;
}
}
} }
} }
} }
......
...@@ -42,16 +42,6 @@ div.answer-block { ...@@ -42,16 +42,6 @@ div.answer-block {
padding-top: 20px; padding-top: 20px;
width: 100%; width: 100%;
div.official-stamp {
background: $mit-red;
color: #fff;
font-size: 12px;
margin-top: 10px;
padding: 2px 5px;
text-align: center;
margin-left: -1px;
}
img.answer-img-accept { img.answer-img-accept {
margin: 10px 0px 10px 16px; margin: 10px 0px 10px 16px;
} }
......
div.question-header { div.question-header {
div.official-stamp {
background: $mit-red;
color: #fff;
font-size: 12px;
margin-top: 10px;
padding: 2px 5px;
text-align: center;
margin-left: -1px;
}
div.vote-buttons { div.vote-buttons {
display: inline-block; display: inline-block;
float: left; float: left;
......
li.calc-main { li.calc-main {
bottom: -36px; bottom: -126px;
left: 0; left: 0;
position: fixed; position: fixed;
@include transition(bottom);
-webkit-appearance: none;
width: 100%; width: 100%;
z-index: 99;
&.open {
bottom: -36px;
div#calculator_wrapper form div.input-wrapper div.help-wrapper dl {
display: block;
}
}
a.calc { a.calc {
@include hide-text;
background: url("../images/calc-icon.png") rgba(#111, .9) no-repeat center; background: url("../images/calc-icon.png") rgba(#111, .9) no-repeat center;
border-bottom: 0; border-bottom: 0;
@include border-radius(3px 3px 0 0);
color: #fff; color: #fff;
float: right; float: right;
margin-right: 10px; height: 20px;
@include border-radius(3px 3px 0 0); @include hide-text;
@include inline-block; @include inline-block;
margin-right: 10px;
padding: 8px 12px; padding: 8px 12px;
width: 16px;
height: 20px;
position: relative; position: relative;
top: -36px; top: -36px;
width: 16px;
&:hover { &:hover {
opacity: .8; opacity: .8;
...@@ -30,14 +41,15 @@ li.calc-main { ...@@ -30,14 +41,15 @@ li.calc-main {
div#calculator_wrapper { div#calculator_wrapper {
background: rgba(#111, .9); background: rgba(#111, .9);
clear: both;
max-height: 90px;
position: relative; position: relative;
top: -36px; top: -36px;
clear: both;
form { form {
padding: lh();
@extend .clearfix; @extend .clearfix;
@include box-sizing(border-box);
padding: lh();
input#calculator_button { input#calculator_button {
background: #111; background: #111;
...@@ -46,13 +58,14 @@ li.calc-main { ...@@ -46,13 +58,14 @@ li.calc-main {
@include box-shadow(none); @include box-shadow(none);
@include box-sizing(border-box); @include box-sizing(border-box);
color: #fff; color: #fff;
float: left;
font-size: 30px; font-size: 30px;
font-weight: bold; font-weight: bold;
margin: 0 (flex-gutter() / 2);
padding: 0; padding: 0;
text-shadow: none; text-shadow: none;
-webkit-appearance: none;
width: flex-grid(.5) + flex-gutter(); width: flex-grid(.5) + flex-gutter();
float: left;
margin: 0 (flex-gutter() / 2);
&:hover { &:hover {
color: #333; color: #333;
...@@ -70,29 +83,31 @@ li.calc-main { ...@@ -70,29 +83,31 @@ li.calc-main {
font-weight: bold; font-weight: bold;
margin: 1px 0 0; margin: 1px 0 0;
padding: 10px; padding: 10px;
-webkit-appearance: none;
width: flex-grid(4); width: flex-grid(4);
} }
div.input-wrapper { div.input-wrapper {
position: relative;
@extend .clearfix; @extend .clearfix;
width: flex-grid(7.5);
margin: 0;
float: left; float: left;
margin: 0;
position: relative;
width: flex-grid(7.5);
input#calculator_input { input#calculator_input {
border: none;
@include box-shadow(none);
@include box-sizing(border-box);
font-size: 16px;
padding: 10px;
width: 100%;
&:focus {
outline: none;
border: none; border: none;
@include box-shadow(none);
@include box-sizing(border-box);
font-size: 16px;
padding: 10px;
-webkit-appearance: none;
width: 100%;
&:focus {
outline: none;
border: none;
}
} }
}
div.help-wrapper { div.help-wrapper {
position: absolute; position: absolute;
...@@ -100,10 +115,10 @@ li.calc-main { ...@@ -100,10 +115,10 @@ li.calc-main {
top: 15px; top: 15px;
a { a {
background: url("../images/info-icon.png") center center no-repeat;
height: 17px;
@include hide-text; @include hide-text;
width: 17px; width: 17px;
height: 17px;
background: url("../images/info-icon.png") center center no-repeat;
} }
dl { dl {
...@@ -111,13 +126,14 @@ li.calc-main { ...@@ -111,13 +126,14 @@ li.calc-main {
@include border-radius(3px); @include border-radius(3px);
@include box-shadow(0 0 3px #999); @include box-shadow(0 0 3px #999);
color: #333; color: #333;
display: none;
opacity: 0; opacity: 0;
padding: 10px; padding: 10px;
position: absolute; position: absolute;
right: -40px; right: -40px;
top: -110px; top: -110px;
width: 500px;
@include transition(); @include transition();
width: 500px;
&.shown { &.shown {
opacity: 1; opacity: 1;
......
...@@ -68,11 +68,11 @@ footer { ...@@ -68,11 +68,11 @@ footer {
} }
a { a {
border-bottom: 0;
display: block; display: block;
height: 29px; height: 29px;
width: 28px;
text-indent: -9999px; text-indent: -9999px;
border-bottom: 0; width: 28px;
&:hover { &:hover {
opacity: .8; opacity: .8;
......
...@@ -100,12 +100,12 @@ div.header-wrapper { ...@@ -100,12 +100,12 @@ div.header-wrapper {
float: left; float: left;
a { a {
border: none;
color: #fff; color: #fff;
display: block; display: block;
font-style: normal;
font-weight: bold; font-weight: bold;
padding: 10px lh() 8px; padding: 10px lh() 8px;
border: none;
font-style: normal;
@media screen and (max-width: 1020px) { @media screen and (max-width: 1020px) {
padding: 10px lh(.7) 8px; padding: 10px lh(.7) 8px;
...@@ -125,10 +125,10 @@ div.header-wrapper { ...@@ -125,10 +125,10 @@ div.header-wrapper {
ul { ul {
li { li {
padding: auto;
display: table-cell; display: table-cell;
width: 16.6666666667%; padding: auto;
text-align: center; text-align: center;
width: 16.6666666667%;
} }
} }
} }
......
...@@ -2,11 +2,11 @@ html { ...@@ -2,11 +2,11 @@ html {
margin-top: 0; margin-top: 0;
body { body {
background: #f4f4f4; //#f3f1e5
color: $dark-gray; color: $dark-gray;
font: $body-font-size $body-font-family; font: $body-font-size $body-font-family;
text-align: center;
margin: 0; margin: 0;
background: #f4f4f4; //#f3f1e5 text-align: center;
section.main-content { section.main-content {
@extend .clearfix; @extend .clearfix;
...@@ -17,7 +17,7 @@ html { ...@@ -17,7 +17,7 @@ html {
@include box-shadow(0 0 4px #dfdfdf); @include box-shadow(0 0 4px #dfdfdf);
@include box-sizing(border-box); @include box-sizing(border-box);
margin-top: 3px; margin-top: 3px;
// overflow: hidden; overflow: hidden;
@media print { @media print {
border-bottom: 0; border-bottom: 0;
...@@ -30,6 +30,18 @@ html { ...@@ -30,6 +30,18 @@ html {
} }
} }
div.qtip {
div.ui-tooltip-content {
background: #000;
background: rgba(#000, .8);
border: none;
color: #fff;
font: 12px $body-font-family;
margin-right: -20px;
margin-top: -30px;
}
}
section.outside-app { section.outside-app {
@extend .main-content; @extend .main-content;
max-width: 600px; max-width: 600px;
......
#lean_overlay { #lean_overlay {
background: #000;
display: none;
height:100%;
left: 0px;
position: fixed; position: fixed;
z-index:100;
top: 0px; top: 0px;
left: 0px;
height:100%;
width:100%; width:100%;
background: #000; z-index:100;
display: none;
} }
div.leanModal_box { div.leanModal_box {
...@@ -31,8 +31,8 @@ div.leanModal_box { ...@@ -31,8 +31,8 @@ div.leanModal_box {
z-index: 2; z-index: 2;
&:hover{ &:hover{
text-decoration: none;
color: $mit-red; color: $mit-red;
text-decoration: none;
} }
} }
...@@ -55,8 +55,8 @@ div.leanModal_box { ...@@ -55,8 +55,8 @@ div.leanModal_box {
li { li {
&.terms, &.honor-code { &.terms, &.honor-code {
width: auto;
float: none; float: none;
width: auto;
} }
div.tip { div.tip {
...@@ -118,8 +118,8 @@ div.leanModal_box { ...@@ -118,8 +118,8 @@ div.leanModal_box {
} }
&.honor-code { &.honor-code {
width: auto;
float: none; float: none;
width: auto;
} }
label { label {
...@@ -128,8 +128,8 @@ div.leanModal_box { ...@@ -128,8 +128,8 @@ div.leanModal_box {
} }
#{$all-text-inputs}, textarea { #{$all-text-inputs}, textarea {
width: 100%;
@include box-sizing(border-box); @include box-sizing(border-box);
width: 100%;
} }
input[type="checkbox"] { input[type="checkbox"] {
...@@ -176,15 +176,15 @@ div#login { ...@@ -176,15 +176,15 @@ div#login {
ol { ol {
li { li {
width: auto;
float: none; float: none;
width: auto;
} }
} }
} }
div.lost-password { div.lost-password {
text-align: left;
margin-top: lh(); margin-top: lh();
text-align: left;
a { a {
color: #999; color: #999;
...@@ -218,9 +218,9 @@ div#deactivate-account { ...@@ -218,9 +218,9 @@ div#deactivate-account {
margin-bottom: lh(.5); margin-bottom: lh(.5);
textarea, #{$all-text-inputs} { textarea, #{$all-text-inputs} {
@include box-sizing(border-box);
display: block; display: block;
width: 100%; width: 100%;
@include box-sizing(border-box);
} }
textarea { textarea {
......
...@@ -2,5 +2,5 @@ ...@@ -2,5 +2,5 @@
@import "base/reset", "base/font-face", "base/functions"; @import "base/reset", "base/font-face", "base/functions";
// pages // pages
@import "index/variables", "index/extends", "index/base", "index/header", "index/footer", "index/index"; @import "marketing/variables", "marketing/extends", "marketing/base", "marketing/header", "marketing/footer", "marketing/index";
@import "layout/leanmodal"; @import "layout/leanmodal";
...@@ -6,7 +6,7 @@ footer { ...@@ -6,7 +6,7 @@ footer {
div.footer-wrapper { div.footer-wrapper {
border-top: 1px solid #e5e5e5; border-top: 1px solid #e5e5e5;
padding: lh() 0; padding: lh() 0;
background: url('../images/marketing/mit-logo.png') right center no-repeat; background: url('/static/images/marketing/mit-logo.png') right center no-repeat;
@media screen and (max-width: 780px) { @media screen and (max-width: 780px) {
background-position: left bottom; background-position: left bottom;
...@@ -84,15 +84,15 @@ footer { ...@@ -84,15 +84,15 @@ footer {
} }
&.twitter a { &.twitter a {
background: url('../images/marketing/twitter.png') 0 0 no-repeat; background: url('/static/images/marketing/twitter.png') 0 0 no-repeat;
} }
&.facebook a { &.facebook a {
background: url('../images/marketing/facebook.png') 0 0 no-repeat; background: url('/static/images/marketing/facebook.png') 0 0 no-repeat;
} }
&.linkedin a { &.linkedin a {
background: url('../images/marketing/linkedin.png') 0 0 no-repeat; background: url('/static/images/marketing/linkedin.png') 0 0 no-repeat;
} }
} }
} }
......
...@@ -6,10 +6,10 @@ header.announcement { ...@@ -6,10 +6,10 @@ header.announcement {
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
&.home { &.home {
background: #e3e3e3 url("../images/marketing/shot-5-medium.jpg"); background: #e3e3e3 url("/static/images/marketing/shot-5-medium.jpg");
@media screen and (min-width: 1200px) { @media screen and (min-width: 1200px) {
background: #e3e3e3 url("../images/marketing/shot-5-large.jpg"); background: #e3e3e3 url("/static/images/marketing/shot-5-large.jpg");
} }
div { div {
...@@ -33,14 +33,14 @@ header.announcement { ...@@ -33,14 +33,14 @@ header.announcement {
} }
&.course { &.course {
background: #e3e3e3 url("../images/marketing/course-bg-small.jpg"); background: #e3e3e3 url("/static/images/marketing/course-bg-small.jpg");
@media screen and (min-width: 1200px) { @media screen and (min-width: 1200px) {
background: #e3e3e3 url("../images/marketing/course-bg-large.jpg"); background: #e3e3e3 url("/static/images/marketing/course-bg-large.jpg");
} }
@media screen and (max-width: 1199px) and (min-width: 700px) { @media screen and (max-width: 1199px) and (min-width: 700px) {
background: #e3e3e3 url("../images/marketing/course-bg-medium.jpg"); background: #e3e3e3 url("/static/images/marketing/course-bg-medium.jpg");
} }
div { div {
......
...@@ -20,6 +20,7 @@ section.index-content { ...@@ -20,6 +20,7 @@ section.index-content {
p { p {
line-height: lh(); line-height: lh();
margin-bottom: lh(); margin-bottom: lh();
} }
ul { ul {
...@@ -221,22 +222,35 @@ section.index-content { ...@@ -221,22 +222,35 @@ section.index-content {
&.course { &.course {
h2 { h2 {
padding-top: lh(5); padding-top: lh(5);
background: url('../images/marketing/circuits-bg.jpg') 0 0 no-repeat; background: url('/static/images/marketing/circuits-bg.jpg') 0 0 no-repeat;
@include background-size(contain); @include background-size(contain);
@media screen and (max-width: 998px) and (min-width: 781px){ @media screen and (max-width: 998px) and (min-width: 781px){
background: url('../images/marketing/circuits-medium-bg.jpg') 0 0 no-repeat; background: url('/static/images/marketing/circuits-medium-bg.jpg') 0 0 no-repeat;
} }
@media screen and (max-width: 780px) { @media screen and (max-width: 780px) {
padding-top: lh(5); padding-top: lh(5);
background: url('../images/marketing/circuits-bg.jpg') 0 0 no-repeat; background: url('/static/images/marketing/circuits-bg.jpg') 0 0 no-repeat;
} }
@media screen and (min-width: 500px) and (max-width: 781px) { @media screen and (min-width: 500px) and (max-width: 781px) {
padding-top: lh(8); padding-top: lh(8);
} }
} }
div.announcement {
p.announcement-button {
a {
margin-top: 0;
}
}
img {
max-width: 100%;
margin-bottom: lh();
}
}
} }
......
form#wiki_revision { form#wiki_revision {
float: left; float: left;
width: flex-grid(6, 9);
margin-right: flex-gutter(9); margin-right: flex-gutter(9);
width: flex-grid(6, 9);
label { label {
display: block; display: block;
margin-bottom: 7px ; margin-bottom: 7px ;
} }
.CodeMirror-scroll { .CodeMirror-scroll {
min-height: 550px; min-height: 550px;
width: 100%; width: 100%;
} }
.CodeMirror { .CodeMirror {
@extend textarea; @extend textarea;
@include box-sizing(border-box); @include box-sizing(border-box);
font-family: monospace; font-family: monospace;
margin-bottom: 20px; margin-bottom: 20px;
} }
textarea { textarea {
@include box-sizing(border-box); @include box-sizing(border-box);
margin-bottom: 20px; margin-bottom: 20px;
...@@ -32,25 +33,25 @@ form#wiki_revision { ...@@ -32,25 +33,25 @@ form#wiki_revision {
} }
#submit_delete { #submit_delete {
@include box-shadow(none);
background: none; background: none;
border: none; border: none;
@include box-shadow(none);
color: #999; color: #999;
float: right; float: right;
text-decoration: underline;
font-weight: normal; font-weight: normal;
text-decoration: underline;
} }
input[type="submit"] { input[type="submit"] {
margin-top: 20px; margin-top: 20px;
} }
} }
#wiki_edit_instructions { #wiki_edit_instructions {
color: #666;
float: left; float: left;
width: flex-grid(3, 9);
margin-top: lh(); margin-top: lh();
color: #666; width: flex-grid(3, 9);
&:hover { &:hover {
color: #333; color: #333;
...@@ -58,16 +59,14 @@ form#wiki_revision { ...@@ -58,16 +59,14 @@ form#wiki_revision {
.markdown-example { .markdown-example {
background-color: #e3e3e3; background-color: #e3e3e3;
text-shadow: 0 1px 0 #fff; line-height: 1.0;
margin: 5px 0 7px;
padding: { padding: {
top: 5px; top: 5px;
right: 2px; right: 2px;
bottom: 5px; bottom: 5px;
left: 5px; left: 5px;
} }
text-shadow: 0 1px 0 #fff;
margin: 5px 0 7px;
line-height: 1.0;
} }
} }
...@@ -3,20 +3,20 @@ div#wiki_panel { ...@@ -3,20 +3,20 @@ div#wiki_panel {
overflow: auto; overflow: auto;
h2 { h2 {
padding: lh(.5) lh(); @extend .bottom-border;
font-size: 18px; font-size: 18px;
margin: 0 ; margin: 0 ;
@extend .bottom-border; padding: lh(.5) lh();
} }
input[type="button"] { input[type="button"] {
@extend h3; @extend h3;
@include transition();
color: lighten($text-color, 10%); color: lighten($text-color, 10%);
font-size: $body-font-size; font-size: $body-font-size;
margin: 0 !important; margin: 0 !important;
padding: 7px lh(); padding: 7px lh();
text-align: left; text-align: left;
@include transition();
width: 100%; width: 100%;
&:hover { &:hover {
...@@ -28,8 +28,8 @@ div#wiki_panel { ...@@ -28,8 +28,8 @@ div#wiki_panel {
ul { ul {
li { li {
&.search { &.search {
@include box-shadow(0 1px 0 #eee);
border-bottom: 1px solid #d3d3d3; border-bottom: 1px solid #d3d3d3;
@include box-shadow(0 1px 0 #eee);
padding: 7px lh(); padding: 7px lh();
label { label {
...@@ -49,15 +49,15 @@ div#wiki_panel { ...@@ -49,15 +49,15 @@ div#wiki_panel {
div#wiki_create_form { div#wiki_create_form {
@extend .clearfix; @extend .clearfix;
padding: 15px;
background: #d6d6d6; background: #d6d6d6;
border-bottom: 1px solid #bbb; border-bottom: 1px solid #bbb;
padding: 15px;
input[type="text"] { input[type="text"] {
margin-bottom: 6px; @include box-sizing(border-box);
display: block; display: block;
margin-bottom: 6px;
width: 100%; width: 100%;
@include box-sizing(border-box);
} }
ul { ul {
......
...@@ -7,15 +7,14 @@ div.wiki-wrapper { ...@@ -7,15 +7,14 @@ div.wiki-wrapper {
@extend .content; @extend .content;
position: relative; position: relative;
header { header {
@extend .topbar; @extend .topbar;
height:46px;
@include box-shadow(inset 0 1px 0 white); @include box-shadow(inset 0 1px 0 white);
height:46px;
&:empty { &:empty {
display: none !important;
border-bottom: 0; border-bottom: 0;
display: none !important;
} }
a { a {
...@@ -23,10 +22,10 @@ div.wiki-wrapper { ...@@ -23,10 +22,10 @@ div.wiki-wrapper {
} }
p { p {
float: left;
margin-bottom: 0;
color: darken($cream, 55%); color: darken($cream, 55%);
float: left;
line-height: 46px; line-height: 46px;
margin-bottom: 0;
padding-left: lh(); padding-left: lh();
} }
...@@ -48,8 +47,8 @@ div.wiki-wrapper { ...@@ -48,8 +47,8 @@ div.wiki-wrapper {
@include box-shadow(inset 1px 0 0 lighten(#f6efd4, 5%)); @include box-shadow(inset 1px 0 0 lighten(#f6efd4, 5%));
color: darken($cream, 80%); color: darken($cream, 80%);
display: block; display: block;
font-weight: normal;
font-size: 12px; font-size: 12px;
font-weight: normal;
letter-spacing: 1px; letter-spacing: 1px;
line-height: 46px; line-height: 46px;
margin: 0; margin: 0;
...@@ -89,15 +88,15 @@ div.wiki-wrapper { ...@@ -89,15 +88,15 @@ div.wiki-wrapper {
width: flex-grid(2.5, 9); width: flex-grid(2.5, 9);
@media screen and (max-width:900px) { @media screen and (max-width:900px) {
border-right: 0;
display: block; display: block;
width: auto; width: auto;
border-right: 0;
} }
@media print { @media print {
border-right: 0;
display: block; display: block;
width: auto; width: auto;
border-right: 0;
} }
} }
...@@ -106,9 +105,9 @@ div.wiki-wrapper { ...@@ -106,9 +105,9 @@ div.wiki-wrapper {
} }
section.results { section.results {
border-left: 1px dashed #ddd;
@include box-sizing(border-box); @include box-sizing(border-box);
display: inline-block; display: inline-block;
border-left: 1px dashed #ddd;
float: left; float: left;
padding-left: 10px; padding-left: 10px;
width: flex-grid(6.5, 9); width: flex-grid(6.5, 9);
...@@ -123,8 +122,8 @@ div.wiki-wrapper { ...@@ -123,8 +122,8 @@ div.wiki-wrapper {
@media print { @media print {
display: block; display: block;
width: auto;
padding: 0; padding: 0;
width: auto;
canvas, img { canvas, img {
page-break-inside: avoid; page-break-inside: avoid;
...@@ -140,14 +139,15 @@ div.wiki-wrapper { ...@@ -140,14 +139,15 @@ div.wiki-wrapper {
} }
li { li {
border-bottom: 1px solid #eee;
list-style: none; list-style: none;
margin: 0; margin: 0;
padding: 10px 0; padding: 10px 0;
border-bottom: 1px solid #eee;
&:last-child { &:last-child {
border-bottom: 0; border-bottom: 0;
} }
h3 { h3 {
font-size: 18px; font-size: 18px;
font-weight: normal; font-weight: normal;
...@@ -155,6 +155,5 @@ div.wiki-wrapper { ...@@ -155,6 +155,5 @@ div.wiki-wrapper {
} }
} }
} }
} }
} }
...@@ -5,6 +5,30 @@ import tempfile ...@@ -5,6 +5,30 @@ import tempfile
import djcelery import djcelery
### Dark code. Should be enabled in local settings for devel.
ENABLE_MULTICOURSE = False # set to False to disable multicourse display (see lib.util.views.mitxhome)
QUICKEDIT = False
###
MITX_ROOT_URL = ''
COURSE_NAME = "6.002_Spring_2012"
COURSE_NUMBER = "6.002x"
COURSE_TITLE = "Circuits and Electronics"
COURSE_DEFAULT = '6.002_Spring_2012'
COURSE_SETTINGS = {'6.002_Spring_2012': {'number' : '6.002x',
'title' : 'Circuits and Electronics',
'xmlpath': '6002x/',
}
}
ROOT_URLCONF = 'urls'
# from settings2.askbotsettings import LIVESETTINGS_OPTIONS # from settings2.askbotsettings import LIVESETTINGS_OPTIONS
DEFAULT_GROUPS = [] DEFAULT_GROUPS = []
...@@ -28,7 +52,6 @@ sys.path.append(BASE_DIR + "/mitx/lib") ...@@ -28,7 +52,6 @@ sys.path.append(BASE_DIR + "/mitx/lib")
COURSEWARE_ENABLED = True COURSEWARE_ENABLED = True
ASKBOT_ENABLED = True ASKBOT_ENABLED = True
CSRF_COOKIE_DOMAIN = '127.0.0.1'
# Defaults to be overridden # Defaults to be overridden
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
...@@ -39,8 +62,8 @@ DEFAULT_FEEDBACK_EMAIL = 'feedback@mitx.mit.edu' ...@@ -39,8 +62,8 @@ DEFAULT_FEEDBACK_EMAIL = 'feedback@mitx.mit.edu'
GENERATE_RANDOM_USER_CREDENTIALS = False GENERATE_RANDOM_USER_CREDENTIALS = False
WIKI_REQUIRE_LOGIN_EDIT = True SIMPLE_WIKI_REQUIRE_LOGIN_EDIT = True
WIKI_REQUIRE_LOGIN_VIEW = True SIMPLE_WIKI_REQUIRE_LOGIN_VIEW = False
PERFSTATS = False PERFSTATS = False
...@@ -116,9 +139,11 @@ MIDDLEWARE_CLASSES = ( ...@@ -116,9 +139,11 @@ MIDDLEWARE_CLASSES = (
'django.middleware.csrf.CsrfViewMiddleware', 'django.middleware.csrf.CsrfViewMiddleware',
#'django.contrib.auth.middleware.AuthenticationMiddleware', #'django.contrib.auth.middleware.AuthenticationMiddleware',
'cache_toolbox.middleware.CacheBackedAuthenticationMiddleware', 'cache_toolbox.middleware.CacheBackedAuthenticationMiddleware',
'masquerade.middleware.MasqueradeMiddleware',
'django.contrib.messages.middleware.MessageMiddleware', 'django.contrib.messages.middleware.MessageMiddleware',
'track.middleware.TrackMiddleware', 'track.middleware.TrackMiddleware',
'mitxmako.middleware.MakoMiddleware', 'mitxmako.middleware.MakoMiddleware',
#'ssl_auth.ssl_auth.NginxProxyHeaderMiddleware', # ssl authentication behind nginx proxy
#'debug_toolbar.middleware.DebugToolbarMiddleware', #'debug_toolbar.middleware.DebugToolbarMiddleware',
# Uncommenting the following will prevent csrf token from being re-set if you # Uncommenting the following will prevent csrf token from being re-set if you
...@@ -146,6 +171,10 @@ INSTALLED_APPS = ( ...@@ -146,6 +171,10 @@ INSTALLED_APPS = (
'circuit', 'circuit',
'perfstats', 'perfstats',
'util', 'util',
'masquerade',
'django_jasmine',
#'ssl_auth', ## Broken. Disabled for now.
'multicourse', # multiple courses
# Uncomment the next line to enable the admin: # Uncomment the next line to enable the admin:
# 'django.contrib.admin', # 'django.contrib.admin',
# Uncomment the next line to enable admin documentation: # Uncomment the next line to enable admin documentation:
...@@ -346,6 +375,7 @@ PROJECT_ROOT = os.path.dirname(__file__) ...@@ -346,6 +375,7 @@ PROJECT_ROOT = os.path.dirname(__file__)
TEMPLATE_CONTEXT_PROCESSORS = ( TEMPLATE_CONTEXT_PROCESSORS = (
'django.core.context_processors.request', 'django.core.context_processors.request',
'django.core.context_processors.static',
'askbot.context.application_settings', 'askbot.context.application_settings',
#'django.core.context_processors.i18n', #'django.core.context_processors.i18n',
'askbot.user_messages.context_processors.user_messages',#must be before auth 'askbot.user_messages.context_processors.user_messages',#must be before auth
...@@ -369,8 +399,8 @@ INSTALLED_APPS = INSTALLED_APPS + ( ...@@ -369,8 +399,8 @@ INSTALLED_APPS = INSTALLED_APPS + (
CACHE_MIDDLEWARE_ANONYMOUS_ONLY = True CACHE_MIDDLEWARE_ANONYMOUS_ONLY = True
ASKBOT_URL = 'discussion/' ASKBOT_URL = 'discussion/'
LOGIN_REDIRECT_URL = '/' LOGIN_REDIRECT_URL = MITX_ROOT_URL + '/'
LOGIN_URL = '/' LOGIN_URL = MITX_ROOT_URL + '/'
# ASKBOT_UPLOADED_FILES_URL = '%s%s' % (ASKBOT_URL, 'upfiles/') # ASKBOT_UPLOADED_FILES_URL = '%s%s' % (ASKBOT_URL, 'upfiles/')
ALLOW_UNICODE_SLUGS = False ALLOW_UNICODE_SLUGS = False
...@@ -500,10 +530,11 @@ LIVESETTINGS_OPTIONS = { ...@@ -500,10 +530,11 @@ LIVESETTINGS_OPTIONS = {
'CUSTOM_HEADER' : u'', 'CUSTOM_HEADER' : u'',
'CUSTOM_HTML_HEAD' : u'', 'CUSTOM_HTML_HEAD' : u'',
'CUSTOM_JS' : u'', 'CUSTOM_JS' : u'',
'SITE_FAVICON' : u'/images/favicon.gif', 'MITX_ROOT_URL' : MITX_ROOT_URL, # for askbot header.html file
'SITE_LOGO_URL' : u'/images/logo.gif', 'SITE_FAVICON' : unicode(MITX_ROOT_URL) + u'/images/favicon.gif',
'SITE_LOGO_URL' :unicode(MITX_ROOT_URL) + u'/images/logo.gif',
'SHOW_LOGO' : False, 'SHOW_LOGO' : False,
'LOCAL_LOGIN_ICON' : u'/images/pw-login.gif', 'LOCAL_LOGIN_ICON' : unicode(MITX_ROOT_URL) + u'/images/pw-login.gif',
'ALWAYS_SHOW_ALL_UI_FUNCTIONS' : False, 'ALWAYS_SHOW_ALL_UI_FUNCTIONS' : False,
'ASKBOT_DEFAULT_SKIN' : u'default', 'ASKBOT_DEFAULT_SKIN' : u'default',
'USE_CUSTOM_HTML_HEAD' : False, 'USE_CUSTOM_HTML_HEAD' : False,
...@@ -536,12 +567,12 @@ LIVESETTINGS_OPTIONS = { ...@@ -536,12 +567,12 @@ LIVESETTINGS_OPTIONS = {
'SIGNIN_WORDPRESS_ENABLED' : True, 'SIGNIN_WORDPRESS_ENABLED' : True,
'SIGNIN_WORDPRESS_SITE_ENABLED' : False, 'SIGNIN_WORDPRESS_SITE_ENABLED' : False,
'SIGNIN_YAHOO_ENABLED' : True, 'SIGNIN_YAHOO_ENABLED' : True,
'WORDPRESS_SITE_ICON' : u'/images/logo.gif', 'WORDPRESS_SITE_ICON' : unicode(MITX_ROOT_URL) + u'/images/logo.gif',
'WORDPRESS_SITE_URL' : '', 'WORDPRESS_SITE_URL' : '',
}, },
'LICENSE_SETTINGS' : { 'LICENSE_SETTINGS' : {
'LICENSE_ACRONYM' : u'cc-by-sa', 'LICENSE_ACRONYM' : u'cc-by-sa',
'LICENSE_LOGO_URL' : u'/images/cc-by-sa.png', 'LICENSE_LOGO_URL' : unicode(MITX_ROOT_URL) + u'/images/cc-by-sa.png',
'LICENSE_TITLE' : u'Creative Commons Attribution Share Alike 3.0', 'LICENSE_TITLE' : u'Creative Commons Attribution Share Alike 3.0',
'LICENSE_URL' : 'http://creativecommons.org/licenses/by-sa/3.0/legalcode', 'LICENSE_URL' : 'http://creativecommons.org/licenses/by-sa/3.0/legalcode',
'LICENSE_USE_LOGO' : True, 'LICENSE_USE_LOGO' : True,
...@@ -682,3 +713,5 @@ if MAKO_MODULE_DIR == None: ...@@ -682,3 +713,5 @@ if MAKO_MODULE_DIR == None:
djcelery.setup_loader() djcelery.setup_loader()
# Jasmine Settings
JASMINE_TEST_DIRECTORY = PROJECT_DIR+'/templates/coffee'
// Generated by CoffeeScript 1.3.2-pre
(function() {
window.Calculator = (function() {
function Calculator() {}
Calculator.bind = function() {
var calculator;
calculator = new Calculator;
$('.calc').click(calculator.toggle);
$('form#calculator').submit(calculator.calculate).submit(function(e) {
return e.preventDefault();
});
return $('div.help-wrapper a').hover(calculator.helpToggle).click(function(e) {
return e.preventDefault();
});
};
Calculator.prototype.toggle = function() {
$('li.calc-main').toggleClass('open');
$('#calculator_wrapper #calculator_input').focus();
return $('.calc').toggleClass('closed');
};
Calculator.prototype.helpToggle = function() {
return $('.help').toggleClass('shown');
};
Calculator.prototype.calculate = function() {
return $.getJSON('/calculate', {
equation: $('#calculator_input').val()
}, function(data) {
return $('#calculator_output').val(data.result);
});
};
return Calculator;
})();
window.Courseware = (function() {
function Courseware() {}
Courseware.bind = function() {
return this.Navigation.bind();
};
Courseware.Navigation = (function() {
function Navigation() {}
Navigation.bind = function() {
var active, navigation;
if ($('#accordion').length) {
navigation = new Navigation;
active = $('#accordion ul:has(li.active)').index('#accordion ul');
$('#accordion').bind('accordionchange', navigation.log).accordion({
active: active >= 0 ? active : 1,
header: 'h3',
autoHeight: false
});
return $('#open_close_accordion a').click(navigation.toggle);
}
};
Navigation.prototype.log = function(event, ui) {
return log_event('accordion', {
newheader: ui.newHeader.text(),
oldheader: ui.oldHeader.text()
});
};
Navigation.prototype.toggle = function() {
return $('.course-wrapper').toggleClass('closed');
};
return Navigation;
})();
return Courseware;
}).call(this);
window.FeedbackForm = (function() {
function FeedbackForm() {}
FeedbackForm.bind = function() {
return $('#feedback_button').click(function() {
var data;
data = {
subject: $('#feedback_subject').val(),
message: $('#feedback_message').val(),
url: window.location.href
};
return $.post('/send_feedback', data, function() {
return $('#feedback_div').html('Feedback submitted. Thank you');
}, 'json');
});
};
return FeedbackForm;
})();
$(function() {
$.ajaxSetup({
headers: {
'X-CSRFToken': $.cookie('csrftoken')
}
});
Calculator.bind();
Courseware.bind();
FeedbackForm.bind();
return $("a[rel*=leanModal]").leanModal();
});
}).call(this);
This source diff could not be displayed because it is too large. You can view the blob instead.
/////////////////////////////////////////////////////////////////////////////
//
// Simple image input
//
////////////////////////////////////////////////////////////////////////////////
// click on image, return coordinates
// put a dot at location of click, on imag
// window.image_input_click = function(id,event){
function image_input_click(id,event){
iidiv = document.getElementById("imageinput_"+id);
pos_x = event.offsetX?(event.offsetX):event.pageX-document.iidiv.offsetLeft;
pos_y = event.offsetY?(event.offsetY):event.pageY-document.iidiv.offsetTop;
result = "[" + pos_x + "," + pos_y + "]";
cx = (pos_x-15) +"px";
cy = (pos_y-15) +"px" ;
// alert(result);
document.getElementById("cross_"+id).style.left = cx;
document.getElementById("cross_"+id).style.top = cy;
document.getElementById("cross_"+id).style.visibility = "visible" ;
document.getElementById("input_"+id).value =result;
}
/*
* jQuery UI Touch Punch 0.2.2
*
* Copyright 2011, Dave Furfero
* Dual licensed under the MIT or GPL Version 2 licenses.
*
* Depends:
* jquery.ui.widget.js
* jquery.ui.mouse.js
*/
(function(b){b.support.touch="ontouchend" in document;if(!b.support.touch){return;}var c=b.ui.mouse.prototype,e=c._mouseInit,a;function d(g,h){if(g.originalEvent.touches.length>1){return;}g.preventDefault();var i=g.originalEvent.changedTouches[0],f=document.createEvent("MouseEvents");f.initMouseEvent(h,true,true,window,1,i.screenX,i.screenY,i.clientX,i.clientY,false,false,false,false,0,null);g.target.dispatchEvent(f);}c._touchStart=function(g){var f=this;if(a||!f._mouseCapture(g.originalEvent.changedTouches[0])){return;}a=true;f._touchMoved=false;d(g,"mouseover");d(g,"mousemove");d(g,"mousedown");};c._touchMove=function(f){if(!a){return;}this._touchMoved=true;d(f,"mousemove");};c._touchEnd=function(f){if(!a){return;}d(f,"mouseup");d(f,"mouseout");if(!this._touchMoved){d(f,"click");}a=false;};c._mouseInit=function(){var f=this;f.element.bind("touchstart",b.proxy(f,"_touchStart")).bind("touchmove",b.proxy(f,"_touchMove")).bind("touchend",b.proxy(f,"_touchEnd"));e.call(f);};})(jQuery);
\ No newline at end of file
...@@ -18,6 +18,14 @@ This set of questions and answers accompanies MIT&rsquo;s February 13, ...@@ -18,6 +18,14 @@ This set of questions and answers accompanies MIT&rsquo;s February 13,
6.002x: Circuits and Electronics. 6.002x: Circuits and Electronics.
</p> </p>
<h2> How do I register? </h2>
<p> We will have a link to a form where you can sign up for our database and mailing list shortly. Please check back in the next two weeks to this website for further instruction. </p>
<h2> Where can I find a list of courses available? When do the next classes begin? </h2>
<p> Courses will begin again in the Fall Semester (September). We anticipate offering 4-5 courses this Fall, one of which will be 6.002x again. The additional classes will be announced in early summer. </p>
<h2> I tried to register for the course, but it says the username <h2> I tried to register for the course, but it says the username
is already taken.</h2> is already taken.</h2>
......
...@@ -7,22 +7,14 @@ ...@@ -7,22 +7,14 @@
<ul> <ul>
% for section in chapter['sections']: % for section in chapter['sections']:
<li <li${' class="active"' if 'active' in section and section['active'] else ''}>
% if 'active' in section and section['active']:
class="active"
% endif
>
<a href="${reverse('courseware_section', args=format_url_params([course_name, chapter['name'], section['name']]))}"> <a href="${reverse('courseware_section', args=format_url_params([course_name, chapter['name'], section['name']]))}">
<p>${section['name']}</p> <p>${section['name']}
<span class="subtitle">
<p class="subtitle"> ${section['format']} ${"due " + section['due'] if 'due' in section and section['due'] != '' else ''}
${section['format']} </span>
% if 'due' in section and section['due']!="":
due ${section['due']}
% endif
</p> </p>
</a> </a>
% endfor % endfor
</ul> </ul>
......
$("#accordion").accordion({
active: ${ active_chapter },
header: 'h3',
autoHeight: false,
});
$("#open_close_accordion a").click(function(){
if ($(".course-wrapper").hasClass("closed")){
$(".course-wrapper").removeClass("closed");
} else {
$(".course-wrapper").addClass("closed");
}
});
$('.ui-accordion').bind('accordionchange', function(event, ui) {
var event_data = {'newheader':ui.newHeader.text(),
'oldheader':ui.oldHeader.text()};
log_event('accordion', event_data);
});
<form class="multiple-choice">
% for choice_id, choice_description in choices.items():
<label for="input_${id}_${choice_id}"> <input type="${type}" name="input_${id}" id="input_${id}_${choice_id}" value="${choice_id}"
% if choice_id in value:
checked="true"
% endif
/> ${choice_description} </label>
% endfor
<span id="answer_${id}"></span>
% if state == 'unsubmitted':
<span class="unanswered" style="display:inline-block;" id="status_${id}"></span>
% elif state == 'correct':
<span class="correct" id="status_${id}"></span>
% elif state == 'incorrect':
<span class="incorrect" id="status_${id}"></span>
% elif state == 'incomplete':
<span class="incorrect" id="status_${id}"></span>
% endif
</form>
CoffeeScript
============
This folder contains the CoffeeScript file that will be compiled to the static
directory. By default, we're compile and merge all the files ending `.coffee`
into `static/js/application.js`.
Install the Compiler
--------------------
CoffeeScript compiler are written in JavaScript. You'll need to install Node and
npm (Node Package Manager) to be able to install the CoffeeScript compiler.
### Mac OS X
Install Node via Homebrew, then use npm:
brew install node
curl http://npmjs.org/install.sh | sh
npm install -g git://github.com/jashkenas/coffee-script.git
(Note that we're using the edge version of CoffeeScript for now, as there was
some issue with directory watching in 1.3.1.)
Try to run `coffee` and make sure you get a coffee prompt.
### Debian/Ubuntu
Conveniently, you can install Node via `apt-get`, then use npm:
sudo apt-get install nodejs npm &&
sudo npm install -g git://github.com/jashkenas/coffee-script.git
Compiling
---------
Run this command in the `mitx` directory to easily make the compiler watch for
changes in your file, and join the result into `application.js`:
coffee -j static/js/application.js -cw templates/coffee/src
Please note that the compiler will not be able to detect the file that get added
after you've ran the command, so you'll need to restart the compiler if there's
a new CoffeeScript file.
Testing
=======
We're also using Jasmine to unit-testing the JavaScript files. All the specs are
written in CoffeeScript for the consistency. Because of the limitation of
`django-jasmine` plugin, we'll need to also running another compiler to compile
the test file.
Using this command to compile the test files:
coffee -cw templates/coffee/spec/*.coffee
Then start the server in debug mode, navigate to http://127.0.0.1:8000/_jasmine
to see the test result.
{
"js_files": [
"/static/js/jquery-1.6.2.min.js",
"/static/js/jquery-ui-1.8.16.custom.min.js",
"/static/js/jquery.leanModal.js"
],
"static_files": [
"js/application.js"
]
}
<div class="course-wrapper">
<header id="open_close_accordion">
<a href="#">close</a>
</header>
<div id="accordion"></div>
</div>
<ul>
<li class="calc-main">
<a href="#" class="calc">Calculator</a>
<div id="calculator_wrapper">
<form id="calculator">
<div class="input-wrapper">
<input type="text" id="calculator_input" />
<div class="help-wrapper">
<a href="#">Hints</a>
<dl class="help"></dl>
</div>
</div>
<input id="calculator_button" type="submit" value="="/>
<input type="text" id="calculator_output" readonly />
</form>
</div>
</li>
</ul>
<div id="feedback_div">
<form>
<label>Subject:</label> <input type="text" id="feedback_subject">
<label>Feedback: </label><textarea id="feedback_message"></textarea>
<input id="feedback_button" type="button" value="Submit">
</form>
</div>
describe 'Calculator', ->
beforeEach ->
loadFixtures 'calculator.html'
@calculator = new Calculator
describe 'bind', ->
beforeEach ->
Calculator.bind()
it 'bind the calculator button', ->
expect($('.calc')).toHandleWith 'click', @calculator.toggle
it 'bind the help button', ->
# These events are bind by $.hover()
expect($('div.help-wrapper a')).toHandleWith 'mouseenter', @calculator.helpToggle
expect($('div.help-wrapper a')).toHandleWith 'mouseleave', @calculator.helpToggle
it 'prevent default behavior on help button', ->
$('div.help-wrapper a').click (e) ->
expect(e.isDefaultPrevented()).toBeTruthy()
$('div.help-wrapper a').click()
it 'bind the calculator submit', ->
expect($('form#calculator')).toHandleWith 'submit', @calculator.calculate
it 'prevent default behavior on form submit', ->
$('form#calculator').submit (e) ->
expect(e.isDefaultPrevented()).toBeTruthy()
e.preventDefault()
$('form#calculator').submit()
describe 'toggle', ->
it 'toggle the calculator and focus the input', ->
spyOn $.fn, 'focus'
@calculator.toggle()
expect($('li.calc-main')).toHaveClass('open')
expect($('#calculator_wrapper #calculator_input').focus).toHaveBeenCalled()
it 'toggle the close button on the calculator button', ->
@calculator.toggle()
expect($('.calc')).toHaveClass('closed')
@calculator.toggle()
expect($('.calc')).not.toHaveClass('closed')
describe 'helpToggle', ->
it 'toggle the help overlay', ->
@calculator.helpToggle()
expect($('.help')).toHaveClass('shown')
@calculator.helpToggle()
expect($('.help')).not.toHaveClass('shown')
describe 'calculate', ->
beforeEach ->
$('#calculator_input').val '1+2'
spyOn($, 'getJSON').andCallFake (url, data, callback) ->
callback({ result: 3 })
@calculator.calculate()
it 'send data to /calculate', ->
expect($.getJSON).toHaveBeenCalledWith '/calculate',
equation: '1+2'
, jasmine.any(Function)
it 'update the calculator output', ->
expect($('#calculator_output').val()).toEqual('3')
// Generated by CoffeeScript 1.3.2-pre
(function() {
describe('Calculator', function() {
beforeEach(function() {
loadFixtures('calculator.html');
return this.calculator = new Calculator;
});
describe('bind', function() {
beforeEach(function() {
return Calculator.bind();
});
it('bind the calculator button', function() {
return expect($('.calc')).toHandleWith('click', this.calculator.toggle);
});
it('bind the help button', function() {
expect($('div.help-wrapper a')).toHandleWith('mouseenter', this.calculator.helpToggle);
return expect($('div.help-wrapper a')).toHandleWith('mouseleave', this.calculator.helpToggle);
});
it('prevent default behavior on help button', function() {
$('div.help-wrapper a').click(function(e) {
return expect(e.isDefaultPrevented()).toBeTruthy();
});
return $('div.help-wrapper a').click();
});
it('bind the calculator submit', function() {
return expect($('form#calculator')).toHandleWith('submit', this.calculator.calculate);
});
return it('prevent default behavior on form submit', function() {
$('form#calculator').submit(function(e) {
expect(e.isDefaultPrevented()).toBeTruthy();
return e.preventDefault();
});
return $('form#calculator').submit();
});
});
describe('toggle', function() {
it('toggle the calculator and focus the input', function() {
spyOn($.fn, 'focus');
this.calculator.toggle();
expect($('li.calc-main')).toHaveClass('open');
return expect($('#calculator_wrapper #calculator_input').focus).toHaveBeenCalled();
});
return it('toggle the close button on the calculator button', function() {
this.calculator.toggle();
expect($('.calc')).toHaveClass('closed');
this.calculator.toggle();
return expect($('.calc')).not.toHaveClass('closed');
});
});
describe('helpToggle', function() {
return it('toggle the help overlay', function() {
this.calculator.helpToggle();
expect($('.help')).toHaveClass('shown');
this.calculator.helpToggle();
return expect($('.help')).not.toHaveClass('shown');
});
});
return describe('calculate', function() {
beforeEach(function() {
$('#calculator_input').val('1+2');
spyOn($, 'getJSON').andCallFake(function(url, data, callback) {
return callback({
result: 3
});
});
return this.calculator.calculate();
});
it('send data to /calculate', function() {
return expect($.getJSON).toHaveBeenCalledWith('/calculate', {
equation: '1+2'
}, jasmine.any(Function));
});
return it('update the calculator output', function() {
return expect($('#calculator_output').val()).toEqual('3');
});
});
});
}).call(this);
describe 'Courseware', ->
describe 'bind', ->
it 'bind the navigation', ->
spyOn Courseware.Navigation, 'bind'
Courseware.bind()
expect(Courseware.Navigation.bind).toHaveBeenCalled()
describe 'Navigation', ->
beforeEach ->
loadFixtures 'accordion.html'
@navigation = new Courseware.Navigation
describe 'bind', ->
describe 'when the #accordion exists', ->
describe 'when there is an active section', ->
it 'activate the accordion with correct active section', ->
spyOn $.fn, 'accordion'
$('#accordion').append('<ul><li></li></ul><ul><li class="active"></li></ul>')
Courseware.Navigation.bind()
expect($('#accordion').accordion).toHaveBeenCalledWith
active: 1
header: 'h3'
autoHeight: false
describe 'when there is no active section', ->
it 'activate the accordian with section 1 as active', ->
spyOn $.fn, 'accordion'
$('#accordion').append('<ul><li></li></ul><ul><li></li></ul>')
Courseware.Navigation.bind()
expect($('#accordion').accordion).toHaveBeenCalledWith
active: 1
header: 'h3'
autoHeight: false
it 'binds the accordionchange event', ->
Courseware.Navigation.bind()
expect($('#accordion')).toHandleWith 'accordionchange', @navigation.log
it 'bind the navigation toggle', ->
Courseware.Navigation.bind()
expect($('#open_close_accordion a')).toHandleWith 'click', @navigation.toggle
describe 'when the #accordion does not exists', ->
beforeEach ->
$('#accordion').remove()
it 'does not activate the accordion', ->
spyOn $.fn, 'accordion'
Courseware.Navigation.bind()
expect($('#accordion').accordion).wasNotCalled()
describe 'toggle', ->
it 'toggle closed class on the wrapper', ->
$('.course-wrapper').removeClass('closed')
@navigation.toggle()
expect($('.course-wrapper')).toHaveClass('closed')
@navigation.toggle()
expect($('.course-wrapper')).not.toHaveClass('closed')
describe 'log', ->
beforeEach ->
window.log_event = ->
spyOn window, 'log_event'
it 'submit event log', ->
@navigation.log {}, {
newHeader:
text: -> "new"
oldHeader:
text: -> "old"
}
expect(window.log_event).toHaveBeenCalledWith 'accordion',
newheader: 'new'
oldheader: 'old'
// Generated by CoffeeScript 1.3.2-pre
(function() {
describe('Courseware', function() {
describe('bind', function() {
return it('bind the navigation', function() {
spyOn(Courseware.Navigation, 'bind');
Courseware.bind();
return expect(Courseware.Navigation.bind).toHaveBeenCalled();
});
});
return describe('Navigation', function() {
beforeEach(function() {
loadFixtures('accordion.html');
return this.navigation = new Courseware.Navigation;
});
describe('bind', function() {
describe('when the #accordion exists', function() {
describe('when there is an active section', function() {
return it('activate the accordion with correct active section', function() {
spyOn($.fn, 'accordion');
$('#accordion').append('<ul><li></li></ul><ul><li class="active"></li></ul>');
Courseware.Navigation.bind();
return expect($('#accordion').accordion).toHaveBeenCalledWith({
active: 1,
header: 'h3',
autoHeight: false
});
});
});
describe('when there is no active section', function() {
return it('activate the accordian with section 1 as active', function() {
spyOn($.fn, 'accordion');
$('#accordion').append('<ul><li></li></ul><ul><li></li></ul>');
Courseware.Navigation.bind();
return expect($('#accordion').accordion).toHaveBeenCalledWith({
active: 1,
header: 'h3',
autoHeight: false
});
});
});
it('binds the accordionchange event', function() {
Courseware.Navigation.bind();
return expect($('#accordion')).toHandleWith('accordionchange', this.navigation.log);
});
return it('bind the navigation toggle', function() {
Courseware.Navigation.bind();
return expect($('#open_close_accordion a')).toHandleWith('click', this.navigation.toggle);
});
});
return describe('when the #accordion does not exists', function() {
beforeEach(function() {
return $('#accordion').remove();
});
return it('does not activate the accordion', function() {
spyOn($.fn, 'accordion');
Courseware.Navigation.bind();
return expect($('#accordion').accordion).wasNotCalled();
});
});
});
describe('toggle', function() {
return it('toggle closed class on the wrapper', function() {
$('.course-wrapper').removeClass('closed');
this.navigation.toggle();
expect($('.course-wrapper')).toHaveClass('closed');
this.navigation.toggle();
return expect($('.course-wrapper')).not.toHaveClass('closed');
});
});
return describe('log', function() {
beforeEach(function() {
window.log_event = function() {};
return spyOn(window, 'log_event');
});
return it('submit event log', function() {
this.navigation.log({}, {
newHeader: {
text: function() {
return "new";
}
},
oldHeader: {
text: function() {
return "old";
}
}
});
return expect(window.log_event).toHaveBeenCalledWith('accordion', {
newheader: 'new',
oldheader: 'old'
});
});
});
});
});
}).call(this);
describe 'FeedbackForm', ->
beforeEach ->
loadFixtures 'feedback_form.html'
describe 'bind', ->
beforeEach ->
FeedbackForm.bind()
spyOn($, 'post').andCallFake (url, data, callback, format) ->
callback()
it 'binds to the #feedback_button', ->
expect($('#feedback_button')).toHandle 'click'
it 'post data to /send_feedback on click', ->
$('#feedback_subject').val 'Awesome!'
$('#feedback_message').val 'This site is really good.'
$('#feedback_button').click()
expect($.post).toHaveBeenCalledWith '/send_feedback', {
subject: 'Awesome!'
message: 'This site is really good.'
url: window.location.href
}, jasmine.any(Function), 'json'
it 'replace the form with a thank you message', ->
$('#feedback_button').click()
expect($('#feedback_div').html()).toEqual 'Feedback submitted. Thank you'
// Generated by CoffeeScript 1.3.2-pre
(function() {
describe('FeedbackForm', function() {
beforeEach(function() {
return loadFixtures('feedback_form.html');
});
return describe('bind', function() {
beforeEach(function() {
FeedbackForm.bind();
return spyOn($, 'post').andCallFake(function(url, data, callback, format) {
return callback();
});
});
it('binds to the #feedback_button', function() {
return expect($('#feedback_button')).toHandle('click');
});
it('post data to /send_feedback on click', function() {
$('#feedback_subject').val('Awesome!');
$('#feedback_message').val('This site is really good.');
$('#feedback_button').click();
return expect($.post).toHaveBeenCalledWith('/send_feedback', {
subject: 'Awesome!',
message: 'This site is really good.',
url: window.location.href
}, jasmine.any(Function), 'json');
});
return it('replace the form with a thank you message', function() {
$('#feedback_button').click();
return expect($('#feedback_div').html()).toEqual('Feedback submitted. Thank you');
});
});
});
}).call(this);
jasmine.getFixtures().fixturesPath = "/_jasmine/fixtures/"
// Generated by CoffeeScript 1.3.2-pre
(function() {
jasmine.getFixtures().fixturesPath = "/_jasmine/fixtures/";
}).call(this);
class window.Calculator
@bind: ->
calculator = new Calculator
$('.calc').click calculator.toggle
$('form#calculator').submit(calculator.calculate).submit (e) ->
e.preventDefault()
$('div.help-wrapper a').hover(calculator.helpToggle).click (e) ->
e.preventDefault()
toggle: ->
$('li.calc-main').toggleClass 'open'
$('#calculator_wrapper #calculator_input').focus()
$('.calc').toggleClass 'closed'
helpToggle: ->
$('.help').toggleClass 'shown'
calculate: ->
$.getJSON '/calculate', { equation: $('#calculator_input').val() }, (data) ->
$('#calculator_output').val(data.result)
class window.Courseware
@bind: ->
@Navigation.bind()
class @Navigation
@bind: ->
if $('#accordion').length
navigation = new Navigation
active = $('#accordion ul:has(li.active)').index('#accordion ul')
$('#accordion').bind('accordionchange', navigation.log).accordion
active: if active >= 0 then active else 1
header: 'h3'
autoHeight: false
$('#open_close_accordion a').click navigation.toggle
log: (event, ui) ->
log_event 'accordion',
newheader: ui.newHeader.text()
oldheader: ui.oldHeader.text()
toggle: ->
$('.course-wrapper').toggleClass('closed')
class window.FeedbackForm
@bind: ->
$('#feedback_button').click ->
data =
subject: $('#feedback_subject').val()
message: $('#feedback_message').val()
url: window.location.href
$.post '/send_feedback', data, ->
$('#feedback_div').html 'Feedback submitted. Thank you'
,'json'
$ ->
$.ajaxSetup
headers : { 'X-CSRFToken': $.cookie 'csrftoken' }
Calculator.bind()
Courseware.bind()
FeedbackForm.bind()
$("a[rel*=leanModal]").leanModal()
<%inherit file="main.html" /> <%inherit file="main.html" />
<%block name="bodyclass">courseware</%block>
<%block name="title"><title>Courseware – MITx 6.002x</title></%block> <%block name="title"><title>Courseware – MITx 6.002x</title></%block>
<%block name="headextra">
<script type="text/javascript" src="/static/js/flot/jquery.flot.js"></script>
</%block>
<%block name="js_extra"> <%block name="js_extra">
##Is there a reason this isn't in header_extra? Is it important that the javascript is at the bottom of the generated document?
<!-- TODO: http://docs.jquery.com/Plugins/Validation --> <!-- TODO: http://docs.jquery.com/Plugins/Validation -->
<script type="text/javascript"> <script type="text/javascript">
$(function() { $(function() {
${init} ${init}
$(".sequence-nav li a").hover(function(){
$(this).siblings().toggleClass("shown");
});
}); });
</script> </script>
</%block> </%block>
...@@ -25,7 +27,9 @@ ...@@ -25,7 +27,9 @@
</header> </header>
<div id="accordion"> <div id="accordion">
${accordion} <nav>
${accordion}
</nav>
</div> </div>
</section> </section>
......
...@@ -6,9 +6,7 @@ ...@@ -6,9 +6,7 @@
<p class="ie-warning"> Enrollment requires a modern web browser with JavaScript enabled. You don't have this. You can&rsquo;t enroll without upgrading, since you couldn&rsquo;t take the course without upgrading. Feel free to download the latest version of <a href="http://www.mozilla.org/en-US/firefox/new/">Mozilla Firefox</a> or <a href="http://support.google.com/chrome/bin/answer.py?hl=en&answer=95346">Google Chrome</a>, for free, to enroll and take this course.</p> <p class="ie-warning"> Enrollment requires a modern web browser with JavaScript enabled. You don't have this. You can&rsquo;t enroll without upgrading, since you couldn&rsquo;t take the course without upgrading. Feel free to download the latest version of <a href="http://www.mozilla.org/en-US/firefox/new/">Mozilla Firefox</a> or <a href="http://support.google.com/chrome/bin/answer.py?hl=en&answer=95346">Google Chrome</a>, for free, to enroll and take this course.</p>
<![endif]--> <![endif]-->
<p class="disclaimer"> <p class="disclaimer">
Please note that 6.002x has already started. Please note that 6.002x has now passed its half-way point. The midterm exam and several assignment due dates for 6.002x have already passed. It is now impossible for newly enrolled students to earn a passing grade and a completion certificate for the course. However, new students have access to all of the course material that has been released for the course, so you are welcome to enroll and browse the course. </p>
Several assignment due dates for 6.002x have already passed. It is now impossible for newly enrolled students to get 100% of the points in the course, although new students can still earn points for assignments whose due dates have not passed, and students have access to all of the course material that has been released for the course.
</p>
<form name="enroll" id="enroll_form" method="get"> <form name="enroll" id="enroll_form" method="get">
<fieldset><% if 'error' in locals(): e = error %> <fieldset><% if 'error' in locals(): e = error %>
......
MITx's prototype offering, 6.002x, is now open. To log in, visit MITx's prototype offering, 6.002x, is open. To log in, visit
% if is_secure: % if is_secure:
https://6002x.mitx.mit.edu https://6002x.mitx.mit.edu
...@@ -16,7 +16,7 @@ place to reset it. ...@@ -16,7 +16,7 @@ place to reset it.
Once you log in, we recommend that you start the course by reviewing Once you log in, we recommend that you start the course by reviewing
the "System Usage Sequence" in the Overview section, and the "6.002x the "System Usage Sequence" in the Overview section, and the "6.002x
At-a-Glance (Calendar)" handout under the Course Info tab. After you At-a-Glance (Calendar)" handout under the Course Info tab. After you
familiarize yourself with the various features of the MITx platform, familiarize yourself with the features of the MITx platform,
you can jump right into the coursework by working on "Administrivia you can jump right into the coursework by working on "Administrivia
and Circuit Elements", the first Lecture Sequence in Week 1. and Circuit Elements", the first Lecture Sequence in Week 1.
......
...@@ -9,7 +9,8 @@ ...@@ -9,7 +9,8 @@
<style type="text/css"> <style type="text/css">
.grade_a {color:green;} .grade_a {color:green;}
.grade_b {color:Chocolate;} .grade_b {color:Chocolate;}
.grade_c {color:DimGray;} .grade_c {color:DarkSlateGray;}
.grade_f {color:DimGray;}
.grade_none {color:LightGray;} .grade_none {color:LightGray;}
</style> </style>
...@@ -30,16 +31,10 @@ ...@@ -30,16 +31,10 @@
<tr> <!-- Header Row --> <tr> <!-- Header Row -->
<th>Student</th> <th>Student</th>
%for section in templateSummary: %for section in templateSummary['section_breakdown']:
%if 'subscores' in section: <th>${section['label']}</th>
%for subsection in section['subscores']:
<th>${subsection['label']}</th>
%endfor
<th>${section['totallabel']}</th>
%else:
<th>${section['category']}</th>
%endif
%endfor %endfor
<th>Total</th>
</tr> </tr>
<%def name="percent_data(percentage)"> <%def name="percent_data(percentage)">
...@@ -51,6 +46,8 @@ ...@@ -51,6 +46,8 @@
data_class = "grade_b" data_class = "grade_b"
elif percentage > .6: elif percentage > .6:
data_class = "grade_c" data_class = "grade_c"
elif percentage > 0:
data_class = "grade_f"
%> %>
<td class="${data_class}">${ "{0:.0%}".format( percentage ) }</td> <td class="${data_class}">${ "{0:.0%}".format( percentage ) }</td>
</%def> </%def>
...@@ -58,16 +55,10 @@ ...@@ -58,16 +55,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']: %for section in student['grade_info']['grade_summary']['section_breakdown']:
%if 'subscores' in section: ${percent_data( section['percent'] )}
%for subsection in section['subscores']:
${percent_data( subsection['percentage'] )}
%endfor
${percent_data( section['totalscore'] )}
%else:
${percent_data( section['totalscore'] )}
%endif
%endfor %endfor
<th>${percent_data( student['grade_info']['grade_summary']['percent'])}</th>
</tr> </tr>
%endfor %endfor
</table> </table>
......
<span>
<input type="hidden" class="imageinput" src="${src}" name="input_${id}" id="input_${id}" value="${value}" />
<div id="imageinput_${id}" onclick="image_input_click('${id}',event);" style = "background-image:url('${src}');width:${width}px;height:${height}px;position: relative; left: 0; top: 0;">
<img src="/static/green-pointer.png" id="cross_${id}" style="position: absolute;top: ${gy}px;left: ${gx}px;" />
</div>
% if state == 'unsubmitted':
<span class="unanswered" style="display:inline-block;" id="status_${id}"></span>
% elif state == 'correct':
<span class="correct" id="status_${id}"></span>
% elif state == 'incorrect':
<span class="incorrect" id="status_${id}"></span>
% elif state == 'incomplete':
<span class="incorrect" id="status_${id}"></span>
% endif
</span>
...@@ -10,9 +10,9 @@ ...@@ -10,9 +10,9 @@
<section> <section>
<h1>Circuits &amp; Electronics</h1> <h1>Circuits &amp; Electronics</h1>
<h2>6.002x</h2> <h2>6.002x</h2>
<a class="enroll" rel="leanModal" href="#enroll"><noscript>In order to</noscript> Enroll in 6.002x Circuits <span>&amp;</span> Electronics <noscript>you need to have javascript enabled</noscript></a> <a class="enroll" rel="leanModal" href="/info">View 6.002x Circuits <span>&amp;</span> Electronics as a guest</a>
</section> </section>
<p>6.002x (Circuits and Electronics) is an experimental on-line adaptation of MIT&rsquo;s first undergraduate analog design course: 6.002. This course will run, free of charge, for students worldwide from March 5, 2012 through June 8, 2012.</p> <p>6.002x (Circuits and Electronics) is an experimental on-line adaptation of MIT&rsquo;s first undergraduate analog design course: 6.002. This course is running, free of charge, for students worldwide from March 5, 2012 through June 8, 2012.</p>
</section> </section>
</%block> </%block>
...@@ -52,7 +52,7 @@ ...@@ -52,7 +52,7 @@
</section> </section>
<section class="cta"> <section class="cta">
<a class="enroll" rel="leanModal" href="#enroll"><noscript>In order to</noscript> Enroll in 6.002x Circuits &amp; Electronics <noscript>you need to have javascript enabled</noscript></a> <a class="enroll" rel="leanModal" href="/info">View 6.002x Circuits &amp; Electronics as a guest</a>
</section> </section>
</section> </section>
......
...@@ -23,11 +23,20 @@ $(document).ready(function(){ ...@@ -23,11 +23,20 @@ $(document).ready(function(){
<section class="main-content"> <section class="main-content">
<div class="info-wrapper"> <div class="info-wrapper">
% if user.is_authenticated():
<section class="updates"> <section class="updates">
<%include file="updates.html" /> <%include file="updates.html" />
</section> </section>
<section class="handouts"> <section class="handouts">
<%include file="handouts.html" /> <%include file="handouts.html" />
</section> </section>
% else:
<section class="updates">
<%include file="guest_updates.html" />
</section>
<section class="handouts">
<%include file="guest_handouts.html" />
</section>
% endif
</div> </div>
</section> </section>
<section class="text-input">
<input type="text" name="input_${id}" id="input_${id}" value="${value}"
% if size:
size="${size}"
% endif
% if dojs == 'math':
onkeyup="DoUpdateMath('${id}')"
% endif
/>
% if dojs == 'math':
<span id="display_${id}">`{::}`</span>
% endif
<span id="answer_${id}"></span>
% if dojs == 'math':
<textarea style="display:none" id="input_${id}_fromjs" name="input_${id}_fromjs"></textarea>
% endif
% if state == 'unsubmitted':
<span class="unanswered" style="display:inline-block;" id="status_${id}"></span>
% elif state == 'correct':
<span class="correct" id="status_${id}"></span>
% elif state == 'incorrect':
<span class="incorrect" id="status_${id}"></span>
% elif state == 'incomplete':
<span class="incorrect" id="status_${id}"></span>
% endif
% if msg:
<br/>
<span class="debug">${msg|n}</span>
% endif
</section>
...@@ -10,6 +10,7 @@ ...@@ -10,6 +10,7 @@
<script type="text/javascript" src="${static.url('js/jquery-1.6.2.min.js')}"></script> <script type="text/javascript" src="${static.url('js/jquery-1.6.2.min.js')}"></script>
<script type="text/javascript" src="${static.url('js/jquery-ui-1.8.16.custom.min.js')}"></script> <script type="text/javascript" src="${static.url('js/jquery-ui-1.8.16.custom.min.js')}"></script>
<script type="text/javascript" src="${static.url('js/swfobject/swfobject.js')}"></script> <script type="text/javascript" src="${static.url('js/swfobject/swfobject.js')}"></script>
<script type="text/javascript" src="${static.url('js/application.js')}"></script>
<!--[if lt IE 9]> <!--[if lt IE 9]>
<script src="${static.url('js/html5shiv.js')}"></script> <script src="${static.url('js/html5shiv.js')}"></script>
...@@ -29,7 +30,7 @@ ...@@ -29,7 +30,7 @@
<script type="text/javascript" src="/static/js/mathjax-MathJax-c9db6ac/MathJax.js?config=TeX-AMS_HTML-full"></script> <script type="text/javascript" src="/static/js/mathjax-MathJax-c9db6ac/MathJax.js?config=TeX-AMS_HTML-full"></script>
</head> </head>
<body class="<%block name="bodyclass"/>"> <body class="<%block name='bodyclass'/>">
<!--[if lte IE 9]> <!--[if lte IE 9]>
<p class="ie-warning">You are using a browser that is not supported by <em>MITx</em>, and you might not be able to complete pieces of the course. Please download the latest version of <a href="http://www.mozilla.org/en-US/firefox/new/">Firefox</a> or <a href="https://www.google.com/chrome">Chrome</a> to get the full experience.</p> <p class="ie-warning">You are using a browser that is not supported by <em>MITx</em>, and you might not be able to complete pieces of the course. Please download the latest version of <a href="http://www.mozilla.org/en-US/firefox/new/">Firefox</a> or <a href="https://www.google.com/chrome">Chrome</a> to get the full experience.</p>
<![endif]--> <![endif]-->
...@@ -88,25 +89,25 @@ ...@@ -88,25 +89,25 @@
</div> </div>
</li> </li>
<li><a href="/s/help.html">Help</a></li> <li><a href="/s/help.html">Help</a></li>
<li><a href="/logout">Log out</a></li> % if user.is_authenticated():
<li><a href="${ settings.MITX_ROOT_URL }/logout">Log out</a></li>
% endif
</ul> </ul>
</nav> </nav>
</footer> </footer>
<div id="feedback_div" class="leanModal_box"> <div id="feedback_div" class="leanModal_box">
<h1>Feedback for MITx</h1> <h1>Feedback for MITx</h1>
<p>Found a bug? Got an idea for improving our system? Let us know.</p> <p>Found a bug? Got an idea for improving our system? Let us know.</p>
<form>
<ol>
<li><label>Subject:</label> <input type="text" id="feedback_subject"></li>
<li><label>Feedback: </label><textarea id="feedback_message"></textarea></li>
<li><input id="feedback_button" type="button" value="Submit"></li>
</ol>
</form>
</div>
<form>
<ol>
<li><label>Subject:</label> <input type="text" id="feedback_subject"></li>
<li><label>Feedback: </label><textarea id="feedback_message"></textarea></li>
<li><input id="feedback_button" type="button" value="Submit"></li>
</ol>
</form>
</div>
<script type="text/javascript" src="${static.url('js/jquery.treeview.js')}"></script> <script type="text/javascript" src="${static.url('js/jquery.treeview.js')}"></script>
<script type="text/javascript" src="${static.url('js/jquery.leanModal.min.js')}"></script> <script type="text/javascript" src="${static.url('js/jquery.leanModal.min.js')}"></script>
...@@ -115,57 +116,7 @@ ...@@ -115,57 +116,7 @@
<script type="text/javascript" src="${static.url('js/video_player.js')}"></script> <script type="text/javascript" src="${static.url('js/video_player.js')}"></script>
<script type="text/javascript" src="${static.url('js/schematic.js')}"></script> <script type="text/javascript" src="${static.url('js/schematic.js')}"></script>
<script type="text/javascript" src="${static.url('js/cktsim.js')}"></script> <script type="text/javascript" src="${static.url('js/cktsim.js')}"></script>
<script>
// Feedback form
$(function() {
$("#feedback_button").click(function(){
postJSON("/send_feedback", {"subject":$("#feedback_subject").attr("value"),
"url":document.URL,
"message":$("#feedback_message").attr("value")},
function(data){
$("#feedback_subject").attr("value","");
$("#feedback_message").attr("value","");
$("#feedback_div").html("Feedback submitted. Thank you");
});
});
});
// Calculator
$(function() {
$("#calculator_wrapper").hide();
$(".calc").click(function(){
$("#calculator_wrapper").slideToggle("fast");
$("#calculator_wrapper #calculator_input").focus();
$(this).toggleClass("closed");
return false;
});
$("div.help-wrapper a").hover(function(){
$(".help").toggleClass("shown");
});
$("div.help-wrapper a").click(function(){
return false;
});
$("form#calculator").submit(function(e){
e.preventDefault();
$.getJSON("/calculate", {"equation":$("#calculator_input").attr("value")},
function(data){
$("#calculator_output").attr("value",data.result);
});
});
});
$(function(){
$("a[rel*=leanModal]").leanModal();
});
</script>
<%block name="js_extra"/> <%block name="js_extra"/>
</body> </body>
</html> </html>
<%namespace name='static' file='static_content.html'/> <%namespace name='static' file='static_content.html'/>
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<title><%block name="title">MITx: MIT's new online learning initiative</%block></title> <title><%block name="title">MITx: MIT's new online learning initiative</%block></title>
<meta name="description" content="<%block name="description">MITx will offer a portfolio of MIT courses for free to a virtual community of learners around the world</%block>" /> <meta name="description" content="<%block name="description">MITx will offer a portfolio of MIT courses for free to a virtual community of learners around the world</%block>" />
<meta name="keywords" content="<%block name="keywords">MITx, online learning, MIT, online laboratory, education, learners, undergraduate, certificate</%block>" /> <meta name="keywords" content="<%block name="keywords">MITx, online learning, MIT, online laboratory, education, learners, undergraduate, certificate</%block>" />
<!--link rel="stylesheet" href="${static.url('js/jquery.treeview.css')}" type="text/css" media="all" /--> <!--link rel="stylesheet" href="${ settings.LIB_URL }jquery.treeview.css" type="text/css" media="all" /-->
<%static:css group='marketing'/> <%static:css group='marketing'/>
<meta name="viewport" content="width=device-width, initial-scale=1"/> <meta name="viewport" content="width=device-width, initial-scale=1"/>
<!--[if lt IE 8]> <!--[if lt IE 8]>
<%static:css group='marketing-ie'/> <%static:css group='marketing-ie'/>
<![endif]--> <![endif]-->
<script type="text/javascript" src="${static.url('js/jquery-1.6.2.min.js')}"></script> <script type="text/javascript" src="${static.url('js/jquery-1.6.2.min.js')}"></script>
<script type="text/javascript" src="${static.url('js/jquery-ui-1.8.16.custom.min.js')}"></script> <script type="text/javascript" src="${static.url('js/jquery-ui-1.8.16.custom.min.js')}"></script>
<script type="text/javascript" src="${static.url('js/jquery.leanModal.min.js')}"></script> <script type="text/javascript" src="${static.url('js/jquery.leanModal.min.js')}"></script>
<!--script type="text/javascript" src="${static.url('js/swfobject/swfobject.js')}"></script--> <!--script type="text/javascript" src="${static.url('js/swfobject/swfobject.js')}"></script-->
<!--script type="text/javascript" src="${static.url('js/jquery.treeview.js')}"></script--> <!--script type="text/javascript" src="${static.url('js/jquery.treeview.js')}"></script-->
<!--script type="text/javascript" src="${static.url('js/video_player.js')}"></script--> <!--script type="text/javascript" src="${static.url('js/video_player.js')}"></script-->
<!-- <script type="text/javascript" src="${static.url('js/schematic.js')}"></script> --> <!-- <script type="text/javascript" src="${static.url('js/schematic.js')}"></script> -->
<script src="${static.url('js/html5shiv.js')}"></script> <script src="${static.url('js/html5shiv.js')}"></script>
<%block name="headextra"/>
<%block name="headextra"/>
<script type="text/javascript">
<script type="text/javascript"> function getCookie(name) {
function getCookie(name) { var cookieValue = null;
var cookieValue = null; if (document.cookie && document.cookie != '') {
if (document.cookie && document.cookie != '') { var cookies = document.cookie.split(';');
var cookies = document.cookie.split(';'); for (var i = 0; i < cookies.length; i++) {
for (var i = 0; i < cookies.length; i++) { var cookie = jQuery.trim(cookies[i]);
var cookie = jQuery.trim(cookies[i]); // Does this cookie string begin with the name we want?
// Does this cookie string begin with the name we want? if (cookie.substring(0, name.length + 1) == (name + '=')) {
if (cookie.substring(0, name.length + 1) == (name + '=')) { cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); break;
break; }
} }
} }
} return cookieValue;
return cookieValue; }
}
function postJSON(url, data, callback) { function postJSON(url, data, callback) {
$.ajax({type:'POST', $.ajax({type:'POST',
url: url, url: url,
dataType: 'json', dataType: 'json',
data: data, data: data,
success: callback, success: callback,
headers : {'X-CSRFToken':getCookie('csrftoken')} headers : {'X-CSRFToken':getCookie('csrftoken')}
}); });
} }
</script> </script>
...@@ -62,25 +61,25 @@ function postJSON(url, data, callback) { ...@@ -62,25 +61,25 @@ function postJSON(url, data, callback) {
<body> <body>
<%block name="header"> <%block name="header">
<header class="announcement <%block name="header_class"/>"> <header class="announcement <%block name="header_class"/>">
<div class="anouncement-wrapper"> <div class="anouncement-wrapper">
<%block name="header_nav"> <%block name="header_nav">
<nav> <nav>
<h1><a href="http://mitx.mit.edu/">MITx</a></h1> <h1><a href="http://mitx.mit.edu/">MITx</a></h1>
% if settings.COURSEWARE_ENABLED: % if settings.COURSEWARE_ENABLED:
<%block name="login_area"> <%block name="login_area">
<a rel="leanModal" class="login" href="#login">Log In</a> <a rel="leanModal" class="login" href="#login">Log In</a>
</%block> </%block>
% endif % endif
</nav> </nav>
</%block> </%block>
<%block name="header_text"> <%block name="header_text">
<section> <section>
<h1><em>MITx</em></h1> <h1><em>MITx</em></h1>
<h2>MIT&rsquo;s new online learning initiative</h2> <h2>MIT&rsquo;s new online learning initiative</h2>
</section> </section>
</%block> </%block>
</div> </div>
</header> </header>
</%block> </%block>
...@@ -88,95 +87,95 @@ function postJSON(url, data, callback) { ...@@ -88,95 +87,95 @@ function postJSON(url, data, callback) {
<%block name="bodyextra"/> <%block name="bodyextra"/>
<footer> <footer>
<div class="footer-wrapper"> <div class="footer-wrapper">
<p> Copyright &copy; 2012. MIT. <a href="/t/copyright.html">Some rights reserved.</a></p> <p> Copyright &copy; 2012. MIT. <a href="/t/copyright.html">Some rights reserved.</a></p>
<ul> <ul>
<li><a href="/t/tos.html">Terms of Service</a></li> <li><a href="/t/tos.html">Terms of Service</a></li>
<li><a href="/t/privacy.html">Privacy Policy</a></li> <li><a href="/t/privacy.html">Privacy Policy</a></li>
<li><a href="/t/honor.html">Honor Code</a></li> <li><a href="/t/honor.html">Honor Code</a></li>
<li><a href="/t/mitx_help.html">Help</a></li> <li><a href="/t/mitx_help.html">Help</a></li>
</ul> </ul>
<ul class="social"> <ul class="social">
<li class="linkedin"> <li class="linkedin">
<a href="http://www.linkedin.com/groups/Friends-Alumni-MITx-4316538">Linked In</a> <a href="http://www.linkedin.com/groups/Friends-Alumni-MITx-4316538">Linked In</a>
</li> </li>
<li class="twitter"> <li class="twitter">
<a href="https://twitter.com/#!/MyMITx">Twitter</a> <a href="https://twitter.com/#!/MyMITx">Twitter</a>
</li> </li>
<li class="facebook"> <li class="facebook">
<a href="http://www.facebook.com/pages/MITx/378592442151504">Facebook</a> <a href="http://www.facebook.com/pages/MITx/378592442151504">Facebook</a>
</li> </li>
</ul> </ul>
</div> </div>
</footer> </footer>
% if settings.COURSEWARE_ENABLED: % if settings.COURSEWARE_ENABLED:
<div id="login" class="leanModal_box"><%include file="login.html" /></div> <div id="login" class="leanModal_box"><%include file="login.html" /></div>
% endif % endif
<div id="pwd_reset" class="leanModal_box"><%include file="password_reset_form.html" /></div> <div id="pwd_reset" class="leanModal_box"><%include file="password_reset_form.html" /></div>
<div id="reset_done" class="leanModal_box"></div> <div id="reset_done" class="leanModal_box"></div>
<script> <script>
$(document).ready(function(){ $(document).ready(function(){
/* Handles when the user tries to log in. Grabs form data. Does AJAX. /* Handles when the user tries to log in. Grabs form data. Does AJAX.
Either shows error, or redirects. */ Either shows error, or redirects. */
$('form#login_form').submit(function(e) { $('form#login_form').submit(function(e) {
e.preventDefault(); e.preventDefault();
var submit_data={}; var submit_data={};
$.each($("[id^=li_]"), function(index,value){ $.each($("[id^=li_]"), function(index,value){
submit_data[value.name]=value.value; submit_data[value.name]=value.value;
}); });
submit_data["remember"] = ($('#remember').attr("checked")? true : false); submit_data["remember"] = ($('#remember').attr("checked")? true : false);
postJSON('/login', postJSON('/login',
submit_data, submit_data,
function(json) { function(json) {
if(json.success) { if(json.success) {
location.href="/info"; location.href="/info";
} else if($('#login_error').length == 0) { } else if($('#login_error').length == 0) {
$('#login_form').prepend('<div id="login_error">Email or password is incorrect.</div>'); $('#login_form').prepend('<div id="login_error">Email or password is incorrect.</div>');
} else { } else {
$('#login_error').stop().css("background-color", "#933").animate({ backgroundColor: "#333"}, 2000); $('#login_error').stop().css("background-color", "#933").animate({ backgroundColor: "#333"}, 2000);
}
} }
} );
); });
});
$('form#pwd_reset_form').submit(function(e) {
$('form#pwd_reset_form').submit(function(e) { e.preventDefault();
e.preventDefault(); var submit_data = {};
var submit_data = {}; submit_data['email'] = $('#id_email').val();
submit_data['email'] = $('#id_email').val(); postJSON('/password_reset/',
postJSON('/password_reset/', submit_data,
submit_data, function(json){
function(json){ if (json.success) {
if (json.success) { $('#pwd_reset').html(json.value);
$('#pwd_reset').html(json.value); } else {
} else { $('#pwd_error').html(json.error).stop().css("background-color", "#933").animate({ backgroundColor: "#333"}, 2000);
$('#pwd_error').html(json.error).stop().css("background-color", "#933").animate({ backgroundColor: "#333"}, 2000); }
} }
} );
); });
});
}); });
$(function(){ $(function(){
$("a[rel*=leanModal]").leanModal(); $("a[rel*=leanModal]").leanModal();
$("a.login").click(function(){ $("a.login").click(function(){
$("#login_form #li_email").focus(); $("#login_form #li_email").focus();
}); });
$("a.enroll").click(function(){ $("a.enroll").click(function(){
$("#enroll_form #ca_email").focus(); $("#enroll_form #ca_email").focus();
}); });
}); });
</script> </script>
<%block name="js_extra"/> <%block name="js_extra"/>
</body> </body>
</html> </html>
<section class="math-string">
% if isinline:
<span>[mathjaxinline]${mathstr}[/mathjaxinline]</span>
% else:
<span>[mathjax]${mathstr}[/mathjax]</span>
% endif
<span>${tail}</span>
</section>
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
<section class="intro"> <section class="intro">
<section class="intro-text"> <section class="intro-text">
<p><em>MITx</em> will offer a portfolio of MIT courses for free to a virtual community of learners around the world. It will also enhance the educational experience of its on-campus students, offering them online tools that supplement and enrich their classroom and laboratory experiences.</p> <p><em>MITx</em> will offer a portfolio of MIT courses for free to a virtual community of learners around the world. It will also enhance the educational experience of its on-campus students, offering them online tools that supplement and enrich their classroom and laboratory experiences.</p>
<p>The first <em>MITx</em> course, 6.002x (Circuits and Electronics), will be launched in an experimental prototype form. Watch this space for further upcoming courses, which will become available in Fall 2012.</p> <p>The first <em>MITx</em> course, 6.002x (Circuits and Electronics), was launched in an experimental prototype form. Watch this space for further upcoming courses, which will become available in Fall 2012.</p>
</section> </section>
<section class="intro-video"> <section class="intro-video">
...@@ -34,17 +34,29 @@ ...@@ -34,17 +34,29 @@
</section> </section>
<section class="course"> <section class="course">
<hgroup> <div class="announcement">
<h1>Spring 2012 Course offering</h1> <h1> Announcement </h1>
<h2>Circuits and Electronics</h2> <img src="/static/images/marketing/edx-logo.png" alt="" />
<h3>6.002x</h3> <p>
</hgroup> On May 2, it was announced that Harvard University will join MIT as a partner in edX. MITx, which offers online versions of MIT courses, will be a core offering of edX, as will Harvardx, a set of course offerings from Harvard.
</p>
<p class="announcement-button">
<a href="http://edxonline.org">Read more details here <span class="arrow">&#8227;</span></a>
</p>
</div>
<hgroup>
<h1>Spring 2012 Course offering</h1>
<h2>Circuits and Electronics</h2>
<h3>6.002x</h3>
</hgroup>
<p> <p>
<a href="http://6002x.mitx.mit.edu/" class="more-info">More information <span>&amp;</span> Enroll <span class="arrow">&#8227;</span></a> <a href="http://6002x.mitx.mit.edu/" class="more-info">More information <span>&amp;</span> Enroll <span class="arrow">&#8227;</span></a>
</p> </p>
<p>Taught by Anant Agarwal, with Gerald Sussman and Piotr Mitros, 6.002x (Circuits and Electronics) is an on-line adaption of 6.002, MIT&rsquo;s first undergraduate analog design course. This prototype course will run, free of charge, for students worldwide from March 5, 2012 through June 8, 2012. Students will be given the opportunity to demonstrate their mastery of the material and earn a certificate from <em>MITx</em>.</p> <p>Taught by Anant Agarwal, with Gerald Sussman and Piotr Mitros, 6.002x (Circuits and Electronics) is an on-line adaption of 6.002, MIT&rsquo;s first undergraduate analog design course. This prototype course is running, free of charge, for students worldwide from March 5, 2012 through June 8, 2012. Students are given the opportunity to demonstrate their mastery of the material and earn a certificate from <em>MITx</em>.</p>
</section> </section>
</section> </section>
......
<%inherit file="main.html" />
<%block name="js_extra">
<script>
$(document).ready(function(){
//if(!page) {
// cookie_page = $.cookie("book_page");
// if(cookie_page) {
// goto_page(cookie_page);
// }
//}
$(".handouts ol").treeview({collapsed:true, unique:true/*, cookieId: "treeview-book-nav", persist: "cookie"*/});
});
</script>
</%block>
<%block name="title"><title>MITx Home</title></%block>
<%include file="navigation.html" args="active_page='info'" />
<section class="main-content">
<div class="info-wrapper">
<section class="updates">
<h2>Welcome to MITx</h2>
<hr width="100%">
<h3>Courses available:</h3>
<ul>
<li><a href=${ MITX_ROOT_URL }/courseware/6.002_Spring_2012/>6.002 (Spring 2012)</a></li>
<li><a href=${ MITX_ROOT_URL }/courseware/8.02_Spring_2013/>8.02 (Spring 2013)</a></li>
<li><a href=${ MITX_ROOT_URL }/courseware/8.01_Spring_2013/>8.01 (Spring 201x)</a></li>
</ul>
</section>
</div>
</section>
...@@ -2,18 +2,28 @@ ...@@ -2,18 +2,28 @@
<div class="header-wrapper"> <div class="header-wrapper">
<header> <header>
<hgroup> <hgroup>
<h1><em>MITx</em></h1> <h1><em>
<h2><a href="/courseware">Circuits and Electronics</a></h2> % if settings.ENABLE_MULTICOURSE:
<a href="${ MITX_ROOT_URL }/mitxhome" style="color:black;">MITx</a>
% else:
MITx
% endif
</em></h1>
<h2><a href="${ MITX_ROOT_URL }/courseware/">${ settings.COURSE_TITLE }</a></h2>
</hgroup> </hgroup>
<nav class="${active_page}"> <nav class="${active_page}">
<ul class="coursenav"> <ul class="coursenav">
<li class="courseware"><a href="/courseware">Courseware</a></li> <li class="courseware"><a href="${ MITX_ROOT_URL }/courseware/">Courseware</a></li>
<li class="info"><a href="/info">Course Info</a></li> <li class="info"><a href="${ MITX_ROOT_URL }/info">Course Info</a></li>
<li class="book"><a href="/book">Textbook</a></li> % if user.is_authenticated():
<li class="discussion"><a href="/discussion/questions">Discussion</a></li> <li class="book"><a href="${ MITX_ROOT_URL }/book">Textbook</a></li>
<li class="wiki"><a href="/wiki/view">Wiki</a></li> <li class="discussion"><a href="${ MITX_ROOT_URL }/discussion/questions/">Discussion</a></li>
<li class="profile"><a href="/profile">Profile</a></li> % endif
<li class="wiki"><a href="${ MITX_ROOT_URL }/wiki/view/">Wiki</a></li>
% if user.is_authenticated():
<li class="profile"><a href="${ MITX_ROOT_URL }/profile">Profile</a></li>
% endif
</ul> </ul>
</nav> </nav>
</header> </header>
......
function ${ id }_load() { function ${ id }_content_updated() {
$('#main_${ id }').load('${ ajax_url }problem_get?id=${ id }',
function() {
MathJax.Hub.Queue(["Typeset",MathJax.Hub]); MathJax.Hub.Queue(["Typeset",MathJax.Hub]);
update_schematics(); update_schematics();
$('#check_${ id }').click(function() { $('#check_${ id }').unbind('click').click(function() {
$("input.schematic").each(function(index,element){ element.schematic.update_value(); }); $("input.schematic").each(function(index,element){ element.schematic.update_value(); });
$(".CodeMirror").each(function(index,element){ if (element.CodeMirror.save) element.CodeMirror.save(); });
var submit_data={}; var submit_data={};
$.each($("[id^=input_${ id }_]"), function(index,value){ $.each($("[id^=input_${ id }_]"), function(index,value){
submit_data[value.id]=value.value; if (value.type==="checkbox"){
if (value.checked) {
if (typeof submit_data[value.name] == 'undefined'){
submit_data[value.name]=[];
}
submit_data[value.name].push(value.value);
}
}
if (value.type==="radio"){
if (value.checked) {
submit_data[value.name]= value.value;
}
}
else{
submit_data[value.id]=value.value;
}
}); });
postJSON('/modx/problem/${ id }/problem_check', postJSON('${ MITX_ROOT_URL }/modx/problem/${ id }/problem_check',
submit_data, submit_data,
function(json) { function(json) {
switch(json.success) { switch(json.success) {
case 'incorrect': // Worked, but answer not case 'incorrect': // Worked, but answer not
case 'correct': case 'correct':
${ id }_load(); $('#main_${ id }').html(json.contents);
//alert("!!"+json.success); ${ id }_content_updated();
break; break;
default: default:
alert(json.success); alert(json.success);
} }}
}); );
log_event('problem_check', submit_data); log_event('problem_check', submit_data);
}); });
$('#reset_${ id }').click(function() { $('#reset_${ id }').unbind('click').click(function() {
var submit_data={}; var submit_data={};
$.each($("[id^=input_${ id }_]"), function(index,value){ $.each($("[id^=input_${ id }_]"), function(index,value){
submit_data[value.id]=value.value; submit_data[value.id]=value.value;
}); });
postJSON('/modx/problem/${ id }/problem_reset', {'id':'${ id }'}, function(json) { postJSON('${ MITX_ROOT_URL }/modx/problem/${ id }/problem_reset', {'id':'${ id }'}, function(html_as_json) {
${ id }_load(); $('#main_${ id }').html(html_as_json);
${ id }_content_updated();
}); });
log_event('problem_reset', submit_data); log_event('problem_reset', submit_data);
}); });
$('#show_${ id }').click(function() { // show answer button
postJSON('/modx/problem/${ id }/problem_show', {}, function(data) { // TODO: the button should turn into "hide answer" afterwards
$('#show_${ id }').unbind('click').click(function() {
postJSON('${ MITX_ROOT_URL }/modx/problem/${ id }/problem_show', {}, function(data) {
for (var key in data) { for (var key in data) {
$("#answer_"+key).text(data[key]); if ($.isArray(data[key])){
for (var ans_index in data[key]){
var choice_id = 'input_'+key+'_'+data[key][ans_index];
$("label[for="+choice_id+"]").attr("correct_answer", "true");
}
}
$("#answer_"+key).text(data[key]);
} }
}); });
log_event('problem_show', {'problem':'${ id }'}); log_event('problem_show', {'problem':'${ id }'});
}); });
$('#save_${ id }').click(function() { $('#save_${ id }').unbind('click').click(function() {
$("input.schematic").each(function(index,element){ element.schematic.update_value(); }); $("input.schematic").each(function(index,element){ element.schematic.update_value(); });
var submit_data={}; var submit_data={};
$.each($("[id^=input_${ id }_]"), function(index,value){ $.each($("[id^=input_${ id }_]"), function(index,value) {
submit_data[value.id]=value.value;}); submit_data[value.id]=value.value;
postJSON('/modx/problem/${ id }/problem_save', });
submit_data, function(data){ postJSON('${ MITX_ROOT_URL }/modx/problem/${ id }/problem_save',
if(data.success) { submit_data,
alert('Saved'); function(data) {
}} if(data.success) {
); alert('Saved');
}});
log_event('problem_save', submit_data); log_event('problem_save', submit_data);
}); });
} }
);}
function ${ id }_load() {
$('#main_${ id }').load('${ ajax_url }problem_get?id=${ id }', ${ id }_content_updated);
}
$(function() { $(function() {
${ id }_load(); ${ id }_load();
......
...@@ -151,11 +151,11 @@ $(function() { ...@@ -151,11 +151,11 @@ $(function() {
<% <%
earned = section['section_total'].earned earned = section['section_total'].earned
total = section['section_total'].possible total = section['section_total'].possible
percentageString = "{0:.0%}".format( float(earned)/total) if earned > 0 else "" percentageString = "{0:.0%}".format( float(earned)/total) if earned > 0 and total > 0 else ""
%> %>
<h3><a href="${reverse('courseware_section', args=format_url_params([chapter['course'], chapter['chapter'], section['section']])) }"> <h3><a href="${reverse('courseware_section', args=format_url_params([chapter['course'], chapter['chapter'], section['section']])) }">
${ section['section'] }</a> ${"({0:g}/{1:g}) {2}".format( earned, total, percentageString )}</h3> ${ section['section'] }</a> ${"({0:.3n}/{1:.3n}) {2}".format( float(earned), float(total), percentageString )}</h3>
${section['subtitle']} ${section['subtitle']}
%if 'due' in section and section['due']!="": %if 'due' in section and section['due']!="":
due ${section['due']} due ${section['due']}
...@@ -165,7 +165,7 @@ $(function() { ...@@ -165,7 +165,7 @@ $(function() {
<ol class="scores"> <ol class="scores">
${ "Problem Scores: " if section['graded'] else "Practice Scores: "} ${ "Problem Scores: " if section['graded'] else "Practice Scores: "}
%for score in section['scores']: %for score in section['scores']:
<li class="score">${"{0:g}/{1:g}".format(score.earned,score.possible)}</li> <li class="score">${"{0:.3n}/{1:.3n}".format(float(score.earned),float(score.possible))}</li>
%endfor %endfor
</ol> </ol>
%endif %endif
......
...@@ -9,7 +9,7 @@ $(function () { ...@@ -9,7 +9,7 @@ $(function () {
position: 'absolute', position: 'absolute',
display: 'none', display: 'none',
top: y + 5, top: y + 5,
left: x + 5, left: x + 15,
border: '1px solid #000', border: '1px solid #000',
padding: '4px 6px', padding: '4px 6px',
color: '#fff', color: '#fff',
...@@ -19,96 +19,81 @@ $(function () { ...@@ -19,96 +19,81 @@ $(function () {
} }
/* -------------------------------- Grade detail bars -------------------------------- */ /* -------------------------------- Grade detail bars -------------------------------- */
<% <%
colors = ["#b72121", "#600101", "#666666", "#333333"] colors = ["#b72121", "#600101", "#666666", "#333333"]
categories = {}
tickIndex = 1 tickIndex = 1
sectionSpacer = 0.5 sectionSpacer = 0.25
sectionIndex = 0 sectionIndex = 0
series = []
ticks = [] #These are the indices and x-axis labels for the data ticks = [] #These are the indices and x-axis labels for the data
bottomTicks = [] #Labels on the bottom bottomTicks = [] #Labels on the bottom
detail_tooltips = {} #This an dictionary mapping from 'section' -> array of detail_tooltips detail_tooltips = {} #This an dictionary mapping from 'section' -> array of detail_tooltips
droppedScores = [] #These are the datapoints to indicate assignments which aren't factored into the total score droppedScores = [] #These are the datapoints to indicate assignments which are not factored into the total score
dropped_score_tooltips = [] dropped_score_tooltips = []
for section in grade_summary: for section in grade_summary['section_breakdown']:
if 'subscores' in section: ##This is for sections like labs or homeworks, with several smaller components and a total if section.get('prominent', False):
series.append({ tickIndex += sectionSpacer
'label' : section['category'],
'data' : [[i + tickIndex, score['percentage']] for i,score in enumerate(section['subscores'])], if section['category'] not in categories:
'color' : colors[sectionIndex] colorIndex = len(categories) % len(colors)
}) categories[ section['category'] ] = {'label' : section['category'],
'data' : [],
ticks += [[i + tickIndex, score['label'] ] for i,score in enumerate(section['subscores'])] 'color' : colors[colorIndex]}
bottomTicks.append( [tickIndex + len(section['subscores'])/2, section['category']] )
detail_tooltips[ section['category'] ] = [score['summary'] for score in section['subscores']] categoryData = categories[ section['category'] ]
droppedScores += [[tickIndex + index, 0.05] for index in section['dropped_indices']] categoryData['data'].append( [tickIndex, section['percent']] )
ticks.append( [tickIndex, section['label'] ] )
dropExplanation = "The lowest {0} {1} scores are dropped".format( len(section['dropped_indices']), section['category'] )
dropped_score_tooltips += [dropExplanation] * len(section['dropped_indices']) if section['category'] in detail_tooltips:
detail_tooltips[ section['category'] ].append( section['detail'] )
else:
tickIndex += len(section['subscores']) + sectionSpacer detail_tooltips[ section['category'] ] = [ section['detail'], ]
if 'mark' in section:
category_total_label = section['category'] + " Total" droppedScores.append( [tickIndex, 0.05] )
series.append({ dropped_score_tooltips.append( section['mark']['detail'] )
'label' : category_total_label,
'data' : [ [tickIndex, section['totalscore']] ],
'color' : colors[sectionIndex]
})
ticks.append( [tickIndex, section['totallabel']] )
detail_tooltips[category_total_label] = [section['totalscore_summary']]
else:
series.append({
'label' : section['category'],
'data' : [ [tickIndex, section['totalscore']] ],
'color' : colors[sectionIndex]
})
ticks.append( [tickIndex, section['totallabel']] )
detail_tooltips[section['category']] = [section['totalscore_summary']]
tickIndex += 1 + sectionSpacer tickIndex += 1
sectionIndex += 1
detail_tooltips['Dropped Scores'] = dropped_score_tooltips
if section.get('prominent', False):
tickIndex += sectionSpacer
## ----------------------------- Grade overviewew bar ------------------------- ## ## ----------------------------- Grade overviewew bar ------------------------- ##
totalWeight = 0.0 tickIndex += sectionSpacer
sectionIndex = 0
totalScore = 0.0 series = categories.values()
overviewBarX = tickIndex overviewBarX = tickIndex
extraColorIndex = len(categories) #Keeping track of the next color to use for categories not in categories[]
for section in grade_summary:
weighted_score = section['totalscore'] * section['weight'] for section in grade_summary['grade_breakdown']:
summary_text = "{0} - {1:.1%} of a possible {2:.0%}".format(section['category'], weighted_score, section['weight']) if section['percent'] > 0:
if section['category'] in categories:
weighted_category_label = section['category'] + " - Weighted" color = categories[ section['category'] ]['color']
else:
if section['totalscore'] > 0: color = colors[ extraColorIndex % len(colors) ]
extraColorIndex += 1
series.append({ series.append({
'label' : weighted_category_label, 'label' : section['category'] + "-grade_breakdown",
'data' : [ [overviewBarX, weighted_score] ], 'data' : [ [overviewBarX, section['percent']] ],
'color' : colors[sectionIndex] 'color' : color
}) })
detail_tooltips[weighted_category_label] = [ summary_text ] detail_tooltips[section['category'] + "-grade_breakdown"] = [ section['detail'] ]
sectionIndex += 1
totalWeight += section['weight']
totalScore += section['totalscore'] * section['weight']
ticks += [ [overviewBarX, "Total"] ] ticks += [ [overviewBarX, "Total"] ]
tickIndex += 1 + sectionSpacer tickIndex += 1 + sectionSpacer
totalScore = grade_summary['percent']
detail_tooltips['Dropped Scores'] = dropped_score_tooltips
%> %>
var series = ${ json.dumps(series) }; var series = ${ json.dumps( series ) };
var ticks = ${ json.dumps(ticks) }; var ticks = ${ json.dumps(ticks) };
var bottomTicks = ${ json.dumps(bottomTicks) }; var bottomTicks = ${ json.dumps(bottomTicks) };
var detail_tooltips = ${ json.dumps(detail_tooltips) }; var detail_tooltips = ${ json.dumps(detail_tooltips) };
...@@ -132,7 +117,7 @@ $(function () { ...@@ -132,7 +117,7 @@ $(function () {
var $grade_detail_graph = $("#${graph_div_id}"); var $grade_detail_graph = $("#${graph_div_id}");
if ($grade_detail_graph.length > 0) { if ($grade_detail_graph.length > 0) {
var plot = $.plot($grade_detail_graph, series, options); var plot = $.plot($grade_detail_graph, series, options);
//We need to put back the plotting of the percent here
var o = plot.pointOffset({x: ${overviewBarX} , y: ${totalScore}}); var o = plot.pointOffset({x: ${overviewBarX} , y: ${totalScore}});
$grade_detail_graph.append('<div style="position:absolute;left:' + (o.left - 12) + 'px;top:' + (o.top - 20) + 'px">${"{totalscore:.0%}".format(totalscore=totalScore)}</div>'); $grade_detail_graph.append('<div style="position:absolute;left:' + (o.left - 12) + 'px;top:' + (o.top - 20) + 'px">${"{totalscore:.0%}".format(totalscore=totalScore)}</div>');
} }
......
<html>
<head>
<link rel="stylesheet" href="${ settings.LIB_URL }jquery.treeview.css" type="text/css" media="all" />
<link rel="stylesheet" href="/static/css/codemirror.css" type="text/css" media="all" />
<script type="text/javascript" src="${ settings.LIB_URL }jquery-1.6.2.min.js"></script>
<script type="text/javascript" src="${ settings.LIB_URL }jquery-ui-1.8.16.custom.min.js"></script>
<script type="text/javascript" src="${ settings.LIB_URL }codemirror-compressed.js"></script>
<script type="text/javascript" src="/static/js/schematic.js"></script>
<%include file="mathjax_include.html" />
<script>
function postJSON(url, data, callback) {
$.ajax({type:'POST',
url: url,
dataType: 'json',
data: data,
success: callback,
headers : {'X-CSRFToken':'none'} // getCookie('csrftoken')}
});
}
</script>
</head>
<body>
<!--[if lt IE 9]>
<script src="/static/js/html5shiv.js"></script>
<![endif]-->
<style type="text/css">
.CodeMirror {border-style: solid;
border-width: 1px;}
.CodeMirror-scroll {
height: 500;
width: 100%
}
</style>
## -----------------------------------------------------------------------------
## information and i4x PSL code
<hr width="100%">
<h2>QuickEdit</h2>
<hr width="100%">
<ul>
<li>File = ${filename}</li>
<li>ID = ${id}</li>
</ul>
<form method="post">
<textarea rows="40" cols="160" name="quickedit_${id}" id="quickedit_${id}">${pxmls|h}</textarea>
<br/>
<input type="submit" value="Change Problem" name="qesubmit" />
<input type="submit" value="Revert to original" name="qesubmit" />
</form>
<span>${msg|n}</span>
## -----------------------------------------------------------------------------
## rendered problem display
<script>
// height: auto;
// overflow-y: hidden;
// overflow-x: auto;
$(function(){
var cm = CodeMirror.fromTextArea(document.getElementById("quickedit_${id}"),
{ 'mode': {name: "xml", alignCDATA: true},
lineNumbers: true
});
// $('.my-wymeditor').wymeditor();
});
</script>
<hr width="100%">
<script>
${init_js}
</script>
<style type="text/css">
.staff {display:none;}
}
</style>
<form>
${phtml}
</form>
</body>
</html>
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
var ${ id }contents=["", var ${ id }contents=["",
%for t in items: %for t in items:
${t['content']} , ${t['content']} ,
%endfor %endfor
"" ""
]; ];
...@@ -16,7 +16,7 @@ var ${ id }types=["", ...@@ -16,7 +16,7 @@ var ${ id }types=["",
var ${ id }init_functions=["", var ${ id }init_functions=["",
%for t in items: %for t in items:
function(){ ${t['init_js']} }, function(){ ${t['init_js']} },
%endfor %endfor
""]; ""];
...@@ -24,12 +24,12 @@ var ${ id }titles=${titles}; ...@@ -24,12 +24,12 @@ var ${ id }titles=${titles};
var ${ id }destroy_functions=["", var ${ id }destroy_functions=["",
%for t in items: %for t in items:
function(){ ${t['destroy_js']} }, function(){ ${t['destroy_js']} },
%endfor %endfor
""]; ""];
var ${ id }loc = -1; var ${ id }loc = -1;
function disablePrev() { function disablePrev() {
var i=${ id }loc-1; var i=${ id }loc-1;
log_event("seq_prev", {'old':${id}loc, 'new':i,'id':'${id}'}); log_event("seq_prev", {'old':${id}loc, 'new':i,'id':'${id}'});
if (i < 1 ) { if (i < 1 ) {
...@@ -39,7 +39,7 @@ function disablePrev() { ...@@ -39,7 +39,7 @@ function disablePrev() {
}; };
} }
function disableNext() { function disableNext() {
var i=${ id }loc+1; var i=${ id }loc+1;
log_event("seq_next", {'old':${id}loc, 'new':i,'id':'${id}'}); log_event("seq_next", {'old':${id}loc, 'new':i,'id':'${id}'});
...@@ -53,7 +53,7 @@ function disablePrev() { ...@@ -53,7 +53,7 @@ function disablePrev() {
function ${ id }goto(i) { function ${ id }goto(i) {
log_event("seq_goto", {'old':${id}loc, 'new':i,'id':'${id}'}); log_event("seq_goto", {'old':${id}loc, 'new':i,'id':'${id}'});
postJSON('/modx/sequential/${ id }/goto_position', postJSON('${ MITX_ROOT_URL }/modx/sequential/${ id }/goto_position',
{'position' : i }); {'position' : i });
if (${ id }loc!=-1) if (${ id }loc!=-1)
...@@ -77,11 +77,11 @@ function ${ id }goto(i) { ...@@ -77,11 +77,11 @@ function ${ id }goto(i) {
function ${ id }setup_click(i) { function ${ id }setup_click(i) {
$('#tt_'+i).click(function(eo) { ${ id }goto(i);}); $('#tt_'+i).click(function(eo) { ${ id }goto(i);});
$('#tt_'+i).addClass("seq_"+${ id }types[i]+"_inactive"); $('#tt_'+i).addClass("seq_"+${ id }types[i]+"_inactive");
$('#tt_'+i).parent().append("<p>" + ${ id }titles[i-1] + "</p>"); $('#tt_'+i).append("<p>" + ${ id }titles[i-1] + "</p>");
} }
function ${ id }next() { function ${ id }next() {
var i=${ id }loc+1; var i=${ id }loc+1;
log_event("seq_next", {'old':${id}loc, 'new':i,'id':'${id}'}); log_event("seq_next", {'old':${id}loc, 'new':i,'id':'${id}'});
if(i > ${ len(items) } ) { if(i > ${ len(items) } ) {
...@@ -92,7 +92,7 @@ function ${ id }next() { ...@@ -92,7 +92,7 @@ function ${ id }next() {
} }
function ${ id }prev() { function ${ id }prev() {
var i=${ id }loc-1; var i=${ id }loc-1;
log_event("seq_prev", {'old':${id}loc, 'new':i,'id':'${id}'}); log_event("seq_prev", {'old':${id}loc, 'new':i,'id':'${id}'});
if (i < 1 ) { if (i < 1 ) {
...@@ -105,7 +105,7 @@ function ${ id }prev() { ...@@ -105,7 +105,7 @@ function ${ id }prev() {
$(function() { $(function() {
var i; var i;
for(i=1; i<${ len(items)+1 }; i++) { for(i=1; i<${ len(items)+1 }; i++) {
${ id }setup_click(i); ${ id }setup_click(i);
} }
......
<section class="solution-span">
<span id="solution_${id}"></span>
</section>
<%!
import json
import math
%>
var rawData = ${json.dumps(histogram)};
var maxx = 1;
var maxy = 1.5;
var xticks = Array();
var yticks = Array();
var data = Array();
for (var i = 0; i < rawData.length; i++) {
var score = rawData[i][0];
var count = rawData[i][1];
var log_count = Math.log(count + 1);
data.push( [score, log_count] );
xticks.push( [score, score.toString()] );
yticks.push( [log_count, count.toString()] );
maxx = Math.max( score + 1, maxx );
maxy = Math.max(log_count*1.1, maxy );
}
$.plot($("#histogram_${module_id}"), [{
data: data,
bars: { show: true,
align: 'center',
lineWidth: 0,
fill: 1.0 },
color: "#b72121",
}],
{
xaxis: {min: -1, max: maxx, ticks: xticks, tickLength: 0},
yaxis: {min: 0.0, max: maxy, ticks: yticks, labelWidth: 50},
}
);
<div class="staff_info"> <div class="staff_info">
${xml | h} ${xml | h}
</div> </div>
<div> %if render_histogram:
${ str(histogram) } <div id="histogram_${module_id}" style="width:200px;height:150px"></div>
</div> %endif
<section class="text-input">
<textarea rows="30" cols="80" name="input_${id}" id="input_${id}">${value|h}</textarea>
<span id="answer_${id}"></span>
% if state == 'unsubmitted':
<span class="unanswered" style="display:inline-block;" id="status_${id}"></span>
% elif state == 'correct':
<span class="correct" id="status_${id}"></span>
% elif state == 'incorrect':
<span class="incorrect" id="status_${id}"></span>
% elif state == 'incomplete':
<span class="incorrect" id="status_${id}"></span>
% endif
<br/>
<span class="debug">(${state})</span>
<br/>
<span class="debug">${msg|n}</span>
<br/>
<br/>
<script>
// Note: We need to make the area follow the CodeMirror for this to
// work.
$(function(){
var cm = CodeMirror.fromTextArea(document.getElementById("input_${id}"),
{'mode':"python"});
});
</script>
<style type="text/css">
.CodeMirror {border-style: solid;
border-width: 1px;}
</style>
</section>
% if name is not UNDEFINED and name != None: % if name is not UNDEFINED and name != None:
<h1> ${name} </h1> <h1> ${name} </h1>
% endif % endif
<div class="video-subtitles"> <div class="video-subtitles">
<div class="tc-wrapper">
<div class="video-wrapper"> <div class="video-wrapper">
<div class="video-player"> <div class="video-player">
...@@ -28,12 +29,16 @@ ...@@ -28,12 +29,16 @@
<div class="secondary-controls"> <div class="secondary-controls">
<div class="speeds"> <div class="speeds">
<h3>Speed</h3> <a href="#">
<p class="active"></p> <h3>Speed</h3>
<ol id="video_speeds"></ol> <p class="active"></p>
<ol id="video_speeds"></ol>
</a>
</div> </div>
<a href="#" class="hide-subtitles">Captions</a> <a href="#" class="add-fullscreen" title="Fill browser">Fill Browser</a>
<a href="#" class="hide-subtitles" title="Turn off captions">Captions</a>
</div> </div>
</section> </section>
</section> </section>
...@@ -57,18 +62,71 @@ ...@@ -57,18 +62,71 @@
<li id="stt_p6"><div id="std_p7" onclick="title_seek( 7);"></div></li> <li id="stt_p6"><div id="std_p7" onclick="title_seek( 7);"></div></li>
<li id="stt_p6"><div id="std_p7" onclick="title_seek( 8);"></div></li> <li id="stt_p6"><div id="std_p7" onclick="title_seek( 8);"></div></li>
</ol> </ol>
</div>
</div> </div>
<%block name="js_extra"> <%block name="js_extra">
<script src="/static/js/jquery.ui.touch-punch.min.js"></script>
<script type="text/javascript" charset="utf-8"> <script type="text/javascript" charset="utf-8">
$(function() { $(function() {
// tooltips for full browser and closed caption
$('.add-fullscreen, .hide-subtitles ').qtip({
position: {
my: 'top right',
at: 'top center'
}
});
//full browser
$('.add-fullscreen').click(function() {
$('div.video-subtitles').toggleClass('fullscreen');
if ($('div.video-subtitles').hasClass('fullscreen')) {
$('div.video-subtitles').append('<a href="#" class="exit">Exit</a>');
} else {
$('a.exit').remove();
}
$('.exit').click(function() {
$('div.video-subtitles').removeClass('fullscreen');
$(this).remove();
return false;
});
var link_title = $(this).attr('title');
$(this).attr('title', (link_title == 'Exit fill browser') ? 'Fill browser' : 'Exit fill browser');
return false;
});
//hide subtitles
$('.hide-subtitles').click(function() { $('.hide-subtitles').click(function() {
$('div.video-subtitles').toggleClass('closed'); $('div.video-subtitles').toggleClass('closed');
$(this).toggleClass("off"); var link_title = $(this).attr('title');
$(this).attr('title', (link_title == 'Turn on captions') ? 'Turn off captions' : 'Turn on captions');
return false; return false;
}); });
$("div.speeds a").hover(function() {
$(this).toggleClass("open");
});
$("div.speeds a").click(function() {
return false;
});
}); });
</script> </script>
</%block> </%block>
<ol class="video-mod">
% for t in annotations:
<li id="video-${annotations.index(t)}">
${t[1]['content']}
</li>
% endfor
</ol>
...@@ -88,6 +88,7 @@ function add_speed(key, stream) { ...@@ -88,6 +88,7 @@ function add_speed(key, stream) {
var active = $(this).text(); var active = $(this).text();
$("p.active").text(active); $("p.active").text(active);
}); });
} }
var l=[] var l=[]
...@@ -128,6 +129,9 @@ $(document).ready(function() { ...@@ -128,6 +129,9 @@ $(document).ready(function() {
add_speed(l[i], streams[l[i]]) add_speed(l[i], streams[l[i]])
} }
var dropUpHeight = $('ol#video_speeds').height();
console.log(dropUpHeight);
$('ol#video_speeds').css('top', -(dropUpHeight + 2));
}); });
function toggleVideo(){ function toggleVideo(){
......
GRADER = [
{
'type' : "Homework",
'min_count' : 12,
'drop_count' : 2,
'short_label' : "HW",
'weight' : 0.15,
},
{
'type' : "Lab",
'min_count' : 12,
'drop_count' : 2,
'category' : "Labs",
'weight' : 0.15
},
{
'type' : "Midterm",
'name' : "Midterm Exam",
'short_label' : "Midterm",
'weight' : 0.3,
},
{
'type' : "Final",
'name' : "Final Exam",
'short_label' : "Final",
'weight' : 0.4,
}
]
...@@ -49,6 +49,7 @@ if settings.COURSEWARE_ENABLED: ...@@ -49,6 +49,7 @@ if settings.COURSEWARE_ENABLED:
url(r'^courseware/$', 'courseware.views.index', name="courseware"), url(r'^courseware/$', 'courseware.views.index', name="courseware"),
url(r'^info$', 'util.views.info'), url(r'^info$', 'util.views.info'),
url(r'^wiki/', include('simplewiki.urls')), url(r'^wiki/', include('simplewiki.urls')),
url(r'^masquerade/', include('masquerade.urls')),
url(r'^courseware/(?P<course>[^/]*)/(?P<chapter>[^/]*)/(?P<section>[^/]*)/$', 'courseware.views.index', name="courseware_section"), url(r'^courseware/(?P<course>[^/]*)/(?P<chapter>[^/]*)/(?P<section>[^/]*)/$', 'courseware.views.index', name="courseware_section"),
url(r'^courseware/(?P<course>[^/]*)/(?P<chapter>[^/]*)/$', 'courseware.views.index', name="courseware_chapter"), url(r'^courseware/(?P<course>[^/]*)/(?P<chapter>[^/]*)/$', 'courseware.views.index', name="courseware_chapter"),
url(r'^courseware/(?P<course>[^/]*)/$', 'courseware.views.index', name="courseware_course"), url(r'^courseware/(?P<course>[^/]*)/$', 'courseware.views.index', name="courseware_course"),
...@@ -68,6 +69,12 @@ if settings.COURSEWARE_ENABLED: ...@@ -68,6 +69,12 @@ if settings.COURSEWARE_ENABLED:
url(r'^calculate$', 'util.views.calculate'), url(r'^calculate$', 'util.views.calculate'),
) )
if settings.ENABLE_MULTICOURSE:
urlpatterns += (url(r'^mitxhome$', 'util.views.mitxhome'),)
if settings.QUICKEDIT:
urlpatterns += (url(r'^quickedit/(?P<id>[^/]*)$', 'courseware.views.quickedit'),)
if settings.ASKBOT_ENABLED: if settings.ASKBOT_ENABLED:
urlpatterns += (url(r'^%s' % settings.ASKBOT_URL, include('askbot.urls')), \ urlpatterns += (url(r'^%s' % settings.ASKBOT_URL, include('askbot.urls')), \
url(r'^admin/', include(admin.site.urls)), \ url(r'^admin/', include(admin.site.urls)), \
...@@ -76,6 +83,10 @@ if settings.ASKBOT_ENABLED: ...@@ -76,6 +83,10 @@ if settings.ASKBOT_ENABLED:
# url(r'^robots.txt$', include('robots.urls')), # url(r'^robots.txt$', include('robots.urls')),
) )
if settings.DEBUG:
## Jasmine
urlpatterns=urlpatterns + (url(r'^_jasmine/', include('django_jasmine.urls')),)
urlpatterns = patterns(*urlpatterns) urlpatterns = patterns(*urlpatterns)
if settings.DEBUG: if settings.DEBUG:
......
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