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
This commit is contained in:
Calen Pennington
2012-05-17 15:58:47 -04:00
147 changed files with 6168 additions and 1850 deletions

3
.gitignore vendored
View File

@@ -4,6 +4,9 @@
*.swp
*.orig
*.DS_Store
:2e_*
:2e#
.AppleDouble
database.sqlite
courseware/static/js/mathjax/*
db.newaskbot

View File

@@ -78,17 +78,23 @@ def evaluator(variables, functions, string, cs=False):
# log.debug("functions: {0}".format(functions))
# 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.update(variables)
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)
if not cs:
string_cs = string.lower()
for v in all_variables.keys():
all_variables[v.lower()]=all_variables[v]
for f in all_functions.keys():
all_functions[f.lower()]=all_functions[f]
all_functions = lower_dict(all_functions)
all_variables = lower_dict(all_variables)
CasedLiteral = CaselessLiteral
else:
string_cs = string

View File

@@ -1,3 +1,12 @@
#
# File: courseware/capa/capa_problem.py
#
'''
Main module which shows problems (of "capa" type).
This is used by capa_module.
'''
import copy
import logging
import math
@@ -10,32 +19,45 @@ import struct
from lxml import etree
from lxml.etree import Element
from xml.sax.saxutils import escape, unescape
from mako.template import Template
from util import contextualize_text
from inputtypes import textline, schematic
from responsetypes import numericalresponse, formularesponse, customresponse, schematicresponse, StudentInputError
import inputtypes
from responsetypes import NumericalResponse, FormulaResponse, CustomResponse, SchematicResponse, MultipleChoiceResponse, StudentInputError, TrueFalseResponse, ExternalResponse,ImageResponse,OptionResponse
import calc
import eia
log = logging.getLogger("mitx.courseware")
response_types = {'numericalresponse':numericalresponse,
'formularesponse':formularesponse,
'customresponse':customresponse,
'schematicresponse':schematicresponse}
entry_types = ['textline', 'schematic']
response_properties = ["responseparam", "answer"]
response_types = {'numericalresponse':NumericalResponse,
'formularesponse':FormulaResponse,
'customresponse':CustomResponse,
'schematicresponse':SchematicResponse,
'externalresponse':ExternalResponse,
'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
# We should do this with xlst later
html_transforms = {'problem': {'tag':'div'},
"numericalresponse": {'tag':'span'},
"customresponse": {'tag':'span'},
"externalresponse": {'tag':'span'},
"schematicresponse": {'tag':'span'},
"formularesponse": {'tag':'span'},
"text": {'tag':'span'}}
"multiplechoiceresponse": {'tag':'span'},
"text": {'tag':'span'},
"math": {'tag':'span'},
}
global_context={'random':random,
'numpy':numpy,
@@ -47,30 +69,28 @@ global_context={'random':random,
# These should be removed from HTML output, including all subelements
html_problem_semantics = ["responseparam", "answer", "script"]
# These should be removed from HTML output, but keeping subelements
html_skip = ["numericalresponse", "customresponse", "schematicresponse", "formularesponse", "text"]
# These should be transformed
html_special_response = {"textline":textline.render,
"schematic":schematic.render}
html_skip = ["numericalresponse", "customresponse", "schematicresponse", "formularesponse", "text","externalresponse"]
# removed in MC
## These should be transformed
#html_special_response = {"textline":textline.render,
# "schematic":schematic.render,
# "textbox":textbox.render,
# "solution":solution.render,
# }
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
self.seed = None
self.student_answers = dict()
self.correct_map = dict()
self.done = False
self.filename = filename
self.problem_id = id
if seed != None:
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 'seed' in state:
self.seed = state['seed']
@@ -81,17 +101,13 @@ class LoncapaProblem(object):
if 'done' in state:
self.done = state['done']
# print self.seed
# TODO: Does this deplete the Linux entropy pool? Is this fast enough?
if not self.seed:
self.seed=struct.unpack('i', os.urandom(4))[0]
# print filename, self.seed, seed
## Parse XML file
#log.debug(u"LoncapaProblem() opening file {0}".format(filename))
file_text = open(filename).read()
file_text = fileobject.read()
self.fileobject = fileobject # save it, so we can use for debugging information later
# Convert startouttext and endouttext to proper <text></text>
# TODO: Do with XML operations
file_text = re.sub("startouttext\s*/","text",file_text)
@@ -100,6 +116,9 @@ class LoncapaProblem(object):
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)
for response in self.tree.xpath('//'+"|//".join(response_types)):
responder = response_types[response.tag](response, self.context)
responder.preprocess_response()
def get_state(self):
''' Stored per-user session data neeeded to:
@@ -111,6 +130,9 @@ class LoncapaProblem(object):
'done':self.done}
def get_max_score(self):
'''
TODO: multiple points for programming problems.
'''
sum = 0
for et in entry_types:
sum = sum + self.tree.xpath('count(//'+et+')')
@@ -129,41 +151,76 @@ class LoncapaProblem(object):
'total':self.get_max_score()}
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
context=self.extract_context(self.tree)
self.correct_map = dict()
problems_simple = self.extract_problems(self.tree)
for response in problems_simple:
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)
return self.correct_map
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)
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:
responder = response_types[response.tag](response, self.context)
responder = response_types[response.tag](response, self.context) # instance of numericalresponse, customresponse,...
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)):
answer = entry.get('correct_answer')
answer = entry.get('correct_answer') # correct answer, when specified elsewhere, eg in a textline
if answer:
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
# ======= 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)
context = dict()
for script in tree.xpath('/problem/script'):
exec script.text in global_context, context
### IKE: Why do we need these two lines?
context = {'global_context':global_context} # save global context in here also
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
def get_html(self):
@@ -175,21 +232,46 @@ class LoncapaProblem(object):
if problemtree.tag in html_problem_semantics:
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"
if problemtree.get('id') in self.correct_map:
if problemid in self.correct_map:
status = self.correct_map[problemtree.get('id')]
value = ""
if self.student_answers and problemtree.get('id') in self.student_answers:
value = self.student_answers[problemtree.get('id')]
if self.student_answers and problemid in self.student_answers:
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)
for item in problemtree:
subitems = self.extract_html(item)
if subitems:
if subitems is not None:
for subitem in subitems:
tree.append(subitem)
for (key,value) in problemtree.items():
@@ -210,11 +292,11 @@ class LoncapaProblem(object):
# TODO: Fix. This loses Element().tail
#if problemtree.tag in html_skip:
# return tree
return [tree]
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.)
Annoted correctness and value
In-place transformation
@@ -228,13 +310,21 @@ class LoncapaProblem(object):
response.attrib['state'] = correct
response_id = response_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):
# assign one answer_id for each entry_type or solution_type
entry.attrib['response_id'] = str(response_id)
entry.attrib['answer_id'] = str(answer_id)
entry.attrib['id'] = "%s_%i_%i"%(self.problem_id, response_id, answer_id)
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):
''' Remove layout from the problem, and give a purified XML tree of just the problems '''
problem_tree=copy.deepcopy(problem_tree)

View File

@@ -1,40 +1,251 @@
#
# 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 import etree
from mitxmako.shortcuts import render_to_response, render_to_string
from mitxmako.shortcuts import render_to_string
class textline(object):
@staticmethod
def render(element, value, state):
#-----------------------------------------------------------------------------
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)
#-----------------------------------------------------------------------------
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')
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)
dojs = element.get('dojs') # dojs is used for client-side javascript display & return
# 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)
class schematic(object):
@staticmethod
def render(element, value, state):
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)
#-----------------------------------------------------------------------------
## TODO: Make a wrapper for <codeinput>
def textbox(element, value, status, msg=''):
'''
The textbox is used for code input. The message is the return HTML string from
evaluating the code, eg error messages, and output from the code tests.
TODO: make this use rows and cols attribs, not size
'''
eid=element.get('id')
count = int(eid.split('_')[-2])-1 # HACK
size = element.get('size')
context = {'id':eid, 'value':value, 'state':status, 'count':count, 'size': size, 'msg':msg}
html=render_to_string("textbox.html", context)
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)

View File

@@ -1,26 +1,38 @@
#
# 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 math
import numbers
import numpy
import random
import re
import requests
import scipy
import traceback
import copy
import abc
# specific library imports
from calc import evaluator, UndefinedVariable
from django.conf import settings
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 eia
# TODO: Should be the same object as in capa_problem
global_context={'random':random,
'numpy':numpy,
'math':math,
'scipy':scipy,
'calc':calc,
'eia':eia}
from util import contextualize_text
def compare_with_tolerance(v1, v2, tol):
''' Compare v1 to v2 with maximum tolerance tol
@@ -34,15 +46,153 @@ def compare_with_tolerance(v1, v2, tol):
tolerance = evaluator(dict(),dict(),tol)
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):
self.xml = xml
self.correct_answer = contextualize_text(xml.get('answer'), context)
self.tolerance_xml = xml.xpath('//*[@id=$id]//responseparam[@type="tolerance"]/@default',
id=xml.get('id'))[0]
self.tolerance = contextualize_text(self.tolerance_xml, context)
self.answer_id = xml.xpath('//*[@id=$id]//textline/@id',
id=xml.get('id'))[0]
try:
self.tolerance_xml = xml.xpath('//*[@id=$id]//responseparam[@type="tolerance"]/@default',
id=xml.get('id'))[0]
self.tolerance = contextualize_text(self.tolerance_xml, context)
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):
''' Display HTML for a numeric response '''
@@ -63,7 +213,50 @@ class numericalresponse(object):
def get_answers(self):
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):
self.xml = xml
## CRITICAL TODO: Should cover all entrytypes
@@ -72,19 +265,201 @@ class customresponse(object):
self.answer_ids = xml.xpath('//*[@id=$id]//textline/@id',
id=xml.get('id'))
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',
id=xml.get('id'))[0]
answer_src = answer.get('src')
if answer_src != None:
self.code = open(settings.DATA_DIR+'src/'+answer_src).read()
else:
self.code = answer.text
self.tests = xml.get('answer')
def grade(self, student_answers):
submission = [student_answers[k] for k in sorted(self.answer_ids)]
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):
# Since this is explicitly specified in the problem, this will
@@ -94,16 +469,27 @@ class customresponse(object):
class StudentInputError(Exception):
pass
class formularesponse(object):
#-----------------------------------------------------------------------------
class FormulaResponse(GenericResponse):
def __init__(self, xml, context):
self.xml = xml
self.correct_answer = contextualize_text(xml.get('answer'), context)
self.samples = contextualize_text(xml.get('samples'), context)
self.tolerance_xml = xml.xpath('//*[@id=$id]//responseparam[@type="tolerance"]/@default',
id=xml.get('id'))[0]
self.tolerance = contextualize_text(self.tolerance_xml, context)
self.answer_id = xml.xpath('//*[@id=$id]//textline/@id',
id=xml.get('id'))[0]
try:
self.tolerance_xml = xml.xpath('//*[@id=$id]//responseparam[@type="tolerance"]/@default',
id=xml.get('id'))[0]
self.tolerance = contextualize_text(self.tolerance_xml, context)
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
ts = xml.get('type')
if ts == None:
@@ -129,7 +515,7 @@ class formularesponse(object):
for i in range(numsamples):
instructor_variables = self.strip_dict(dict(self.context))
student_variables = dict()
for var in ranges:
for var in ranges: # ranges give numerical ranges for testing
value = random.uniform(*ranges[var])
instructor_variables[str(var)] = value
student_variables[str(var)] = value
@@ -164,7 +550,9 @@ class formularesponse(object):
def get_answers(self):
return {self.answer_id:self.correct_answer}
class schematicresponse(object):
#-----------------------------------------------------------------------------
class SchematicResponse(GenericResponse):
def __init__(self, xml, context):
self.xml = xml
self.answer_ids = xml.xpath('//*[@id=$id]//schematic/@id',
@@ -179,6 +567,7 @@ class schematicresponse(object):
self.code = answer.text
def grade(self, student_answers):
from capa_problem import global_context
submission = [json.loads(student_answers[k]) for k in sorted(self.answer_ids)]
self.context.update({'submission':submission})
exec self.code in global_context, self.context
@@ -188,3 +577,64 @@ class schematicresponse(object):
# Since this is explicitly specified in the problem, this will
# be handled by capa_problem
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])

View File

@@ -1,132 +0,0 @@
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")

View File

@@ -1,5 +1,13 @@
'''
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 json
import logging
import os
import re
@@ -14,9 +22,11 @@ try: # This lets us do __name__ == ='__main__'
from student.models import UserProfile
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 multicourse import multicourse_settings
except:
print "Could not import/content_parser"
settings = None
''' This file will eventually form an abstraction layer between the
@@ -97,20 +107,9 @@ def item(l, default="", process=lambda x:x):
def id_tag(course):
''' 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
default_ids = courseware.modules.get_default_ids()
#print default_ids, old_ids
#print default_ids == old_ids
# Tag elements with unique IDs
elements = course.xpath("|".join(['//'+c for c in default_ids]))
for elem in elements:
@@ -153,6 +152,9 @@ def propogate_downward_tag(element, attribute_name, parent_attribute = None):
return
def user_groups(user):
if not user.is_authenticated():
return []
# TODO: Rewrite in Django
key = 'user_group_names_{user.id}'.format(user=user)
cache_expiration = 60 * 60 # one hour
@@ -177,15 +179,23 @@ def course_xml_process(tree):
propogate_downward_tag(tree, "due")
propogate_downward_tag(tree, "graded")
propogate_downward_tag(tree, "graceperiod")
propogate_downward_tag(tree, "showanswer")
propogate_downward_tag(tree, "rerandomize")
return tree
def course_file(user):
def course_file(user,coursename=None):
''' 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)
options = {'dev_content':settings.DEV_CONTENT,
'groups' : groups}
@@ -207,13 +217,24 @@ def course_file(user):
return tree
def section_file(user, section):
''' Given a user and the name of a section, return that section
def section_file(user, section, coursename=None, dironly=False):
'''
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"
if filename not in os.listdir(settings.DATA_DIR + '/sections/'):
print filename+" not in "+str(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
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
options = {'dev_content':settings.DEV_CONTENT,
@@ -223,7 +244,7 @@ def section_file(user, section):
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
module occurs once in courseware XML file or hidden section. '''
# Sanitize input
@@ -236,14 +257,15 @@ def module_xml(user, module, id_tag, module_id):
id_tag=id_tag,
id=module_id)
#result_set=doc.xpathEval(xpath_search)
doc = course_file(user)
section_list = (s[:-4] for s in os.listdir(settings.DATA_DIR+'/sections') if s[-4:]=='.xml')
doc = course_file(user,coursename)
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)
if len(result_set)<1:
for section in section_list:
try:
s = section_file(user, section)
s = section_file(user, section, coursename)
except etree.XMLSyntaxError:
ex= sys.exc_info()
raise ContentException("Malformed XML in " + section+ "("+str(ex[1].msg)+")")

View File

@@ -0,0 +1,28 @@
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,
}
]

View File

@@ -0,0 +1,276 @@
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
}

View File

@@ -1,52 +1,73 @@
"""
Course settings module. The settings are based of django.conf. All settings in
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 imp
import logging
import sys
import types
from django.conf import settings
from courseware import global_course_settings
from courseware import graders
from courseware.graders import Score
from models import StudentModule
import courseware.content_parser as content_parser
import courseware.modules
import logging
import random
import urllib
from collections import namedtuple
from django.conf import settings
from lxml import etree
from models import StudentModule
from student.models import UserProfile
_log = logging.getLogger("mitx.courseware")
log = logging.getLogger("mitx.courseware")
Score = namedtuple("Score", "earned possible weight graded section")
def get_grade(user, problem, cache):
## 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
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))
# 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()
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)
return (correct, total)
course_settings = Settings()
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:
@@ -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
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
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.
- grade_summary is the output from the course grader. More information on the format is in the docstring for CourseGrader.
"""
dom=content_parser.course_file(student)
dom=content_parser.course_file(student,coursename)
course = dom.xpath('//course/@name')[0]
xmlChapters = dom.xpath('//course[@name=$course]/chapter', course=course)
@@ -68,7 +87,6 @@ def grade_sheet(student):
response_by_id[response.module_id] = response
totaled_scores = {}
chapters=[]
for c in xmlChapters:
@@ -85,33 +103,29 @@ def grade_sheet(student):
scores=[]
if len(problems)>0:
for p in problems:
(correct,total) = get_grade(student, p, response_by_id)
# 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
(correct,total) = get_score(student, p, response_by_id, coursename=coursename)
if settings.GENERATE_PROFILE_SCORES:
if total > 1:
correct = random.randrange( max(total-2, 1) , total + 1 )
else:
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
format = s.get('format') if s.get('format') else ""
subtitle = s.get('subtitle') if s.get('subtitle') else format
format = s.get('format', "")
subtitle = s.get('subtitle', format)
if format and graded_total[1] > 0:
format_scores = totaled_scores.get(format, [])
format_scores.append( graded_total )
totaled_scores[ format ] = format_scores
score={'section':s.get("name"),
section_score={'section':s.get("name"),
'scores':scores,
'section_total' : section_total,
'format' : format,
@@ -119,154 +133,79 @@ def grade_sheet(student):
'due' : s.get("due") or "",
'graded' : graded,
}
sections.append(score)
sections.append(section_score)
chapters.append({'course':course,
'chapter' : c.get("name"),
'sections' : sections,})
grade_summary = grade_summary_6002x(totaled_scores)
return {'courseware_summary' : chapters, #all assessments as they appear in the course definition
'grade_summary' : grade_summary, #graded assessments only
}
def aggregate_scores(scores):
scores = filter( lambda score: score.possible > 0, scores )
grader = course_settings.GRADER
grade_summary = grader.grade(totaled_scores)
total_correct_graded = sum((score.earned*1.0/score.possible)*score.weight for score in scores if score.graded)
total_possible_graded = sum(score.weight for score in scores if score.graded)
total_correct = sum((score.earned*1.0/score.possible)*score.weight for score in scores)
total_possible = sum(score.weight for score in scores)
return {'courseware_summary' : chapters,
'grade_summary' : grade_summary}
def aggregate_scores(scores, section_name = "summary"):
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 = sum(score.earned for score in scores)
total_possible = sum(score.possible for score in scores)
#regardless of whether or not it is graded
all_total = Score(total_correct,
total_possible,
1,
False,
"summary")
section_name)
#selecting only graded things
graded_total = Score(total_correct_graded,
total_possible_graded,
1,
True,
"summary")
section_name)
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 get_score(user, problem, cache, coursename=None):
## HACK: assumes max score is fixed per problem
id = problem.get('id')
correct = 0.0
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']
# 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=float(response.grade)
aggregate_score /= len(scores) - drop_count
# 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:
## 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()
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.
midterm_score = totaled_scores['Midterm'][0] if 'Midterm' in totaled_scores else Score('?', '?', '?', True, "?")
midterm_percentage = midterm_score.earned * 1.0 / midterm_score.possible if 'Midterm' in totaled_scores else 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:
midterm_score = Score(random.randrange(50, 150), 150, 150, True, "?")
midterm_percentage = midterm_score.earned / float(midterm_score.possible)
final_score = Score(random.randrange(100, 300), 300, 300, True, "?")
final_percentage = final_score.earned / float(final_score.possible)
grade_summary = [
{
'category': 'Homework',
'subscores' : homework_percentages,
'dropped_indices' : homework_dropped_indices,
'totalscore' : homework_total,
'totalscore_summary' : "Homework Average - {0:.0%}".format(homework_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
#Now we re-weight the problem, if specified
weight = problem.get("weight", None)
if weight:
weight = float(weight)
correct = correct * weight / total
total = weight
return (correct, total)

View File

@@ -6,9 +6,9 @@ from django.core.management.base import BaseCommand
from django.conf import settings
from django.contrib.auth.models import User
from mitx.courseware.content_parser import course_file
import mitx.courseware.module_render
import mitx.courseware.modules
from courseware.content_parser import course_file
import courseware.module_render
import courseware.modules
class Command(BaseCommand):
help = "Does basic validity tests on course.xml."
@@ -25,15 +25,15 @@ class Command(BaseCommand):
check = False
print "Confirming all modules render. Nothing should print during this step. "
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
try:
instance=module_class(etree.tostring(module),
module.get('id'),
ajax_url='',
state=None,
track_function = lambda x,y,z:None,
render_function = lambda x: {'content':'','destroy_js':'','init_js':'','type':'video'})
module_class(etree.tostring(module),
module.get('id'),
ajax_url='',
state=None,
track_function = lambda x,y,z:None,
render_function = lambda x: {'content':'','destroy_js':'','init_js':'','type':'video'})
except:
print "==============> Error in ", etree.tostring(module)
check = False

View File

@@ -1,35 +1,44 @@
import StringIO
import json
import logging
import os
import sys
import sys
import urllib
import uuid
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 HttpResponse
from django.shortcuts import redirect
from django.template import Context
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 student.models import UserProfile
import track.views
import courseware.content_parser as content_parser
import courseware.modules
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):
# We don't look up on user -- all queries include user
# Additional lookup would require a DB hit the way Django
@@ -51,60 +60,19 @@ def make_track_function(request):
return track.views.server_track(request, event_type, event, page='x_module')
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):
''' Print out a histogram of grades on a given problem.
Part of staff member debug info.
'''
from django.db import connection, transaction
from django.db import connection
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])
grades = list(cursor.fetchall())
print grades
grades.sort(key=lambda x:x[0]) # Probably not necessary
if (len(grades) == 1 and grades[0][0] == None):
return []
return grades
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:
state = smod.state
# get coursename if stored
if 'coursename' in request.session: coursename = request.session['coursename']
else: coursename = None
# Create a new instance
ajax_url = '/modx/'+module_type+'/'+module_id+'/'
instance=module_class(etree.tostring(xml_module),
ajax_url = settings.MITX_ROOT_URL + '/modx/'+module_type+'/'+module_id+'/'
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,
ajax_url=ajax_url,
state=state,
track_function = make_track_function(request),
render_function = lambda x: render_module(user, request, x, module_object_preload))
state=state)
# If instance wasn't already in the database, create it
if not smod:
# If instance wasn't already in the database, and this
# isn't a guest user, create it
if not smod and user.is_authenticated():
smod=StudentModule(student=user,
module_type = module_type,
module_id=module_id,
state=instance.get_state())
smod.save()
module_object_preload.append(smod)
# Grab content
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:
histogram = grade_histogram(module_id)
render_histogram = len(histogram) > 0
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,
"destroy_js":instance.get_destroy_js(),
'init_js':instance.get_init_js(),
"destroy_js":destory_js,
'init_js':init_js,
'type':module_type}
return content

View File

@@ -16,13 +16,12 @@ import traceback
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 django.http import Http404
from mitxmako.shortcuts import render_to_string
from x_module import XModule
from courseware.capa.capa_problem import LoncapaProblem, StudentInputError
import courseware.content_parser as content_parser
from multicourse import multicourse_settings
log = logging.getLogger("mitx.courseware")
@@ -92,10 +91,13 @@ class Module(XModule):
# User submitted a problem, and hasn't reset. We don't want
# more submissions.
if self.lcp.done and self.rerandomize == "always":
#print "!"
check_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
if not self.lcp.done:
reset_button = False
@@ -114,25 +116,26 @@ class Module(XModule):
if len(explain) == 0:
explain = False
html=render_to_string('problem.html',
{'problem' : content,
'id' : self.item_id,
'check_button' : check_button,
'reset_button' : reset_button,
'save_button' : save_button,
'answer_available' : self.answer_available(),
'ajax_url' : self.ajax_url,
'attempts_used': self.attempts,
'attempts_allowed': self.max_attempts,
'explain': explain
})
context = {'problem' : content,
'id' : self.item_id,
'check_button' : check_button,
'reset_button' : reset_button,
'save_button' : save_button,
'answer_available' : self.answer_available(),
'ajax_url' : self.ajax_url,
'attempts_used': self.attempts,
'attempts_allowed': self.max_attempts,
'explain': explain,
}
html=render_to_string('problem.html', context)
if encapsulate:
html = '<div id="main_{id}">'.format(id=self.item_id)+html+"</div>"
return html
def __init__(self, xml, item_id, ajax_url=None, track_url=None, state=None, track_function=None, render_function = None, meta = 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)
self.attempts = 0
self.max_attempts = None
@@ -185,17 +188,24 @@ class Module(XModule):
if state!=None and 'attempts' in state:
self.attempts=state['attempts']
self.filename=content_parser.item(dom2.xpath('/problem/@filename'))
filename=settings.DATA_DIR+"/problems/"+self.filename+".xml"
self.filename="problems/"+content_parser.item(dom2.xpath('/problem/@filename'))+".xml"
self.name=content_parser.item(dom2.xpath('/problem/@name'))
self.weight=content_parser.item(dom2.xpath('/problem/@weight'))
if self.rerandomize == 'never':
seed = 1
else:
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):
'''
This is called by courseware.module_render, to handle an AJAX call. "get" is request.POST
'''
if dispatch=='problem_get':
response = self.get_problem(get)
elif False: #self.close_date >
@@ -241,16 +251,22 @@ class Module(XModule):
return True
if self.show_answer == 'closed' and not self.closed():
return False
print "aa", self.show_answer
raise Http404
if self.show_answer == 'always':
return True
raise self.system.exception404 #TODO: Not 404
def get_answer(self, get):
if not self.answer_available():
raise Http404
else:
return json.dumps(self.lcp.get_question_answers(),
cls=ComplexEncoder)
'''
For the "show answer" button.
TODO: show answer events should be logged here, not just in the problem.js
'''
if not self.answer_available():
raise self.system.exception404
else:
answers = self.lcp.get_question_answers()
return json.dumps(answers,
cls=ComplexEncoder)
# Figure out if we should move these to capa_problem?
def get_problem(self, get):
@@ -265,66 +281,56 @@ class Module(XModule):
event_info['state'] = self.lcp.get_state()
event_info['filename'] = self.filename
# make a dict of all the student responses ("answers").
answers=dict()
# input_resistor_1 ==> resistor_1
for key in get:
answers['_'.join(key.split('_')[1:])]=get[key]
# print "XXX", answers, get
event_info['answers']=answers
# Too late. Cannot submit
if self.closed():
event_info['failure']='closed'
self.tracker('save_problem_check_fail', event_info)
print "cp"
raise Http404
raise self.system.exception404
# Problem submitted. Student should reset before checking
# again.
if self.lcp.done and self.rerandomize == "always":
event_info['failure']='unreset'
self.tracker('save_problem_check_fail', event_info)
print "cpdr"
raise Http404
raise self.system.exception404
try:
old_state = self.lcp.get_state()
lcp_id = self.lcp.problem_id
filename = self.lcp.filename
correct_map = self.lcp.grade_answers(answers)
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()
# print {'error':sys.exc_info(),
# 'answers':answers,
# 'seed':self.lcp.seed,
# 'filename':self.lcp.filename}
return json.dumps({'success':inst.message})
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()
raise Exception,"error in capa_module"
return json.dumps({'success':'Unknown Error'})
self.attempts = self.attempts + 1
self.lcp.done=True
success = 'correct'
for i in correct_map:
if correct_map[i]!='correct':
success = 'incorrect'
js=json.dumps({'correct_map' : correct_map,
'success' : success})
event_info['correct_map']=correct_map
event_info['success']=success
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):
event_info = dict()
@@ -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.seed=None
filename=settings.DATA_DIR+"problems/"+self.filename+".xml"
self.lcp=LoncapaProblem(filename, self.item_id, self.lcp.get_state())
self.lcp=LoncapaProblem(self.filestore.open(self.filename), self.item_id, self.lcp.get_state())
event_info['new_state']=self.lcp.get_state()
self.tracker('reset_problem', event_info)

View File

@@ -1,7 +1,5 @@
import json
## TODO: Abstract out from Django
from django.conf import settings
from mitxmako.shortcuts import render_to_response, render_to_string
from x_module import XModule
@@ -24,13 +22,13 @@ class Module(XModule):
textlist=[i for i in textlist if type(i)==str]
return "".join(textlist)
try:
filename=settings.DATA_DIR+"html/"+self.filename
return open(filename).read()
filename="html/"+self.filename
return self.filestore.open(filename).read()
except: # For backwards compatibility. TODO: Remove
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):
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)
self.filename = None
filename_l=xmltree.xpath("/html/@filename")

View File

@@ -19,6 +19,6 @@ class Module(XModule):
def get_html(self):
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):
XModule.__init__(self, xml, item_id, ajax_url, track_url, state, render_function)
def __init__(self, system, xml, item_id, state=None):
XModule.__init__(self, system, xml, item_id, state)

View File

@@ -2,10 +2,7 @@ import json
from lxml import etree
## TODO: Abstract out from Django
from django.http import Http404
from django.conf import settings
from mitxmako.shortcuts import render_to_response, render_to_string
from mitxmako.shortcuts import render_to_string
from x_module import XModule
@@ -38,12 +35,10 @@ class Module(XModule):
return self.destroy_js
def handle_ajax(self, dispatch, get):
print "GET", get
print "DISPATCH", dispatch
if dispatch=='goto_position':
self.position = int(get['position'])
return json.dumps({'success':True})
raise Http404()
raise self.system.exception404
def render(self):
if self.rendered:
@@ -107,14 +102,13 @@ class Module(XModule):
self.rendered = True
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)
self.xmltree=etree.fromstring(xml)
def __init__(self, system, xml, item_id, state=None):
XModule.__init__(self, system, xml, item_id, state)
self.xmltree = etree.fromstring(xml)
self.position = 1
if state!=None:
if state != None:
state = json.loads(state)
if 'position' in state: self.position = int(state['position'])

View File

@@ -14,16 +14,16 @@ class Module(XModule):
@classmethod
def get_xml_tags(c):
## TODO: Abstract out from filesystem
tags = os.listdir(settings.DATA_DIR+'/custom_tags')
return tags
def get_html(self):
return self.html
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)
filename = xmltree.tag
params = dict(xmltree.items())
# print params
self.html = render_to_string(filename, params, namespace = 'custom_tags')

View File

@@ -1,7 +1,5 @@
import json
## TODO: Abstract out from Django
from django.conf import settings
from mitxmako.shortcuts import render_to_response, render_to_string
from x_module import XModule
@@ -26,8 +24,9 @@ class Module(XModule):
def get_destroy_js(self):
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)
self.contents=[(e.get("name"),self.render_function(e)) \
for e in xmltree]

View File

@@ -3,8 +3,6 @@ import logging
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 x_module import XModule
@@ -42,7 +40,8 @@ class Module(XModule):
return render_to_string('video.html',{'streams':self.video_list(),
'id':self.item_id,
'position':self.position,
'name':self.name})
'name':self.name,
'annotations':self.annotations})
def get_init_js(self):
'''JavaScript code to be run when problem is shown. Be aware
@@ -52,19 +51,23 @@ class Module(XModule):
log.debug(u"INIT POSITION {0}".format(self.position))
return render_to_string('video_init.js',{'streams':self.video_list(),
'id':self.item_id,
'position':self.position})
'position':self.position})+self.annotations_init
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):
XModule.__init__(self, xml, item_id, ajax_url, track_url, state, track_function, render_function)
self.youtube = etree.XML(xml).get('youtube')
self.name = etree.XML(xml).get('name')
def __init__(self, system, xml, item_id, state=None):
XModule.__init__(self, system, xml, item_id, state)
xmltree=etree.fromstring(xml)
self.youtube = xmltree.get('youtube')
self.name = xmltree.get('name')
self.position = 0
if state != None:
state = json.loads(state)
if 'position' in state:
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]])

View File

@@ -45,13 +45,17 @@ class XModule(object):
get is a dictionary-like object '''
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'''
self.xml = xml
self.item_id = item_id
self.ajax_url = ajax_url
self.track_url = track_url
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

View File

@@ -0,0 +1,15 @@
<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>

View File

@@ -0,0 +1,21 @@
<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>

View File

@@ -0,0 +1,21 @@
<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>

View File

@@ -0,0 +1,63 @@
<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>

View File

@@ -0,0 +1,21 @@
<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>

View File

@@ -1,10 +1,14 @@
import unittest
import os
import numpy
import courseware.modules
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):
def setUp(self):
@@ -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, "e^(j*pi)")+1)<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
try:
calc.evaluator({},{}, "5+7 QWSEKO")
@@ -54,42 +63,284 @@ class ModelsTest(unittest.TestCase):
exception_happened = True
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):
scores = []
Score.__sub__=lambda me, other: (me.earned - other.earned) + (me.possible - other.possible)
all, graded = aggregate_scores(scores)
self.assertEqual(all, Score(earned=0, possible=0, weight=1, graded=False, section="summary"))
self.assertEqual(graded, Score(earned=0, possible=0, weight=1, graded=True, section="summary"))
self.assertEqual(all, Score(earned=0, possible=0, graded=False, 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)
self.assertEqual(all, Score(earned=0, possible=1, weight=1, graded=False, section="summary"))
self.assertEqual(graded, Score(earned=0, possible=0, weight=1, graded=True, section="summary"))
self.assertEqual(all, Score(earned=0, possible=5, graded=False, 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)
self.assertAlmostEqual(all, Score(earned=3.0/5, possible=2, weight=1, graded=False, section="summary"))
self.assertAlmostEqual(graded, Score(earned=3.0/5, possible=1, weight=1, graded=True, section="summary"))
self.assertAlmostEqual(all, Score(earned=3, possible=10, graded=False, 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)
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"))
self.assertAlmostEqual(all, Score(earned=5, possible=15, graded=False, 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"))
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"))
class GraderTest(unittest.TestCase):
scores.append(Score(earned=2, possible=5, weight=3, graded=False, section="summary"))
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"))
empty_gradesheet = {
}
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"))

View File

@@ -1,31 +1,25 @@
import json
import logging
import os
import random
import sys
import StringIO
import urllib
import uuid
from django.conf import settings
from django.core.context_processors import csrf
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.template import Context, loader
from mitxmako.shortcuts import render_to_response, render_to_string
#from django.views.decorators.csrf import ensure_csrf_cookie
from django.db import connection
from django.views.decorators.cache import cache_control
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 student.models import UserProfile
from multicourse import multicourse_settings
import courseware.content_parser as content_parser
import courseware.modules.capa_module
import courseware.modules
import courseware.grades as grades
@@ -40,22 +34,26 @@ template_imports={'urllib':urllib}
def gradebook(request):
if 'course_admin' not in content_parser.user_groups(request.user):
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_info = [{'username' :s.username,
'id' : s.id,
'email': s.email,
'grade_info' : grades.grade_sheet(s),
'grade_info' : grades.grade_sheet(s,coursename),
'realname' : UserProfile.objects.get(user = s).name
} for s in student_objects]
return render_to_response('gradebook.html',{'students':student_info})
@login_required
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
def profile(request, student_id = None):
''' User profile. Show username, location, etc, as well as grades .
We need to allow the user to change some of these settings .'''
if not request.user.is_authenticated():
return redirect('/')
if student_id == None:
student = request.user
@@ -67,6 +65,9 @@ def profile(request, student_id = None):
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,
'username':student.username,
'location':user_info.location,
@@ -75,7 +76,7 @@ def profile(request, student_id = None):
'format_url_params' : content_parser.format_url_params,
'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)
@@ -84,8 +85,8 @@ def render_accordion(request,course,chapter,section):
parameter. Returns (initialization_javascript, content)'''
if not course:
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
for i in range(len(toc)):
if toc[i]['active']:
@@ -96,19 +97,21 @@ def render_accordion(request,course,chapter,section):
['format_url_params',content_parser.format_url_params],
['csrf',csrf(request)['csrf_token']]] + \
template_imports.items())
return {'init_js':render_to_string('accordion_init.js',context),
'content':render_to_string('accordion.html',context)}
return render_to_string('accordion.html',context)
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
def render_section(request, section):
''' TODO: Consolidate with index
'''
user = request.user
if not settings.COURSEWARE_ENABLED or not user.is_authenticated():
if not settings.COURSEWARE_ENABLED:
return redirect('/')
if 'coursename' in request.session: coursename = request.session['coursename']
else: coursename = None
# try:
dom = content_parser.section_file(user, section)
dom = content_parser.section_file(user, section, coursename)
#except:
# raise Http404
@@ -116,16 +119,19 @@ def render_section(request, section):
module_ids = dom.xpath("//@id")
module_object_preload = list(StudentModule.objects.filter(student=user,
module_id__in=module_ids))
if user.is_authenticated():
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)
if 'init_js' not in module:
module['init_js']=''
context={'init':accordion['init_js']+module['init_js'],
'accordion':accordion['content'],
context={'init':module['init_js'],
'accordion':accordion,
'content':module['content'],
'csrf':csrf(request)['csrf_token']}
@@ -134,13 +140,21 @@ def render_section(request, section):
@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.
'''
user = request.user
if not settings.COURSEWARE_ENABLED or not user.is_authenticated():
if not settings.COURSEWARE_ENABLED:
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
# so they remain readable
## TODO: Properly replace underscores
@@ -148,16 +162,18 @@ def index(request, course="6.002 Spring 2012", chapter="Using the System", secti
chapter=chapter.replace("_"," ")
section=section.replace("_"," ")
# HACK: Force course to 6.002 for now
# Without this, URLs break
if course!="6.002 Spring 2012":
# use multicourse module to determine if "course" is valid
#if course!=settings.COURSE_NAME.replace('_',' '):
if not multicourse_settings.is_valid_course(course):
return redirect('/')
#import logging
#log = logging.getLogger("mitx")
#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]",
course=course, chapter=chapter, section=section)
if len(dom_module) == 0:
@@ -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",
course=course, chapter=chapter, section=section)
module_object_preload = list(StudentModule.objects.filter(student=user,
module_id__in=module_ids))
if user.is_authenticated():
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)
@@ -179,10 +198,156 @@ def index(request, course="6.002 Spring 2012", chapter="Using the System", secti
if 'init_js' not in module:
module['init_js']=''
context={'init':accordion['init_js']+module['init_js'],
'accordion':accordion['content'],
context={'init':module['init_js'],
'accordion':accordion,
'content':module['content'],
'COURSE_TITLE':multicourse_settings.get_course_title(course),
'csrf':csrf(request)['csrf_token']}
result = render_to_response('courseware.html', context)
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

View File

View File

@@ -0,0 +1,73 @@
# 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')

View File

@@ -0,0 +1 @@
# multicourse/views.py

View File

@@ -9,7 +9,7 @@ from django.db.models import signals
from django.utils.translation import ugettext_lazy as _
from markdown import markdown
from settings import *
from wiki_settings import *
from util.cache import cache

View File

@@ -3,7 +3,7 @@ from django.conf import settings
from django.template.defaultfilters import stringfilter
from django.utils.http import urlquote as django_urlquote
from simplewiki.settings import *
from simplewiki.wiki_settings import *
register = template.Library()

View File

@@ -1,36 +1,29 @@
# -*- coding: utf-8 -*-
import types
from django.conf import settings
from django.conf import settings as settings
from django.contrib.auth.decorators import login_required
from django.core.context_processors import csrf
from django.core.urlresolvers import get_callable
from django.core.urlresolvers import reverse
from django.db.models import Q
from django.http import Http404, HttpResponse, HttpResponseRedirect, HttpResponseServerError, HttpResponseForbidden, HttpResponseNotAllowed
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.http import HttpResponse, HttpResponseRedirect
from django.utils import simplejson
from django.utils.translation import ugettext_lazy as _
from mitxmako.shortcuts import render_to_response, render_to_string
from mako.lookup import TemplateLookup
from mako.template import Template
import mitxmako.middleware
from mitxmako.shortcuts import render_to_response
from models import * # TODO: Clean up
from settings import *
from multicourse import multicourse_settings
from models import Revision, Article, CreateArticleForm, RevisionFormWithTitle, RevisionForm
import wiki_settings
def view(request, wiki_url):
if not request.user.is_authenticated():
return redirect('/')
(article, path, err) = fetch_from_url(request, wiki_url)
if 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)
if perm_err:
return perm_err
@@ -39,15 +32,12 @@ def view(request, wiki_url):
'wiki_write': article.can_write_l(request.user),
'wiki_attachments_write': article.can_attach(request.user),
'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))
return render_to_response('simplewiki_view.html', d)
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)
if err:
return err
@@ -76,8 +66,6 @@ def view_revision(request, revision_number, wiki_url, revision=None):
def root_redirect(request):
if not request.user.is_authenticated():
return redirect('/')
try:
root = Article.get_root()
except:
@@ -87,8 +75,6 @@ def root_redirect(request):
return HttpResponseRedirect(reverse('wiki_view', args=(root.get_url())))
def create(request, wiki_url):
if not request.user.is_authenticated():
return redirect('/')
url_path = get_url_path(wiki_url)
@@ -161,9 +147,6 @@ def create(request, wiki_url):
return render_to_response('simplewiki_edit.html', d)
def edit(request, wiki_url):
if not request.user.is_authenticated():
return redirect('/')
(article, path, err) = fetch_from_url(request, wiki_url)
if err:
return err
@@ -173,7 +156,7 @@ def edit(request, wiki_url):
if perm_err:
return perm_err
if WIKI_ALLOW_TITLE_EDIT:
if wiki_settings.WIKI_ALLOW_TITLE_EDIT:
EditForm = RevisionFormWithTitle
else:
EditForm = RevisionForm
@@ -195,7 +178,7 @@ def edit(request, wiki_url):
if not request.user.is_anonymous():
new_revision.revision_user = request.user
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.save()
return HttpResponseRedirect(reverse('wiki_view', args=(article.get_url(),)))
@@ -215,9 +198,6 @@ def edit(request, wiki_url):
return render_to_response('simplewiki_edit.html', d)
def history(request, wiki_url, page=1):
if not request.user.is_authenticated():
return redirect('/')
(article, path, err) = fetch_from_url(request, wiki_url)
if err:
return err
@@ -302,9 +282,6 @@ def history(request, wiki_url, page=1):
def revision_feed(request, page=1):
if not request.user.is_superuser:
return redirect('/')
page_size = 10
try:
@@ -332,8 +309,6 @@ def revision_feed(request, page=1):
return render_to_response('simplewiki_revision_feed.html', d)
def search_articles(request):
if not request.user.is_authenticated():
return redirect('/')
# 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.
# Adding some context to results (eg where matches were) would also be nice.
@@ -380,9 +355,6 @@ def search_articles(request):
def search_add_related(request, wiki_url):
if not request.user.is_authenticated():
return redirect('/')
(article, path, err) = fetch_from_url(request, wiki_url)
if err:
return err
@@ -435,9 +407,6 @@ def add_related(request, wiki_url):
return HttpResponseRedirect(reverse('wiki_view', args=(article.get_url(),)))
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)
if err:
return err
@@ -457,8 +426,6 @@ def remove_related(request, wiki_url, related_id):
return HttpResponseRedirect(reverse('wiki_view', args=(article.get_url(),)))
def random_article(request):
if not request.user.is_authenticated():
return redirect('/')
from random import randint
num_arts = Article.objects.count()
article = Article.objects.all()[randint(0, num_arts-1)]
@@ -470,8 +437,6 @@ def encode_err(request, url):
return render_to_response('simplewiki_error.html', d)
def not_found(request, wiki_url):
if not request.user.is_authenticated():
return redirect('/')
"""Generate a NOT FOUND message for some URL"""
d = {'wiki_err_notfound': True,
'wiki_url': wiki_url}
@@ -543,17 +508,22 @@ def check_permissions(request, article, check_read=False, check_write=False, che
# LOGIN PROTECTION #
####################
if WIKI_REQUIRE_LOGIN_VIEW:
view = login_required(view)
history = login_required(history)
# search_related = login_required(search_related)
# wiki_encode_err = login_required(wiki_encode_err)
if wiki_settings.WIKI_REQUIRE_LOGIN_VIEW:
view = login_required(view)
history = login_required(history)
search_articles = login_required(search_articles)
root_redirect = login_required(root_redirect)
revision_feed = login_required(revision_feed)
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_REQUIRE_LOGIN_EDIT:
if wiki_settings.WIKI_REQUIRE_LOGIN_EDIT:
create = login_required(create)
edit = login_required(edit)
add_related = login_required(add_related)
remove_related = login_required(remove_related)
if WIKI_CONTEXT_PREPROCESSORS:
settings.TEMPLATE_CONTEXT_PROCESSORS = settings.TEMPLATE_CONTEXT_PROCESSORS + WIKI_CONTEXT_PREPROCESSORS
if wiki_settings.WIKI_CONTEXT_PREPROCESSORS:
settings.TEMPLATE_CONTEXT_PROCESSORS += wiki_settings.WIKI_CONTEXT_PREPROCESSORS

View File

@@ -3,14 +3,21 @@ import os
from django.contrib.auth.decorators import login_required
from django.core.servers.basehttp import FileWrapper
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 settings import * # TODO: Clean up
from models import Article, ArticleAttachment, get_attachment_filepath
from views import not_found, check_permissions, get_url_path, fetch_from_url
from models import ArticleAttachment, get_attachment_filepath
from views import check_permissions, fetch_from_url
from simplewiki.settings import WIKI_ALLOW_ANON_ATTACHMENTS
from wiki_settings import (
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):

View File

281
djangoapps/ssl_auth/ssl_auth.py Executable file
View File

@@ -0,0 +1,281 @@
"""
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

View File

@@ -1,14 +1,8 @@
# Create your views here.
import os
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
from django.contrib.auth.decorators import login_required
from mitxmako.shortcuts import render_to_response
@login_required
def index(request, page=0):
if not request.user.is_authenticated():
return redirect('/')
return render_to_response('staticbook.html',{'page':int(page)})
def index_shifted(request, page):

View File

@@ -10,14 +10,14 @@ from django.conf import settings
from django.contrib.auth import logout, authenticate, login
from django.contrib.auth.forms import PasswordResetForm
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.mail import send_mail
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.shortcuts import redirect
from mitxmako.shortcuts import render_to_response, render_to_string
from mako import exceptions
from django_future.csrf import ensure_csrf_cookie
@@ -93,12 +93,11 @@ def logout_user(request):
logout(request)
return redirect('/')
@login_required
@ensure_csrf_cookie
def change_setting(request):
''' 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
if 'location' in request.POST:
up.location=request.POST['location']
@@ -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'],
email=post_vars['email'],
is_active=False)
@@ -178,7 +168,20 @@ def create_account(request, post_override=None):
r=Registration()
# 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
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)
up = UserProfile(user=u)

View File

@@ -8,7 +8,8 @@ Common traits:
"""
import json
from common import *
from envs.logsettings import get_logger_config
from envs.common import *
############################### ALWAYS THE SAME ################################
DEBUG = False
@@ -24,7 +25,6 @@ with open(ENV_ROOT / "env.json") as env_file:
ENV_TOKENS = json.load(env_file)
SITE_NAME = ENV_TOKENS['SITE_NAME']
CSRF_COOKIE_DOMAIN = ENV_TOKENS['CSRF_COOKIE_DOMAIN']
BOOK_URL = ENV_TOKENS['BOOK_URL']
MEDIA_URL = ENV_TOKENS['MEDIA_URL']
@@ -32,10 +32,10 @@ LOG_DIR = ENV_TOKENS['LOG_DIR']
CACHES = ENV_TOKENS['CACHES']
LOGGING = logsettings.get_logger_config(LOG_DIR,
logging_env=ENV_TOKENS['LOGGING_ENV'],
syslog_addr=(ENV_TOKENS['SYSLOG_SERVER'], 514),
debug=False)
LOGGING = get_logger_config(LOG_DIR,
logging_env=ENV_TOKENS['LOGGING_ENV'],
syslog_addr=(ENV_TOKENS['SYSLOG_SERVER'], 514),
debug=False)
############################## SECURE AUTH ITEMS ###############################
# Secret things: passwords, access keys, etc.
@@ -47,4 +47,4 @@ SECRET_KEY = AUTH_TOKENS['SECRET_KEY']
AWS_ACCESS_KEY_ID = AUTH_TOKENS["AWS_ACCESS_KEY_ID"]
AWS_SECRET_ACCESS_KEY = AUTH_TOKENS["AWS_SECRET_ACCESS_KEY"]
DATABASES = AUTH_TOKENS['DATABASES']
DATABASES = AUTH_TOKENS['DATABASES']

View File

@@ -24,8 +24,7 @@ import tempfile
import djcelery
from path import path
from askbotsettings import * # this is where LIVESETTINGS_OPTIONS comes from
import logsettings
from envs.askbotsettings import * # this is where LIVESETTINGS_OPTIONS comes from
################################### FEATURES ###################################
COURSEWARE_ENABLED = True
@@ -81,6 +80,7 @@ TEMPLATE_DIRS = (
TEMPLATE_CONTEXT_PROCESSORS = (
'django.core.context_processors.request',
'askbot.context.application_settings',
'django.contrib.messages.context_processors.messages',
#'django.core.context_processors.i18n',
'askbot.user_messages.context_processors.user_messages',#must be before auth
'django.core.context_processors.auth', #this is required for admin
@@ -113,7 +113,6 @@ TEMPLATE_DEBUG = False
# Site info
SITE_ID = 1
SITE_NAME = "localhost:8000"
CSRF_COOKIE_DOMAIN = '127.0.0.1'
HTTPS = 'on'
ROOT_URLCONF = 'mitx.urls'
IGNORABLE_404_ENDS = ('favicon.ico')
@@ -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
# contents for each of them. (Right now we just use symlinks.)
STATICFILES_DIRS = (
STATICFILES_DIRS = [
PROJECT_ROOT / "static",
ASKBOT_ROOT / "askbot" / "skins",
("circuits", DATA_DIR / "images"),
@@ -143,7 +142,7 @@ STATICFILES_DIRS = (
# This is how you would use the textbook images locally
# ("book", ENV_ROOT / "book_images")
)
]
# Locale/Internationalization
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
USE_I18N = True
USE_L10N = True
# Messages
MESSAGE_STORAGE = 'django.contrib.messages.storage.session.SessionStorage'
#################################### AWS #######################################
# S3BotoStorage insists on a timeout for uploaded assets. We should make it
# permanent instead, but rather than trying to figure out exactly where that
@@ -179,8 +181,8 @@ CELERY_ALWAYS_EAGER = True
djcelery.setup_loader()
################################# SIMPLEWIKI ###################################
WIKI_REQUIRE_LOGIN_EDIT = True
WIKI_REQUIRE_LOGIN_VIEW = True
SIMPLE_WIKI_REQUIRE_LOGIN_EDIT = True
SIMPLE_WIKI_REQUIRE_LOGIN_VIEW = False
################################# Middleware ###################################
# List of finder classes that know how to find static files in

View File

@@ -2,7 +2,7 @@
These are debug machines used for content creators, so they're kind of a cross
between dev machines and AWS machines.
"""
from aws import *
from envs.aws import *
DEBUG = True
TEMPLATE_DEBUG = True

View File

@@ -7,16 +7,17 @@ sessions. Assumes structure:
/mitx # The location of this repo
/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
PIPELINE = True
TEMPLATE_DEBUG = True
LOGGING = logsettings.get_logger_config(ENV_ROOT / "log",
logging_env="dev",
tracking_filename="tracking.log",
debug=True)
LOGGING = get_logger_config(ENV_ROOT / "log",
logging_env="dev",
tracking_filename="tracking.log",
debug=True)
DATABASES = {
'default': {
@@ -74,7 +75,8 @@ DEBUG_TOOLBAR_PANELS = (
############################ FILE UPLOADS (ASKBOT) #############################
DEFAULT_FILE_STORAGE = 'django.core.files.storage.FileSystemStorage'
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_HANDLERS = (
'django.core.files.uploadhandler.MemoryFileUploadHandler',

View File

@@ -13,7 +13,7 @@ Dir structure:
/log # Where we're going to write log files
"""
from dev import *
from envs.dev import *
DATABASES = {
'default': {

59
envs/static.py Normal file
View File

@@ -0,0 +1,59 @@
"""
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',
)

84
envs/test.py Normal file
View File

@@ -0,0 +1,84 @@
"""
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',
)

98
fixtures/anonymize_fixtures.py Executable file
View File

@@ -0,0 +1,98 @@
#! /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())

3
lib/loncapa/__init__.py Normal file
View File

@@ -0,0 +1,3 @@
#!/usr/bin/python
from loncapa_check import *

View File

@@ -0,0 +1,17 @@
#!/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)

View File

@@ -27,12 +27,16 @@ def render_to_string(template_name, dictionary, context=None, namespace='main'):
# collapse context_instance to a single dictionary for mako
context_dictionary = {}
context_instance['settings'] = settings
context_instance['MITX_ROOT_URL'] = settings.MITX_ROOT_URL
for d in mitxmako.middleware.requestcontext:
context_dictionary.update(d)
for d in context_instance:
context_dictionary.update(d)
if 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
template = middleware.lookup[namespace].get_template(template_name)
return template.render(**context_dictionary)

View File

461
lib/sympy_check/formula.py Normal file
View File

@@ -0,0 +1,461 @@
#!/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)

View File

@@ -0,0 +1,271 @@
#!/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

View File

@@ -2,7 +2,6 @@ import datetime
import json
import sys
from django.conf import settings
from django.conf import settings
from django.contrib.auth.models import User
from django.core.context_processors import csrf
@@ -60,7 +59,10 @@ def send_feedback(request):
def info(request):
''' Info page (link from main header) '''
if not request.user.is_authenticated():
return redirect('/')
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)

View File

@@ -4,14 +4,15 @@ require 'tempfile'
# Build Constants
REPO_ROOT = File.dirname(__FILE__)
BUILD_DIR = File.join(REPO_ROOT, "build")
REPORT_DIR = File.join(REPO_ROOT, "reports")
# Packaging constants
DEPLOY_DIR = "/opt/wwc"
PACKAGE_NAME = "mitx"
LINK_PATH = "/opt/wwc/mitx"
VERSION = "0.1"
PKG_VERSION = "0.1"
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()
if BRANCH == "master"
@@ -19,14 +20,41 @@ if BRANCH == "master"
else
DEPLOY_NAME = "#{PACKAGE_NAME}-#{BRANCH}-#{BUILD_NUMBER}-#{COMMIT}"
end
INSTALL_DIR_PATH = File.join(DEPLOY_DIR, DEPLOY_NAME)
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
CLOBBER.include('build')
CLOBBER.include(BUILD_DIR, REPORT_DIR, 'cover*', '.coverage')
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
FileUtils.mkdir_p(BUILD_DIR)
@@ -53,10 +81,15 @@ task :package do
args = ["fakeroot", "fpm", "-s", "dir", "-t", "deb",
"--after-install=#{postinstall.path}",
"--prefix=#{INSTALL_DIR_PATH}",
"--exclude=build",
"--exclude=rakefile",
"--exclude=.git",
"--exclude=**/*.pyc",
"--exclude=reports",
"-C", "#{REPO_ROOT}",
"--provides=#{PACKAGE_NAME}",
"--name=#{DEPLOY_NAME}",
"--version=#{VERSION}",
"--name=#{NORMALIZED_DEPLOY_NAME}",
"--version=#{PKG_VERSION}",
"-a", "all",
"."]
system(*args) || raise("fpm failed to build the .deb")
@@ -64,5 +97,5 @@ task :package do
end
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

View File

@@ -15,4 +15,8 @@ path.py
django_debug_toolbar
django-pipeline
django-staticfiles>=1.2.1
django-masquerade
fs
django-jasmine
beautifulsoup
requests

View File

@@ -2,10 +2,10 @@ section.help.main-content {
padding: lh();
h1 {
margin-top: 0;
margin-bottom: lh();
padding-bottom: lh();
border-bottom: 1px solid #ddd;
margin-bottom: lh();
margin-top: 0;
padding-bottom: lh();
}
p {
@@ -17,9 +17,9 @@ section.help.main-content {
}
section.self-help {
float: left;
margin-bottom: lh();
margin-right: flex-gutter();
float: left;
width: flex-grid(6);
ul {
@@ -36,17 +36,17 @@ section.help.main-content {
width: flex-grid(6);
dl {
margin-bottom: lh();
display: block;
margin-bottom: lh();
dd {
margin-bottom: lh();
}
dt {
font-weight: bold;
float: left;
clear: left;
float: left;
font-weight: bold;
width: flex-grid(2, 6);
}
}

View File

@@ -16,28 +16,28 @@ div.info-wrapper {
list-style: none;
> li {
padding-bottom: lh(.5);
margin-bottom: lh(.5);
@extend .clearfix;
border-bottom: 1px solid #e3e3e3;
margin-bottom: lh(.5);
padding-bottom: lh(.5);
&:first-child {
padding: lh(.5);
margin: 0 (-(lh(.5))) lh();
background: $cream;
border-bottom: 1px solid darken($cream, 10%);
margin: 0 (-(lh(.5))) lh();
padding: lh(.5);
}
h2 {
float: left;
width: flex-grid(2, 9);
margin: 0 flex-gutter() 0 0;
width: flex-grid(2, 9);
}
section.update-description {
float: left;
width: flex-grid(7, 9);
margin-bottom: 0;
width: flex-grid(7, 9);
li {
margin-bottom: lh(.5);
@@ -55,9 +55,9 @@ div.info-wrapper {
section.handouts {
@extend .sidebar;
border-left: 1px solid #d3d3d3;
@include border-radius(0 4px 4px 0);
border-right: 0;
border-left: 1px solid #d3d3d3;
header {
@extend .bottom-border;
@@ -69,32 +69,32 @@ div.info-wrapper {
}
p {
color: #666;
font-size: 12px;
margin-bottom: 0;
margin-top: 4px;
font-size: 12px;
color: #666;
}
}
ol {
list-style: none;
background: none;
list-style: none;
li {
@include box-shadow(0 1px 0 #eee);
border-bottom: 1px solid #d3d3d3;
@include box-sizing(border-box);
@extend .clearfix;
padding: 7px lh(.75);
background: none;
border-bottom: 1px solid #d3d3d3;
@include box-shadow(0 1px 0 #eee);
@include box-sizing(border-box);
padding: 7px lh(.75);
position: relative;
&.expandable,
&.collapsable {
h4 {
padding-left: 18px;
font-style: $body-font-size;
font-weight: normal;
padding-left: 18px;
}
}
@@ -103,10 +103,10 @@ div.info-wrapper {
margin: 7px (-(lh(.75))) 0;
li {
padding-left: 18px + lh(.75);
@include box-shadow(inset 0 1px 0 #eee);
border-top: 1px solid #d3d3d3;
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 {
div.hitarea {
background-image: url('../images/treeview-default.gif');
width: 100%;
height: 100%;
max-height: 20px;
display: block;
position: absolute;
height: 100%;
left: lh(.75);
margin-left: 0;
max-height: 20px;
position: absolute;
width: 100%;
&:hover {
opacity: 0.6;
@@ -140,27 +140,27 @@ div.info-wrapper {
h3 {
border-bottom: 0;
text-transform: uppercase;
font-weight: bold;
color: #999;
@include box-shadow(none);
color: #999;
font-size: 12px;
font-weight: bold;
text-transform: uppercase;
}
p {
font-size: $body-font-size;
letter-spacing: 0;
margin: 0;
text-transform: none;
letter-spacing: 0;
font-size: $body-font-size;
a {
padding-right: 8px;
&:before {
color: #ccc;
content: "";
@include inline-block();
padding-right: 8px;
color: #ccc;
}
&:first-child {
@@ -173,10 +173,10 @@ div.info-wrapper {
}
a {
@include transition();
color: lighten($text-color, 10%);
text-decoration: none;
@include inline-block();
text-decoration: none;
@include transition();
&:hover {
color: $mit-red;

View File

@@ -4,14 +4,14 @@ div.profile-wrapper {
section.user-info {
@extend .sidebar;
@include border-radius(0px 4px 4px 0);
border-left: 1px solid #d3d3d3;
@include border-radius(0px 4px 4px 0);
border-right: 0;
header {
padding: lh(.5) lh();
margin: 0 ;
@extend .bottom-border;
margin: 0 ;
padding: lh(.5) lh();
h1 {
font-size: 18px;
@@ -20,12 +20,12 @@ div.profile-wrapper {
}
a {
color: #999;
font-size: 12px;
position: absolute;
top: 13px;
right: lh(.5);
text-transform: uppercase;
font-size: 12px;
color: #999;
top: 13px;
&:hover {
color: #555;
@@ -37,14 +37,14 @@ div.profile-wrapper {
list-style: none;
li {
@include transition();
border-bottom: 1px solid #d3d3d3;
@include box-shadow(0 1px 0 #eee);
color: lighten($text-color, 10%);
display: block;
text-decoration: none;
@include box-shadow(0 1px 0 #eee);
padding: 7px lh();
border-bottom: 1px solid #d3d3d3;
position: relative;
text-decoration: none;
@include transition();
div#location_sub, div#language_sub {
font-weight: bold;
@@ -57,9 +57,9 @@ div.profile-wrapper {
input {
&[type="text"] {
@include box-sizing(border-box);
margin: lh(.5) 0;
width: 100%;
@include box-sizing(border-box);
}
&[type="input"]{
@@ -80,12 +80,12 @@ div.profile-wrapper {
a.edit-email,
a.name-edit,
a.email-edit {
color: #999;
font-size: 12px;
position: absolute;
top: 9px;
right: lh(.5);
text-transform: uppercase;
font-size: 12px;
color: #999;
top: 9px;
&:hover {
color: #555;
@@ -93,10 +93,10 @@ div.profile-wrapper {
}
p {
color: #999;
font-size: 12px;
margin-bottom: 0;
margin-top: 4px;
color: #999;
}
a.deactivate {
@@ -132,10 +132,10 @@ div.profile-wrapper {
padding: 7px lh();
h2 {
margin-top: 0;
font-weight: bold;
text-transform: uppercase;
font-size: $body-font-size;
font-weight: bold;
margin-top: 0;
text-transform: uppercase;
}
}
}
@@ -148,14 +148,14 @@ div.profile-wrapper {
@extend .clearfix;
h1 {
margin: 0;
float: left;
margin: 0;
}
}
div#grade-detail-graph {
width: 100%;
min-height: 300px;
width: 100%;
}
> ol {

View File

@@ -3,8 +3,8 @@ div.book-wrapper {
section.book-sidebar {
@extend .sidebar;
@include box-sizing(border-box);
@extend .tran;
@include box-sizing(border-box);
ul#booknav {
font-size: 12px;
@@ -22,14 +22,14 @@ div.book-wrapper {
padding-left: 30px;
div.hitarea {
margin-left: -22px;
background-image: url('../images/treeview-default.gif');
margin-left: -22px;
position: relative;
top: 4px;
&:hover {
opacity: 0.6;
filter: alpha(opacity=60);
opacity: 0.6;
}
}
@@ -63,13 +63,13 @@ div.book-wrapper {
li {
&.last {
float: left;
display: block;
float: left;
a {
@include box-shadow(inset -1px 0 0 lighten(#f6efd4, 5%));
border-right: 1px solid darken(#f6efd4, 20%);
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 {
}
&.bottom-nav {
margin-top: lh();
margin-bottom: -(lh());
border-bottom: 0;
border-top: 1px solid #EDDFAA;
margin-bottom: -(lh());
margin-top: lh();
}
}
@@ -110,18 +110,18 @@ div.book-wrapper {
}
h2 {
padding: 0;
visibility: hidden;
width: 10px;
padding: 0;
}
}
ul#booknav {
max-height: 100px;
overflow: hidden;
padding: 0;
visibility: hidden;
width: 10px;
padding: 0;
overflow: hidden;
max-height: 100px;
}
}

View File

@@ -35,10 +35,10 @@ img {
}
#{$all-text-inputs}, textarea {
@include box-shadow(0 -1px 0 #fff);
@include linear-gradient(#eee, #fff);
border: 1px solid #999;
@include box-shadow(0 -1px 0 #fff);
font: $body-font-size $body-font-family;
@include linear-gradient(#eee, #fff);
padding: 4px;
&:focus {
@@ -63,7 +63,7 @@ a {
p &, li > &, span > &, .inline {
border-bottom: 1px solid #bbb;
font-style: italic;
// font-style: italic;
}
&:hover, &:focus {

View File

@@ -1,8 +1,8 @@
.clearfix:after {
clear: both;
content: ".";
display: block;
height: 0;
clear: both;
visibility: hidden;
}
@@ -40,27 +40,27 @@ h1.top-header {
-webkit-font-smoothing: antialiased;
&:hover, &:focus {
border: 1px solid darken(#888, 20%);
@include box-shadow(inset 0 1px 0 lighten(#888, 20%), 0 0 3px #ccc);
@include linear-gradient(lighten(#888, 10%), darken(#888, 5%));
border: 1px solid darken(#888, 20%);
}
}
.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;
padding: 4px 8px;
@include border-radius(3px);
@include box-shadow(inset 0 1px 0 #fff);
color: #666;
font: normal $body-font-size $body-font-family;
text-decoration: none;
cursor: pointer;
font: normal $body-font-size $body-font-family;
@include linear-gradient(#fff, lighten(#888, 40%));
padding: 4px 8px;
text-decoration: none;
-webkit-font-smoothing: antialiased;
&:hover, &:focus {
@include linear-gradient(#fff, lighten(#888, 37%));
border: 1px solid #ccc;
@include linear-gradient(#fff, lighten(#888, 37%));
text-decoration: none;
}
}
@@ -70,8 +70,8 @@ h1.top-header {
color: $mit-red;
&:hover {
text-decoration: none;
color: darken($mit-red, 20%);
text-decoration: none;
}
}
}
@@ -110,13 +110,13 @@ h1.top-header {
}
a {
font-style: normal;
border: none;
font-style: normal;
}
.bottom-border {
@include box-shadow(0 1px 0 #eee);
border-bottom: 1px solid #d3d3d3;
@include box-shadow(0 1px 0 #eee);
}
@media print {
@@ -124,10 +124,10 @@ h1.top-header {
}
h3 {
border: none;
border-bottom: 1px solid #d3d3d3;
@extend .bottom-border;
background: none;
border: none;
border-bottom: 1px solid #d3d3d3;
color: #000;
font-weight: normal;
margin: 0;
@@ -172,8 +172,8 @@ h1.top-header {
position: relative;
h2 {
padding-right: 20px;
margin: 0;
padding-right: 20px;
}
a {
@@ -205,10 +205,10 @@ h1.top-header {
border-bottom: 1px solid darken($cream, 10%);
@include box-shadow(inset 0 1px 0 #fff, inset 1px 0 0 #fff);
font-size: 12px;
height:46px;
line-height: 46px;
margin: (-$body-line-height) (-$body-line-height) $body-line-height;
text-shadow: 0 1px 0 #fff;
line-height: 46px;
height:46px;
@media print {
display: none;
@@ -242,10 +242,10 @@ h1.top-header {
}
p.ie-warning {
background: yellow;
display: block !important;
line-height: 1.3em;
background: yellow;
margin-bottom: 0;
padding: lh();
text-align: left;
margin-bottom: 0;
}

View File

@@ -1,21 +1,3 @@
// 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
@function lh($amount: 1) {
@return $body-line-height * $amount;

View File

@@ -1,32 +1,22 @@
// Variables
// ---------------------------------------- //
// fonts
$body-font-family: "Open Sans", "Lucida Grande", "Lucida Sans Unicode", "Lucida Sans", Geneva, Verdana, sans-serif;;
// Type
$body-font-family: "Open Sans", "Lucida Grande", "Lucida Sans Unicode", "Lucida Sans", Geneva, Verdana, sans-serif;
$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);
//Flexible grid
$fg-column: $column-width;
$fg-gutter: $gutter-width;
$fg-max-columns: $columns;
// Grid
$fg-column: 80px;
$fg-gutter: 25px;
$fg-max-columns: 12;
$fg-max-width: 1400px;
$fg-min-width: 810px;
// color
// Color
$light-gray: #ddd;
$dark-gray: #333;
$mit-red: #993333;
$cream: #F6EFD4;
$text-color: $dark-gray;
$border-color: $light-gray;

View File

@@ -1,101 +1,243 @@
// JM MOSFET AMPLIFIER
div#graph-container {
section.tool-wrapper {
@extend .clearfix;
border-top: 1px solid #ddd;
padding-top: lh(1.0);
canvas#graph {
background: #073642;
border-bottom: 1px solid darken(#002b36, 10%);
border-top: 1px solid darken(#002b36, 10%);
@include box-shadow(inset 0 0 0 4px darken(#094959, 2%));
color: #839496;
display: table;
margin: lh() (-(lh())) 0;
div#graph-container {
background: none;
@include box-sizing(border-box);
display: table-cell;
padding: lh();
vertical-align: top;
width: flex-grid(4.5, 9) + flex-gutter(9);
.ui-widget-content {
background: none;
border: none;
@include border-radius(0);
}
canvas {
width: 100%;
}
ul.ui-tabs-nav {
background: darken(#073642, 2%);
border-bottom: 1px solid darken(#073642, 8%);
@include border-radius(0);
margin: (-(lh())) (-(lh())) 0;
padding: 0;
position: relative;
width: 110%;
li {
background: none;
border: none;
@include border-radius(0);
color: #fff;
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#controlls-container {
@extend .clearfix;
background: darken(#073642, 2%);
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);
float: left;
margin-right: flex-gutter(9);
}
div.graph-controls {
width: flex-grid(4.5, 9);
float: left;
div.graph-controls {
select#musicTypeSelect {
display: block;
margin-bottom: lh();
div.music-wrapper {
@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();
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#graph-output {
display: block;
margin-bottom: lh();
label {
@include border-radius(2px);
color: #fff;
font-weight: bold;
padding: 3px;
-webkit-font-smoothing: antialiased;
}
div#graph-listen {
display: block;
margin-bottom: lh();
}
p {
margin-bottom: lh(.5);
}
div#label {
display: inline-block;
}
input#playButton {
display: block;
}
}
}
div#schematic-container {
@extend .clearfix;
canvas {
width: flex-grid(4.5, 9);
float: left;
margin-right: flex-gutter(9);
}
div.schematic-sliders {
width: flex-grid(4.5, 9);
float: left;
div.slider-label#vs {
margin-top: lh(2.0);
}
div.slider-label {
margin-bottom: lh(0.5);
}
div.slider {
margin-bottom: lh(1);
}
}
}
//End JM MOSFET AMPLIFIER
// Labels
div.graph-controls, div#graph-listen {
label {
@include border-radius(2px);
font-weight: bold;
padding: 3px;
}
//MOSFET AMPLIFIER
label[for="vinCheckbox"], label[for="vinRadioButton"]{
//MOSFET AMPLIFIER
label[for="vinCheckbox"], label[for="vinRadioButton"]{
color: desaturate(#00bfff, 50%);
}
label[for="voutCheckbox"], label[for="voutRadioButton"]{
}
label[for="voutCheckbox"], label[for="voutRadioButton"]{
color: darken(#ffcf48, 20%);
}
label[for="vrCheckbox"], label[for="vrRadioButton"]{
}
label[for="vrCheckbox"], label[for="vrRadioButton"]{
color: desaturate(#1df914, 40%);
}
//RC Filters
label[for="vcCheckbox"], label[for="vcRadioButton"]{
}
//RC Filters
label[for="vcCheckbox"], label[for="vcRadioButton"]{
color: darken(#ffcf48, 20%);
}
//RLC Series
label[for="vlCheckbox"], label[for="vlRadioButton"]{
}
//RLC Series
label[for="vlCheckbox"], label[for="vlRadioButton"]{
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% );
}
}
}
}
}
}

View File

@@ -1,3 +1,13 @@
html {
height: 100%;
max-height: 100%;
}
body.courseware {
height: 100%;
max-height: 100%;
}
div.course-wrapper {
@extend .table-wrapper;
@@ -7,9 +17,12 @@ div.course-wrapper {
section.course-content {
@extend .content;
overflow: hidden;
@include border-top-right-radius(4px);
@include border-bottom-right-radius(4px);
h1 {
@extend .top-header;
margin: 0 0 lh();
}
p {
@@ -159,18 +172,71 @@ div.course-wrapper {
margin-bottom: 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 {
border-bottom: none;
margin-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 {
h2 {
margin-bottom: lh();
}
ul {
list-style: disc outside none;
margin-left: lh();
margin: 0;
@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 {
}
}
}
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 {

View File

@@ -1,13 +1,14 @@
nav.sequence-nav {
@extend .topbar;
@include box-sizing(border-box);
border-bottom: 1px solid darken($cream, 20%);
margin-bottom: $body-line-height;
position: relative;
@include border-top-right-radius(4px);
ol {
border-bottom: 1px solid darken($cream, 20%);
@include box-sizing(border-box);
display: table;
height: 100%;
padding-right: flex-grid(1, 9);
width: 100%;
@@ -61,116 +62,117 @@ nav.sequence-nav {
display: block;
height: 17px;
padding: 15px 0 14px;
position: relative;
@include transition(all, .4s, $ease-in-out-quad);
width: 100%;
// @media screen and (max-width: 800px) {
// padding: 12px 8px;
// }
//video
&.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 {
@extend .active;
background-image: url('../images/sequence-nav/video-icon-current.png');
background-position: center;
}
//other
&.seq_other_inactive {
@extend .inactive;
background-image: url('../images/sequence-nav/document-icon-normal.png');
background-position: center;
}
&.seq_other_visited {
@extend .visited;
background-image: url('../images/sequence-nav/document-icon-visited.png');
background-position: center;
}
&.seq_other_active {
@extend .active;
background-image: url('../images/sequence-nav/document-icon-current.png');
background-position: center;
}
//vertical & problems
&.seq_vertical_inactive, &.seq_problem_inactive {
@extend .inactive;
background-image: url('../images/sequence-nav/list-icon-normal.png');
background-position: center;
}
&.seq_vertical_visited, &.seq_problem_visited {
@extend .visited;
background-image: url('../images/sequence-nav/list-icon-visited.png');
background-position: center;
}
&.seq_vertical_active, &.seq_problem_active {
@extend .active;
background-image: url('../images/sequence-nav/list-icon-current.png');
background-position: center;
}
//video
&.seq_video_inactive {
@extend .inactive;
background-image: url('../images/sequence-nav/video-icon-normal.png');
background-position: center;
}
p {
// display: none;
// visibility: hidden;
background: #333;
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;
&.seq_video_visited {
@extend .visited;
background-image: url('../images/sequence-nav/video-icon-visited.png');
background-position: center;
}
&:empty {
background: none;
&.seq_video_active {
@extend .active;
background-image: url('../images/sequence-nav/video-icon-current.png');
background-position: center;
}
//other
&.seq_other_inactive {
@extend .inactive;
background-image: url('../images/sequence-nav/document-icon-normal.png');
background-position: center;
}
&.seq_other_visited {
@extend .visited;
background-image: url('../images/sequence-nav/document-icon-visited.png');
background-position: center;
}
&.seq_other_active {
@extend .active;
background-image: url('../images/sequence-nav/document-icon-current.png');
background-position: center;
}
//vertical & problems
&.seq_vertical_inactive, &.seq_problem_inactive {
@extend .inactive;
background-image: url('../images/sequence-nav/list-icon-normal.png');
background-position: center;
}
&.seq_vertical_visited, &.seq_problem_visited {
@extend .visited;
background-image: url('../images/sequence-nav/list-icon-visited.png');
background-position: center;
}
&.seq_vertical_active, &.seq_problem_active {
@extend .active;
background-image: url('../images/sequence-nav/list-icon-current.png');
background-position: center;
}
p {
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 {
display: none;
background: #333;
content: " ";
display: block;
height: 10px;
left: 18px;
position: absolute;
top: -5px;
@include transform(rotate(45deg));
width: 10px;
}
}
&::after {
background: #333;
content: " ";
display: block;
height: 10px;
left: 18px;
position: absolute;
top: -5px;
@include transform(rotate(45deg));
width: 10px;
&:hover {
p {
display: block;
margin-top: 4px;
opacity: 1;
}
}
}
}
}
ul {
margin-right: 1px;
list-style: none !important;
height: 100%;
position: absolute;
right: 0;
top: 0;
@@ -220,6 +222,7 @@ nav.sequence-nav {
&.next {
a {
background-image: url('../images/sequence-nav/next-icon.png');
@include border-top-right-radius(4px);
&:hover {
background-color: none;
@@ -232,26 +235,20 @@ nav.sequence-nav {
section.course-content {
div#seq_content {
margin-bottom: 60px;
}
position: relative;
nav.sequence-bottom {
bottom: (-(lh()));
position: relative;
margin: lh(2) 0 0;
text-align: center;
ul {
@extend .clearfix;
background-color: darken(#F6EFD4, 5%);
background-color: darken($cream, 5%);
border: 1px solid darken(#f6efd4, 20%);
border-bottom: 0;
@include border-radius(3px 3px 0 0);
@include border-radius(3px);
@include box-shadow(inset 0 0 0 1px lighten(#f6efd4, 5%));
margin: 0 auto;
overflow: hidden;
width: 106px;
@include inline-block();
li {
float: left;
@@ -264,15 +261,13 @@ section.course-content {
background-repeat: no-repeat;
border-bottom: none;
display: block;
display: block;
padding: lh(.75) 4px;
padding: lh(.5) 4px;
text-indent: -9999px;
@include transition(all, .4s, $ease-in-out-quad);
width: 45px;
&:hover {
background-color: darken($cream, 10%);
color: darken(#F6EFD4, 60%);
color: darken($cream, 60%);
opacity: .5;
text-decoration: none;
@@ -288,6 +283,7 @@ section.course-content {
&.prev {
a {
background-image: url('../images/sequence-nav/previous-icon.png');
border-right: 1px solid darken(#f6efd4, 20%);
&:hover {
background-color: none;

View File

@@ -26,6 +26,7 @@ section.course-index {
}
&.ui-state-active {
@include background-image(linear-gradient(-90deg, rgb(245,245,245), rgb(225,225,225)));
@extend .active;
}
}
@@ -33,58 +34,99 @@ section.course-index {
ul.ui-accordion-content {
@include border-radius(0);
@include box-shadow( inset -1px 0 0 #e6e6e6);
@include box-shadow(inset -1px 0 0 #e6e6e6);
background: #dadada;
border: none;
border-bottom: 1px solid #c3c3c3;
font-size: 12px;
margin: 0;
// overflow: visible;
padding: 1em 1.5em;
li {
background: transparent;
border: 1px solid transparent;
@include border-radius(4px);
margin-bottom: lh(.5);
position: relative;
&.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));
// }
}
padding: 5px 36px 5px 10px;
a {
text-decoration: none;
margin-bottom: lh(.5);
display: block;
color: #000;
&:hover {
color: #666;
}
color: #666;
p {
font-weight: bold;
margin-bottom: 0;
&.subtitle {
span.subtitle {
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;
}
}
}
}
}

View File

@@ -1,78 +1,81 @@
@-moz-document url-prefix() {
a.add-fullscreen {
display: none !important;
}
}
section.course-content {
.dullify {
opacity: .4;
@include transition();
&:hover {
opacity: 1;
}
}
div.video-subtitles {
padding: 6px lh();
margin: 0 (-(lh()));
border-top: 1px solid #e1e1e1;
border-bottom: 1px solid #e1e1e1;
background: #f3f3f3;
position: relative;
border-bottom: 1px solid #e1e1e1;
border-top: 1px solid #e1e1e1;
@include clearfix();
display: block;
margin: 0 (-(lh()));
padding: 6px lh();
div.video-wrapper {
float: left;
width: flex-grid(6, 9);
margin-right: flex-gutter(9);
width: flex-grid(6, 9);
div.video-player {
position: relative;
padding-bottom: 56.25%;
padding-top: 30px;
height: 0;
overflow: hidden;
padding-bottom: 56.25%;
padding-top: 30px;
position: relative;
object {
height: 100%;
left: 0;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
iframe#html5_player {
border: none;
display: none;
height: 100%;
left: 0;
position: absolute;
top: 0;
left: 0;
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 {
@extend .clearfix;
background: #333;
position: relative;
border: 1px solid #000;
border-top: 0;
color: #ccc;
position: relative;
&:hover {
ul, div {
opacity: 1;
}
}
div#slider {
@extend .clearfix;
@include border-radius(0);
@include box-shadow(inset 0 1px 0 #eee, 0 1px 0 #555);
background: #c2c2c2;
border: none;
border-top: 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;
@include transition(height 2.0s ease-in-out);
@@ -89,80 +92,71 @@ section.course-content {
color: #fff;
font: bold 12px $body-font-family;
margin-bottom: 6px;
margin-right: 0;
overflow: visible;
padding: 4px;
text-align: center;
-webkit-font-smoothing: antialiased;
text-shadow: 0 -1px 0 darken($mit-red, 10%);
overflow: visible;
-webkit-font-smoothing: antialiased;
&::after {
content: " ";
width: 7px;
height: 7px;
display: block;
position: absolute;
background: $mit-red;
border-bottom: 1px solid darken($mit-red, 20%);
border-right: 1px solid darken($mit-red, 20%);
bottom: -5px;
content: " ";
display: block;
height: 7px;
left: 50%;
margin-left: -3px;
position: absolute;
@include transform(rotate(45deg));
background: $mit-red;
border-right: 1px solid darken($mit-red, 20%);
border-bottom: 1px solid darken($mit-red, 20%);
width: 7px;
}
}
a.ui-slider-handle {
background: $mit-red url(../images/slider-handle.png) center center no-repeat;
@include background-size(50%);
border: 1px solid darken($mit-red, 20%);
@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;
border: 1px solid darken($mit-red, 20%);
cursor: pointer;
height: 15px;
margin-left: -7px;
top: -4px;
width: 15px;
@include transition(height 2.0s ease-in-out, width 2.0s ease-in-out);
@include background-size(50%);
width: 15px;
&:focus, &:hover {
background-color: lighten($mit-red, 10%);
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 {
@extend .dullify;
float: left;
list-style: none;
margin-right: lh();
padding: 0;
li {
float: left;
margin-bottom: 0;
a {
@include box-shadow(1px 0 0 #555);
border-bottom: none;
border-right: 1px solid #000;
display: block;
@include box-shadow(1px 0 0 #555);
cursor: pointer;
height: 14px;
padding: lh(.75);
display: block;
line-height: 46px;
padding: 0 lh(.75);
text-indent: -9999px;
width: 14px;
@include transition();
width: 14px;
&.play {
background: url('../images/play-icon.png') center center no-repeat;
@@ -179,145 +173,188 @@ section.course-content {
background-color: #444;
}
}
}
div#vidtime {
padding-left: lh(.75);
font-weight: bold;
line-height: 46px; //height of play pause buttons
padding-left: lh(.75);
-webkit-font-smoothing: antialiased;
}
}
}
div.secondary-controls {
@extend .dullify;
float: right;
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;
line-height: 0;
padding-right: lh(.25);
margin-right: 0;
@include transition();
cursor: pointer;
-webkit-font-smoothing: antialiased;
h3 {
float: left;
padding: 0 lh(.25) 0 lh(.5);
font-weight: normal;
text-transform: uppercase;
font-size: 12px;
letter-spacing: 1px;
color: #999;
a {
background: url('/static/images/closed-arrow.png') 10px center no-repeat;
border-left: 1px solid #000;
border-right: 1px solid #000;
@include box-shadow(1px 0 0 #555, inset 1px 0 0 #555);
@include clearfix();
color: #fff;
cursor: pointer;
display: block;
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 {
@include inline-block();
padding: 0 lh(.5) 0 0;
margin-bottom: 0;
font-weight: bold;
display: none;
}
&.open {
background: url('/static/images/open-arrow.png') 10px center no-repeat;
// fix for now
ol#video_speeds {
@include inline-block();
li {
float: left;
color: #fff;
cursor: pointer;
padding: 0 lh(.25);
line-height: 46px; //height of play pause buttons
&.active {
font-weight: bold;
}
&:last-child {
border-bottom: 0;
margin-top: 0;
@include box-shadow(none);
}
&:hover {
background-color: #444;
ol#video_speeds {
display: block;
opacity: 1;
}
}
h3 {
color: #999;
float: left;
font-size: 12px;
font-weight: normal;
letter-spacing: 1px;
padding: 0 lh(.25) 0 lh(.5);
text-transform: uppercase;
}
p.active {
float: left;
font-weight: bold;
margin-bottom: 0;
padding: 0 lh(.5) 0 0;
}
// fix for now
ol#video_speeds {
background-color: #444;
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 {
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 {
opacity: 1;
background-color: #444;
color: #fff;
text-decoration: none;
}
}
a.hide-subtitles {
float: left;
display: block;
padding: 0 lh(.5);
margin-left: 0;
color: #797979;
line-height: 46px; //height of play pause buttons
width: 30px;
text-indent: -9999px;
font-weight: 800;
background: url('../images/cc.png') center no-repeat;
-webkit-font-smoothing: antialiased;
@include transition();
color: #797979;
display: block;
float: left;
font-weight: 800;
line-height: 46px; //height of play pause buttons
margin-left: 0;
opacity: 1;
padding: 0 lh(.5);
position: relative;
&:after {
text-indent: 0;
position: absolute;
top: 0;
right: -40px;
content: "turn off";
display: block;
width: 70px;
opacity: 0;
visibility: hidden;
@include transition();
}
text-indent: -9999px;
@include transition();
-webkit-font-smoothing: antialiased;
width: 30px;
&:hover {
background-color: #444;
color: #fff;
text-decoration: none;
background-color: #444;
padding-right: 80px;
background-position: 11px center;
&:after {
right: 0;
opacity: 1;
visibility: visible;
}
}
&.off {
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 {
float: left;
width: flex-grid(3, 9);
padding-top: 10px;
max-height: 460px;
overflow: hidden;
padding-top: 10px;
width: flex-grid(3, 9);
li {
border: 0;
@@ -354,8 +391,97 @@ section.course-content {
}
ol.subtitles {
width: 0px;
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;
}
}
}
}
}

View File

@@ -42,16 +42,6 @@ div.answer-block {
padding-top: 20px;
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 {
margin: 10px 0px 10px 16px;
}

View File

@@ -1,4 +1,15 @@
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 {
display: inline-block;
float: left;

View File

@@ -1,23 +1,34 @@
li.calc-main {
bottom: -36px;
bottom: -126px;
left: 0;
position: fixed;
@include transition(bottom);
-webkit-appearance: none;
width: 100%;
z-index: 99;
&.open {
bottom: -36px;
div#calculator_wrapper form div.input-wrapper div.help-wrapper dl {
display: block;
}
}
a.calc {
@include hide-text;
background: url("../images/calc-icon.png") rgba(#111, .9) no-repeat center;
border-bottom: 0;
@include border-radius(3px 3px 0 0);
color: #fff;
float: right;
margin-right: 10px;
@include border-radius(3px 3px 0 0);
@include inline-block;
padding: 8px 12px;
width: 16px;
height: 20px;
@include hide-text;
@include inline-block;
margin-right: 10px;
padding: 8px 12px;
position: relative;
top: -36px;
width: 16px;
&:hover {
opacity: .8;
@@ -30,14 +41,15 @@ li.calc-main {
div#calculator_wrapper {
background: rgba(#111, .9);
clear: both;
max-height: 90px;
position: relative;
top: -36px;
clear: both;
form {
padding: lh();
@extend .clearfix;
@include box-sizing(border-box);
padding: lh();
input#calculator_button {
background: #111;
@@ -46,13 +58,14 @@ li.calc-main {
@include box-shadow(none);
@include box-sizing(border-box);
color: #fff;
float: left;
font-size: 30px;
font-weight: bold;
margin: 0 (flex-gutter() / 2);
padding: 0;
text-shadow: none;
-webkit-appearance: none;
width: flex-grid(.5) + flex-gutter();
float: left;
margin: 0 (flex-gutter() / 2);
&:hover {
color: #333;
@@ -70,29 +83,31 @@ li.calc-main {
font-weight: bold;
margin: 1px 0 0;
padding: 10px;
-webkit-appearance: none;
width: flex-grid(4);
}
div.input-wrapper {
position: relative;
@extend .clearfix;
width: flex-grid(7.5);
margin: 0;
float: left;
margin: 0;
position: relative;
width: flex-grid(7.5);
input#calculator_input {
border: none;
@include box-shadow(none);
@include box-sizing(border-box);
font-size: 16px;
padding: 10px;
width: 100%;
&:focus {
outline: none;
input#calculator_input {
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 {
position: absolute;
@@ -100,10 +115,10 @@ li.calc-main {
top: 15px;
a {
background: url("../images/info-icon.png") center center no-repeat;
height: 17px;
@include hide-text;
width: 17px;
height: 17px;
background: url("../images/info-icon.png") center center no-repeat;
}
dl {
@@ -111,13 +126,14 @@ li.calc-main {
@include border-radius(3px);
@include box-shadow(0 0 3px #999);
color: #333;
display: none;
opacity: 0;
padding: 10px;
position: absolute;
right: -40px;
top: -110px;
width: 500px;
@include transition();
width: 500px;
&.shown {
opacity: 1;

View File

@@ -68,11 +68,11 @@ footer {
}
a {
border-bottom: 0;
display: block;
height: 29px;
width: 28px;
text-indent: -9999px;
border-bottom: 0;
width: 28px;
&:hover {
opacity: .8;

View File

@@ -100,12 +100,12 @@ div.header-wrapper {
float: left;
a {
border: none;
color: #fff;
display: block;
font-style: normal;
font-weight: bold;
padding: 10px lh() 8px;
border: none;
font-style: normal;
@media screen and (max-width: 1020px) {
padding: 10px lh(.7) 8px;
@@ -125,10 +125,10 @@ div.header-wrapper {
ul {
li {
padding: auto;
display: table-cell;
width: 16.6666666667%;
padding: auto;
text-align: center;
width: 16.6666666667%;
}
}
}

View File

@@ -2,11 +2,11 @@ html {
margin-top: 0;
body {
background: #f4f4f4; //#f3f1e5
color: $dark-gray;
font: $body-font-size $body-font-family;
text-align: center;
margin: 0;
background: #f4f4f4; //#f3f1e5
text-align: center;
section.main-content {
@extend .clearfix;
@@ -17,7 +17,7 @@ html {
@include box-shadow(0 0 4px #dfdfdf);
@include box-sizing(border-box);
margin-top: 3px;
// overflow: hidden;
overflow: hidden;
@media print {
border-bottom: 0;
@@ -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 {
@extend .main-content;
max-width: 600px;

View File

@@ -1,12 +1,12 @@
#lean_overlay {
position: fixed;
z-index:100;
top: 0px;
left: 0px;
height:100%;
width:100%;
background: #000;
display: none;
height:100%;
left: 0px;
position: fixed;
top: 0px;
width:100%;
z-index:100;
}
div.leanModal_box {
@@ -31,8 +31,8 @@ div.leanModal_box {
z-index: 2;
&:hover{
text-decoration: none;
color: $mit-red;
text-decoration: none;
}
}
@@ -55,8 +55,8 @@ div.leanModal_box {
li {
&.terms, &.honor-code {
width: auto;
float: none;
width: auto;
}
div.tip {
@@ -118,8 +118,8 @@ div.leanModal_box {
}
&.honor-code {
width: auto;
float: none;
width: auto;
}
label {
@@ -128,8 +128,8 @@ div.leanModal_box {
}
#{$all-text-inputs}, textarea {
width: 100%;
@include box-sizing(border-box);
width: 100%;
}
input[type="checkbox"] {
@@ -176,15 +176,15 @@ div#login {
ol {
li {
width: auto;
float: none;
width: auto;
}
}
}
div.lost-password {
text-align: left;
margin-top: lh();
text-align: left;
a {
color: #999;
@@ -218,9 +218,9 @@ div#deactivate-account {
margin-bottom: lh(.5);
textarea, #{$all-text-inputs} {
@include box-sizing(border-box);
display: block;
width: 100%;
@include box-sizing(border-box);
}
textarea {

View File

@@ -2,5 +2,5 @@
@import "base/reset", "base/font-face", "base/functions";
// 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";

View File

@@ -6,7 +6,7 @@ footer {
div.footer-wrapper {
border-top: 1px solid #e5e5e5;
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) {
background-position: left bottom;
@@ -84,15 +84,15 @@ footer {
}
&.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 {
background: url('../images/marketing/facebook.png') 0 0 no-repeat;
background: url('/static/images/marketing/facebook.png') 0 0 no-repeat;
}
&.linkedin a {
background: url('../images/marketing/linkedin.png') 0 0 no-repeat;
background: url('/static/images/marketing/linkedin.png') 0 0 no-repeat;
}
}
}

View File

@@ -6,10 +6,10 @@ header.announcement {
-webkit-font-smoothing: antialiased;
&.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) {
background: #e3e3e3 url("../images/marketing/shot-5-large.jpg");
background: #e3e3e3 url("/static/images/marketing/shot-5-large.jpg");
}
div {
@@ -33,14 +33,14 @@ header.announcement {
}
&.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) {
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) {
background: #e3e3e3 url("../images/marketing/course-bg-medium.jpg");
background: #e3e3e3 url("/static/images/marketing/course-bg-medium.jpg");
}
div {

View File

@@ -20,6 +20,7 @@ section.index-content {
p {
line-height: lh();
margin-bottom: lh();
}
ul {
@@ -221,22 +222,35 @@ section.index-content {
&.course {
h2 {
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);
@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) {
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) {
padding-top: lh(8);
}
}
div.announcement {
p.announcement-button {
a {
margin-top: 0;
}
}
img {
max-width: 100%;
margin-bottom: lh();
}
}
}

View File

@@ -1,24 +1,25 @@
form#wiki_revision {
float: left;
width: flex-grid(6, 9);
margin-right: flex-gutter(9);
width: flex-grid(6, 9);
label {
display: block;
margin-bottom: 7px ;
}
.CodeMirror-scroll {
min-height: 550px;
width: 100%;
}
.CodeMirror {
@extend textarea;
@include box-sizing(border-box);
font-family: monospace;
margin-bottom: 20px;
}
textarea {
@include box-sizing(border-box);
margin-bottom: 20px;
@@ -32,25 +33,25 @@ form#wiki_revision {
}
#submit_delete {
@include box-shadow(none);
background: none;
border: none;
@include box-shadow(none);
color: #999;
float: right;
text-decoration: underline;
font-weight: normal;
text-decoration: underline;
}
input[type="submit"] {
margin-top: 20px;
}
}
#wiki_edit_instructions {
float: left;
width: flex-grid(3, 9);
margin-top: lh();
color: #666;
float: left;
margin-top: lh();
width: flex-grid(3, 9);
&:hover {
color: #333;
@@ -58,16 +59,14 @@ form#wiki_revision {
.markdown-example {
background-color: #e3e3e3;
text-shadow: 0 1px 0 #fff;
line-height: 1.0;
margin: 5px 0 7px;
padding: {
top: 5px;
right: 2px;
bottom: 5px;
left: 5px;
}
margin: 5px 0 7px;
line-height: 1.0;
text-shadow: 0 1px 0 #fff;
}
}

View File

@@ -3,20 +3,20 @@ div#wiki_panel {
overflow: auto;
h2 {
padding: lh(.5) lh();
@extend .bottom-border;
font-size: 18px;
margin: 0 ;
@extend .bottom-border;
padding: lh(.5) lh();
}
input[type="button"] {
@extend h3;
@include transition();
color: lighten($text-color, 10%);
font-size: $body-font-size;
margin: 0 !important;
padding: 7px lh();
text-align: left;
@include transition();
width: 100%;
&:hover {
@@ -28,8 +28,8 @@ div#wiki_panel {
ul {
li {
&.search {
@include box-shadow(0 1px 0 #eee);
border-bottom: 1px solid #d3d3d3;
@include box-shadow(0 1px 0 #eee);
padding: 7px lh();
label {
@@ -49,15 +49,15 @@ div#wiki_panel {
div#wiki_create_form {
@extend .clearfix;
padding: 15px;
background: #d6d6d6;
border-bottom: 1px solid #bbb;
padding: 15px;
input[type="text"] {
margin-bottom: 6px;
display: block;
width: 100%;
@include box-sizing(border-box);
display: block;
margin-bottom: 6px;
width: 100%;
}
ul {

View File

@@ -7,15 +7,14 @@ div.wiki-wrapper {
@extend .content;
position: relative;
header {
@extend .topbar;
height:46px;
@include box-shadow(inset 0 1px 0 white);
height:46px;
&:empty {
display: none !important;
border-bottom: 0;
display: none !important;
}
a {
@@ -23,10 +22,10 @@ div.wiki-wrapper {
}
p {
float: left;
margin-bottom: 0;
color: darken($cream, 55%);
float: left;
line-height: 46px;
margin-bottom: 0;
padding-left: lh();
}
@@ -48,8 +47,8 @@ div.wiki-wrapper {
@include box-shadow(inset 1px 0 0 lighten(#f6efd4, 5%));
color: darken($cream, 80%);
display: block;
font-weight: normal;
font-size: 12px;
font-weight: normal;
letter-spacing: 1px;
line-height: 46px;
margin: 0;
@@ -89,15 +88,15 @@ div.wiki-wrapper {
width: flex-grid(2.5, 9);
@media screen and (max-width:900px) {
border-right: 0;
display: block;
width: auto;
border-right: 0;
}
@media print {
border-right: 0;
display: block;
width: auto;
border-right: 0;
}
}
@@ -106,9 +105,9 @@ div.wiki-wrapper {
}
section.results {
border-left: 1px dashed #ddd;
@include box-sizing(border-box);
display: inline-block;
border-left: 1px dashed #ddd;
float: left;
padding-left: 10px;
width: flex-grid(6.5, 9);
@@ -123,8 +122,8 @@ div.wiki-wrapper {
@media print {
display: block;
width: auto;
padding: 0;
width: auto;
canvas, img {
page-break-inside: avoid;
@@ -140,14 +139,15 @@ div.wiki-wrapper {
}
li {
border-bottom: 1px solid #eee;
list-style: none;
margin: 0;
padding: 10px 0;
border-bottom: 1px solid #eee;
&:last-child {
border-bottom: 0;
}
h3 {
font-size: 18px;
font-weight: normal;
@@ -155,6 +155,5 @@ div.wiki-wrapper {
}
}
}
}
}

View File

@@ -5,6 +5,30 @@ import tempfile
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
DEFAULT_GROUPS = []
@@ -28,7 +52,6 @@ sys.path.append(BASE_DIR + "/mitx/lib")
COURSEWARE_ENABLED = True
ASKBOT_ENABLED = True
CSRF_COOKIE_DOMAIN = '127.0.0.1'
# Defaults to be overridden
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
@@ -39,8 +62,8 @@ DEFAULT_FEEDBACK_EMAIL = 'feedback@mitx.mit.edu'
GENERATE_RANDOM_USER_CREDENTIALS = False
WIKI_REQUIRE_LOGIN_EDIT = True
WIKI_REQUIRE_LOGIN_VIEW = True
SIMPLE_WIKI_REQUIRE_LOGIN_EDIT = True
SIMPLE_WIKI_REQUIRE_LOGIN_VIEW = False
PERFSTATS = False
@@ -116,9 +139,11 @@ MIDDLEWARE_CLASSES = (
'django.middleware.csrf.CsrfViewMiddleware',
#'django.contrib.auth.middleware.AuthenticationMiddleware',
'cache_toolbox.middleware.CacheBackedAuthenticationMiddleware',
'masquerade.middleware.MasqueradeMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'track.middleware.TrackMiddleware',
'mitxmako.middleware.MakoMiddleware',
#'ssl_auth.ssl_auth.NginxProxyHeaderMiddleware', # ssl authentication behind nginx proxy
#'debug_toolbar.middleware.DebugToolbarMiddleware',
# Uncommenting the following will prevent csrf token from being re-set if you
@@ -146,6 +171,10 @@ INSTALLED_APPS = (
'circuit',
'perfstats',
'util',
'masquerade',
'django_jasmine',
#'ssl_auth', ## Broken. Disabled for now.
'multicourse', # multiple courses
# Uncomment the next line to enable the admin:
# 'django.contrib.admin',
# Uncomment the next line to enable admin documentation:
@@ -346,6 +375,7 @@ PROJECT_ROOT = os.path.dirname(__file__)
TEMPLATE_CONTEXT_PROCESSORS = (
'django.core.context_processors.request',
'django.core.context_processors.static',
'askbot.context.application_settings',
#'django.core.context_processors.i18n',
'askbot.user_messages.context_processors.user_messages',#must be before auth
@@ -369,8 +399,8 @@ INSTALLED_APPS = INSTALLED_APPS + (
CACHE_MIDDLEWARE_ANONYMOUS_ONLY = True
ASKBOT_URL = 'discussion/'
LOGIN_REDIRECT_URL = '/'
LOGIN_URL = '/'
LOGIN_REDIRECT_URL = MITX_ROOT_URL + '/'
LOGIN_URL = MITX_ROOT_URL + '/'
# ASKBOT_UPLOADED_FILES_URL = '%s%s' % (ASKBOT_URL, 'upfiles/')
ALLOW_UNICODE_SLUGS = False
@@ -500,10 +530,11 @@ LIVESETTINGS_OPTIONS = {
'CUSTOM_HEADER' : u'',
'CUSTOM_HTML_HEAD' : u'',
'CUSTOM_JS' : u'',
'SITE_FAVICON' : u'/images/favicon.gif',
'SITE_LOGO_URL' : u'/images/logo.gif',
'MITX_ROOT_URL' : MITX_ROOT_URL, # for askbot header.html file
'SITE_FAVICON' : unicode(MITX_ROOT_URL) + u'/images/favicon.gif',
'SITE_LOGO_URL' :unicode(MITX_ROOT_URL) + u'/images/logo.gif',
'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,
'ASKBOT_DEFAULT_SKIN' : u'default',
'USE_CUSTOM_HTML_HEAD' : False,
@@ -536,12 +567,12 @@ LIVESETTINGS_OPTIONS = {
'SIGNIN_WORDPRESS_ENABLED' : True,
'SIGNIN_WORDPRESS_SITE_ENABLED' : False,
'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' : '',
},
'LICENSE_SETTINGS' : {
'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_URL' : 'http://creativecommons.org/licenses/by-sa/3.0/legalcode',
'LICENSE_USE_LOGO' : True,
@@ -682,3 +713,5 @@ if MAKO_MODULE_DIR == None:
djcelery.setup_loader()
# Jasmine Settings
JASMINE_TEST_DIRECTORY = PROJECT_DIR+'/templates/coffee'

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

121
static/js/application.js Normal file
View File

@@ -0,0 +1,121 @@
// 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);

File diff suppressed because one or more lines are too long

24
static/js/imageinput.js Normal file
View File

@@ -0,0 +1,24 @@
/////////////////////////////////////////////////////////////////////////////
//
// 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;
}

11
static/js/jquery.ui.touch-punch.min.js vendored Normal file
View File

@@ -0,0 +1,11 @@
/*
* 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);

View File

@@ -18,6 +18,14 @@ This set of questions and answers accompanies MIT&rsquo;s February 13,
6.002x: Circuits and Electronics.
</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
is already taken.</h2>

View File

@@ -7,22 +7,14 @@
<ul>
% for section in chapter['sections']:
<li
% if 'active' in section and section['active']:
class="active"
% endif
>
<li${' class="active"' if 'active' in section and section['active'] else ''}>
<a href="${reverse('courseware_section', args=format_url_params([course_name, chapter['name'], section['name']]))}">
<p>${section['name']}</p>
<p class="subtitle">
${section['format']}
% if 'due' in section and section['due']!="":
due ${section['due']}
% endif
<p>${section['name']}
<span class="subtitle">
${section['format']} ${"due " + section['due'] if 'due' in section and section['due'] != '' else ''}
</span>
</p>
</a>
% endfor
</ul>

View File

@@ -1,19 +0,0 @@
$("#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);
});

View File

@@ -0,0 +1,21 @@
<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>

Some files were not shown because too many files have changed in this diff Show More