diff --git a/.gitignore b/.gitignore index f98fdf7bf9..e2340d2aa7 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,9 @@ *.swp *.orig *.DS_Store +:2e_* +:2e# +.AppleDouble database.sqlite courseware/static/js/mathjax/* db.newaskbot diff --git a/djangoapps/courseware/capa/calc.py b/djangoapps/courseware/capa/calc.py index fb64c58139..42bb5c3112 100644 --- a/djangoapps/courseware/capa/calc.py +++ b/djangoapps/courseware/capa/calc.py @@ -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 diff --git a/djangoapps/courseware/capa/capa_problem.py b/djangoapps/courseware/capa/capa_problem.py index c5a81e8100..f5739fd8b0 100644 --- a/djangoapps/courseware/capa/capa_problem.py +++ b/djangoapps/courseware/capa/capa_problem.py @@ -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 # 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: 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 ... 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 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 + # ... 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) diff --git a/djangoapps/courseware/capa/inputtypes.py b/djangoapps/courseware/capa/inputtypes.py index 789887243d..0388b35d0b 100644 --- a/djangoapps/courseware/capa/inputtypes.py +++ b/djangoapps/courseware/capa/inputtypes.py @@ -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: + + The location of the sky + ''' + 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 tags should be immediate children of a " + 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 `{::}` + # 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 +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: + + $\displaystyle U(r)=4 U_0 + $r_0$ + + 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 = '%s%s' % (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 ... 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 + + + 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) diff --git a/djangoapps/courseware/capa/responsetypes.py b/djangoapps/courseware/capa/responsetypes.py index 62705d3a70..d9b18428fe 100644 --- a/djangoapps/courseware/capa/responsetypes.py +++ b/djangoapps/courseware/capa/responsetypes.py @@ -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: + + + + `a+b`
+ a+b^2
+ a+b+c + a+b+d +
+
+ + 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 stanzas in the 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: + + + The location of the sky + The location of the earth + + + 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 .... Example: + + + +
+ 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)\). +
+ + + + correct=['correct'] + try: + r = str(submission[0]) + except ValueError: + correct[0] ='incorrect' + r = '0' + if not(r=="IS*u(t-t0)"): + correct[0] ='incorrect' + +
+ + Alternatively, the check function can be defined in Example: + + + + + + + + + ''' 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 has an "expect" attribute then save that + self.expect = xml.get('expect') + self.myid = xml.get('id') + + # the ... stanza should be local to the current . 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 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 = ''+msg+'' + msg = etree.tostring(fromstring_bs(msg),pretty_print=True) + msg = msg.replace(' ','') + #msg = re.sub('(.*)','\\1',msg,flags=re.M|re.DOTALL) # python 2.7 + msg = re.sub('(?ms)(.*)','\\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 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 has a inside it. That + doesn't make sense to me (Ike). Instead, let's have it such that + should contain one or more stanzas. Each should specify + a rectangle, given as an attribute, defining the correct answer. + + Example: + + + + + + + """ + 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 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]) diff --git a/djangoapps/courseware/capa/unit.py b/djangoapps/courseware/capa/unit.py deleted file mode 100644 index 2cbe4f93f0..0000000000 --- a/djangoapps/courseware/capa/unit.py +++ /dev/null @@ -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") diff --git a/djangoapps/courseware/content_parser.py b/djangoapps/courseware/content_parser.py index eb1678536d..adb10f7dfc 100644 --- a/djangoapps/courseware/content_parser.py +++ b/djangoapps/courseware/content_parser.py @@ -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)+")") diff --git a/djangoapps/courseware/global_course_settings.py b/djangoapps/courseware/global_course_settings.py new file mode 100644 index 0000000000..f4e9696d1d --- /dev/null +++ b/djangoapps/courseware/global_course_settings.py @@ -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, + } +] diff --git a/djangoapps/courseware/graders.py b/djangoapps/courseware/graders.py new file mode 100644 index 0000000000..94b2ca78cd --- /dev/null +++ b/djangoapps/courseware/graders.py @@ -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 + } diff --git a/djangoapps/courseware/grades.py b/djangoapps/courseware/grades.py index 68e0f11dcd..2013dc28b5 100644 --- a/djangoapps/courseware/grades.py +++ b/djangoapps/courseware/grades.py @@ -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) diff --git a/djangoapps/courseware/management/commands/check_course.py b/djangoapps/courseware/management/commands/check_course.py index 4d0b9840ab..2f069ee5f3 100644 --- a/djangoapps/courseware/management/commands/check_course.py +++ b/djangoapps/courseware/management/commands/check_course.py @@ -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 diff --git a/djangoapps/courseware/module_render.py b/djangoapps/courseware/module_render.py index 38f9e0211b..0e317f7004 100644 --- a/djangoapps/courseware/module_render.py +++ b/djangoapps/courseware/module_render.py @@ -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 diff --git a/djangoapps/courseware/modules/capa_module.py b/djangoapps/courseware/modules/capa_module.py index 86958dcf1c..0b75faa491 100644 --- a/djangoapps/courseware/modules/capa_module.py +++ b/djangoapps/courseware/modules/capa_module.py @@ -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 = '
'.format(id=self.item_id)+html+"
" return html - def __init__(self, xml, item_id, ajax_url=None, track_url=None, state=None, track_function=None, render_function = None, meta = None): - 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) diff --git a/djangoapps/courseware/modules/html_module.py b/djangoapps/courseware/modules/html_module.py index 4194f73e74..77bcbb4bbc 100644 --- a/djangoapps/courseware/modules/html_module.py +++ b/djangoapps/courseware/modules/html_module.py @@ -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") diff --git a/djangoapps/courseware/modules/schematic_module.py b/djangoapps/courseware/modules/schematic_module.py index e253f1acc6..5fef265e01 100644 --- a/djangoapps/courseware/modules/schematic_module.py +++ b/djangoapps/courseware/modules/schematic_module.py @@ -19,6 +19,6 @@ class Module(XModule): def get_html(self): return ''.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) diff --git a/djangoapps/courseware/modules/seq_module.py b/djangoapps/courseware/modules/seq_module.py index 02796a9768..00350f0c62 100644 --- a/djangoapps/courseware/modules/seq_module.py +++ b/djangoapps/courseware/modules/seq_module.py @@ -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']) diff --git a/djangoapps/courseware/modules/template_module.py b/djangoapps/courseware/modules/template_module.py index d9dbb613f0..41556eb9d4 100644 --- a/djangoapps/courseware/modules/template_module.py +++ b/djangoapps/courseware/modules/template_module.py @@ -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') diff --git a/djangoapps/courseware/modules/vertical_module.py b/djangoapps/courseware/modules/vertical_module.py index c068cb9a76..f64e45fe7f 100644 --- a/djangoapps/courseware/modules/vertical_module.py +++ b/djangoapps/courseware/modules/vertical_module.py @@ -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] diff --git a/djangoapps/courseware/modules/video_module.py b/djangoapps/courseware/modules/video_module.py index 2063c18953..c678838f2b 100644 --- a/djangoapps/courseware/modules/video_module.py +++ b/djangoapps/courseware/modules/video_module.py @@ -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]]) diff --git a/djangoapps/courseware/modules/x_module.py b/djangoapps/courseware/modules/x_module.py index 4a379253c4..9e70a16891 100644 --- a/djangoapps/courseware/modules/x_module.py +++ b/djangoapps/courseware/modules/x_module.py @@ -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 diff --git a/djangoapps/courseware/test_files/imageresponse.xml b/djangoapps/courseware/test_files/imageresponse.xml new file mode 100644 index 0000000000..72bf06401a --- /dev/null +++ b/djangoapps/courseware/test_files/imageresponse.xml @@ -0,0 +1,15 @@ + +

+Two skiers are on frictionless black diamond ski slopes. +Hello

+ + + +Click on the image where the top skier will stop momentarily if the top skier starts from rest. + +Click on the image where the lower skier will stop momentarily if the lower skier starts from rest. + +

Use conservation of energy.

+
+
+
\ No newline at end of file diff --git a/djangoapps/courseware/test_files/multi_bare.xml b/djangoapps/courseware/test_files/multi_bare.xml new file mode 100644 index 0000000000..20bc8f853d --- /dev/null +++ b/djangoapps/courseware/test_files/multi_bare.xml @@ -0,0 +1,21 @@ + + + + + This is foil One. + + + This is foil Two. + + + This is foil Three. + + + This is foil Four. + + + This is foil Five. + + + + diff --git a/djangoapps/courseware/test_files/multichoice.xml b/djangoapps/courseware/test_files/multichoice.xml new file mode 100644 index 0000000000..60bf02ec59 --- /dev/null +++ b/djangoapps/courseware/test_files/multichoice.xml @@ -0,0 +1,21 @@ + + + + + This is foil One. + + + This is foil Two. + + + This is foil Three. + + + This is foil Four. + + + This is foil Five. + + + + diff --git a/djangoapps/courseware/test_files/optionresponse.xml b/djangoapps/courseware/test_files/optionresponse.xml new file mode 100644 index 0000000000..99a17e8fac --- /dev/null +++ b/djangoapps/courseware/test_files/optionresponse.xml @@ -0,0 +1,63 @@ + + +

+Why do bicycles benefit from having larger wheels when going up a bump as shown in the picture?
+Assume that for both bicycles:
+1.) The tires have equal air pressure.
+2.) The bicycles never leave the contact with the bump.
+3.) The bicycles have the same mass. The bicycle tires (regardless of size) have the same mass.
+

+
+ +
    +
  • + +

    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.

    +
    + + +
  • +
  • + +

    The bicycles with larger wheels always have a smaller vertical displacement regardless of speed.

    +
    + + +
  • +
  • + +

    The bicycles with larger wheels experience a force backward with less magnitude for the same amount of time.

    +
    + + +
  • +
  • + +

    The bicycles with larger wheels experience a force backward with less magnitude for a greater amount of time.

    +
    + + +
  • +
  • + +

    The bicycles with larger wheels have more kinetic energy turned into gravitational potential energy.

    +
    + + +
  • +
  • + +

    The bicycles with larger wheels have more rotational kinetic energy, so the horizontal velocity of the biker changes less.

    +
    + + +
  • +
+ + +
+
+
+
+
+
diff --git a/djangoapps/courseware/test_files/truefalse.xml b/djangoapps/courseware/test_files/truefalse.xml new file mode 100644 index 0000000000..60018f7a2d --- /dev/null +++ b/djangoapps/courseware/test_files/truefalse.xml @@ -0,0 +1,21 @@ + + + + + This is foil One. + + + This is foil Two. + + + This is foil Three. + + + This is foil Four. + + + This is foil Five. + + + + diff --git a/djangoapps/courseware/tests.py b/djangoapps/courseware/tests.py index 7eb6aa27de..682927efb7 100644 --- a/djangoapps/courseware/tests.py +++ b/djangoapps/courseware/tests.py @@ -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")) diff --git a/djangoapps/courseware/views.py b/djangoapps/courseware/views.py index f7d1ba9293..5f67e599c0 100644 --- a/djangoapps/courseware/views.py +++ b/djangoapps/courseware/views.py @@ -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 '\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 \"%s\"" % err + + if isok: + filename = instance.lcp.fileobject.name + fp = open(filename,'w') # TODO - replace with filestore call? + fp.write(newcode) + fp.close() + msg = "Problem changed! (%s)" % 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 diff --git a/djangoapps/multicourse/__init__.py b/djangoapps/multicourse/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/djangoapps/multicourse/multicourse_settings.py b/djangoapps/multicourse/multicourse_settings.py new file mode 100644 index 0000000000..99c9ef8620 --- /dev/null +++ b/djangoapps/multicourse/multicourse_settings.py @@ -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') + diff --git a/djangoapps/multicourse/views.py b/djangoapps/multicourse/views.py new file mode 100644 index 0000000000..d0662b710e --- /dev/null +++ b/djangoapps/multicourse/views.py @@ -0,0 +1 @@ +# multicourse/views.py diff --git a/djangoapps/simplewiki/models.py b/djangoapps/simplewiki/models.py index 71a57c601c..ade95ed491 100644 --- a/djangoapps/simplewiki/models.py +++ b/djangoapps/simplewiki/models.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 diff --git a/djangoapps/simplewiki/templatetags/simplewiki_utils.py b/djangoapps/simplewiki/templatetags/simplewiki_utils.py index 1534a7b401..18c6332b1a 100644 --- a/djangoapps/simplewiki/templatetags/simplewiki_utils.py +++ b/djangoapps/simplewiki/templatetags/simplewiki_utils.py @@ -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() diff --git a/djangoapps/simplewiki/views.py b/djangoapps/simplewiki/views.py index 7d743139ba..34a81e6b57 100644 --- a/djangoapps/simplewiki/views.py +++ b/djangoapps/simplewiki/views.py @@ -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 diff --git a/djangoapps/simplewiki/views_attachments.py b/djangoapps/simplewiki/views_attachments.py index 205f836ab9..e75802413f 100644 --- a/djangoapps/simplewiki/views_attachments.py +++ b/djangoapps/simplewiki/views_attachments.py @@ -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): diff --git a/djangoapps/simplewiki/settings.py b/djangoapps/simplewiki/wiki_settings.py similarity index 100% rename from djangoapps/simplewiki/settings.py rename to djangoapps/simplewiki/wiki_settings.py diff --git a/djangoapps/ssl_auth/__init__.py b/djangoapps/ssl_auth/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/djangoapps/ssl_auth/ssl_auth.py b/djangoapps/ssl_auth/ssl_auth.py new file mode 100755 index 0000000000..8c96d4b9a6 --- /dev/null +++ b/djangoapps/ssl_auth/ssl_auth.py @@ -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 diff --git a/djangoapps/staticbook/views.py b/djangoapps/staticbook/views.py index f7ad8fab63..84fcc79c1f 100644 --- a/djangoapps/staticbook/views.py +++ b/djangoapps/staticbook/views.py @@ -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): diff --git a/djangoapps/student/views.py b/djangoapps/student/views.py index a7b02b5bb1..eb71f5ba6a 100644 --- a/djangoapps/student/views.py +++ b/djangoapps/student/views.py @@ -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) diff --git a/envs/aws.py b/envs/aws.py index 9ce621c2bd..7363cbb3f7 100644 --- a/envs/aws.py +++ b/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'] \ No newline at end of file +DATABASES = AUTH_TOKENS['DATABASES'] diff --git a/envs/common.py b/envs/common.py index 4694e1f5fd..6c596dc7fa 100644 --- a/envs/common.py +++ b/envs/common.py @@ -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 diff --git a/envs/content.py b/envs/content.py index c02e7bb72e..495bbeedac 100644 --- a/envs/content.py +++ b/envs/content.py @@ -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 diff --git a/envs/dev.py b/envs/dev.py index 757bd1164a..b459f0a34f 100644 --- a/envs/dev.py +++ b/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', diff --git a/envs/devplus.py b/envs/devplus.py index 2ec38b53a0..4f763b925c 100644 --- a/envs/devplus.py +++ b/envs/devplus.py @@ -13,7 +13,7 @@ Dir structure: /log # Where we're going to write log files """ -from dev import * +from envs.dev import * DATABASES = { 'default': { diff --git a/envs/static.py b/envs/static.py new file mode 100644 index 0000000000..65309c4795 --- /dev/null +++ b/envs/static.py @@ -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', +) diff --git a/envs/test.py b/envs/test.py new file mode 100644 index 0000000000..bf83511786 --- /dev/null +++ b/envs/test.py @@ -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', +) diff --git a/fixtures/anonymize_fixtures.py b/fixtures/anonymize_fixtures.py new file mode 100755 index 0000000000..ba62652de5 --- /dev/null +++ b/fixtures/anonymize_fixtures.py @@ -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()) diff --git a/lib/loncapa/__init__.py b/lib/loncapa/__init__.py new file mode 100644 index 0000000000..b734967d0a --- /dev/null +++ b/lib/loncapa/__init__.py @@ -0,0 +1,3 @@ +#!/usr/bin/python + +from loncapa_check import * diff --git a/lib/loncapa/loncapa_check.py b/lib/loncapa/loncapa_check.py new file mode 100644 index 0000000000..259c7909ac --- /dev/null +++ b/lib/loncapa/loncapa_check.py @@ -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) + diff --git a/lib/mitxmako/shortcuts.py b/lib/mitxmako/shortcuts.py index ca626b5c85..7286a4e259 100644 --- a/lib/mitxmako/shortcuts.py +++ b/lib/mitxmako/shortcuts.py @@ -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) diff --git a/lib/sympy_check/__init__.py b/lib/sympy_check/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lib/sympy_check/formula.py b/lib/sympy_check/formula.py new file mode 100644 index 0000000000..44bd020c4e --- /dev/null +++ b/lib/sympy_check/formula.py @@ -0,0 +1,461 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# File: formula.py +# Date: 04-May-12 +# Author: I. Chuang +# +# 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 '%s{}{}' % (xs[1:-1]) + if xs[0]=='$': + return '[mathjax]%s[/mathjax]
' % (xs[1:-1]) # for sympy v6 + return '[mathjax]%s[/mathjax]
' % (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 'f-2" + # 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 container + xml.insert(idx+1,deepcopy(k[1])) + xml.remove(k) + fix_pmathml(k) + + fix_pmathml(xml) + + # hat i is turned into i^ ; mangle this into hat(f) + # hat i also somtimes turned into j ^ + + 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 , , , and + ''' + 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 '

Maxima Input Form

' in k: + mode = 0 + continue + cmathml.append(k) + # return '\n'.join(cmathml) + cmathml = '\n'.join(cmathml[2:]) + cmathml = '\n' + unescape(cmathml) + '\n' + # print cmathml + #return unicode(cmathml) + return cmathml + +#----------------------------------------------------------------------------- + +def test1(): + xmlstr = ''' + + + + 1 + 2 + + + ''' + return formula(xmlstr) + +def test2(): + xmlstr = u''' + + + + 1 + + + 2 + α + + + + ''' + return formula(xmlstr) + +def test3(): + xmlstr = ''' + + + + 1 + + + 2 + γ + + + + ''' + return formula(xmlstr) + +def test4(): + xmlstr = u''' + + + 1 + + + + 2 + α + + + +''' + return formula(xmlstr) diff --git a/lib/sympy_check/sympy_check2.py b/lib/sympy_check/sympy_check2.py new file mode 100644 index 0000000000..abb70ed165 --- /dev/null +++ b/lib/sympy_check/sympy_check2.py @@ -0,0 +1,271 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# File: sympy_check2.py +# Date: 02-May-12 +# Author: I. Chuang +# +# 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
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
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
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
in normalizing trace of your expression %s' % (err,to_latex(xgiven))} + try: + xexpect /= xexpect.trace() + except Exception, err: + return {'ok': False,'msg': 'Error %s
in normalizing trace of OUR expression %s' % (err,to_latex(xexpect))} + + msg = 'Your expression was evaluated as ' + to_latex(xgiven) + # msg += '
Expected ' + to_latex(xexpect) + + # msg += "
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())expect='%s', given='%s'" % (expect,given) # debugging + # msg += "

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 += '

abname=%s' % abname + # msg += '

adict=%s' % (repr(adict).replace('<','<')) + + threshold = 1.0e-3 + DEBUG = True + + # parse expected answer + try: + fexpect = my_sympify(str(expect)) + except Exception,err: + msg += '

Error %s in parsing OUR expected answer "%s"

' % (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) + + + 1 + 2 + + + ( + 1 + + + + + + k + e + + + Q + + q + + + m + + + g + + + + h + 2 + + + + ) + + + +'''.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 + diff --git a/lib/util/views.py b/lib/util/views.py index f0936a0c76..d95f1e9a22 100644 --- a/lib/util/views.py +++ b/lib/util/views.py @@ -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) diff --git a/rakefile b/rakefile index fcfe49776a..e2dc36e4bf 100644 --- a/rakefile +++ b/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 diff --git a/requirements.txt b/requirements.txt index 57db618145..10564b09db 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,4 +15,8 @@ path.py django_debug_toolbar django-pipeline django-staticfiles>=1.2.1 - +django-masquerade +fs +django-jasmine +beautifulsoup +requests diff --git a/sass/_help.scss b/sass/_help.scss index 2784b41c82..cb505814e9 100644 --- a/sass/_help.scss +++ b/sass/_help.scss @@ -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); } } diff --git a/sass/_info.scss b/sass/_info.scss index 29a0f16a88..a4629c9e03 100644 --- a/sass/_info.scss +++ b/sass/_info.scss @@ -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; diff --git a/sass/_profile.scss b/sass/_profile.scss index 4662248baa..a772f1a293 100644 --- a/sass/_profile.scss +++ b/sass/_profile.scss @@ -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 { diff --git a/sass/_textbook.scss b/sass/_textbook.scss index 2067a09fca..35902c7a57 100644 --- a/sass/_textbook.scss +++ b/sass/_textbook.scss @@ -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; } } diff --git a/sass/base/_base.scss b/sass/base/_base.scss index 3f985ea666..41c421844c 100644 --- a/sass/base/_base.scss +++ b/sass/base/_base.scss @@ -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 { diff --git a/sass/base/_extends.scss b/sass/base/_extends.scss index c805a76d61..11409be60c 100644 --- a/sass/base/_extends.scss +++ b/sass/base/_extends.scss @@ -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; } diff --git a/sass/base/_functions.scss b/sass/base/_functions.scss index 8efe9e5796..a947d94034 100644 --- a/sass/base/_functions.scss +++ b/sass/base/_functions.scss @@ -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; diff --git a/sass/base/_variables.scss b/sass/base/_variables.scss index 60dd764872..674de12ee6 100644 --- a/sass/base/_variables.scss +++ b/sass/base/_variables.scss @@ -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; diff --git a/sass/courseware/_amplifier.scss b/sass/courseware/_amplifier.scss index 6bae79fd21..ae8764d6ee 100644 --- a/sass/courseware/_amplifier.scss +++ b/sass/courseware/_amplifier.scss @@ -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% ); + } + } + } + } } } - diff --git a/sass/courseware/_courseware.scss b/sass/courseware/_courseware.scss index c0ff766bb3..520440c234 100644 --- a/sass/courseware/_courseware.scss +++ b/sass/courseware/_courseware.scss @@ -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 { diff --git a/sass/courseware/_sequence-nav.scss b/sass/courseware/_sequence-nav.scss index 26b019cdbe..0e541e7f07 100644 --- a/sass/courseware/_sequence-nav.scss +++ b/sass/courseware/_sequence-nav.scss @@ -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; diff --git a/sass/courseware/_sidebar.scss b/sass/courseware/_sidebar.scss index d93e672378..8327da1548 100644 --- a/sass/courseware/_sidebar.scss +++ b/sass/courseware/_sidebar.scss @@ -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; + } + } } } } diff --git a/sass/courseware/_video.scss b/sass/courseware/_video.scss index b1daf24a01..bf3cb8131b 100644 --- a/sass/courseware/_video.scss +++ b/sass/courseware/_video.scss @@ -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; + } + } } } } diff --git a/sass/discussion/_answers.scss b/sass/discussion/_answers.scss index cc6133a12f..a65c147c0c 100644 --- a/sass/discussion/_answers.scss +++ b/sass/discussion/_answers.scss @@ -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; } diff --git a/sass/discussion/_question-view.scss b/sass/discussion/_question-view.scss index c771c48803..72408f0ff8 100644 --- a/sass/discussion/_question-view.scss +++ b/sass/discussion/_question-view.scss @@ -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; diff --git a/sass/layout/_calculator.scss b/sass/layout/_calculator.scss index 18336ff325..117f5a78be 100644 --- a/sass/layout/_calculator.scss +++ b/sass/layout/_calculator.scss @@ -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; diff --git a/sass/layout/_footer.scss b/sass/layout/_footer.scss index 64b2263829..18e6a8d4e7 100644 --- a/sass/layout/_footer.scss +++ b/sass/layout/_footer.scss @@ -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; diff --git a/sass/layout/_header.scss b/sass/layout/_header.scss index 1d88eac639..fc897df7eb 100644 --- a/sass/layout/_header.scss +++ b/sass/layout/_header.scss @@ -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%; } } } diff --git a/sass/layout/_layout.scss b/sass/layout/_layout.scss index 1cc9e2cdd1..1b476a30db 100644 --- a/sass/layout/_layout.scss +++ b/sass/layout/_layout.scss @@ -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; diff --git a/sass/layout/_leanmodal.scss b/sass/layout/_leanmodal.scss index 8b1f05f37f..81639493ee 100644 --- a/sass/layout/_leanmodal.scss +++ b/sass/layout/_leanmodal.scss @@ -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 { diff --git a/sass/marketing.scss b/sass/marketing.scss index 7b93c36a3b..c0e9488016 100644 --- a/sass/marketing.scss +++ b/sass/marketing.scss @@ -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"; diff --git a/sass/index/_base.scss b/sass/marketing/_base.scss similarity index 100% rename from sass/index/_base.scss rename to sass/marketing/_base.scss diff --git a/sass/index/_extends.scss b/sass/marketing/_extends.scss similarity index 100% rename from sass/index/_extends.scss rename to sass/marketing/_extends.scss diff --git a/sass/index/_footer.scss b/sass/marketing/_footer.scss similarity index 82% rename from sass/index/_footer.scss rename to sass/marketing/_footer.scss index 6fddb8ca91..dc3747dd64 100644 --- a/sass/index/_footer.scss +++ b/sass/marketing/_footer.scss @@ -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; } } } diff --git a/sass/index/_header.scss b/sass/marketing/_header.scss similarity index 89% rename from sass/index/_header.scss rename to sass/marketing/_header.scss index 9ea3bed0d5..4cfe1578b5 100644 --- a/sass/index/_header.scss +++ b/sass/marketing/_header.scss @@ -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 { diff --git a/sass/index/_index.scss b/sass/marketing/_index.scss similarity index 93% rename from sass/index/_index.scss rename to sass/marketing/_index.scss index d32eb89710..e7ceb2d46d 100644 --- a/sass/index/_index.scss +++ b/sass/marketing/_index.scss @@ -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(); + } + } } diff --git a/sass/index/_variables.scss b/sass/marketing/_variables.scss similarity index 100% rename from sass/index/_variables.scss rename to sass/marketing/_variables.scss diff --git a/sass/wiki/_create.scss b/sass/wiki/_create.scss index 159e028339..35b0798501 100644 --- a/sass/wiki/_create.scss +++ b/sass/wiki/_create.scss @@ -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; } } diff --git a/sass/wiki/_sidebar.scss b/sass/wiki/_sidebar.scss index 61575d811d..a4ebfe628d 100644 --- a/sass/wiki/_sidebar.scss +++ b/sass/wiki/_sidebar.scss @@ -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 { diff --git a/sass/wiki/_wiki.scss b/sass/wiki/_wiki.scss index aa85b4191b..179902223b 100644 --- a/sass/wiki/_wiki.scss +++ b/sass/wiki/_wiki.scss @@ -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 { } } } - } } diff --git a/settings.py b/settings.py index 84fbac2333..2be6be485a 100644 --- a/settings.py +++ b/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' diff --git a/static/images/amplifier-slider-handle.png b/static/images/amplifier-slider-handle.png new file mode 100644 index 0000000000..47e3c8d449 Binary files /dev/null and b/static/images/amplifier-slider-handle.png differ diff --git a/static/images/closed-arrow.png b/static/images/closed-arrow.png new file mode 100644 index 0000000000..4cfb9b9861 Binary files /dev/null and b/static/images/closed-arrow.png differ diff --git a/static/images/fullscreen.png b/static/images/fullscreen.png new file mode 100644 index 0000000000..e2f9054fe1 Binary files /dev/null and b/static/images/fullscreen.png differ diff --git a/static/images/marketing/edx-logo.png b/static/images/marketing/edx-logo.png new file mode 100644 index 0000000000..71dbafc375 Binary files /dev/null and b/static/images/marketing/edx-logo.png differ diff --git a/static/images/open-arrow.png b/static/images/open-arrow.png new file mode 100644 index 0000000000..4bedb61081 Binary files /dev/null and b/static/images/open-arrow.png differ diff --git a/static/js/application.js b/static/js/application.js new file mode 100644 index 0000000000..876a926ff3 --- /dev/null +++ b/static/js/application.js @@ -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); diff --git a/static/js/codemirror-compressed.js b/static/js/codemirror-compressed.js new file mode 100644 index 0000000000..855a7bd32f --- /dev/null +++ b/static/js/codemirror-compressed.js @@ -0,0 +1 @@ +var CodeMirror=function(){function a(d,e){function Vb(a){return a>=0&&a=c.to||b.linee-400&&Y(yb.pos,d))return C(a),setTimeout(Dc,20),$c(d.line);if(xb&&xb.time>e-400&&Y(xb.pos,d))return yb={time:e,pos:d},C(a),Zc(d);xb={time:e,pos:d};var g=d,h;if(R&&!f.readOnly&&!Y(vb.from,vb.to)&&!Z(d,vb.from)&&!Z(vb.to,d)){O&&(ib.draggable=!0);var i=I(document,"mouseup",Td(function(b){O&&(ib.draggable=!1),Ab=!1,i(),Math.abs(a.clientX-b.clientX)+Math.abs(a.clientY-b.clientY)<10&&(C(b),Rc(d.line,d.ch,!0),Dc())}),!0);Ab=!0,ib.dragDrop&&ib.dragDrop();return}C(a),Rc(d.line,d.ch,!0);var l=I(document,"mousemove",Td(function(a){clearTimeout(h),C(a),!M&&!G(a)?k(a):j(a)}),!0),i=I(document,"mouseup",Td(k),!0)}function ac(a){for(var b=F(a);b!=s;b=b.parentNode)if(b.parentNode==hb)return C(a);var c=Gd(a);if(!c)return;yb={time:+(new Date),pos:c},C(a),Zc(c)}function bc(a){a.preventDefault();var b=Gd(a,!0),c=a.dataTransfer.files;if(!b||f.readOnly)return;if(c&&c.length&&window.FileReader&&window.File){function d(a,c){var d=new FileReader;d.onload=function(){g[c]=d.result,++h==e&&(b=Tc(b),Td(function(){var a=sc(g.join(""),b,b);Oc(b,a)})())},d.readAsText(a)}var e=c.length,g=Array(e),h=0;for(var i=0;i-1&&setTimeout(Td(function(){ad(vb.to.line,"smart")}),75);if(fc(a,d))return;zc()}function kc(a){if(f.onKeyEvent&&f.onKeyEvent(Wb,B(a)))return;H(a,"keyCode")==16&&(wb=null)}function lc(){if(f.readOnly=="nocursor")return;ub||(f.onFocus&&f.onFocus(Wb),ub=!0,s.className.search(/\bCodeMirror-focused\b/)==-1&&(s.className+=" CodeMirror-focused"),Ib||Cc(!0)),yc(),Id()}function mc(){ub&&(f.onBlur&&f.onBlur(Wb),ub=!1,Pb&&Td(function(){Pb&&(Pb(),Pb=null)})(),s.className=s.className.replace(" CodeMirror-focused","")),clearInterval(qb),setTimeout(function(){ub||(wb=null)},150)}function nc(a,b,c,d,e){if(Cb)return;if(Tb){var g=[];sb.iter(a.line,b.line+1,function(a){g.push(a.text)}),Tb.addChange(a.line,c.length,g);while(Tb.done.length>f.undoDepth)Tb.done.shift()}rc(a,b,c,d,e)}function oc(a,b){if(!a.length)return;var c=a.pop(),d=[];for(var e=c.length-1;e>=0;e-=1){var f=c[e],g=[],h=f.start+f.added;sb.iter(f.start,h,function(a){g.push(a.text)}),d.push({start:f.start,added:f.old.length,old:g});var i=Tc({line:f.start+f.old.length-1,ch:bb(g[g.length-1],f.old[f.old.length-1])});rc({line:f.start,ch:0},{line:h-1,ch:Xb(h-1).text.length},f.old,i,i)}Db=!0,b.push(d)}function pc(){oc(Tb.done,Tb.undone)}function qc(){oc(Tb.undone,Tb.done)}function rc(a,b,c,d,e){function y(a){return a<=Math.min(b.line,b.line+s)?a:a+s}if(Cb)return;var g=!1,h=Qb.length;f.lineWrapping||sb.iter(a.line,b.line,function(a){if(a.text.length==h)return g=!0,!0});if(a.line!=b.line||c.length>1)Jb=!0;var i=b.line-a.line,j=Xb(a.line),k=Xb(b.line);if(a.ch==0&&b.ch==0&&c[c.length-1]==""){var l=[],m=null;a.line?(m=Xb(a.line-1),m.fixMarkEnds(k)):k.fixMarkStarts();for(var n=0,o=c.length-1;n1&&sb.remove(a.line+1,i-1,Kb),sb.insert(a.line+1,l)}if(f.lineWrapping){var p=S.clientWidth/Dd()-3;sb.iter(a.line,a.line+c.length,function(a){if(a.hidden)return;var b=Math.ceil(a.text.length/p)||1;b!=a.height&&Yb(a,b)})}else sb.iter(a.line,n+c.length,function(a){var b=a.text;b.length>h&&(Qb=b,h=b.length,Rb=null,g=!1)}),g&&(h=0,Qb="",Rb=null,sb.iter(0,sb.size,function(a){var b=a.text;b.length>h&&(h=b.length,Qb=b)}));var q=[],s=c.length-i-1;for(var n=0,t=tb.length;nb.line&&q.push(u+s)}var v=a.line+Math.min(c.length,500);Nd(a.line,v),q.push(v),tb=q,Pd(100),Fb.push({from:a.line,to:b.line+1,diff:s});var w={from:a,to:b,text:c};if(Gb){for(var x=Gb;x.next;x=x.next);x.next=w}else Gb=w;Pc(d,e,y(vb.from.line),y(vb.to.line)),S.clientHeight&&(T.style.height=sb.height*Ad()+2*Ed()+"px")}function sc(a,b,c){function d(d){if(Z(d,b))return d;if(!Z(c,d))return e;var f=d.line+a.length-(c.line-b.line)-1,g=d.ch;return d.line==c.line&&(g+=a[a.length-1].length-(c.ch-(c.line==b.line?b.ch:0))),{line:f,ch:g}}b=Tc(b),c?c=Tc(c):c=b,a=eb(a);var e;return uc(a,b,c,function(a){return e=a,{from:d(vb.from),to:d(vb.to)}}),e}function tc(a,b){uc(eb(a),vb.from,vb.to,function(a){return b=="end"?{from:a,to:a}:b=="start"?{from:vb.from,to:vb.from}:{from:vb.from,to:a}})}function uc(a,b,c,d){var e=a.length==1?a[0].length+b.ch:a[a.length-1].length,f=d({line:b.line+a.length-1,ch:e});nc(b,c,a,f.from,f.to)}function vc(a,b){var c=a.line,d=b.line;if(c==d)return Xb(c).text.slice(a.ch,b.ch);var e=[Xb(c).text.slice(a.ch)];return sb.iter(c+1,d,function(a){e.push(a.text)}),e.push(Xb(d).text.slice(0,b.ch)),e.join("\n")}function wc(){return vc(vb.from,vb.to)}function yc(){if(xc)return;ob.set(f.pollInterval,function(){Qd(),Bc(),ub&&yc(),Rd()})}function zc(){function b(){Qd();var c=Bc();!c&&!a?(a=!0,ob.set(60,b)):(xc=!1,yc()),Rd()}var a=!1;xc=!0,ob.set(20,b)}function Bc(){if(Ib||!ub||fb(D)||f.readOnly)return!1;var a=D.value;if(a==Ac)return!1;wb=null;var b=0,c=Math.min(Ac.length,a.length);while(bb)&&kb.scrollIntoView()}function Fc(){var a=ud(vb.inverted?vb.from:vb.to),b=f.lineWrapping?Math.min(a.x,ib.offsetWidth):a.x;return Gc(b,a.y,b,a.yBot)}function Gc(a,b,c,d){var e=Fd(),g=Ed();b+=g,d+=g,a+=e,c+=e;var h=S.clientHeight,i=S.scrollTop,j=!1,k=!0;bi+h&&(S.scrollTop=d-h,j=!0);var l=S.clientWidth,m=S.scrollLeft,n=f.fixedGutter?_.clientWidth:0;return al+m-3&&(S.scrollLeft=c+10-l,j=!0,c>T.clientWidth&&(k=!1)),j&&f.onScroll&&f.onScroll(Wb),k}function Hc(){var a=Ad(),b=S.scrollTop-Ed(),c=Math.max(0,Math.floor(b/a)),d=Math.ceil((b+S.clientHeight)/a);return{from:x(sb,c),to:x(sb,d)}}function Ic(a,b){function n(){Rb=S.clientWidth;var a=mb.firstChild,b=!1;return sb.iter(Mb,Nb,function(c){if(!c.hidden){var d=Math.round(a.offsetHeight/k)||1;c.height!=d&&(Yb(c,d),Jb=b=!0)}a=a.nextSibling}),b&&(T.style.height=sb.height*k+2*Ed()+"px"),b}if(!S.clientWidth){Mb=Nb=Lb=0;return}var c=Hc();if(a!==!0&&a.length==0&&c.from>Mb&&c.toe&&Nb-e<20&&(e=Math.min(sb.size,Nb));var g=a===!0?[]:Jc([{from:Mb,to:Nb,domStart:0}],a),h=0;for(var i=0;ie&&(j.to=e),j.from>=j.to?g.splice(i--,1):h+=j.to-j.from}if(h==e-d)return;g.sort(function(a,b){return a.domStart-b.domStart});var k=Ad(),l=_.style.display;mb.style.display="none",Kc(d,e,g),mb.style.display=_.style.display="";var m=d!=Mb||e!=Nb||Ob!=S.clientHeight+k;m&&(Ob=S.clientHeight+k),Mb=d,Nb=e,Lb=y(sb,d),U.style.top=Lb*k+"px",S.clientHeight&&(T.style.height=sb.height*k+2*Ed()+"px");if(mb.childNodes.length!=Nb-Mb)throw new Error("BAD PATCH! "+JSON.stringify(g)+" size="+(Nb-Mb)+" nodes="+mb.childNodes.length);return f.lineWrapping?n():(Rb==null&&(Rb=qd(Qb)),Rb>S.clientWidth?(ib.style.width=Rb+"px",T.style.width="",T.style.width=S.scrollWidth+"px"):ib.style.width=T.style.width=""),_.style.display=l,(m||Jb)&&Lc()&&f.lineWrapping&&n()&&Lc(),Mc(),!b&&f.onUpdate&&f.onUpdate(Wb),!0}function Jc(a,b){for(var c=0,d=b.length||0;c=j.to?f.push(j):(e.from>j.from&&f.push({from:j.from,to:e.from,domStart:j.domStart}),e.toe)f=d(f),e++;for(var j=0,k=i.to-i.from;jj){if(a.hidden)var b=m.innerHTML="
";else{var b=""+a.getHTML(ed)+"";a.bgClassName&&(b='
 
'+b+"
")}m.innerHTML=b,mb.insertBefore(m.firstChild,f)}else f=f.nextSibling;++j})}function Lc(){if(!f.gutter&&!f.lineNumbers)return;var a=U.offsetHeight,b=S.clientHeight;_.style.height=(a-b<2?b:a)+"px";var c=[],d=Mb,e;sb.iter(Mb,Math.max(Nb,Mb+1),function(a){if(a.hidden)c.push("
");else{var b=a.gutterMarker,g=f.lineNumbers?d+f.firstLineNumber:null;b&&b.text?g=b.text.replace("%N%",g!=null?g:""):g==null&&(g="\u00a0"),c.push(b&&b.style?'
':"
",g);for(var h=1;h ");c.push("
"),b||(e=d)}++d}),_.style.display="none",hb.innerHTML=c.join("");if(e!=null){var g=hb.childNodes[e-Mb],h=String(sb.size).length,i=W(g),j="";while(i.length+j.length2;return ib.style.marginLeft=_.offsetWidth+"px",Jb=!1,k}function Mc(){var a=Y(vb.from,vb.to),b=ud(vb.from,!0),c=a?b:ud(vb.to,!0),d=vb.inverted?b:c,e=Ad(),g=V(s),h=V(mb);A.style.top=Math.max(0,Math.min(S.offsetHeight,d.y+h.top-g.top))+"px",A.style.left=Math.max(0,Math.min(S.offsetWidth,d.x+h.left-g.left))+"px";if(a)kb.style.top=d.y+"px",kb.style.left=(f.lineWrapping?Math.min(d.x,ib.offsetWidth):d.x)+"px",kb.style.display="",lb.style.display="none";else{var i=b.y==c.y,j="";function k(a,b,c,d){j+='
'}var l=ib.clientWidth||ib.offsetWidth,m=ib.clientHeight||ib.offsetHeight;if(vb.from.ch&&b.y>=0){var n=i?l-c.x:0;k(b.x,b.y,n,e)}var o=Math.max(0,b.y+(vb.from.ch?e:0)),p=Math.min(c.y,m)-o;p>.2*e&&k(0,o,0,p),(!i||!vb.from.ch)&&c.yc||g>f.text.length)g=f.text.length;return{line:d,ch:g}}d+=b}}var e=Xb(a.line);return e.hidden?a.line>=b?d(1)||d(-1):d(-1)||d(1):a}function Rc(a,b,c){var d=Tc({line:a,ch:b||0});(c?Oc:Pc)(d,d)}function Sc(a){return Math.max(0,Math.min(a,sb.size-1))}function Tc(a){if(a.line<0)return{line:0,ch:0};if(a.line>=sb.size)return{line:sb.size-1,ch:Xb(sb.size-1).text.length};var b=a.ch,c=Xb(a.line).text.length;return b==null||b>c?{line:a.line,ch:c}:b<0?{line:a.line,ch:0}:a}function Uc(a,b){function g(){for(var b=d+a,c=a<0?-1:sb.size;b!=c;b+=a){var e=Xb(b);if(!e.hidden)return d=b,f=e,!0}}function h(b){if(e==(a<0?0:f.text.length)){if(!!b||!g())return!1;e=a<0?f.text.length:0}else e+=a;return!0}var c=vb.inverted?vb.from:vb.to,d=c.line,e=c.ch,f=Xb(d);if(b=="char")h();else if(b=="column")h(!0);else if(b=="word"){var i=!1;for(;;){if(a<0&&!h())break;if(db(f.text.charAt(e)))i=!0;else if(i){a<0&&(a=1,h());break}if(a>0&&!h())break}}return{line:d,ch:e}}function Vc(a,b){var c=a<0?vb.from:vb.to;if(wb||Y(vb.from,vb.to))c=Uc(a,b);Rc(c.line,c.ch,!0)}function Wc(a,b){Y(vb.from,vb.to)?a<0?sc("",Uc(a,b),vb.to):sc("",vb.from,Uc(a,b)):sc("",vb.from,vb.to),Eb=!0}function Yc(a,b){var c=0,d=ud(vb.inverted?vb.from:vb.to,!0);Xc!=null&&(d.x=Xc),b=="page"?c=Math.min(S.clientHeight,window.innerHeight||document.documentElement.clientHeight):b=="line"&&(c=Ad());var e=vd(d.x,d.y+c*a+2);b=="page"&&(S.scrollTop+=ud(e,!0).y-d.y),Rc(e.line,e.ch,!0),Xc=d.x}function Zc(a){var b=Xb(a.line).text,c=a.ch,d=a.ch;while(c>0&&db(b.charAt(c-1)))--c;while(dQb.length&&(Qb=a.text)});Fb.push({from:0,to:sb.size})}function ed(a){var b=f.tabSize-a%f.tabSize,c=Sb[b];if(c)return c;for(var d='',e=0;e",width:b}}function fd(){S.className=S.className.replace(/\s*cm-s-\w+/g,"")+f.theme.replace(/(^|\s)\s*/g," cm-s-")}function gd(){this.set=[]}function hd(a,b,c){function e(a,b,c,e){Xb(a).addMark(new p(b,c,e,d))}a=Tc(a),b=Tc(b);var d=new gd;if(!Z(a,b))return d;if(a.line==b.line)e(a.line,a.ch,b.ch,c);else{e(a.line,a.ch,null,c);for(var f=a.line+1,g=b.line;f=a.ch)&&b.push(f.marker||f)}return b}function kd(a,b,c){return typeof a=="number"&&(a=Xb(Sc(a))),a.gutterMarker={text:b,style:c},Jb=!0,a}function ld(a){typeof a=="number"&&(a=Xb(Sc(a))),a.gutterMarker=null,Jb=!0}function md(a,b){var c=a,d=a;return typeof a=="number"?d=Xb(Sc(a)):c=w(a),c==null?null:b(d,c)?(Fb.push({from:c,to:c+1}),d):null}function nd(a,b,c){return md(a,function(a){if(a.className!=b||a.bgClassName!=c)return a.className=b,a.bgClassName=c,!0})}function od(a,b){return md(a,function(a,c){if(a.hidden!=b){a.hidden=b,Yb(a,b?0:1);var d=vb.from.line,e=vb.to.line;if(b&&(d==c||e==c)){var f=d==c?Qc({line:d,ch:0},d,0):vb.from,g=e==c?Qc({line:e,ch:0},e,0):vb.to;if(!g)return;Pc(f,g)}return Jb=!0}})}function pd(a){if(typeof a=="number"){if(!Vb(a))return null;var b=a;a=Xb(a);if(!a)return null}else{var b=w(a);if(b==null)return null}var c=a.gutterMarker;return{line:b,handle:a,text:a.text,markerText:c&&c.text,markerClass:c&&c.style,lineClass:a.className,bgClass:a.bgClassName}}function qd(a){return jb.innerHTML="
x
",jb.firstChild.firstChild.firstChild.nodeValue=a,jb.firstChild.firstChild.offsetWidth||10}function rd(a,b){function e(a){return jb.innerHTML="
"+c.getHTML(ed,a)+"
",jb.firstChild.firstChild.offsetWidth}if(b<=0)return 0;var c=Xb(a),d=c.text,f=0,g=0,h=d.length,i,j=Math.min(h,Math.ceil(b/Dd()));for(;;){var k=e(j);if(!(k<=b&&ji)return h;j=Math.floor(h*.8),k=e(j),kb-g?f:h;var l=Math.ceil((f+h)/2),m=e(l);m>b?(h=l,i=m):(f=l,g=m)}}function td(a,b){if(b==0)return{top:0,left:0};var c="";if(f.lineWrapping){var d=a.text.indexOf(" ",b+6);c=ab(a.text.slice(b+1,d<0?a.text.length:d+(M?5:0)))}jb.innerHTML="
"+a.getHTML(ed,b)+''+ab(a.text.charAt(b)||" ")+""+c+"
";var e=document.getElementById("CodeMirror-temp-"+sd),g=e.offsetTop,h=e.offsetLeft;if(M&&g==0&&h==0){var i=document.createElement("span");i.innerHTML="x",e.parentNode.insertBefore(i,e.nextSibling),g=i.offsetTop}return{top:g,left:h}}function ud(a,b){var c,d=Ad(),e=d*(y(sb,a.line)-(b?Lb:0));if(a.ch==0)c=0;else{var g=td(Xb(a.line),a.ch);c=g.left,f.lineWrapping&&(e+=Math.max(0,g.top))}return{x:c,y:e,yBot:e+d}}function vd(a,b){function l(a){var b=td(h,a);if(j){var d=Math.round(b.top/c);return Math.max(0,b.left+(d-k)*S.clientWidth)}return b.left}b<0&&(b=0);var c=Ad(),d=Dd(),e=Lb+Math.floor(b/c),g=x(sb,e);if(g>=sb.size)return{line:sb.size-1,ch:Xb(sb.size-1).text.length};var h=Xb(g),i=h.text,j=f.lineWrapping,k=j?e-y(sb,g):0;if(a<=0&&k==0)return{line:g,ch:0};var m=0,n=0,o=i.length,p,q=Math.min(o,Math.ceil((a+k*S.clientWidth*.9)/d));for(;;){var r=l(q);if(!(r<=a&&qp)return{line:g,ch:o};q=Math.floor(o*.8),r=l(q),ra-n?m:o};var s=Math.ceil((m+o)/2),t=l(s);t>a?(o=s,p=t):(m=s,n=t)}}function wd(a){var b=ud(a,!0),c=V(ib);return{x:c.left+b.x,y:c.top+b.y,yBot:c.top+b.yBot}}function Ad(){if(zd==null){zd="
";for(var a=0;a<49;++a)zd+="x
";zd+="x
"}var b=mb.clientHeight;return b==yd?xd:(yd=b,jb.innerHTML=zd,xd=jb.firstChild.offsetHeight/50||1,jb.innerHTML="",xd)}function Dd(){return S.clientWidth==Cd?Bd:(Cd=S.clientWidth,Bd=qd("x"))}function Ed(){return ib.offsetTop}function Fd(){return ib.offsetLeft}function Gd(a,b){var c=V(S,!0),d,e;try{d=a.clientX,e=a.clientY}catch(a){return null}if(!b&&(d-c.left>S.clientWidth||e-c.top>S.clientHeight))return null;var f=V(ib,!0);return vd(d-f.left,e-f.top)}function Hd(a){function f(){var a=eb(D.value).join("\n");a!=e&&Td(tc)(a,"end"),A.style.position="relative",D.style.cssText=d,N&&(S.scrollTop=c),Ib=!1,Cc(!0),yc()}var b=Gd(a),c=S.scrollTop;if(!b||window.opera)return;(Y(vb.from,vb.to)||Z(b,vb.from)||!Z(b,vb.to))&&Td(Rc)(b.line,b.ch);var d=D.style.cssText;A.style.position="absolute",D.style.cssText="position: fixed; width: 30px; height: 30px; top: "+(a.clientY-5)+"px; left: "+(a.clientX-5)+"px; z-index: 1000; background: white; "+"border-width: 0; outline: none; overflow: hidden; opacity: .05; filter: alpha(opacity=5);",Ib=!0;var e=D.value=wc();Dc(),X(D);if(L){E(a);var g=I(window,"mouseup",function(){g(),setTimeout(f,20)},!0)}else setTimeout(f,50)}function Id(){clearInterval(qb);var a=!0;kb.style.visibility="",qb=setInterval(function(){kb.style.visibility=(a=!a)?"":"hidden"},650)}function Kd(a){function p(a,b,c){if(!a.text)return;var d=a.styles,e=g?0:a.text.length-1,f;for(var i=g?0:d.length-2,j=g?d.length:-2;i!=j;i+=2*h){var k=d[i];if(d[i+1]!=null&&d[i+1]!=m){e+=h*k.length;continue}for(var l=g?0:k.length-1,p=g?k.length:-1;l!=p;l+=h,e+=h)if(e>=b&&e"==g)n.push(f);else{if(n.pop()!=q.charAt(0))return{pos:e,match:!1};if(!n.length)return{pos:e,match:!0}}}}}var b=vb.inverted?vb.from:vb.to,c=Xb(b.line),d=b.ch-1,e=d>=0&&Jd[c.text.charAt(d)]||Jd[c.text.charAt(++d)];if(!e)return;var f=e.charAt(0),g=e.charAt(1)==">",h=g?1:-1,i=c.styles;for(var j=d+1,k=0,l=i.length;ke;--d){if(d==0)return 0;var g=Xb(d-1);if(g.stateAfter)return d;var h=g.indentation(f.tabSize);if(c==null||b>h)c=d-1,b=h}return c}function Md(a){var b=Ld(a),c=b&&Xb(b-1).stateAfter;return c?c=m(rb,c):c=n(rb),sb.iter(b,a,function(a){a.highlight(rb,c,f.tabSize),a.stateAfter=m(rb,c)}),b=sb.size)continue;var d=Ld(c),e=d&&Xb(d-1).stateAfter;e?e=m(rb,e):e=n(rb);var g=0,h=rb.compareStates,i=!1,j=d,k=!1;sb.iter(j,sb.size,function(b){var d=b.stateAfter;if(+(new Date)>a)return tb.push(j),Pd(f.workDelay),i&&Fb.push({from:c,to:j+1}),k=!0;var l=b.highlight(rb,e,f.tabSize);l&&(i=!0),b.stateAfter=m(rb,e);if(h){if(d&&h(d,e))return!0}else if(l!==!1||!d)g=0;else if(++g>3&&(!rb.indent||rb.indent(d,"")==rb.indent(e,"")))return!0;++j});if(k)return;i&&Fb.push({from:c,to:j+1})}b&&f.onHighlightComplete&&f.onHighlightComplete(Wb)}function Pd(a){if(!tb.length)return;pb.set(a,Td(Od))}function Qd(){Db=Eb=Gb=null,Fb=[],Hb=!1,Kb=[]}function Rd(){var a=!1,b;Hb&&(a=!Fc()),Fb.length?b=Ic(Fb,!0):(Hb&&Mc(),Jb&&Lc()),a&&Fc(),Hb&&(Ec(),Id()),ub&&!Ib&&(Db===!0||Db!==!1&&Hb)&&Cc(Eb),Hb&&f.matchBrackets&&setTimeout(Td(function(){Pb&&(Pb(),Pb=null),Y(vb.from,vb.to)&&Kd(!1)}),20);var c=Gb,d=Kb;Hb&&f.onCursorActivity&&f.onCursorActivity(Wb),c&&f.onChange&&Wb&&f.onChange(Wb,c);for(var e=0;eh&&a.y>b.offsetHeight&&(f=a.y-b.offsetHeight),g+b.offsetWidth>i&&(g=i-b.offsetWidth)}b.style.top=f+Ed()+"px",b.style.left=b.style.right="",e=="right"?(g=T.clientWidth-b.offsetWidth,b.style.right="0px"):(e=="left"?g=0:e=="middle"&&(g=(T.clientWidth-b.offsetWidth)/2),b.style.left=g+Fd()+"px"),c&&Gc(g,f,g+b.offsetWidth,f+b.offsetHeight)},lineCount:function(){return sb.size},clipPos:Tc,getCursor:function(a){return a==null&&(a=vb.inverted),$(a?vb.from:vb.to)},somethingSelected:function(){return!Y(vb.from,vb.to)},setCursor:Td(function(a,b,c){b==null&&typeof a.line=="number"?Rc(a.line,a.ch,c):Rc(a,b,c)}),setSelection:Td(function(a,b,c){(c?Oc:Pc)(Tc(a),Tc(b||a))}),getLine:function(a){if(Vb(a))return Xb(a).text},getLineHandle:function(a){if(Vb(a))return Xb(a)},setLine:Td(function(a,b){Vb(a)&&sc(b,{line:a,ch:0},{line:a,ch:Xb(a).text.length})}),removeLine:Td(function(a){Vb(a)&&sc("",{line:a,ch:0},Tc({line:a+1,ch:0}))}),replaceRange:Td(sc),getRange:function(a,b){return vc(Tc(a),Tc(b))},triggerOnKeyDown:Td(ic),execCommand:function(a){return h[a](Wb)},moveH:Td(Vc),deleteH:Td(Wc),moveV:Td(Yc),toggleOverwrite:function(){Bb?(Bb=!1,kb.className=kb.className.replace(" CodeMirror-overwrite","")):(Bb=!0,kb.className+=" CodeMirror-overwrite")},posFromIndex:function(a){var b=0,c;return sb.iter(0,sb.size,function(d){var e=d.text.length+1;if(e>a)return c=a,!0;a-=e,++b}),Tc({line:b,ch:c})},indexFromPos:function(a){if(a.line<0||a.ch<0)return 0;var b=a.ch;return sb.iter(0,a.line,function(a){b+=a.text.length+1}),b},scrollTo:function(a,b){a!=null&&(S.scrollLeft=a),b!=null&&(S.scrollTop=b),Ic([])},operation:function(a){return Td(a)()},refresh:function(){Ic(!0),S.scrollHeight>zb&&(S.scrollTop=zb)},getInputField:function(){return D},getWrapperElement:function(){return s},getScrollerElement:function(){return S},getGutterElement:function(){return _}},gc=null,hc,xc=!1,Ac="",Xc=null;gd.prototype.clear=Td(function(){var a=Infinity,b=-Infinity;for(var c=0,d=this.set.length;c",")":"(<","[":"]>","]":"[<","{":"}>","}":"{<"},Sd=0;for(var Ud in g)g.propertyIsEnumerable(Ud)&&!Wb.propertyIsEnumerable(Ud)&&(Wb[Ud]=g[Ud]);return Wb}function j(a){return typeof a=="string"?i[a]:a}function k(a,b,c,d){function e(b){b=j(b);var c=b[a];if(c!=null&&d(c))return!0;if(b.catchall)return d(b.catchall);var f=b.fallthrough;if(f==null)return!1;if(Object.prototype.toString.call(f)!="[object Array]")return e(f);for(var g=0,h=f.length;ga&&d.push(h.slice(a-f,Math.min(h.length,b-f)),c[e+1]),i>=a&&(g=1)):g==1&&(i>b?d.push(h.slice(0,b-f),c[e+1]):d.push(h,c[e+1])),f=i}}function t(a){this.lines=a,this.parent=null;for(var b=0,c=a.length,d=0;b=0&&d>=0;--c,--d)if(a.charAt(c)!=b.charAt(d))break;return d+1}function cb(a,b){if(a.indexOf)return a.indexOf(b);for(var c=0,d=a.length;c0&&b.ch=this.string.length},sol:function(){return this.pos==0},peek:function(){return this.string.charAt(this.pos)},next:function(){if(this.posb},eatSpace:function(){var a=this.pos;while(/[\s\u00a0]/.test(this.string.charAt(this.pos)))++this.pos;return this.pos>a},skipToEnd:function(){this.pos=this.string.length},skipTo:function(a){var b=this.string.indexOf(a,this.pos);if(b>-1)return this.pos=b,!0},backUp:function(a){this.pos-=a},column:function(){return T(this.string,this.start,this.tabSize)},indentation:function(){return T(this.string,null,this.tabSize)},match:function(a,b,c){if(typeof a!="string"){var e=this.string.slice(this.pos).match(a);return e&&b!==!1&&(this.pos+=e[0].length),e}function d(a){return c?a.toLowerCase():a}if(d(this.string).indexOf(d(a),this.pos)==this.pos)return b!==!1&&(this.pos+=a.length),!0},current:function(){return this.string.slice(this.start,this.pos)}},a.StringStream=o,p.prototype={attach:function(a){this.marker.set.push(a)},detach:function(a){var b=cb(this.marker.set,a);b>-1&&this.marker.set.splice(b,1)},split:function(a,b){if(this.to<=a&&this.to!=null)return null;var c=this.fromthis.from&&(d=b&&(this.from=Math.max(d,this.from)+e),c&&(bthis.from||this.from==null)?this.to=null:this.to!=null&&this.to>b&&(this.to=d=this.to},sameSet:function(a){return this.marker==a.marker}},q.prototype={attach:function(a){this.line=a},detach:function(a){this.line==a&&(this.line=null)},split:function(a,b){if(athis.to},clipTo:function(a,b,c,d,e){(a||bthis.to)?(this.from=0,this.to=-1):this.from>b&&(this.from=this.to=Math.max(d,this.from)+e)},sameSet:function(a){return!1},find:function(){return!this.line||!this.line.parent?null:{line:w(this.line),ch:this.from}},clear:function(){if(this.line){var a=cb(this.line.marked,this);a!=-1&&this.line.marked.splice(a,1),this.line=null}}},r.inheritMarks=function(a,b){var c=new r(a),d=b&&b.marked;if(d)for(var e=0;e5e3){e[f++]=this.text.slice(d.pos),e[f++]=null;break}}return e.length!=f&&(e.length=f,g=!0),f&&e[f-2]!=i&&(g=!0),g||(e.length<5&&this.text.length<10?null:!1)},getTokenAt:function(a,b,c){var d=this.text,e=new o(d);while(e.pos',g,"
"):c.push(g)}function k(a){return a?"cm-"+a.replace(/ +/g," cm-"):null}var c=[],d=!0,e=0,g=this.styles,h=this.text,i=this.marked,j=h.length;b!=null&&(j=Math.min(b,j));if(!h&&b==null)f(" ");else if(!i||!i.length)for(var l=0,m=0;mj&&(n=n.slice(0,j-m)),m+=p,f(n,k(o))}else{var q=0,l=0,r="",o,s=0,t=i[0].from||0,u=[],v=0;function w(){var a;while(vy?r.slice(0,y-q):r,A);if(z>=y){r=r.slice(y-q),q=y;break}q=z}r=g[l++],o=k(g[l++])}}}return c.join("")},cleanUp:function(){this.parent=null;if(this.marked)for(var a=0,b=this.marked.length;a50){while(f.lines.length>50){var h=f.lines.splice(f.lines.length-25,25),i=new t(h);f.height-=i.height,this.children.splice(d+1,0,i),i.parent=this}this.maybeSpill()}break}a-=g}},maybeSpill:function(){if(this.children.length<=10)return;var a=this;do{var b=a.children.splice(a.children.length-5,5),c=new u(b);if(!a.parent){var d=new u(a.children);d.parent=a,a.children=[d,c],a=d}else{a.size-=c.size,a.height-=c.height;var e=cb(a.parent.children,a);a.parent.children.splice(e+1,0,c)}c.parent=a.parent}while(a.children.length>10);a.parent.maybeSpill()},iter:function(a,b,c){this.iterN(a,b-a,c)},iterN:function(a,b,c){for(var d=0,e=this.children.length;d400||!f)this.done.push([{start:a,added:b,old:c}]);else if(f.start>a+c.length||f.start+f.added=0;--i)f.old.unshift(c[i]);h=Math.min(0,b-c.length),f.added+=f.start-a+h,f.start=a}else f.start-1&&(S="\r\n")})(),document.documentElement.getBoundingClientRect!=null&&(V=function(a,b){try{var c=a.getBoundingClientRect();c={top:c.top,left:c.left}}catch(d){c={top:0,left:0}}if(!b)if(window.pageYOffset==null){var e=document.documentElement||document.body.parentNode;e.scrollTop==null&&(e=document.body),c.top+=e.scrollTop,c.left+=e.scrollLeft}else c.top+=window.pageYOffset,c.left+=window.pageXOffset;return c});var _=document.createElement("pre");ab("a")=="\na"?ab=function(a){return _.textContent=a,_.innerHTML.slice(1)}:ab(" ")!=" "&&(ab=function(a){return _.innerHTML="",_.appendChild(document.createTextNode(a)),_.innerHTML}),a.htmlEscape=ab;var eb="\n\nb".split(/\n/).length!=3?function(a){var b=0,c,d=[];while((c=a.indexOf("\n",b))>-1)d.push(a.slice(b,a.charAt(c-1)=="\r"?c-1:c)),b=c+1;return d.push(a.slice(b)),d}:function(a){return a.split(/\r?\n/)};a.splitLines=eb;var fb=window.getSelection?function(a){try{return a.selectionStart!=a.selectionEnd}catch(b){return!1}}:function(a){try{var b=a.ownerDocument.selection.createRange()}catch(c){}return!b||b.parentElement()!=a?!1:b.compareEndPoints("StartToEnd",b)!=0};a.defineMode("null",function(){return{token:function(a){a.skipToEnd()}}}),a.defineMIME("text/plain","null");var gb={3:"Enter",8:"Backspace",9:"Tab",13:"Enter",16:"Shift",17:"Ctrl",18:"Alt",19:"Pause",20:"CapsLock",27:"Esc",32:"Space",33:"PageUp",34:"PageDown",35:"End",36:"Home",37:"Left",38:"Up",39:"Right",40:"Down",44:"PrintScrn",45:"Insert",46:"Delete",59:";",91:"Mod",92:"Mod",93:"Mod",127:"Delete",186:";",187:"=",188:",",189:"-",190:".",191:"/",192:"`",219:"[",220:"\\",221:"]",222:"'",63276:"PageUp",63277:"PageDown",63275:"End",63273:"Home",63234:"Left",63232:"Up",63235:"Right",63233:"Down",63302:"Insert",63272:"Delete"};return a.keyNames=gb,function(){for(var a=0;a<10;a++)gb[a+48]=String(a);for(var a=65;a<=90;a++)gb[a]=String.fromCharCode(a);for(var a=1;a<=12;a++)gb[a+111]=gb[a+63235]="F"+a}(),a}();CodeMirror.defineMode("diff",function(){return{token:function(a){var b=a.next();a.skipToEnd();if(b=="+")return"plus";if(b=="-")return"minus";if(b=="@")return"rangeinfo"}}}),CodeMirror.defineMIME("text/x-diff","diff"),CodeMirror.defineMode("htmlembedded",function(a,b){function g(a,b){return a.match(c,!1)?(b.token=h,e.token(a,b.scriptState)):f.token(a,b.htmlState)}function h(a,b){return a.match(d,!1)?(b.token=g,f.token(a,b.htmlState)):e.token(a,b.scriptState)}var c=b.scriptStartRegex||/^<%/i,d=b.scriptEndRegex||/^%>/i,e,f;return{startState:function(){return e=e||CodeMirror.getMode(a,b.scriptingModeSpec),f=f||CodeMirror.getMode(a,"htmlmixed"),{token:b.startOpen?h:g,htmlState:f.startState(),scriptState:e.startState()}},token:function(a,b){return b.token(a,b)},indent:function(a,b){return a.token==g?f.indent(a.htmlState,b):e.indent(a.scriptState,b)},copyState:function(a){return{token:a.token,htmlState:CodeMirror.copyState(f,a.htmlState),scriptState:CodeMirror.copyState(e,a.scriptState)}},electricChars:"/{}:"}}),CodeMirror.defineMIME("application/x-ejs",{name:"htmlembedded",scriptingModeSpec:"javascript"}),CodeMirror.defineMIME("application/x-aspx",{name:"htmlembedded",scriptingModeSpec:"text/x-csharp"}),CodeMirror.defineMIME("application/x-jsp",{name:"htmlembedded",scriptingModeSpec:"text/x-java"}),CodeMirror.defineMode("htmlmixed",function(a,b){function f(a,b){var f=c.token(a,b.htmlState);return f=="tag"&&a.current()==">"&&b.htmlState.context&&(/^script$/i.test(b.htmlState.context.tagName)?(b.token=h,b.localState=d.startState(c.indent(b.htmlState,"")),b.mode="javascript"):/^style$/i.test(b.htmlState.context.tagName)&&(b.token=i,b.localState=e.startState(c.indent(b.htmlState,"")),b.mode="css")),f}function g(a,b,c){var d=a.current(),e=d.search(b);return e>-1&&a.backUp(d.length-e),c}function h(a,b){return a.match(/^<\/\s*script\s*>/i,!1)?(b.token=f,b.localState=null,b.mode="html",f(a,b)):g(a,/<\/\s*script\s*>/,d.token(a,b.localState))}function i(a,b){return a.match(/^<\/\s*style\s*>/i,!1)?(b.token=f,b.localState=null,b.mode="html",f(a,b)):g(a,/<\/\s*style\s*>/,e.token(a,b.localState))}var c=CodeMirror.getMode(a,{name:"xml",htmlMode:!0}),d=CodeMirror.getMode(a,"javascript"),e=CodeMirror.getMode(a,"css");return{startState:function(){var a=c.startState();return{token:f,localState:null,mode:"html",htmlState:a}},copyState:function(a){if(a.localState)var b=CodeMirror.copyState(a.token==i?e:d,a.localState);return{token:a.token,localState:b,mode:a.mode,htmlState:CodeMirror.copyState(c,a.htmlState)}},token:function(a,b){return b.token(a,b)},indent:function(a,b){return a.token==f||/^\s*<\//.test(b)?c.indent(a.htmlState,b):a.token==h?d.indent(a.localState,b):e.indent(a.localState,b)},compareStates:function(a,b){return c.compareStates(a.htmlState,b.htmlState)},electricChars:"/{}:"}}),CodeMirror.defineMIME("text/html","htmlmixed"),CodeMirror.defineMode("javascript",function(a,b){function g(a,b,c){return b.tokenize=c,c(a,b)}function h(a,b){var c=!1,d;while((d=a.next())!=null){if(d==b&&!c)return!1;c=!c&&d=="\\"}return c}function k(a,b,c){return i=a,j=c,b}function l(a,b){var c=a.next();if(c=='"'||c=="'")return g(a,b,m(c));if(/[\[\]{}\(\),;\:\.]/.test(c))return k(c);if(c=="0"&&a.eat(/x/i))return a.eatWhile(/[\da-f]/i),k("number","number");if(/\d/.test(c))return a.match(/^\d*(?:\.\d*)?(?:[eE][+\-]?\d+)?/),k("number","number");if(c=="/")return a.eat("*")?g(a,b,n):a.eat("/")?(a.skipToEnd(),k("comment","comment")):b.reAllowed?(h(a,"/"),a.eatWhile(/[gimy]/),k("regexp","string-2")):(a.eatWhile(f),k("operator",null,a.current()));if(c=="#")return a.skipToEnd(),k("error","error");if(f.test(c))return a.eatWhile(f),k("operator",null,a.current());a.eatWhile(/[\w\$_]/);var d=a.current(),i=e.propertyIsEnumerable(d)&&e[d];return i&&b.kwAllowed?k(i.type,i.style,d):k("variable","variable",d)}function m(a){return function(b,c){return h(b,a)||(c.tokenize=l),k("string","string")}}function n(a,b){var c=!1,d;while(d=a.next()){if(d=="/"&&c){b.tokenize=l;break}c=d=="*"}return k("comment","comment")}function p(a,b,c,d,e,f){this.indented=a,this.column=b,this.type=c,this.prev=e,this.info=f,d!=null&&(this.align=d)}function q(a,b){for(var c=a.localVars;c;c=c.next)if(c.name==b)return!0}function r(a,b,c,e,f){var g=a.cc;s.state=a,s.stream=f,s.marked=null,s.cc=g,a.lexical.hasOwnProperty("align")||(a.lexical.align=!0);for(;;){var h=g.length?g.pop():d?D:C;if(h(c,e)){while(g.length&&g[g.length-1].lex)g.pop()();return s.marked?s.marked:c=="variable"&&q(a,e)?"variable-2":b}}}function t(){for(var a=arguments.length-1;a>=0;a--)s.cc.push(arguments[a])}function u(){return t.apply(null,arguments),!0}function v(a){var b=s.state;if(b.context){s.marked="def";for(var c=b.localVars;c;c=c.next)if(c.name==a)return;b.localVars={name:a,next:b.localVars}}}function x(){s.state.context||(s.state.localVars=w),s.state.context={prev:s.state.context,vars:s.state.localVars}}function y(){s.state.localVars=s.state.context.vars,s.state.context=s.state.context.prev}function z(a,b){var c=function(){var c=s.state;c.lexical=new p(c.indented,s.stream.column(),a,null,c.lexical,b)};return c.lex=!0,c}function A(){var a=s.state;a.lexical.prev&&(a.lexical.type==")"&&(a.indented=a.lexical.indented),a.lexical=a.lexical.prev)}function B(a){return function(c){return c==a?u():a==";"?t():u(arguments.callee)}}function C(a){return a=="var"?u(z("vardef"),L,B(";"),A):a=="keyword a"?u(z("form"),D,C,A):a=="keyword b"?u(z("form"),C,A):a=="{"?u(z("}"),K,A):a==";"?u():a=="function"?u(R):a=="for"?u(z("form"),B("("),z(")"),N,B(")"),A,C,A):a=="variable"?u(z("stat"),G):a=="switch"?u(z("form"),D,z("}","switch"),B("{"),K,A,A):a=="case"?u(D,B(":")):a=="default"?u(B(":")):a=="catch"?u(z("form"),x,B("("),S,B(")"),C,A,y):t(z("stat"),D,B(";"),A)}function D(a){return o.hasOwnProperty(a)?u(F):a=="function"?u(R):a=="keyword c"?u(E):a=="("?u(z(")"),E,B(")"),A,F):a=="operator"?u(D):a=="["?u(z("]"),J(D,"]"),A,F):a=="{"?u(z("}"),J(I,"}"),A,F):u()}function E(a){return a.match(/[;\}\)\],]/)?t():t(D)}function F(a,b){if(a=="operator"&&/\+\+|--/.test(b))return u(F);if(a=="operator")return u(D);if(a==";")return;if(a=="(")return u(z(")"),J(D,")"),A,F);if(a==".")return u(H,F);if(a=="[")return u(z("]"),D,B("]"),A,F)}function G(a){return a==":"?u(A,C):t(F,B(";"),A)}function H(a){if(a=="variable")return s.marked="property",u()}function I(a){a=="variable"&&(s.marked="property");if(o.hasOwnProperty(a))return u(B(":"),D)}function J(a,b){function c(d){return d==","?u(a,c):d==b?u():u(B(b))}return function(e){return e==b?u():t(a,c)}}function K(a){return a=="}"?u():t(C,K)}function L(a,b){return a=="variable"?(v(b),u(M)):u()}function M(a,b){if(b=="=")return u(D,M);if(a==",")return u(L)}function N(a){return a=="var"?u(L,P):a==";"?t(P):a=="variable"?u(O):t(P)}function O(a,b){return b=="in"?u(D):u(F,P)}function P(a,b){return a==";"?u(Q):b=="in"?u(D):u(D,B(";"),Q)}function Q(a){a!=")"&&u(D)}function R(a,b){if(a=="variable")return v(b),u(R);if(a=="(")return u(z(")"),x,J(S,")"),A,C,y)}function S(a,b){if(a=="variable")return v(b),u()}var c=a.indentUnit,d=b.json,e=function(){function a(a){return{type:a,style:"keyword"}}var b=a("keyword a"),c=a("keyword b"),d=a("keyword c"),e=a("operator"),f={type:"atom",style:"atom"};return{"if":b,"while":b,"with":b,"else":c,"do":c,"try":c,"finally":c,"return":d,"break":d,"continue":d,"new":d,"delete":d,"throw":d,"var":a("var"),"const":a("var"),let:a("var"),"function":a("function"),"catch":a("catch"),"for":a("for"),"switch":a("switch"),"case":a("case"),"default":a("default"),"in":e,"typeof":e,"instanceof":e,"true":f,"false":f,"null":f,"undefined":f,NaN:f,Infinity:f}}(),f=/[+\-*&%=<>!?|]/,i,j,o={atom:!0,number:!0,variable:!0,string:!0,regexp:!0},s={state:null,column:null,marked:null,cc:null},w={name:"this",next:{name:"arguments"}};return A.lex=!0,{startState:function(a){return{tokenize:l,reAllowed:!0,kwAllowed:!0,cc:[],lexical:new p((a||0)-c,0,"block",!1),localVars:b.localVars,context:b.localVars&&{vars:b.localVars},indented:0}},token:function(a,b){a.sol()&&(b.lexical.hasOwnProperty("align")||(b.lexical.align=!1),b.indented=a.indentation());if(a.eatSpace())return null;var c=b.tokenize(a,b);return i=="comment"?c:(b.reAllowed=i=="operator"||i=="keyword c"||!!i.match(/^[\[{}\(,;:]$/),b.kwAllowed=i!=".",r(b,c,i,j,a))},indent:function(a,b){if(a.tokenize!=l)return 0;var d=b&&b.charAt(0),e=a.lexical,f=e.type,g=d==f;return f=="vardef"?e.indented+4:f=="form"&&d=="{"?e.indented:f=="stat"||f=="form"?e.indented+c:e.info=="switch"&&!g?e.indented+(/^(?:case|default)\b/.test(b)?c:2*c):e.align?e.column+(g?0:1):e.indented+(g?0:c)},electricChars:":{}"}}),CodeMirror.defineMIME("text/javascript","javascript"),CodeMirror.defineMIME("application/json",{name:"javascript",json:!0}),CodeMirror.defineMode("markdown",function(a,b){function s(a,b,c){return b.f=b.inline=c,c(a,b)}function t(a,b,c){return b.f=b.block=c,c(a,b)}function u(a){return a.em=!1,a.strong=!1,null}function v(a,b){var c;if(b.indentationDiff>=4)return b.indentation-=b.indentationDiff,a.skipToEnd(),e;if(a.eatSpace())return null;if(a.peek()==="#"||a.match(q))b.header=!0;else if(a.eat(">"))b.indentation++,b.quote=!0;else{if(a.peek()==="[")return s(a,b,C);if(a.match(n,!0))return h;if(c=a.match(o,!0)||a.match(p,!0))return b.indentation+=c[0].length,g}return s(a,b,b.inline)}function w(a,b){var d=c.token(a,b.htmlState);return d==="tag"&&b.htmlState.type!=="openTag"&&!b.htmlState.context&&(b.f=z,b.block=v),d}function x(a){var b=[];return a.strong?b.push(a.em?m:l):a.em&&b.push(k),a.header&&b.push(d),a.quote&&b.push(f),b.length?b.join(" "):null}function y(a,b){return a.match(r,!0)?x(b):undefined}function z(a,b){var c=b.text(a,b);if(typeof c!="undefined")return c;var d=a.next();if(d==="\\")return a.next(),x(b);if(d==="`")return s(a,b,F(e,"`"));if(d==="[")return s(a,b,A);if(d==="<"&&a.match(/^\w/,!1))return a.backUp(1),t(a,b,w);var f=x(b);return d==="*"||d==="_"?a.eat(d)?(b.strong=!b.strong)?x(b):f:(b.em=!b.em)?x(b):f:x(b)}function A(a,b){while(!a.eol()){var c=a.next();c==="\\"&&a.next();if(c==="]")return b.inline=b.f=B,i}return i}function B(a,b){a.eatSpace();var c=a.next();return c==="("||c==="["?s(a,b,F(j,c==="("?")":"]")):"error"}function C(a,b){return a.match(/^[^\]]*\]:/,!0)?(b.f=D,i):s(a,b,z)}function D(a,b){return a.eatSpace(),a.match(/^[^\s]+/,!0),b.f=b.inline=z,j}function E(a){return E[a]||(E[a]=new RegExp("^(?:[^\\\\\\"+a+"]|\\\\.)*(?:\\"+a+"|$)")),E[a]}function F(a,b,c){return c=c||z,function(d,e){return d.match(E(b)),e.inline=e.f=c,a}}var c=CodeMirror.getMode(a,{name:"xml",htmlMode:!0}),d="header",e="comment",f="quote",g="string",h="hr",i="link",j="string",k="em",l="strong",m="emstrong",n=/^([*\-=_])(?:\s*\1){2,}\s*$/,o=/^[*\-+]\s+/,p=/^[0-9]+\.\s+/,q=/^(?:\={3,}|-{3,})$/,r=/^[^\[*_\\<>`]+/;return{startState:function(){return{f:v,block:v,htmlState:c.startState(),indentation:0,inline:z,text:y,em:!1,strong:!1,header:!1,quote:!1}},copyState:function(a){return{f:a.f,block:a.block,htmlState:CodeMirror.copyState(c,a.htmlState),indentation:a.indentation,inline:a.inline,text:a.text,em:a.em,strong:a.strong,header:a.header,quote:a.quote}},token:function(a,b){if(a.sol()){if(a.match(/^\s*$/,!0))return u(b);b.header=!1,b.quote=!1,b.f=b.block;var c=a.match(/^\s*/,!0)[0].replace(/\t/g," ").length;b.indentationDiff=c-b.indentation,b.indentation=c;if(c>0)return null}return b.f(a,b)},blankLine:u,getType:x}}),CodeMirror.defineMIME("text/x-markdown","markdown"),CodeMirror.defineMode("python",function(a,b){function d(a){return new RegExp("^(("+a.join(")|(")+"))\\b")}function t(a,b){if(a.sol()){var d=b.scopes[0].offset;if(a.eatSpace()){var l=a.indentation();return l>d?s="indent":l0&&w(a,b)}if(a.eatSpace())return null;var m=a.peek();if(m==="#")return a.skipToEnd(),"comment";if(a.match(/^[0-9\.]/,!1)){var n=!1;a.match(/^\d*\.\d+(e[\+\-]?\d+)?/i)&&(n=!0),a.match(/^\d+\.\d*/)&&(n=!0),a.match(/^\.\d+/)&&(n=!0);if(n)return a.eat(/J/i),"number";var o=!1;a.match(/^0x[0-9a-f]+/i)&&(o=!0),a.match(/^0b[01]+/i)&&(o=!0),a.match(/^0o[0-7]+/i)&&(o=!0),a.match(/^[1-9]\d*(e[\+\-]?\d+)?/)&&(a.eat(/J/i),o=!0),a.match(/^0(?![\dx])/i)&&(o=!0);if(o)return a.eat(/L/i),"number"}return a.match(p)?(b.tokenize=u(a.current()),b.tokenize(a,b)):a.match(i)||a.match(h)?null:a.match(g)||a.match(e)||a.match(k)?"operator":a.match(f)?null:a.match(q)?"keyword":a.match(r)?"builtin":a.match(j)?"variable":(a.next(),c)}function u(a){while("rub".indexOf(a.charAt(0).toLowerCase())>=0)a=a.substr(1);var d=a.length==1,e="string";return function(g,h){while(!g.eol()){g.eatWhile(/[^'"\\]/);if(g.eat("\\")){g.next();if(d&&g.eol())return e}else{if(g.match(a))return h.tokenize=t,e;g.eat(/['"]/)}}if(d){if(b.singleLineStringErrors)return c;h.tokenize=t}return e}}function v(b,c,d){d=d||"py";var e=0;if(d==="py"){if(c.scopes[0].type!=="py"){c.scopes[0].offset=b.indentation();return}for(var f=0;f0&&a.eol()&&b.scopes[0].type=="py"&&(b.scopes.length>1&&b.scopes.shift(),b.dedent-=1),d))}var c="error",e=new RegExp("^[\\+\\-\\*/%&|\\^~<>!]"),f=new RegExp("^[\\(\\)\\[\\]\\{\\}@,:`=;\\.]"),g=new RegExp("^((==)|(!=)|(<=)|(>=)|(<>)|(<<)|(>>)|(//)|(\\*\\*))"),h=new RegExp("^((\\+=)|(\\-=)|(\\*=)|(%=)|(/=)|(&=)|(\\|=)|(\\^=))"),i=new RegExp("^((//=)|(>>=)|(<<=)|(\\*\\*=))"),j=new RegExp("^[_A-Za-z][_A-Za-z0-9]*"),k=d(["and","or","not","is","in"]),l=["as","assert","break","class","continue","def","del","elif","else","except","finally","for","from","global","if","import","lambda","pass","raise","return","try","while","with","yield"],m=["abs","all","any","bin","bool","bytearray","callable","chr","classmethod","compile","complex","delattr","dict","dir","divmod","enumerate","eval","filter","float","format","frozenset","getattr","globals","hasattr","hash","help","hex","id","input","int","isinstance","issubclass","iter","len","list","locals","map","max","memoryview","min","next","object","oct","open","ord","pow","property","range","repr","reversed","round","set","setattr","slice","sorted","staticmethod","str","sum","super","tuple","type","vars","zip","__import__","NotImplemented","Ellipsis","__debug__"],n={builtins:["apply","basestring","buffer","cmp","coerce","execfile","file","intern","long","raw_input","reduce","reload","unichr","unicode","xrange","False","True","None"],keywords:["exec","print"]},o={builtins:["ascii","bytes","exec","print"],keywords:["nonlocal","False","True","None"]};if(!b.version||parseInt(b.version,10)!==3){l=l.concat(n.keywords),m=m.concat(n.builtins);var p=new RegExp("^(([rub]|(ur)|(br))?('{3}|\"{3}|['\"]))","i")}else{l=l.concat(o.keywords),m=m.concat(o.builtins);var p=new RegExp("^(([rb]|(br))?('{3}|\"{3}|['\"]))","i")}var q=d(l),r=d(m),s=null,y={startState:function(a){return{tokenize:t,scopes:[{offset:a||0,type:"py"}],lastToken:null,lambda:!1,dedent:0}},token:function(a,b){var c=x(a,b);return b.lastToken={style:c,content:a.current()},a.eol()&&a.lambda&&(b.lambda=!1),c},indent:function(a,b){return a.tokenize!=t?0:a.scopes[0].offset}};return y}),CodeMirror.defineMIME("text/x-python","python"),CodeMirror.defineMode("xml",function(a,b){function h(a,b){function c(c){return b.tokenize=c,c(a,b)}var d=a.next();if(d=="<"){if(a.eat("!"))return a.eat("[")?a.match("CDATA[")?c(k("atom","]]>")):null:a.match("--")?c(k("comment","-->")):a.match("DOCTYPE",!0,!0)?(a.eatWhile(/[\w\._\-]/),c(l(1))):null;if(a.eat("?"))return a.eatWhile(/[\w\._\-]/),b.tokenize=k("meta","?>"),"meta";g=a.eat("/")?"closeTag":"openTag",a.eatSpace(),f="";var e;while(e=a.eat(/[^\s\u00a0=<>\"\'\/?]/))f+=e;return b.tokenize=i,"tag"}if(d=="&"){var h;return a.eat("#")?a.eat("x")?h=a.eatWhile(/[a-fA-F\d]/)&&a.eat(";"):h=a.eatWhile(/[\d]/)&&a.eat(";"):h=a.eatWhile(/[\w\.\-:]/)&&a.eat(";"),h?"atom":"error"}return a.eatWhile(/[^&<]/),null}function i(a,b){var c=a.next();return c==">"||c=="/"&&a.eat(">")?(b.tokenize=h,g=c==">"?"endTag":"selfcloseTag","tag"):c=="="?(g="equals",null):/[\'\"]/.test(c)?(b.tokenize=j(c),b.tokenize(a,b)):(a.eatWhile(/[^\s\u00a0=<>\"\'\/?]/),"word")}function j(a){return function(b,c){while(!b.eol())if(b.next()==a){c.tokenize=i;break}return"string"}}function k(a,b){return function(c,d){while(!c.eol()){if(c.match(b)){d.tokenize=h;break}c.next()}return a}}function l(a){return function(b,c){var d;while((d=b.next())!=null){if(d=="<")return c.tokenize=l(a+1),c.tokenize(b,c);if(d==">"){if(a==1){c.tokenize=h;break}return c.tokenize=l(a-1),c.tokenize(b,c)}}return"meta"}}function o(){for(var a=arguments.length-1;a>=0;a--)m.cc.push(arguments[a])}function p(){return o.apply(null,arguments),!0}function q(a,b){var c=d.doNotIndent.hasOwnProperty(a)||m.context&&m.context.noIndent;m.context={prev:m.context,tagName:a,indent:m.indented,startOfLine:b,noIndent:c}}function r(){m.context&&(m.context=m.context.prev)}function s(a){if(a=="openTag")return m.tagName=f,p(v,t(m.startOfLine));if(a=="closeTag"){var b=!1;return m.context?b=m.context.tagName!=f:b=!0,b&&(n="error"),p(u(b))}return p()}function t(a){return function(b){return b=="selfcloseTag"||b=="endTag"&&d.autoSelfClosers.hasOwnProperty(m.tagName.toLowerCase())?p():b=="endTag"?(q(m.tagName,a),p()):p()}}function u(a){return function(b){return a&&(n="error"),b=="endTag"?(r(),p()):(n="error",p(arguments.callee))}}function v(a){return a=="word"?(n="attribute",p(w,v)):a=="endTag"||a=="selfcloseTag"?o():(n="error",p(v))}function w(a){return a=="equals"?p(x,v):(d.allowMissing||(n="error"),a=="endTag"||a=="selfcloseTag"?o():p())}function x(a){return a=="string"?p(y):a=="word"&&d.allowUnquoted?(n="string",p()):(n="error",a=="endTag"||a=="selfCloseTag"?o():p())}function y(a){return a=="string"?p(y):o()}var c=a.indentUnit,d=b.htmlMode?{autoSelfClosers:{br:!0,img:!0,hr:!0,link:!0,input:!0,meta:!0,col:!0,frame:!0,base:!0,area:!0},doNotIndent:{pre:!0},allowUnquoted:!0,allowMissing:!1}:{autoSelfClosers:{},doNotIndent:{},allowUnquoted:!1,allowMissing:!1},e=b.alignCDATA,f,g,m,n;return{startState:function(){return{tokenize:h,cc:[],indented:0,startOfLine:!0,tagName:null,context:null}},token:function(a,b){a.sol()&&(b.startOfLine=!0,b.indented=a.indentation());if(a.eatSpace())return null;n=g=f=null;var c=b.tokenize(a,b);b.type=g;if((c||g)&&c!="comment"){m=b;for(;;){var d=b.cc.pop()||s;if(d(g||c))break}}return b.startOfLine=!1,n||c},indent:function(a,b,d){var f=a.context;if(a.tokenize!=i&&a.tokenize!=h||f&&f.noIndent)return d?d.match(/^(\s*)/)[0].length:0;if(e&&/c.keyCol)return a.skipToEnd(),"string";c.literal&&(c.literal=!1);if(a.sol()){c.keyCol=0,c.pair=!1,c.pairStart=!1;if(a.match(/---/))return"def";if(a.match(/\.\.\./))return"def";if(a.match(/\s*-\s+/))return"meta"}if(!c.pair&&a.match(/^\s*([a-z0-9\._-])+(?=\s*:)/i))return c.pair=!0,c.keyCol=a.indentation(),"atom";if(c.pair&&a.match(/^:\s*/))return c.pairStart=!0,"meta";if(a.match(/^(\{|\}|\[|\])/))return d=="{"?c.inlinePairs++:d=="}"?c.inlinePairs--:d=="["?c.inlineList++:c.inlineList--,"meta";if(c.inlineList>0&&!e&&d==",")return a.next(),"meta";if(c.inlinePairs>0&&!e&&d==",")return c.keyCol=0,c.pair=!1,c.pairStart=!1,a.next(),"meta";if(c.pairStart){if(a.match(/^\s*(\||\>)\s*/))return c.literal=!0,"meta";if(a.match(/^\s*(\&|\*)[a-z0-9\._-]+\b/i))return"variable-2";if(c.inlinePairs==0&&a.match(/^\s*-?[0-9\.\,]+\s?$/))return"number";if(c.inlinePairs>0&&a.match(/^\s*-?[0-9\.\,]+\s?(?=(,|}))/))return"number";if(a.match(b))return"keyword"}return c.pairStart=!1,c.escaped=d=="\\",a.next(),null},startState:function(){return{pair:!1,pairStart:!1,keyCol:0,inlinePairs:0,inlineList:0,literal:!1,escaped:!1}}}}),CodeMirror.defineMIME("text/x-yaml","yaml"),CodeMirror.runMode=function(a,b,c,d){var e=CodeMirror.getMode(CodeMirror.defaults,b),f=c.nodeType==1,g=d&&d.tabSize||CodeMirror.defaults.tabSize;if(f){var h=c,i=[],j=0;c=function(a,b){if(a=="\n"){i.push("
"),j=0;return}var c="";for(var d=0;;){var e=a.indexOf(" ",d);if(e==-1){c+=CodeMirror.htmlEscape(a.slice(d)),j+=a.length-d;break}j+=e-d,c+=CodeMirror.htmlEscape(a.slice(d,e));var f=g-j%g;j+=f;for(var h=0;h'+c+""):i.push(c)}}var k=CodeMirror.splitLines(a),l=CodeMirror.startState(e);for(var m=0,n=k.length;m",i+1);if(-1==j){var k=b+1,l=!1,m=a.lineCount();while(k");if(-1!=o){l=!0;var p=n.lastIndexOf("/",o);if(-1!=p&&p/))return k+1}}k++}g=!0}else{var r=f.lastIndexOf("/",j);if(-1==r)g=!0;else{var q=f.substr(r,j-r+1);q.match(/\/\s*\>/)||(g=!0)}}if(g){var s=f.substr(i+1);h=s.match(e),h?(h=h[0],-1!=f.indexOf("",i)&&(g=!1)):g=!1}g||i++}if(g){var t="(\\<\\/"+h+"\\>)|(\\<"+h+"\\>)|(\\<"+h+"\\s)|(\\<"+h+"$)",u=new RegExp(t,"g"),v="",w=1,k=b+1,m=a.lineCount();while(kd)return;var e=a.getTokenAt({line:b,ch:d}).className,f=1,g=a.lineCount(),h;a:for(var i=b+1;i▼%N%'),function(f,g){f.operation(function(){var h=d(f,g);if(h)c.splice(h.pos,1),e(f,h.region);else{var i=a(f,g);if(i==null)return;var j=[];for(var k=g+1;k=g&&(h=f.lastIndexOf(b,d.ch-g))!=-1:(h=f.indexOf(b,d.ch))!=-1)return{from:{line:d.line,ch:h},to:{line:d.line,ch:h+g}}}:this.matches=function(b,c){var d=c.line,g=b?f.length-1:0,h=f[g],i=e(a.getLine(d)),j=b?i.indexOf(h)+h.length:i.lastIndexOf(h);if(b?j>=c.ch||j!=h.length:j<=c.ch||j!=i.length-h.length)return;for(;;){if(b?!d:d==a.lineCount()-1)return;i=e(a.getLine(d+=b?-1:1)),h=f[b?--g:++g];if(g>0&&g-1&&h>-1&&h>g&&(f=f.substr(0,g)+f.substring(g+d.commentStart.length,h)+f.substr(h+d.commentEnd.length)),this.replaceRange(f,b,c)}}),CodeMirror.defineExtension("autoIndentRange",function(a,b){var c=this;this.operation(function(){for(var d=a.line;d<=b.line;d++)c.indentLine(d,"smart")})}),CodeMirror.defineExtension("autoFormatRange",function(a,b){var c=this.indexFromPos(a),d=this.indexFromPos(b),e=this.getModeExt().autoFormatLineBreaks(this.getValue(),c,d),f=this;this.operation(function(){f.replaceRange(e,a,b);var d=f.posFromIndex(c).line,g=f.posFromIndex(c+e.length).line;for(var h=d;h<=g;h++)f.indentLine(h,"smart")})}),CodeMirror.modeExtensions.css={commentStart:"/*",commentEnd:"*/",wordWrapChars:[";","\\{","\\}"],autoFormatLineBreaks:function(a){return a.replace(new RegExp("(;|\\{|\\})([^\r\n])","g"),"$1\n$2")}},CodeMirror.modeExtensions.javascript={commentStart:"/*",commentEnd:"*/",wordWrapChars:[";","\\{","\\}"],getNonBreakableBlocks:function(a){var b=[new RegExp("for\\s*?\\(([\\s\\S]*?)\\)"),new RegExp("'([\\s\\S]*?)('|$)"),new RegExp('"([\\s\\S]*?)("|$)'),new RegExp("//.*([\r\n]|$)")],c=new Array;for(var d=0;db&&(e+=a.substring(b,d[f].start).replace(c,"$1\n$2"),b=d[f].start),d[f].start<=b&&d[f].end>=b&&(e+=a.substring(b,d[f].end),b=d[f].end);return b",wordWrapChars:[">"],autoFormatLineBreaks:function(a){var b=a.split("\n"),c=new RegExp("(^\\s*?<|^[^<]*?)(.+)(>\\s*?$|[^>]*?$)"),d=new RegExp("<","g"),e=new RegExp("(>)([^\r\n])","g");for(var f=0;f3){b[f]=g[1]+g[2].replace(d,"\n$&").replace(e,"$1\n$2")+g[3];continue}}return b.join("\n")}},CodeMirror.modeExtensions.htmlmixed={commentStart:"",wordWrapChars:[">",";","\\{","\\}"],getModeInfos:function(a,b){var c=new Array;c[0]={pos:0,modeExt:CodeMirror.modeExtensions.xml,modeName:"xml"};var d=new Array;d[0]={regex:new RegExp("]*>([\\s\\S]*?)(]*>|$)","i"),modeExt:CodeMirror.modeExtensions.css,modeName:"css"},d[1]={regex:new RegExp("]*>([\\s\\S]*?)(]*>|$)","i"),modeExt:CodeMirror.modeExtensions.javascript,modeName:"javascript"};var e=typeof b!="undefined"?b:a.length-1;for(var f=0;f1&&h[1].length>0){var i=g+h.index+h[0].indexOf(h[1]);c.push({pos:i,modeExt:d[f].modeExt,modeName:d[f].modeName}),c.push({pos:i+h[1].length,modeExt:c[0].modeExt,modeName:c[0].modeName}),g+=h.index+h[0].length;continue}g+=h.index+Math.max(h[0].length,1)}}return c.sort(function(b,c){return b.pos-c.pos}),c},autoFormatLineBreaks:function(a,b,c){var d=this.getModeInfos(a),e=new RegExp("^\\s*?\n"),f=new RegExp("\n\\s*?$"),g="";if(d.length>1)for(var h=1;h<=d.length;h++){var i=d[h-1].pos,j=h=c)break;if(ic&&(j=c);var k=a.substring(i,j);d[h-1].modeName!="xml"&&(!e.test(k)&&i>0&&(k="\n"+k),!f.test(k)&&j=f){var g=c(b),h=b.getSelection();b.operation(function(){if(b.lineCount()<2e3)for(var a=b.getSearchCursor(h);a.findNext();)(a.from().line!==b.getCursor(!0).line||a.from().ch!==b.getCursor(!0).ch)&&g.marked.push(b.markText(a.from(),a.to(),e))})}}var a=2;CodeMirror.defineExtension("matchHighlight",function(a,b){e(this,a,b)})}(),function(){function a(a,c,d,e){b(a,c,e)?(a.replaceSelection("\n\n","end"),a.indentLine(d.line+1),a.indentLine(d.line+2),a.setCursor({line:d.line+1,ch:a.getLine(d.line+1).length})):(a.replaceSelection(""),a.setCursor(d))}function b(a,b,d){if(typeof b=="undefined"||b==null||b==1)b=a.getOption("closeTagIndent");return b||(b=[]),c(b,d.toLowerCase())!=-1}function c(a,b){if(a.indexOf)return a.indexOf(b);for(var c=0,d=a.length;c"),a.setCursor({line:b.line,ch:b.ch+c.length+2})}CodeMirror.defaults.closeTagEnabled=!0,CodeMirror.defaults.closeTagIndent=["applet","blockquote","body","button","div","dl","fieldset","form","frameset","h1","h2","h3","h4","h5","h6","head","html","iframe","layer","legend","object","ol","p","select","table","ul"],CodeMirror.defineExtension("closeTag",function(b,c,e){if(!b.getOption("closeTagEnabled"))throw CodeMirror.Pass;var f=b.getOption("mode");if(f=="text/html"){var g=b.getCursor(),h=b.getTokenAt(g),i=h.state;if(i.mode&&i.mode!="html")throw CodeMirror.Pass;if(c==">"){var j=i.htmlState?i.htmlState.type:i.type;if(h.className=="tag"&&j=="closeTag")throw CodeMirror.Pass;b.replaceSelection(">"),g={line:g.line,ch:g.ch+1},b.setCursor(g),h=b.getTokenAt(b.getCursor()),i=h.state,j=i.htmlState?i.htmlState.type:i.type;if(h.className=="tag"&&j!="selfcloseTag"){var k=i.htmlState?i.htmlState.context.tagName:i.tagName;k.length>0&&a(b,e,g,k);return}b.setSelection({line:g.line,ch:g.ch-1},g),b.replaceSelection("")}else if(c=="/"&&h.className=="tag"&&h.string=="<"){var k=i.htmlState?i.htmlState.context?i.htmlState.context.tagName:"":i.context.tagName;if(k.length>0){d(b,g,k);return}}}else if(f=="xmlpure"){var g=b.getCursor(),h=b.getTokenAt(g),k=h.state.context.tagName;if(c==">"){if(h.string==k){b.replaceSelection(">"),g={line:g.line,ch:g.ch+1},b.setCursor(g),a(b,e,g,k);return}}else if(c=="/"&&h.string=="<"){d(b,g,k);return}}throw CodeMirror.Pass})}(),function(){function b(b){a.push(b),a.length>50&&a.shift()}function c(){return a[a.length-1]||""}function d(){return a.length>1&&a.pop(),c()}var a=[];CodeMirror.keyMap.emacs={"Ctrl-X":function(a){a.setOption("keyMap","emacs-Ctrl-X")},"Ctrl-W":function(a){b(a.getSelection()),a.replaceSelection("")},"Ctrl-Alt-W":function(a){b(a.getSelection()),a.replaceSelection("")},"Alt-W":function(a){b(a.getSelection())},"Ctrl-Y":function(a){a.replaceSelection(c())},"Alt-Y":function(a){a.replaceSelection(d())},"Ctrl-/":"undo","Shift-Ctrl--":"undo","Shift-Alt-,":"goDocStart","Shift-Alt-.":"goDocEnd","Ctrl-S":"findNext","Ctrl-R":"findPrev","Ctrl-G":"clearSearch","Shift-Alt-5":"replace","Ctrl-Z":"undo","Cmd-Z":"undo",fallthrough:["basic","emacsy"]},CodeMirror.keyMap["emacs-Ctrl-X"]={"Ctrl-S":"save","Ctrl-W":"save",S:"saveAll",F:"open",U:"undo",K:"close",auto:"emacs",catchall:function(a){}}}(),function(){function f(){c=""}function g(a){c+=a}function h(b){return function(c){a+=b}}function i(){var b=parseInt(a);return a="",b||1}function j(a){return typeof a=="string"&&(a=CodeMirror.commands[a]),function(b){for(var c=0,d=i();c0&&(e=a.length,f=0);var g=e,h=e;a:for(;b!=e;b+=c)for(var i=0;id?d:c,h=c>d?c:d;a.setCursor(f);for(var i=f;i<=h;i++)g("\n"+a.getLine(f)),a.removeLine(f)}function s(a,b){var c=e[b],d=a.getCursor().line,f=c>d?d:c,h=c>d?c:d;for(var i=f;i<=h;i++)g("\n"+a.getLine(i));a.setCursor(f)}var a="",b="f",c="",d=0,e=[],l=[/\w/,/[^\w\s]/],m=[/\S/],t=CodeMirror.keyMap.vim={0:function(b){a.length>0?h("0")(b):CodeMirror.commands.goLineStart(b)},A:function(a){i(),a.setCursor(a.getCursor().line,a.getCursor().ch+1,!0),a.setOption("keyMap","vim-insert"),q("vim-insert")},"Shift-A":function(a){i(),CodeMirror.commands.goLineEnd(a),a.setOption("keyMap","vim-insert"),q("vim-insert")},I:function(a){i(),a.setOption("keyMap","vim-insert"),q("vim-insert")},"Shift-I":function(a){i(),CodeMirror.commands.goLineStartSmart(a),a.setOption("keyMap","vim-insert"),q("vim-insert")},O:function(a){i(),CodeMirror.commands.goLineEnd(a),a.replaceSelection("\n","end"),a.setOption("keyMap","vim-insert"),q("vim-insert")},"Shift-O":function(a){i(),CodeMirror.commands.goLineStart(a),a.replaceSelection("\n","start"),a.setOption("keyMap","vim-insert"),q("vim-insert")},G:function(a){a.setOption("keyMap","vim-prefix-g")},D:function(a){a.setOption("keyMap","vim-prefix-d"),f()},M:function(a){a.setOption("keyMap","vim-prefix-m"),e=[]},Y:function(a){a.setOption("keyMap","vim-prefix-y"),f(),d=0},"/":function(a){var c=CodeMirror.commands.find;c&&c(a),b="f"},"Shift-/":function(a){var c=CodeMirror.commands.find;c&&(c(a),CodeMirror.commands.findPrev(a),b="r")},N:function(a){var c=CodeMirror.commands.findNext;c&&(b!="r"?c(a):CodeMirror.commands.findPrev(a))},"Shift-N":function(a){var c=CodeMirror.commands.findNext;c&&(b!="r"?CodeMirror.commands.findPrev(a):c.findNext(a))},"Shift-G":function(b){a==""?b.setCursor(b.lineCount()):b.setCursor(parseInt(a)-1),i(),CodeMirror.commands.goLineStart(b)},catchall:function(a){}};for(var u=1;u<10;++u)t[u]=h(u);k({H:"goColumnLeft",L:"goColumnRight",J:"goLineDown",K:"goLineUp",Left:"goColumnLeft",Right:"goColumnRight",Down:"goLineDown",Up:"goLineUp",Backspace:"goCharLeft",Space:"goCharRight",B:function(a){o(a,l,-1,"end")},E:function(a){o(a,l,1,"end")},W:function(a){o(a,l,1,"start")},"Shift-B":function(a){o(a,m,-1,"end")},"Shift-E":function(a){o(a,m,1,"end")},"Shift-W":function(a){o(a,m,1,"start")},X:function(a){CodeMirror.commands.delCharRight(a)},P:function(a){var b=a.getCursor().line;c!=""&&(CodeMirror.commands.goLineEnd(a),a.replaceSelection(c,"end")),a.setCursor(b+1)},"Shift-X":function(a){CodeMirror.commands.delCharLeft(a)},"Shift-J":function(a){p(a)},"Shift-`":function(a){var b=a.getCursor(),c=a.getRange({line:b.line,ch:b.ch},{line:b.line,ch:b.ch+1});c=c!=c.toLowerCase()?c.toLowerCase():c.toUpperCase(),a.replaceRange(c,{line:b.line,ch:b.ch},{line:b.line,ch:b.ch+1}),a.setCursor(b.line,b.ch+1)},"Ctrl-B":function(a){CodeMirror.commands.goPageUp(a)},"Ctrl-F":function(a){CodeMirror.commands.goPageDown(a)},"Ctrl-P":"goLineUp","Ctrl-N":"goLineDown",U:"undo","Ctrl-R":"redo","Shift-4":"goLineEnd"},function(a,b){t[a]=j(b)}),CodeMirror.keyMap["vim-prefix-g"]={E:j(function(a){o(a,l,-1,"start")}),"Shift-E":j(function(a){o(a,m,-1,"start")}),auto:"vim",catchall:function(a){}},CodeMirror.keyMap["vim-prefix-m"]={A:function(a){e.A=a.getCursor().line},"Shift-A":function(a){e["Shift-A"]=a.getCursor().line},B:function(a){e.B=a.getCursor().line},"Shift-B":function(a){e["Shift-B"]=a.getCursor().line},C:function(a){e.C=a.getCursor().line},"Shift-C":function(a){e["Shift-C"]=a.getCursor().line},D:function(a){e.D=a.getCursor().line},"Shift-D":function(a){e["Shift-D"]=a.getCursor().line},E:function(a){e.E=a.getCursor().line},"Shift-E":function(a){e["Shift-E"]=a.getCursor().line},F:function(a){e.F=a.getCursor().line},"Shift-F":function(a){e["Shift-F"]=a.getCursor().line},G:function(a){e.G=a.getCursor().line},"Shift-G":function(a){e["Shift-G"]=a.getCursor().line},H:function(a){e.H=a.getCursor().line},"Shift-H":function(a){e["Shift-H"]=a.getCursor().line},I:function(a){e.I=a.getCursor().line},"Shift-I":function(a){e["Shift-I"]=a.getCursor().line},J:function(a){e.J=a.getCursor().line},"Shift-J":function(a){e["Shift-J"]=a.getCursor().line},K:function(a){e.K=a.getCursor().line},"Shift-K":function(a){e["Shift-K"]=a.getCursor().line},L:function(a){e.L=a.getCursor().line},"Shift-L":function(a){e["Shift-L"]=a.getCursor().line},M:function(a){e.M=a.getCursor().line},"Shift-M":function(a){e["Shift-M"]=a.getCursor().line},N:function(a){e.N=a.getCursor().line},"Shift-N":function(a){e["Shift-N"]=a.getCursor().line},O:function(a){e.O=a.getCursor().line},"Shift-O":function(a){e["Shift-O"]=a.getCursor().line},P:function(a){e.P=a.getCursor().line},"Shift-P":function(a){e["Shift-P"]=a.getCursor().line},Q:function(a){e.Q=a.getCursor().line},"Shift-Q":function(a){e["Shift-Q"]=a.getCursor().line},R:function(a){e.R=a.getCursor().line},"Shift-R":function(a){e["Shift-R"]=a.getCursor().line},S:function(a){e.S=a.getCursor().line},"Shift-S":function(a){e["Shift-S"]=a.getCursor().line},T:function(a){e.T=a.getCursor().line},"Shift-T":function(a){e["Shift-T"]=a.getCursor().line},U:function(a){e.U=a.getCursor().line},"Shift-U":function(a){e["Shift-U"]=a.getCursor().line},V:function(a){e.V=a.getCursor().line},"Shift-V":function(a){e["Shift-V"]=a.getCursor().line},W:function(a){e.W=a.getCursor().line},"Shift-W":function(a){e["Shift-W"]=a.getCursor().line},X:function(a){e.X=a.getCursor().line},"Shift-X":function(a){e["Shift-X"]=a.getCursor().line},Y:function(a){e.Y=a.getCursor().line},"Shift-Y":function(a){e["Shift-Y"]=a.getCursor().line},Z:function(a){e.Z=a.getCursor().line},"Shift-Z":function(a){e["Shift-Z"]=a.getCursor().line},auto:"vim",catchall:function(a){}},CodeMirror.keyMap["vim-prefix-d"]={D:j(function(a){g("\n"+a.getLine(a.getCursor().line)),a.removeLine(a.getCursor().line)}),"'":function(a){a.setOption("keyMap","vim-prefix-d'"),f()},auto:"vim",catchall:function(a){}},CodeMirror.keyMap["vim-prefix-d'"]={A:function(a){r(a,"A")},"Shift-A":function(a){r(a,"Shift-A")},B:function(a){r(a,"B")},"Shift-B":function(a){r(a,"Shift-B")},C:function(a){r(a,"C")},"Shift-C":function(a){r(a,"Shift-C")},D:function(a){r(a,"D")},"Shift-D":function(a){r(a,"Shift-D")},E:function(a){r(a,"E")},"Shift-E":function(a){r(a,"Shift-E")},F:function(a){r(a,"F")},"Shift-F":function(a){r(a,"Shift-F")},G:function(a){r(a,"G")},"Shift-G":function(a){r(a,"Shift-G")},H:function(a){r(a,"H")},"Shift-H":function(a){r(a,"Shift-H")},I:function(a){r(a,"I")},"Shift-I":function(a){r(a,"Shift-I")},J:function(a){r(a,"J")},"Shift-J":function(a){r(a,"Shift-J")},K:function(a){r(a,"K")},"Shift-K":function(a){r(a,"Shift-K")},L:function(a){r(a,"L")},"Shift-L":function(a){r(a,"Shift-L")},M:function(a){r(a,"M")},"Shift-M":function(a){r(a,"Shift-M")},N:function(a){r(a,"N")},"Shift-N":function(a){r(a,"Shift-N")},O:function(a){r(a,"O")},"Shift-O":function(a){r(a,"Shift-O")},P:function(a){r(a,"P")},"Shift-P":function(a){r(a,"Shift-P")},Q:function(a){r(a,"Q")},"Shift-Q":function(a){r(a,"Shift-Q")},R:function(a){r(a,"R")},"Shift-R":function(a){r(a,"Shift-R")},S:function(a){r(a,"S")},"Shift-S":function(a){r(a,"Shift-S")},T:function(a){r(a,"T")},"Shift-T":function(a){r(a,"Shift-T")},U:function(a){r(a,"U")},"Shift-U":function(a){r(a,"Shift-U")},V:function(a){r(a,"V")},"Shift-V":function(a){r(a,"Shift-V")},W:function(a){r(a,"W")},"Shift-W":function(a){r(a,"Shift-W")},X:function(a){r(a,"X")},"Shift-X":function(a){r(a,"Shift-X")},Y:function(a){r(a,"Y")},"Shift-Y":function(a){r(a,"Shift-Y")},Z:function(a){r(a,"Z")},"Shift-Z":function(a){r(a,"Shift-Z")},auto:"vim",catchall:function(a){}},CodeMirror.keyMap["vim-prefix-y'"]={A:function(a){s(a,"A")},"Shift-A":function(a){s(a,"Shift-A")},B:function(a){s(a,"B")},"Shift-B":function(a){s(a,"Shift-B")},C:function(a){s(a,"C")},"Shift-C":function(a){s(a,"Shift-C")},D:function(a){s(a,"D")},"Shift-D":function(a){s(a,"Shift-D")},E:function(a){s(a,"E")},"Shift-E":function(a){s(a,"Shift-E")},F:function(a){s(a,"F")},"Shift-F":function(a){s(a,"Shift-F")},G:function(a){s(a,"G")},"Shift-G":function(a){s(a,"Shift-G")},H:function(a){s(a,"H")},"Shift-H":function(a){s(a,"Shift-H")},I:function(a){s(a,"I")},"Shift-I":function(a){s(a,"Shift-I")},J:function(a){s(a,"J")},"Shift-J":function(a){s(a,"Shift-J")},K:function(a){s(a,"K")},"Shift-K":function(a){s(a,"Shift-K")},L:function(a){s(a,"L")},"Shift-L":function(a){s(a,"Shift-L")},M:function(a){s(a,"M")},"Shift-M":function(a){s(a,"Shift-M")},N:function(a){s(a,"N")},"Shift-N":function(a){s(a,"Shift-N")},O:function(a){s(a,"O")},"Shift-O":function(a){s(a,"Shift-O")},P:function(a){s(a,"P")},"Shift-P":function(a){s(a,"Shift-P")},Q:function(a){s(a,"Q")},"Shift-Q":function(a){s(a,"Shift-Q")},R:function(a){s(a,"R")},"Shift-R":function(a){s(a,"Shift-R")},S:function(a){s(a,"S")},"Shift-S":function(a){s(a,"Shift-S")},T:function(a){s(a,"T")},"Shift-T":function(a){s(a,"Shift-T")},U:function(a){s(a,"U")},"Shift-U":function(a){s(a,"Shift-U")},V:function(a){s(a,"V")},"Shift-V":function(a){s(a,"Shift-V")},W:function(a){s(a,"W")},"Shift-W":function(a){s(a,"Shift-W")},X:function(a){s(a,"X")},"Shift-X":function(a){s(a,"Shift-X")},Y:function(a){s(a,"Y")},"Shift-Y":function(a){s(a,"Shift-Y")},Z:function(a){s(a,"Z")},"Shift-Z":function(a){s(a,"Shift-Z")},auto:"vim",catchall:function(a){}},CodeMirror.keyMap["vim-prefix-y"]={Y:j(function(a){g("\n"+a.getLine(a.getCursor().line+d)),d++}),"'":function(a){a.setOption("keyMap","vim-prefix-y'"),f()},auto:"vim",catchall:function(a){}},CodeMirror.keyMap["vim-insert"]={Esc:function(a){a.setCursor(a.getCursor().line,a.getCursor().ch-1,!0),a.setOption("keyMap","vim"),q("vim")},"Ctrl-N":function(a){},"Ctrl-P":function(a){},fallthrough:["default"]}}() \ No newline at end of file diff --git a/static/js/imageinput.js b/static/js/imageinput.js new file mode 100644 index 0000000000..5b4978ee11 --- /dev/null +++ b/static/js/imageinput.js @@ -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; +} diff --git a/static/js/jquery.ui.touch-punch.min.js b/static/js/jquery.ui.touch-punch.min.js new file mode 100644 index 0000000000..33d6f97e5e --- /dev/null +++ b/static/js/jquery.ui.touch-punch.min.js @@ -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); \ No newline at end of file diff --git a/templates/6002x-faq.html b/templates/6002x-faq.html index 73e04e8a7e..a80c6c2967 100644 --- a/templates/6002x-faq.html +++ b/templates/6002x-faq.html @@ -18,6 +18,14 @@ This set of questions and answers accompanies MIT’s February 13, 6.002x: Circuits and Electronics.

+

How do I register?

+ +

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.

+ +

Where can I find a list of courses available? When do the next classes begin?

+ +

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.

+

I tried to register for the course, but it says the username is already taken.

diff --git a/templates/accordion.html b/templates/accordion.html index a94f738d29..0804c79a2a 100644 --- a/templates/accordion.html +++ b/templates/accordion.html @@ -7,22 +7,14 @@ diff --git a/templates/accordion_init.js b/templates/accordion_init.js deleted file mode 100644 index 7c4ca2603c..0000000000 --- a/templates/accordion_init.js +++ /dev/null @@ -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); -}); diff --git a/templates/choicegroup.html b/templates/choicegroup.html new file mode 100644 index 0000000000..938ce5b535 --- /dev/null +++ b/templates/choicegroup.html @@ -0,0 +1,21 @@ +
+ + % for choice_id, choice_description in choices.items(): + + % endfor + + + % if state == 'unsubmitted': + + % elif state == 'correct': + + % elif state == 'incorrect': + + % elif state == 'incomplete': + + % endif +
diff --git a/templates/coffee/README.md b/templates/coffee/README.md new file mode 100644 index 0000000000..6c26529a5b --- /dev/null +++ b/templates/coffee/README.md @@ -0,0 +1,59 @@ +CoffeeScript +============ + +This folder contains the CoffeeScript file that will be compiled to the static +directory. By default, we're compile and merge all the files ending `.coffee` +into `static/js/application.js`. + +Install the Compiler +-------------------- + +CoffeeScript compiler are written in JavaScript. You'll need to install Node and +npm (Node Package Manager) to be able to install the CoffeeScript compiler. + +### Mac OS X + +Install Node via Homebrew, then use npm: + + brew install node + curl http://npmjs.org/install.sh | sh + npm install -g git://github.com/jashkenas/coffee-script.git + +(Note that we're using the edge version of CoffeeScript for now, as there was +some issue with directory watching in 1.3.1.) + +Try to run `coffee` and make sure you get a coffee prompt. + +### Debian/Ubuntu + +Conveniently, you can install Node via `apt-get`, then use npm: + + sudo apt-get install nodejs npm && + sudo npm install -g git://github.com/jashkenas/coffee-script.git + +Compiling +--------- + +Run this command in the `mitx` directory to easily make the compiler watch for +changes in your file, and join the result into `application.js`: + + coffee -j static/js/application.js -cw templates/coffee/src + +Please note that the compiler will not be able to detect the file that get added +after you've ran the command, so you'll need to restart the compiler if there's +a new CoffeeScript file. + +Testing +======= + +We're also using Jasmine to unit-testing the JavaScript files. All the specs are +written in CoffeeScript for the consistency. Because of the limitation of +`django-jasmine` plugin, we'll need to also running another compiler to compile +the test file. + +Using this command to compile the test files: + + coffee -cw templates/coffee/spec/*.coffee + +Then start the server in debug mode, navigate to http://127.0.0.1:8000/_jasmine +to see the test result. diff --git a/templates/coffee/files.json b/templates/coffee/files.json new file mode 100644 index 0000000000..bfae4dfe87 --- /dev/null +++ b/templates/coffee/files.json @@ -0,0 +1,10 @@ +{ + "js_files": [ + "/static/js/jquery-1.6.2.min.js", + "/static/js/jquery-ui-1.8.16.custom.min.js", + "/static/js/jquery.leanModal.js" + ], + "static_files": [ + "js/application.js" + ] +} diff --git a/templates/coffee/fixtures/accordion.html b/templates/coffee/fixtures/accordion.html new file mode 100644 index 0000000000..148c245c8f --- /dev/null +++ b/templates/coffee/fixtures/accordion.html @@ -0,0 +1,6 @@ +
+
+ close +
+
+
diff --git a/templates/coffee/fixtures/calculator.html b/templates/coffee/fixtures/calculator.html new file mode 100644 index 0000000000..61c6f5e153 --- /dev/null +++ b/templates/coffee/fixtures/calculator.html @@ -0,0 +1,18 @@ + diff --git a/templates/coffee/fixtures/feedback_form.html b/templates/coffee/fixtures/feedback_form.html new file mode 100644 index 0000000000..672663fe10 --- /dev/null +++ b/templates/coffee/fixtures/feedback_form.html @@ -0,0 +1,7 @@ +
+
+ + + +
+
diff --git a/templates/coffee/spec/calculator_spec.coffee b/templates/coffee/spec/calculator_spec.coffee new file mode 100644 index 0000000000..5c3fde5e2d --- /dev/null +++ b/templates/coffee/spec/calculator_spec.coffee @@ -0,0 +1,68 @@ +describe 'Calculator', -> + beforeEach -> + loadFixtures 'calculator.html' + @calculator = new Calculator + + describe 'bind', -> + beforeEach -> + Calculator.bind() + + it 'bind the calculator button', -> + expect($('.calc')).toHandleWith 'click', @calculator.toggle + + it 'bind the help button', -> + # These events are bind by $.hover() + expect($('div.help-wrapper a')).toHandleWith 'mouseenter', @calculator.helpToggle + expect($('div.help-wrapper a')).toHandleWith 'mouseleave', @calculator.helpToggle + + it 'prevent default behavior on help button', -> + $('div.help-wrapper a').click (e) -> + expect(e.isDefaultPrevented()).toBeTruthy() + $('div.help-wrapper a').click() + + it 'bind the calculator submit', -> + expect($('form#calculator')).toHandleWith 'submit', @calculator.calculate + + it 'prevent default behavior on form submit', -> + $('form#calculator').submit (e) -> + expect(e.isDefaultPrevented()).toBeTruthy() + e.preventDefault() + $('form#calculator').submit() + + describe 'toggle', -> + it 'toggle the calculator and focus the input', -> + spyOn $.fn, 'focus' + @calculator.toggle() + + expect($('li.calc-main')).toHaveClass('open') + expect($('#calculator_wrapper #calculator_input').focus).toHaveBeenCalled() + + it 'toggle the close button on the calculator button', -> + @calculator.toggle() + expect($('.calc')).toHaveClass('closed') + + @calculator.toggle() + expect($('.calc')).not.toHaveClass('closed') + + describe 'helpToggle', -> + it 'toggle the help overlay', -> + @calculator.helpToggle() + expect($('.help')).toHaveClass('shown') + + @calculator.helpToggle() + expect($('.help')).not.toHaveClass('shown') + + describe 'calculate', -> + beforeEach -> + $('#calculator_input').val '1+2' + spyOn($, 'getJSON').andCallFake (url, data, callback) -> + callback({ result: 3 }) + @calculator.calculate() + + it 'send data to /calculate', -> + expect($.getJSON).toHaveBeenCalledWith '/calculate', + equation: '1+2' + , jasmine.any(Function) + + it 'update the calculator output', -> + expect($('#calculator_output').val()).toEqual('3') diff --git a/templates/coffee/spec/calculator_spec.js b/templates/coffee/spec/calculator_spec.js new file mode 100644 index 0000000000..6e0f8a0dab --- /dev/null +++ b/templates/coffee/spec/calculator_spec.js @@ -0,0 +1,80 @@ +// Generated by CoffeeScript 1.3.2-pre +(function() { + + describe('Calculator', function() { + beforeEach(function() { + loadFixtures('calculator.html'); + return this.calculator = new Calculator; + }); + describe('bind', function() { + beforeEach(function() { + return Calculator.bind(); + }); + it('bind the calculator button', function() { + return expect($('.calc')).toHandleWith('click', this.calculator.toggle); + }); + it('bind the help button', function() { + expect($('div.help-wrapper a')).toHandleWith('mouseenter', this.calculator.helpToggle); + return expect($('div.help-wrapper a')).toHandleWith('mouseleave', this.calculator.helpToggle); + }); + it('prevent default behavior on help button', function() { + $('div.help-wrapper a').click(function(e) { + return expect(e.isDefaultPrevented()).toBeTruthy(); + }); + return $('div.help-wrapper a').click(); + }); + it('bind the calculator submit', function() { + return expect($('form#calculator')).toHandleWith('submit', this.calculator.calculate); + }); + return it('prevent default behavior on form submit', function() { + $('form#calculator').submit(function(e) { + expect(e.isDefaultPrevented()).toBeTruthy(); + return e.preventDefault(); + }); + return $('form#calculator').submit(); + }); + }); + describe('toggle', function() { + it('toggle the calculator and focus the input', function() { + spyOn($.fn, 'focus'); + this.calculator.toggle(); + expect($('li.calc-main')).toHaveClass('open'); + return expect($('#calculator_wrapper #calculator_input').focus).toHaveBeenCalled(); + }); + return it('toggle the close button on the calculator button', function() { + this.calculator.toggle(); + expect($('.calc')).toHaveClass('closed'); + this.calculator.toggle(); + return expect($('.calc')).not.toHaveClass('closed'); + }); + }); + describe('helpToggle', function() { + return it('toggle the help overlay', function() { + this.calculator.helpToggle(); + expect($('.help')).toHaveClass('shown'); + this.calculator.helpToggle(); + return expect($('.help')).not.toHaveClass('shown'); + }); + }); + return describe('calculate', function() { + beforeEach(function() { + $('#calculator_input').val('1+2'); + spyOn($, 'getJSON').andCallFake(function(url, data, callback) { + return callback({ + result: 3 + }); + }); + return this.calculator.calculate(); + }); + it('send data to /calculate', function() { + return expect($.getJSON).toHaveBeenCalledWith('/calculate', { + equation: '1+2' + }, jasmine.any(Function)); + }); + return it('update the calculator output', function() { + return expect($('#calculator_output').val()).toEqual('3'); + }); + }); + }); + +}).call(this); diff --git a/templates/coffee/spec/courseware_spec.coffee b/templates/coffee/spec/courseware_spec.coffee new file mode 100644 index 0000000000..5933e3e686 --- /dev/null +++ b/templates/coffee/spec/courseware_spec.coffee @@ -0,0 +1,77 @@ +describe 'Courseware', -> + describe 'bind', -> + it 'bind the navigation', -> + spyOn Courseware.Navigation, 'bind' + Courseware.bind() + expect(Courseware.Navigation.bind).toHaveBeenCalled() + + describe 'Navigation', -> + beforeEach -> + loadFixtures 'accordion.html' + @navigation = new Courseware.Navigation + + describe 'bind', -> + describe 'when the #accordion exists', -> + describe 'when there is an active section', -> + it 'activate the accordion with correct active section', -> + spyOn $.fn, 'accordion' + $('#accordion').append('
') + Courseware.Navigation.bind() + expect($('#accordion').accordion).toHaveBeenCalledWith + active: 1 + header: 'h3' + autoHeight: false + + describe 'when there is no active section', -> + it 'activate the accordian with section 1 as active', -> + spyOn $.fn, 'accordion' + $('#accordion').append('
') + Courseware.Navigation.bind() + expect($('#accordion').accordion).toHaveBeenCalledWith + active: 1 + header: 'h3' + autoHeight: false + + it 'binds the accordionchange event', -> + Courseware.Navigation.bind() + expect($('#accordion')).toHandleWith 'accordionchange', @navigation.log + + it 'bind the navigation toggle', -> + Courseware.Navigation.bind() + expect($('#open_close_accordion a')).toHandleWith 'click', @navigation.toggle + + describe 'when the #accordion does not exists', -> + beforeEach -> + $('#accordion').remove() + + it 'does not activate the accordion', -> + spyOn $.fn, 'accordion' + Courseware.Navigation.bind() + expect($('#accordion').accordion).wasNotCalled() + + describe 'toggle', -> + it 'toggle closed class on the wrapper', -> + $('.course-wrapper').removeClass('closed') + + @navigation.toggle() + expect($('.course-wrapper')).toHaveClass('closed') + + @navigation.toggle() + expect($('.course-wrapper')).not.toHaveClass('closed') + + describe 'log', -> + beforeEach -> + window.log_event = -> + spyOn window, 'log_event' + + it 'submit event log', -> + @navigation.log {}, { + newHeader: + text: -> "new" + oldHeader: + text: -> "old" + } + + expect(window.log_event).toHaveBeenCalledWith 'accordion', + newheader: 'new' + oldheader: 'old' diff --git a/templates/coffee/spec/courseware_spec.js b/templates/coffee/spec/courseware_spec.js new file mode 100644 index 0000000000..7070fa0028 --- /dev/null +++ b/templates/coffee/spec/courseware_spec.js @@ -0,0 +1,99 @@ +// Generated by CoffeeScript 1.3.2-pre +(function() { + + describe('Courseware', function() { + describe('bind', function() { + return it('bind the navigation', function() { + spyOn(Courseware.Navigation, 'bind'); + Courseware.bind(); + return expect(Courseware.Navigation.bind).toHaveBeenCalled(); + }); + }); + return describe('Navigation', function() { + beforeEach(function() { + loadFixtures('accordion.html'); + return this.navigation = new Courseware.Navigation; + }); + describe('bind', function() { + describe('when the #accordion exists', function() { + describe('when there is an active section', function() { + return it('activate the accordion with correct active section', function() { + spyOn($.fn, 'accordion'); + $('#accordion').append('
'); + Courseware.Navigation.bind(); + return expect($('#accordion').accordion).toHaveBeenCalledWith({ + active: 1, + header: 'h3', + autoHeight: false + }); + }); + }); + describe('when there is no active section', function() { + return it('activate the accordian with section 1 as active', function() { + spyOn($.fn, 'accordion'); + $('#accordion').append('
'); + Courseware.Navigation.bind(); + return expect($('#accordion').accordion).toHaveBeenCalledWith({ + active: 1, + header: 'h3', + autoHeight: false + }); + }); + }); + it('binds the accordionchange event', function() { + Courseware.Navigation.bind(); + return expect($('#accordion')).toHandleWith('accordionchange', this.navigation.log); + }); + return it('bind the navigation toggle', function() { + Courseware.Navigation.bind(); + return expect($('#open_close_accordion a')).toHandleWith('click', this.navigation.toggle); + }); + }); + return describe('when the #accordion does not exists', function() { + beforeEach(function() { + return $('#accordion').remove(); + }); + return it('does not activate the accordion', function() { + spyOn($.fn, 'accordion'); + Courseware.Navigation.bind(); + return expect($('#accordion').accordion).wasNotCalled(); + }); + }); + }); + describe('toggle', function() { + return it('toggle closed class on the wrapper', function() { + $('.course-wrapper').removeClass('closed'); + this.navigation.toggle(); + expect($('.course-wrapper')).toHaveClass('closed'); + this.navigation.toggle(); + return expect($('.course-wrapper')).not.toHaveClass('closed'); + }); + }); + return describe('log', function() { + beforeEach(function() { + window.log_event = function() {}; + return spyOn(window, 'log_event'); + }); + return it('submit event log', function() { + this.navigation.log({}, { + newHeader: { + text: function() { + return "new"; + } + }, + oldHeader: { + text: function() { + return "old"; + } + } + }); + return expect(window.log_event).toHaveBeenCalledWith('accordion', { + newheader: 'new', + oldheader: 'old' + }); + }); + }); + }); + }); + +}).call(this); diff --git a/templates/coffee/spec/feedback_form_spec.coffee b/templates/coffee/spec/feedback_form_spec.coffee new file mode 100644 index 0000000000..191645b3d3 --- /dev/null +++ b/templates/coffee/spec/feedback_form_spec.coffee @@ -0,0 +1,28 @@ +describe 'FeedbackForm', -> + beforeEach -> + loadFixtures 'feedback_form.html' + + describe 'bind', -> + beforeEach -> + FeedbackForm.bind() + spyOn($, 'post').andCallFake (url, data, callback, format) -> + callback() + + it 'binds to the #feedback_button', -> + expect($('#feedback_button')).toHandle 'click' + + it 'post data to /send_feedback on click', -> + $('#feedback_subject').val 'Awesome!' + $('#feedback_message').val 'This site is really good.' + $('#feedback_button').click() + + expect($.post).toHaveBeenCalledWith '/send_feedback', { + subject: 'Awesome!' + message: 'This site is really good.' + url: window.location.href + }, jasmine.any(Function), 'json' + + it 'replace the form with a thank you message', -> + $('#feedback_button').click() + + expect($('#feedback_div').html()).toEqual 'Feedback submitted. Thank you' diff --git a/templates/coffee/spec/feedback_form_spec.js b/templates/coffee/spec/feedback_form_spec.js new file mode 100644 index 0000000000..2815cd73e5 --- /dev/null +++ b/templates/coffee/spec/feedback_form_spec.js @@ -0,0 +1,35 @@ +// Generated by CoffeeScript 1.3.2-pre +(function() { + + describe('FeedbackForm', function() { + beforeEach(function() { + return loadFixtures('feedback_form.html'); + }); + return describe('bind', function() { + beforeEach(function() { + FeedbackForm.bind(); + return spyOn($, 'post').andCallFake(function(url, data, callback, format) { + return callback(); + }); + }); + it('binds to the #feedback_button', function() { + return expect($('#feedback_button')).toHandle('click'); + }); + it('post data to /send_feedback on click', function() { + $('#feedback_subject').val('Awesome!'); + $('#feedback_message').val('This site is really good.'); + $('#feedback_button').click(); + return expect($.post).toHaveBeenCalledWith('/send_feedback', { + subject: 'Awesome!', + message: 'This site is really good.', + url: window.location.href + }, jasmine.any(Function), 'json'); + }); + return it('replace the form with a thank you message', function() { + $('#feedback_button').click(); + return expect($('#feedback_div').html()).toEqual('Feedback submitted. Thank you'); + }); + }); + }); + +}).call(this); diff --git a/templates/coffee/spec/helper.coffee b/templates/coffee/spec/helper.coffee new file mode 100644 index 0000000000..1f27e257c2 --- /dev/null +++ b/templates/coffee/spec/helper.coffee @@ -0,0 +1 @@ +jasmine.getFixtures().fixturesPath = "/_jasmine/fixtures/" diff --git a/templates/coffee/spec/helper.js b/templates/coffee/spec/helper.js new file mode 100644 index 0000000000..23b980d525 --- /dev/null +++ b/templates/coffee/spec/helper.js @@ -0,0 +1,6 @@ +// Generated by CoffeeScript 1.3.2-pre +(function() { + + jasmine.getFixtures().fixturesPath = "/_jasmine/fixtures/"; + +}).call(this); diff --git a/templates/coffee/src/calculator.coffee b/templates/coffee/src/calculator.coffee new file mode 100644 index 0000000000..7d62f5a794 --- /dev/null +++ b/templates/coffee/src/calculator.coffee @@ -0,0 +1,20 @@ +class window.Calculator + @bind: -> + calculator = new Calculator + $('.calc').click calculator.toggle + $('form#calculator').submit(calculator.calculate).submit (e) -> + e.preventDefault() + $('div.help-wrapper a').hover(calculator.helpToggle).click (e) -> + e.preventDefault() + + toggle: -> + $('li.calc-main').toggleClass 'open' + $('#calculator_wrapper #calculator_input').focus() + $('.calc').toggleClass 'closed' + + helpToggle: -> + $('.help').toggleClass 'shown' + + calculate: -> + $.getJSON '/calculate', { equation: $('#calculator_input').val() }, (data) -> + $('#calculator_output').val(data.result) diff --git a/templates/coffee/src/courseware.coffee b/templates/coffee/src/courseware.coffee new file mode 100644 index 0000000000..a4c93ec0b4 --- /dev/null +++ b/templates/coffee/src/courseware.coffee @@ -0,0 +1,22 @@ +class window.Courseware + @bind: -> + @Navigation.bind() + + class @Navigation + @bind: -> + if $('#accordion').length + navigation = new Navigation + active = $('#accordion ul:has(li.active)').index('#accordion ul') + $('#accordion').bind('accordionchange', navigation.log).accordion + active: if active >= 0 then active else 1 + header: 'h3' + autoHeight: false + $('#open_close_accordion a').click navigation.toggle + + log: (event, ui) -> + log_event 'accordion', + newheader: ui.newHeader.text() + oldheader: ui.oldHeader.text() + + toggle: -> + $('.course-wrapper').toggleClass('closed') diff --git a/templates/coffee/src/feedback_form.coffee b/templates/coffee/src/feedback_form.coffee new file mode 100644 index 0000000000..bbb5c09365 --- /dev/null +++ b/templates/coffee/src/feedback_form.coffee @@ -0,0 +1,10 @@ +class window.FeedbackForm + @bind: -> + $('#feedback_button').click -> + data = + subject: $('#feedback_subject').val() + message: $('#feedback_message').val() + url: window.location.href + $.post '/send_feedback', data, -> + $('#feedback_div').html 'Feedback submitted. Thank you' + ,'json' diff --git a/templates/coffee/src/main.coffee b/templates/coffee/src/main.coffee new file mode 100644 index 0000000000..8a9a892f94 --- /dev/null +++ b/templates/coffee/src/main.coffee @@ -0,0 +1,8 @@ +$ -> + $.ajaxSetup + headers : { 'X-CSRFToken': $.cookie 'csrftoken' } + + Calculator.bind() + Courseware.bind() + FeedbackForm.bind() + $("a[rel*=leanModal]").leanModal() diff --git a/templates/courseware.html b/templates/courseware.html index ec4be8c6c4..050172626a 100644 --- a/templates/courseware.html +++ b/templates/courseware.html @@ -1,15 +1,17 @@ <%inherit file="main.html" /> +<%block name="bodyclass">courseware <%block name="title">Courseware – MITx 6.002x +<%block name="headextra"> + + + <%block name="js_extra"> +##Is there a reason this isn't in header_extra? Is it important that the javascript is at the bottom of the generated document? @@ -25,7 +27,9 @@
- ${accordion} +
diff --git a/templates/create_account.html b/templates/create_account.html index 3eee7a6bc1..318a694658 100644 --- a/templates/create_account.html +++ b/templates/create_account.html @@ -6,9 +6,7 @@

Enrollment requires a modern web browser with JavaScript enabled. You don't have this. You can’t enroll without upgrading, since you couldn’t take the course without upgrading. Feel free to download the latest version of Mozilla Firefox or Google Chrome, for free, to enroll and take this course.

- Please note that 6.002x has already started. - Several assignment due dates for 6.002x have already passed. It is now impossible for newly enrolled students to get 100% of the points in the course, although new students can still earn points for assignments whose due dates have not passed, and students have access to all of the course material that has been released for the course. -

+Please note that 6.002x has now passed its half-way point. The midterm exam and several assignment due dates for 6.002x have already passed. It is now impossible for newly enrolled students to earn a passing grade and a completion certificate for the course. However, new students have access to all of the course material that has been released for the course, so you are welcome to enroll and browse the course.

<% if 'error' in locals(): e = error %> diff --git a/templates/emails/welcome_body.txt b/templates/emails/welcome_body.txt index b08326f6fd..3944f6c325 100644 --- a/templates/emails/welcome_body.txt +++ b/templates/emails/welcome_body.txt @@ -1,4 +1,4 @@ -MITx's prototype offering, 6.002x, is now open. To log in, visit +MITx's prototype offering, 6.002x, is open. To log in, visit % if is_secure: https://6002x.mitx.mit.edu @@ -16,7 +16,7 @@ place to reset it. Once you log in, we recommend that you start the course by reviewing the "System Usage Sequence" in the Overview section, and the "6.002x At-a-Glance (Calendar)" handout under the Course Info tab. After you -familiarize yourself with the various features of the MITx platform, +familiarize yourself with the features of the MITx platform, you can jump right into the coursework by working on "Administrivia and Circuit Elements", the first Lecture Sequence in Week 1. diff --git a/templates/gradebook.html b/templates/gradebook.html index 459f1e9217..c1d54cd084 100644 --- a/templates/gradebook.html +++ b/templates/gradebook.html @@ -9,7 +9,8 @@ @@ -30,16 +31,10 @@ Student - %for section in templateSummary: - %if 'subscores' in section: - %for subsection in section['subscores']: - ${subsection['label']} - %endfor - ${section['totallabel']} - %else: - ${section['category']} - %endif + %for section in templateSummary['section_breakdown']: + ${section['label']} %endfor + Total <%def name="percent_data(percentage)"> @@ -51,6 +46,8 @@ data_class = "grade_b" elif percentage > .6: data_class = "grade_c" + elif percentage > 0: + data_class = "grade_f" %> ${ "{0:.0%}".format( percentage ) } @@ -58,16 +55,10 @@ %for student in students: ${student['username']} - %for section in student['grade_info']['grade_summary']: - %if 'subscores' in section: - %for subsection in section['subscores']: - ${percent_data( subsection['percentage'] )} - %endfor - ${percent_data( section['totalscore'] )} - %else: - ${percent_data( section['totalscore'] )} - %endif + %for section in student['grade_info']['grade_summary']['section_breakdown']: + ${percent_data( section['percent'] )} %endfor + ${percent_data( student['grade_info']['grade_summary']['percent'])} %endfor diff --git a/templates/imageinput.html b/templates/imageinput.html new file mode 100644 index 0000000000..ceda98ee8f --- /dev/null +++ b/templates/imageinput.html @@ -0,0 +1,16 @@ + + +
+ +
+ + % if state == 'unsubmitted': + + % elif state == 'correct': + + % elif state == 'incorrect': + + % elif state == 'incomplete': + + % endif +
diff --git a/templates/index.html b/templates/index.html index f3f399f6d7..dee9f14848 100644 --- a/templates/index.html +++ b/templates/index.html @@ -10,9 +10,9 @@

Circuits & Electronics

6.002x

- Enroll in 6.002x Circuits & Electronics + View 6.002x Circuits & Electronics as a guest
-

6.002x (Circuits and Electronics) is an experimental on-line adaptation of MIT’s first undergraduate analog design course: 6.002. This course will run, free of charge, for students worldwide from March 5, 2012 through June 8, 2012.

+

6.002x (Circuits and Electronics) is an experimental on-line adaptation of MIT’s first undergraduate analog design course: 6.002. This course is running, free of charge, for students worldwide from March 5, 2012 through June 8, 2012.

@@ -52,7 +52,7 @@
- Enroll in 6.002x Circuits & Electronics + View 6.002x Circuits & Electronics as a guest
diff --git a/templates/info.html b/templates/info.html index 3cc93e72d7..8c914f9f60 100644 --- a/templates/info.html +++ b/templates/info.html @@ -23,11 +23,20 @@ $(document).ready(function(){
+ % if user.is_authenticated():
<%include file="updates.html" />
<%include file="handouts.html" />
+ % else: +
+ <%include file="guest_updates.html" /> +
+
+ <%include file="guest_handouts.html" /> +
+ % endif
diff --git a/templates/jstextline.html b/templates/jstextline.html new file mode 100644 index 0000000000..a062252392 --- /dev/null +++ b/templates/jstextline.html @@ -0,0 +1,34 @@ +
+ + + % if dojs == 'math': + `{::}` + % endif + + + + % if dojs == 'math': + + % endif + + % if state == 'unsubmitted': + + % elif state == 'correct': + + % elif state == 'incorrect': + + % elif state == 'incomplete': + + % endif + % if msg: +
+ ${msg|n} + % endif +
diff --git a/templates/main.html b/templates/main.html index 10532e601e..249e2793f1 100644 --- a/templates/main.html +++ b/templates/main.html @@ -10,6 +10,7 @@ + @@ -88,25 +89,25 @@
  • Help
  • -
  • Log out
  • + % if user.is_authenticated(): +
  • Log out
  • + % endif -
    -

    Feedback for MITx

    -

    Found a bug? Got an idea for improving our system? Let us know.

    - - -
      -
    1. -
    2. -
    3. -
    - - -
    +
    +

    Feedback for MITx

    +

    Found a bug? Got an idea for improving our system? Let us know.

    +
    +
      +
    1. +
    2. +
    3. +
    +
    +
    @@ -115,57 +116,7 @@ - - - -<%block name="js_extra"/> + <%block name="js_extra"/> diff --git a/templates/marketing.html b/templates/marketing.html index 8c9458fd34..505703d2e8 100644 --- a/templates/marketing.html +++ b/templates/marketing.html @@ -1,59 +1,58 @@ <%namespace name='static' file='static_content.html'/> - - <%block name="title">MITx: MIT's new online learning initiative</%block> + + <%block name="title">MITx: MIT's new online learning initiative</%block> - MITx will offer a portfolio of MIT courses for free to a virtual community of learners around the world" /> + MITx will offer a portfolio of MIT courses for free to a virtual community of learners around the world" /> - MITx, online learning, MIT, online laboratory, education, learners, undergraduate, certificate" /> + MITx, online learning, MIT, online laboratory, education, learners, undergraduate, certificate" /> - - <%static:css group='marketing'/> - - + + <%static:css group='marketing'/> + + - - + + - - - - - + + + + + - + + <%block name="headextra"/> -<%block name="headextra"/> - - @@ -62,25 +61,25 @@ function postJSON(url, data, callback) { <%block name="header">
    "> -
    +
    <%block name="header_nav"> <%block name="header_text"> -
    -

    MITx

    -

    MIT’s new online learning initiative

    -
    - -
    +
    +

    MITx

    +

    MIT’s new online learning initiative

    +
    + +
    @@ -88,95 +87,95 @@ function postJSON(url, data, callback) { <%block name="bodyextra"/> -% if settings.COURSEWARE_ENABLED: + % if settings.COURSEWARE_ENABLED:
    <%include file="login.html" />
    -% endif + % endif
    <%include file="password_reset_form.html" />
    - + -<%block name="js_extra"/> + <%block name="js_extra"/> diff --git a/templates/mathstring.html b/templates/mathstring.html new file mode 100644 index 0000000000..acd45ff4c3 --- /dev/null +++ b/templates/mathstring.html @@ -0,0 +1,8 @@ +
    + % if isinline: + [mathjaxinline]${mathstr}[/mathjaxinline] + % else: + [mathjax]${mathstr}[/mathjax] + % endif + ${tail} +
    diff --git a/templates/mitx_global.html b/templates/mitx_global.html index a9242f2300..bd3f42ba5b 100644 --- a/templates/mitx_global.html +++ b/templates/mitx_global.html @@ -9,7 +9,7 @@

    MITx will offer a portfolio of MIT courses for free to a virtual community of learners around the world. It will also enhance the educational experience of its on-campus students, offering them online tools that supplement and enrich their classroom and laboratory experiences.

    -

    The first MITx course, 6.002x (Circuits and Electronics), will be launched in an experimental prototype form. Watch this space for further upcoming courses, which will become available in Fall 2012.

    +

    The first MITx course, 6.002x (Circuits and Electronics), was launched in an experimental prototype form. Watch this space for further upcoming courses, which will become available in Fall 2012.

    @@ -34,17 +34,29 @@
    -
    -

    Spring 2012 Course offering

    -

    Circuits and Electronics

    -

    6.002x

    -
    +
    +

    Announcement

    + +

    + On May 2, it was announced that Harvard University will join MIT as a partner in edX. MITx, which offers online versions of MIT courses, will be a core offering of edX, as will Harvardx, a set of course offerings from Harvard. +

    + +

    + Read more details here +

    +
    + +
    +

    Spring 2012 Course offering

    +

    Circuits and Electronics

    +

    6.002x

    +

    More information & Enroll

    -

    Taught by Anant Agarwal, with Gerald Sussman and Piotr Mitros, 6.002x (Circuits and Electronics) is an on-line adaption of 6.002, MIT’s first undergraduate analog design course. This prototype course will run, free of charge, for students worldwide from March 5, 2012 through June 8, 2012. Students will be given the opportunity to demonstrate their mastery of the material and earn a certificate from MITx.

    +

    Taught by Anant Agarwal, with Gerald Sussman and Piotr Mitros, 6.002x (Circuits and Electronics) is an on-line adaption of 6.002, MIT’s first undergraduate analog design course. This prototype course is running, free of charge, for students worldwide from March 5, 2012 through June 8, 2012. Students are given the opportunity to demonstrate their mastery of the material and earn a certificate from MITx.

    diff --git a/templates/mitxhome.html b/templates/mitxhome.html new file mode 100644 index 0000000000..af821c1c84 --- /dev/null +++ b/templates/mitxhome.html @@ -0,0 +1,37 @@ +<%inherit file="main.html" /> + +<%block name="js_extra"> + + + +<%block name="title">MITx Home + +<%include file="navigation.html" args="active_page='info'" /> + +
    +
    +
    +

    Welcome to MITx

    +
    +

    Courses available:

    + +
    +
    +
    diff --git a/templates/navigation.html b/templates/navigation.html index 345e6eaa3c..b134b26856 100644 --- a/templates/navigation.html +++ b/templates/navigation.html @@ -2,18 +2,28 @@
    -

    MITx

    -

    Circuits and Electronics

    +

    + % if settings.ENABLE_MULTICOURSE: + MITx + % else: + MITx + % endif +

    +

    ${ settings.COURSE_TITLE }

    diff --git a/templates/problem.js b/templates/problem.js index 547a1f0e81..80924a79a6 100644 --- a/templates/problem.js +++ b/templates/problem.js @@ -1,68 +1,95 @@ -function ${ id }_load() { - $('#main_${ id }').load('${ ajax_url }problem_get?id=${ id }', - function() { +function ${ id }_content_updated() { MathJax.Hub.Queue(["Typeset",MathJax.Hub]); update_schematics(); - $('#check_${ id }').click(function() { + $('#check_${ id }').unbind('click').click(function() { $("input.schematic").each(function(index,element){ element.schematic.update_value(); }); + $(".CodeMirror").each(function(index,element){ if (element.CodeMirror.save) element.CodeMirror.save(); }); var submit_data={}; $.each($("[id^=input_${ id }_]"), function(index,value){ - submit_data[value.id]=value.value; + if (value.type==="checkbox"){ + if (value.checked) { + if (typeof submit_data[value.name] == 'undefined'){ + submit_data[value.name]=[]; + } + submit_data[value.name].push(value.value); + } + } + if (value.type==="radio"){ + if (value.checked) { + submit_data[value.name]= value.value; + } + } + else{ + submit_data[value.id]=value.value; + } }); - postJSON('/modx/problem/${ id }/problem_check', - submit_data, - function(json) { - switch(json.success) { - case 'incorrect': // Worked, but answer not - case 'correct': - ${ id }_load(); - //alert("!!"+json.success); + postJSON('${ MITX_ROOT_URL }/modx/problem/${ id }/problem_check', + submit_data, + function(json) { + switch(json.success) { + case 'incorrect': // Worked, but answer not + case 'correct': + $('#main_${ id }').html(json.contents); + ${ id }_content_updated(); break; - default: - alert(json.success); - } - }); + default: + alert(json.success); + }} + ); log_event('problem_check', submit_data); }); - $('#reset_${ id }').click(function() { + $('#reset_${ id }').unbind('click').click(function() { var submit_data={}; $.each($("[id^=input_${ id }_]"), function(index,value){ submit_data[value.id]=value.value; }); - postJSON('/modx/problem/${ id }/problem_reset', {'id':'${ id }'}, function(json) { - ${ id }_load(); + postJSON('${ MITX_ROOT_URL }/modx/problem/${ id }/problem_reset', {'id':'${ id }'}, function(html_as_json) { + $('#main_${ id }').html(html_as_json); + ${ id }_content_updated(); }); log_event('problem_reset', submit_data); }); - $('#show_${ id }').click(function() { - postJSON('/modx/problem/${ id }/problem_show', {}, function(data) { + // show answer button + // TODO: the button should turn into "hide answer" afterwards + $('#show_${ id }').unbind('click').click(function() { + postJSON('${ MITX_ROOT_URL }/modx/problem/${ id }/problem_show', {}, function(data) { for (var key in data) { - $("#answer_"+key).text(data[key]); + if ($.isArray(data[key])){ + for (var ans_index in data[key]){ + var choice_id = 'input_'+key+'_'+data[key][ans_index]; + $("label[for="+choice_id+"]").attr("correct_answer", "true"); + } + } + $("#answer_"+key).text(data[key]); } }); - log_event('problem_show', {'problem':'${ id }'}); -}); + log_event('problem_show', {'problem':'${ id }'}); + }); -$('#save_${ id }').click(function() { - $("input.schematic").each(function(index,element){ element.schematic.update_value(); }); - var submit_data={}; - $.each($("[id^=input_${ id }_]"), function(index,value){ - submit_data[value.id]=value.value;}); - postJSON('/modx/problem/${ id }/problem_save', - submit_data, function(data){ - if(data.success) { - alert('Saved'); - }} - ); + $('#save_${ id }').unbind('click').click(function() { + $("input.schematic").each(function(index,element){ element.schematic.update_value(); }); + var submit_data={}; + $.each($("[id^=input_${ id }_]"), function(index,value) { + submit_data[value.id]=value.value; + }); + postJSON('${ MITX_ROOT_URL }/modx/problem/${ id }/problem_save', + submit_data, + function(data) { + if(data.success) { + alert('Saved'); + }}); log_event('problem_save', submit_data); }); } -);} + +function ${ id }_load() { + $('#main_${ id }').load('${ ajax_url }problem_get?id=${ id }', ${ id }_content_updated); +} $(function() { ${ id }_load(); diff --git a/templates/profile.html b/templates/profile.html index 971e57ef63..2ae61a1337 100644 --- a/templates/profile.html +++ b/templates/profile.html @@ -151,11 +151,11 @@ $(function() { <% earned = section['section_total'].earned total = section['section_total'].possible - percentageString = "{0:.0%}".format( float(earned)/total) if earned > 0 else "" + percentageString = "{0:.0%}".format( float(earned)/total) if earned > 0 and total > 0 else "" %>

    - ${ section['section'] } ${"({0:g}/{1:g}) {2}".format( earned, total, percentageString )}

    + ${ section['section'] } ${"({0:.3n}/{1:.3n}) {2}".format( float(earned), float(total), percentageString )} ${section['subtitle']} %if 'due' in section and section['due']!="": due ${section['due']} @@ -165,7 +165,7 @@ $(function() {
      ${ "Problem Scores: " if section['graded'] else "Practice Scores: "} %for score in section['scores']: -
    1. ${"{0:g}/{1:g}".format(score.earned,score.possible)}
    2. +
    3. ${"{0:.3n}/{1:.3n}".format(float(score.earned),float(score.possible))}
    4. %endfor
    %endif diff --git a/templates/profile_graphs.js b/templates/profile_graphs.js index b34a5d1636..58dbeb8ed9 100644 --- a/templates/profile_graphs.js +++ b/templates/profile_graphs.js @@ -9,7 +9,7 @@ $(function () { position: 'absolute', display: 'none', top: y + 5, - left: x + 5, + left: x + 15, border: '1px solid #000', padding: '4px 6px', color: '#fff', @@ -19,96 +19,81 @@ $(function () { } /* -------------------------------- Grade detail bars -------------------------------- */ - + <% colors = ["#b72121", "#600101", "#666666", "#333333"] - + categories = {} + tickIndex = 1 - sectionSpacer = 0.5 + sectionSpacer = 0.25 sectionIndex = 0 - series = [] ticks = [] #These are the indices and x-axis labels for the data bottomTicks = [] #Labels on the bottom detail_tooltips = {} #This an dictionary mapping from 'section' -> array of detail_tooltips - droppedScores = [] #These are the datapoints to indicate assignments which aren't factored into the total score + droppedScores = [] #These are the datapoints to indicate assignments which are not factored into the total score dropped_score_tooltips = [] - for section in grade_summary: - if 'subscores' in section: ##This is for sections like labs or homeworks, with several smaller components and a total - series.append({ - 'label' : section['category'], - 'data' : [[i + tickIndex, score['percentage']] for i,score in enumerate(section['subscores'])], - 'color' : colors[sectionIndex] - }) - - ticks += [[i + tickIndex, score['label'] ] for i,score in enumerate(section['subscores'])] - bottomTicks.append( [tickIndex + len(section['subscores'])/2, section['category']] ) - detail_tooltips[ section['category'] ] = [score['summary'] for score in section['subscores']] - - droppedScores += [[tickIndex + index, 0.05] for index in section['dropped_indices']] - - dropExplanation = "The lowest {0} {1} scores are dropped".format( len(section['dropped_indices']), section['category'] ) - dropped_score_tooltips += [dropExplanation] * len(section['dropped_indices']) - - - tickIndex += len(section['subscores']) + sectionSpacer - - - category_total_label = section['category'] + " Total" - series.append({ - 'label' : category_total_label, - 'data' : [ [tickIndex, section['totalscore']] ], - 'color' : colors[sectionIndex] - }) - - ticks.append( [tickIndex, section['totallabel']] ) - detail_tooltips[category_total_label] = [section['totalscore_summary']] - else: - series.append({ - 'label' : section['category'], - 'data' : [ [tickIndex, section['totalscore']] ], - 'color' : colors[sectionIndex] - }) - - ticks.append( [tickIndex, section['totallabel']] ) - detail_tooltips[section['category']] = [section['totalscore_summary']] - - tickIndex += 1 + sectionSpacer - sectionIndex += 1 - - - detail_tooltips['Dropped Scores'] = dropped_score_tooltips - - ## ----------------------------- Grade overviewew bar ------------------------- ## - totalWeight = 0.0 - sectionIndex = 0 - totalScore = 0.0 - overviewBarX = tickIndex - - for section in grade_summary: - weighted_score = section['totalscore'] * section['weight'] - summary_text = "{0} - {1:.1%} of a possible {2:.0%}".format(section['category'], weighted_score, section['weight']) + for section in grade_summary['section_breakdown']: + if section.get('prominent', False): + tickIndex += sectionSpacer + + if section['category'] not in categories: + colorIndex = len(categories) % len(colors) + categories[ section['category'] ] = {'label' : section['category'], + 'data' : [], + 'color' : colors[colorIndex]} - weighted_category_label = section['category'] + " - Weighted" - - if section['totalscore'] > 0: + categoryData = categories[ section['category'] ] + + categoryData['data'].append( [tickIndex, section['percent']] ) + ticks.append( [tickIndex, section['label'] ] ) + + if section['category'] in detail_tooltips: + detail_tooltips[ section['category'] ].append( section['detail'] ) + else: + detail_tooltips[ section['category'] ] = [ section['detail'], ] + + if 'mark' in section: + droppedScores.append( [tickIndex, 0.05] ) + dropped_score_tooltips.append( section['mark']['detail'] ) + + tickIndex += 1 + + if section.get('prominent', False): + tickIndex += sectionSpacer + + ## ----------------------------- Grade overviewew bar ------------------------- ## + tickIndex += sectionSpacer + + series = categories.values() + overviewBarX = tickIndex + extraColorIndex = len(categories) #Keeping track of the next color to use for categories not in categories[] + + for section in grade_summary['grade_breakdown']: + if section['percent'] > 0: + if section['category'] in categories: + color = categories[ section['category'] ]['color'] + else: + color = colors[ extraColorIndex % len(colors) ] + extraColorIndex += 1 + series.append({ - 'label' : weighted_category_label, - 'data' : [ [overviewBarX, weighted_score] ], - 'color' : colors[sectionIndex] + 'label' : section['category'] + "-grade_breakdown", + 'data' : [ [overviewBarX, section['percent']] ], + 'color' : color }) - detail_tooltips[weighted_category_label] = [ summary_text ] - sectionIndex += 1 - totalWeight += section['weight'] - totalScore += section['totalscore'] * section['weight'] - + detail_tooltips[section['category'] + "-grade_breakdown"] = [ section['detail'] ] + ticks += [ [overviewBarX, "Total"] ] tickIndex += 1 + sectionSpacer + + totalScore = grade_summary['percent'] + detail_tooltips['Dropped Scores'] = dropped_score_tooltips %> - var series = ${ json.dumps(series) }; + var series = ${ json.dumps( series ) }; var ticks = ${ json.dumps(ticks) }; var bottomTicks = ${ json.dumps(bottomTicks) }; var detail_tooltips = ${ json.dumps(detail_tooltips) }; @@ -132,7 +117,7 @@ $(function () { var $grade_detail_graph = $("#${graph_div_id}"); if ($grade_detail_graph.length > 0) { var plot = $.plot($grade_detail_graph, series, options); - + //We need to put back the plotting of the percent here var o = plot.pointOffset({x: ${overviewBarX} , y: ${totalScore}}); $grade_detail_graph.append('
    ${"{totalscore:.0%}".format(totalscore=totalScore)}
    '); } diff --git a/templates/quickedit.html b/templates/quickedit.html new file mode 100644 index 0000000000..c08c5e3f51 --- /dev/null +++ b/templates/quickedit.html @@ -0,0 +1,95 @@ + + + + + + + + + +<%include file="mathjax_include.html" /> + + + + + + + + + + +## ----------------------------------------------------------------------------- +## information and i4x PSL code + +
    +

    QuickEdit

    +
    +
      +
    • File = ${filename}
    • +
    • ID = ${id}
    • +
    + +
    + +
    + + +
    + +${msg|n} + +## ----------------------------------------------------------------------------- +## rendered problem display + + + +
    + + + + + +
    + ${phtml} +
    + + + diff --git a/templates/seq_module.js b/templates/seq_module.js index b4f92db8a1..5fa4070fca 100644 --- a/templates/seq_module.js +++ b/templates/seq_module.js @@ -2,7 +2,7 @@ var ${ id }contents=["", %for t in items: - ${t['content']} , + ${t['content']} , %endfor "" ]; @@ -16,7 +16,7 @@ var ${ id }types=["", var ${ id }init_functions=["", %for t in items: - function(){ ${t['init_js']} }, + function(){ ${t['init_js']} }, %endfor ""]; @@ -24,12 +24,12 @@ var ${ id }titles=${titles}; var ${ id }destroy_functions=["", %for t in items: - function(){ ${t['destroy_js']} }, + function(){ ${t['destroy_js']} }, %endfor ""]; var ${ id }loc = -1; -function disablePrev() { +function disablePrev() { var i=${ id }loc-1; log_event("seq_prev", {'old':${id}loc, 'new':i,'id':'${id}'}); if (i < 1 ) { @@ -39,7 +39,7 @@ function disablePrev() { }; } - function disableNext() { + function disableNext() { var i=${ id }loc+1; log_event("seq_next", {'old':${id}loc, 'new':i,'id':'${id}'}); @@ -53,7 +53,7 @@ function disablePrev() { function ${ id }goto(i) { log_event("seq_goto", {'old':${id}loc, 'new':i,'id':'${id}'}); - postJSON('/modx/sequential/${ id }/goto_position', + postJSON('${ MITX_ROOT_URL }/modx/sequential/${ id }/goto_position', {'position' : i }); if (${ id }loc!=-1) @@ -77,11 +77,11 @@ function ${ id }goto(i) { function ${ id }setup_click(i) { $('#tt_'+i).click(function(eo) { ${ id }goto(i);}); $('#tt_'+i).addClass("seq_"+${ id }types[i]+"_inactive"); - $('#tt_'+i).parent().append("

    " + ${ id }titles[i-1] + "

    "); + $('#tt_'+i).append("

    " + ${ id }titles[i-1] + "

    "); } -function ${ id }next() { +function ${ id }next() { var i=${ id }loc+1; log_event("seq_next", {'old':${id}loc, 'new':i,'id':'${id}'}); if(i > ${ len(items) } ) { @@ -92,7 +92,7 @@ function ${ id }next() { } -function ${ id }prev() { +function ${ id }prev() { var i=${ id }loc-1; log_event("seq_prev", {'old':${id}loc, 'new':i,'id':'${id}'}); if (i < 1 ) { @@ -105,7 +105,7 @@ function ${ id }prev() { $(function() { - var i; + var i; for(i=1; i<${ len(items)+1 }; i++) { ${ id }setup_click(i); } diff --git a/templates/solutionspan.html b/templates/solutionspan.html new file mode 100644 index 0000000000..4e85d3aaf4 --- /dev/null +++ b/templates/solutionspan.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/templates/staff_problem_histogram.js b/templates/staff_problem_histogram.js new file mode 100644 index 0000000000..77ba59d490 --- /dev/null +++ b/templates/staff_problem_histogram.js @@ -0,0 +1,40 @@ +<%! + import json + import math +%> + + +var rawData = ${json.dumps(histogram)}; + +var maxx = 1; +var maxy = 1.5; +var xticks = Array(); +var yticks = Array(); +var data = Array(); +for (var i = 0; i < rawData.length; i++) { + var score = rawData[i][0]; + var count = rawData[i][1]; + var log_count = Math.log(count + 1); + + data.push( [score, log_count] ); + + xticks.push( [score, score.toString()] ); + yticks.push( [log_count, count.toString()] ); + + maxx = Math.max( score + 1, maxx ); + maxy = Math.max(log_count*1.1, maxy ); +} + +$.plot($("#histogram_${module_id}"), [{ + data: data, + bars: { show: true, + align: 'center', + lineWidth: 0, + fill: 1.0 }, + color: "#b72121", + }], + { + xaxis: {min: -1, max: maxx, ticks: xticks, tickLength: 0}, + yaxis: {min: 0.0, max: maxy, ticks: yticks, labelWidth: 50}, + } +); diff --git a/templates/staff_problem_info.html b/templates/staff_problem_info.html index 560713eaa6..20370a9c81 100644 --- a/templates/staff_problem_info.html +++ b/templates/staff_problem_info.html @@ -1,6 +1,6 @@
    ${xml | h}
    -
    -${ str(histogram) } -
    +%if render_histogram: +
    +%endif diff --git a/templates/textbox.html b/templates/textbox.html new file mode 100644 index 0000000000..cbbab7babc --- /dev/null +++ b/templates/textbox.html @@ -0,0 +1,34 @@ +
    + + + + + % if state == 'unsubmitted': + + % elif state == 'correct': + + % elif state == 'incorrect': + + % elif state == 'incomplete': + + % endif +
    + (${state}) +
    + ${msg|n} +
    + +
    + + +
    diff --git a/templates/video.html b/templates/video.html index 0d5a14ce3f..0d1f6c5641 100644 --- a/templates/video.html +++ b/templates/video.html @@ -1,8 +1,9 @@ -% if name is not UNDEFINED and name != None: +% if name is not UNDEFINED and name != None:

    ${name}

    % endif
    +
    @@ -28,12 +29,16 @@ @@ -57,18 +62,71 @@
  • +
    <%block name="js_extra"> + + +
      +% for t in annotations: +
    1. + ${t[1]['content']} +
    2. +% endfor +
    diff --git a/templates/video_init.js b/templates/video_init.js index ea72242539..bcbaecd249 100644 --- a/templates/video_init.js +++ b/templates/video_init.js @@ -88,6 +88,7 @@ function add_speed(key, stream) { var active = $(this).text(); $("p.active").text(active); }); + } var l=[] @@ -128,6 +129,9 @@ $(document).ready(function() { add_speed(l[i], streams[l[i]]) } + var dropUpHeight = $('ol#video_speeds').height(); + console.log(dropUpHeight); + $('ol#video_speeds').css('top', -(dropUpHeight + 2)); }); function toggleVideo(){ diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 0000000000..c9c15b340d --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1,5 @@ +django-nose +coverage +nosexcover +pylint +pep8 diff --git a/test_root/data/course_settings.py b/test_root/data/course_settings.py new file mode 100644 index 0000000000..f4e9696d1d --- /dev/null +++ b/test_root/data/course_settings.py @@ -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, + } +] diff --git a/test_root/data/custom_tags/.git-keep b/test_root/data/custom_tags/.git-keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test_root/log/.git-keep b/test_root/log/.git-keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/urls.py b/urls.py index 279fd75231..efbe4b8d62 100644 --- a/urls.py +++ b/urls.py @@ -49,6 +49,7 @@ if settings.COURSEWARE_ENABLED: url(r'^courseware/$', 'courseware.views.index', name="courseware"), url(r'^info$', 'util.views.info'), url(r'^wiki/', include('simplewiki.urls')), + url(r'^masquerade/', include('masquerade.urls')), url(r'^courseware/(?P[^/]*)/(?P[^/]*)/(?P
    [^/]*)/$', 'courseware.views.index', name="courseware_section"), url(r'^courseware/(?P[^/]*)/(?P[^/]*)/$', 'courseware.views.index', name="courseware_chapter"), url(r'^courseware/(?P[^/]*)/$', 'courseware.views.index', name="courseware_course"), @@ -68,6 +69,12 @@ if settings.COURSEWARE_ENABLED: url(r'^calculate$', 'util.views.calculate'), ) +if settings.ENABLE_MULTICOURSE: + urlpatterns += (url(r'^mitxhome$', 'util.views.mitxhome'),) + +if settings.QUICKEDIT: + urlpatterns += (url(r'^quickedit/(?P[^/]*)$', 'courseware.views.quickedit'),) + if settings.ASKBOT_ENABLED: urlpatterns += (url(r'^%s' % settings.ASKBOT_URL, include('askbot.urls')), \ url(r'^admin/', include(admin.site.urls)), \ @@ -76,6 +83,10 @@ if settings.ASKBOT_ENABLED: # url(r'^robots.txt$', include('robots.urls')), ) +if settings.DEBUG: + ## Jasmine + urlpatterns=urlpatterns + (url(r'^_jasmine/', include('django_jasmine.urls')),) + urlpatterns = patterns(*urlpatterns) if settings.DEBUG: