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:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -4,6 +4,9 @@
|
||||
*.swp
|
||||
*.orig
|
||||
*.DS_Store
|
||||
:2e_*
|
||||
:2e#
|
||||
.AppleDouble
|
||||
database.sqlite
|
||||
courseware/static/js/mathjax/*
|
||||
db.newaskbot
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = {"'": "'", """: '"'}
|
||||
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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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('<','<'))
|
||||
#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(' ','')
|
||||
#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(' ',' ') # 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])
|
||||
|
||||
@@ -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")
|
||||
@@ -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)+")")
|
||||
|
||||
28
djangoapps/courseware/global_course_settings.py
Normal file
28
djangoapps/courseware/global_course_settings.py
Normal 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,
|
||||
}
|
||||
]
|
||||
276
djangoapps/courseware/graders.py
Normal file
276
djangoapps/courseware/graders.py
Normal 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
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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'])
|
||||
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]])
|
||||
|
||||
@@ -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
|
||||
|
||||
15
djangoapps/courseware/test_files/imageresponse.xml
Normal file
15
djangoapps/courseware/test_files/imageresponse.xml
Normal 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>
|
||||
21
djangoapps/courseware/test_files/multi_bare.xml
Normal file
21
djangoapps/courseware/test_files/multi_bare.xml
Normal 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>
|
||||
21
djangoapps/courseware/test_files/multichoice.xml
Normal file
21
djangoapps/courseware/test_files/multichoice.xml
Normal 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>
|
||||
63
djangoapps/courseware/test_files/optionresponse.xml
Normal file
63
djangoapps/courseware/test_files/optionresponse.xml
Normal 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>
|
||||
21
djangoapps/courseware/test_files/truefalse.xml
Normal file
21
djangoapps/courseware/test_files/truefalse.xml
Normal 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>
|
||||
@@ -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"))
|
||||
|
||||
@@ -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
|
||||
|
||||
0
djangoapps/multicourse/__init__.py
Normal file
0
djangoapps/multicourse/__init__.py
Normal file
73
djangoapps/multicourse/multicourse_settings.py
Normal file
73
djangoapps/multicourse/multicourse_settings.py
Normal 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')
|
||||
|
||||
1
djangoapps/multicourse/views.py
Normal file
1
djangoapps/multicourse/views.py
Normal file
@@ -0,0 +1 @@
|
||||
# multicourse/views.py
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
0
djangoapps/ssl_auth/__init__.py
Normal file
0
djangoapps/ssl_auth/__init__.py
Normal file
281
djangoapps/ssl_auth/ssl_auth.py
Executable file
281
djangoapps/ssl_auth/ssl_auth.py
Executable 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
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
14
envs/aws.py
14
envs/aws.py
@@ -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']
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
14
envs/dev.py
14
envs/dev.py
@@ -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',
|
||||
|
||||
@@ -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
59
envs/static.py
Normal 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
84
envs/test.py
Normal 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
98
fixtures/anonymize_fixtures.py
Executable 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
3
lib/loncapa/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
#!/usr/bin/python
|
||||
|
||||
from loncapa_check import *
|
||||
17
lib/loncapa/loncapa_check.py
Normal file
17
lib/loncapa/loncapa_check.py
Normal 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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
0
lib/sympy_check/__init__.py
Normal file
0
lib/sympy_check/__init__.py
Normal file
461
lib/sympy_check/formula.py
Normal file
461
lib/sympy_check/formula.py
Normal 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)
|
||||
271
lib/sympy_check/sympy_check2.py
Normal file
271
lib/sympy_check/sympy_check2.py
Normal 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('<','<'))
|
||||
|
||||
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('<','<')
|
||||
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('<','<')
|
||||
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('<','<')
|
||||
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('<','<')
|
||||
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('<','<')
|
||||
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('<','<')
|
||||
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
|
||||
|
||||
@@ -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)
|
||||
|
||||
47
rakefile
47
rakefile
@@ -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
|
||||
|
||||
@@ -15,4 +15,8 @@ path.py
|
||||
django_debug_toolbar
|
||||
django-pipeline
|
||||
django-staticfiles>=1.2.1
|
||||
|
||||
django-masquerade
|
||||
fs
|
||||
django-jasmine
|
||||
beautifulsoup
|
||||
requests
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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% );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
53
settings.py
53
settings.py
@@ -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'
|
||||
|
||||
BIN
static/images/amplifier-slider-handle.png
Normal file
BIN
static/images/amplifier-slider-handle.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 94 B |
BIN
static/images/closed-arrow.png
Normal file
BIN
static/images/closed-arrow.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 112 B |
BIN
static/images/fullscreen.png
Normal file
BIN
static/images/fullscreen.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 114 B |
BIN
static/images/marketing/edx-logo.png
Normal file
BIN
static/images/marketing/edx-logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.0 KiB |
BIN
static/images/open-arrow.png
Normal file
BIN
static/images/open-arrow.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.8 KiB |
121
static/js/application.js
Normal file
121
static/js/application.js
Normal 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);
|
||||
1
static/js/codemirror-compressed.js
Normal file
1
static/js/codemirror-compressed.js
Normal file
File diff suppressed because one or more lines are too long
24
static/js/imageinput.js
Normal file
24
static/js/imageinput.js
Normal 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
11
static/js/jquery.ui.touch-punch.min.js
vendored
Normal 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);
|
||||
@@ -18,6 +18,14 @@ This set of questions and answers accompanies MIT’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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
21
templates/choicegroup.html
Normal file
21
templates/choicegroup.html
Normal 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
Reference in New Issue
Block a user