Commit f30596c4 by Calen Pennington

Merge branch 'master' into specific-error-handling

Conflicts:
	djangoapps/courseware/views.py
parents 69321b44 cc6e0c55
......@@ -4,6 +4,9 @@
*.swp
*.orig
*.DS_Store
:2e_*
:2e#
.AppleDouble
database.sqlite
courseware/static/js/mathjax/*
db.newaskbot
......
......@@ -25,7 +25,7 @@ from mako.template import Template
from util import contextualize_text
import inputtypes
from responsetypes import NumericalResponse, FormulaResponse, CustomResponse, SchematicResponse, MultipleChoiceResponse, StudentInputError, TrueFalseResponse, ExternalResponse,ImageResponse
from responsetypes import NumericalResponse, FormulaResponse, CustomResponse, SchematicResponse, MultipleChoiceResponse, StudentInputError, TrueFalseResponse, ExternalResponse,ImageResponse,OptionResponse
import calc
import eia
......@@ -40,8 +40,9 @@ response_types = {'numericalresponse':NumericalResponse,
'multiplechoiceresponse':MultipleChoiceResponse,
'truefalseresponse':TrueFalseResponse,
'imageresponse':ImageResponse,
'optionresponse':OptionResponse,
}
entry_types = ['textline', 'schematic', 'choicegroup','textbox','imageinput']
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
......@@ -186,6 +187,13 @@ class LoncapaProblem(object):
if answer:
answer_map[entry.get('id')] = contextualize_text(answer, self.context)
# include solutions from <solution>...</solution> stanzas
# Tentative merge; we should figure out how we want to handle hints and solutions
for entry in self.tree.xpath("//"+"|//".join(solution_types)):
answer = etree.tostring(entry)
if answer:
answer_map[entry.get('id')] = answer
return answer_map
# ======= Private ========
......@@ -241,7 +249,24 @@ class LoncapaProblem(object):
if self.student_answers and problemid in self.student_answers:
value = self.student_answers[problemid]
return getattr(inputtypes, 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:
......@@ -287,6 +312,7 @@ class LoncapaProblem(object):
answer_id = 1
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)
......
......@@ -8,9 +8,14 @@ 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
......@@ -18,6 +23,7 @@ These are matched by *.html files templates/*.html which are mako templates with
# 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
......@@ -27,9 +33,42 @@ from lxml import etree
from mitxmako.shortcuts import render_to_string
#-----------------------------------------------------------------------------
#takes the xml tree as 'element', the student's previous answer as 'value', and the graded status as 'state'
def choicegroup(element, value, state, msg=""):
def optioninput(element, value, status, msg=''):
'''
Select option input type.
Example:
<optioninput options="('Up','Down')" correct="Up"/><text>The location of the sky</text>
'''
eid=element.get('id')
options = element.get('options')
if not options:
raise Exception,"[courseware.capa.inputtypes.optioninput] Missing options specification in " + etree.tostring(element)
oset = shlex.shlex(options[1:-1])
oset.quotes = "'"
oset.whitespace = ","
oset = [x[1:-1] for x in list(oset)]
# osetdict = dict([('option_%s_%s' % (eid,x),oset[x]) for x in range(len(oset)) ]) # make dict with IDs
osetdict = dict([(oset[x],oset[x]) for x in range(len(oset)) ]) # make dict with key,value same
if settings.DEBUG:
print '[courseware.capa.inputtypes.optioninput] osetdict=',osetdict
context={'id':eid,
'value':value,
'state':status,
'msg':msg,
'options':osetdict,
}
html=render_to_string("optioninput.html", context)
return etree.XML(html)
#-----------------------------------------------------------------------------
def choicegroup(element, value, status, msg=''):
'''
Radio button inputs: multiple choice or true/false
......@@ -47,7 +86,7 @@ def choicegroup(element, value, state, msg=""):
for choice in element:
assert choice.tag =="choice", "only <choice> tags should be immediate children of a <choicegroup>"
choices[choice.get("name")] = etree.tostring(choice[0]) # TODO: what if choice[0] has math tags in it?
context={'id':eid, 'value':value, 'state':state, 'type':type, 'choices':choices}
context={'id':eid, 'value':value, 'state':status, 'type':type, 'choices':choices}
html=render_to_string("choicegroup.html", context)
return etree.XML(html)
......@@ -60,9 +99,9 @@ def textline(element, value, state, msg=""):
return etree.XML(html)
#-----------------------------------------------------------------------------
# TODO: Make a wrapper for <formulainput>
# TODO: Make an AJAX loop to confirm equation is okay in real-time as user types
def jstextline(element, value, state, msg=""):
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.
'''
......@@ -72,7 +111,7 @@ def jstextline(element, value, state, msg=""):
dojs = element.get('dojs') # dojs is used for client-side javascript display & return
# when dojs=='math', a <span id=display_eid>`{::}`</span>
# and a hidden textarea with id=input_eid_fromjs will be output
context = {'id':eid, 'value':value, 'state':state, 'count':count, 'size': size,
context = {'id':eid, 'value':value, 'state':status, 'count':count, 'size': size,
'dojs':dojs,
'msg':msg,
}
......@@ -81,7 +120,7 @@ def jstextline(element, value, state, msg=""):
#-----------------------------------------------------------------------------
## TODO: Make a wrapper for <codeinput>
def textbox(element, value, state, msg=''):
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.
......@@ -91,12 +130,12 @@ def textbox(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, 'msg':msg}
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, state):
def schematic(element, value, status, msg=''):
eid = element.get('id')
height = element.get('height')
width = element.get('width')
......@@ -120,7 +159,7 @@ def schematic(element, value, state):
#-----------------------------------------------------------------------------
### TODO: Move out of inputtypes
def math(element, value, state, msg=''):
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.
......@@ -134,21 +173,27 @@ def math(element, value, state, msg=''):
TODO: use shorter tags (but this will require converting problem XML files!)
'''
mathstr = element.text[1:-1]
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})
mathstr = re.sub('\$(.*)\$','[mathjaxinline]\\1[/mathjaxinline]',element.text)
mtag = 'mathjax'
if not '\\displaystyle' in mathstr: mtag += 'inline'
else: mathstr = mathstr.replace('\\displaystyle','')
mathstr = mathstr.replace('mathjaxinline]','%s]'%mtag)
#if '\\displaystyle' in mathstr:
# isinline = False
# mathstr = mathstr.replace('\\displaystyle','')
#else:
# isinline = True
# html=render_to_string("mathstring.html",{'mathstr':mathstr,'isinline':isinline,'tail':element.tail})
html = '<html><html>%s</html><html>%s</html></html>' % (mathstr,element.tail)
xhtml = etree.XML(html)
# xhtml.tail = element.tail # don't forget to include the tail!
return xhtml
#-----------------------------------------------------------------------------
def solution(element, value, state, msg=''):
def solution(element, value, status, msg=''):
'''
This is not really an input type. It is just a <span>...</span> which is given an ID,
that is used for displaying an extended answer (a problem "solution") after "show answers"
......@@ -159,7 +204,7 @@ def solution(element, value, state, msg=''):
size = element.get('size')
context = {'id':eid,
'value':value,
'state':state,
'state':status,
'size': size,
'msg':msg,
}
......
......@@ -32,6 +32,8 @@ from lxml.html.soupparser import fromstring as fromstring_bs # uses Beautiful So
import calc
import eia
from util import contextualize_text
def compare_with_tolerance(v1, v2, tol):
''' Compare v1 to v2 with maximum tolerance tol
tol is relative if it ends in %; otherwise, it is absolute
......@@ -61,6 +63,8 @@ class GenericResponse(object):
#Every response type needs methods "grade" and "get_answers"
#-----------------------------------------------------------------------------
class MultipleChoiceResponse(GenericResponse):
'''
Example:
......@@ -84,6 +88,7 @@ class MultipleChoiceResponse(GenericResponse):
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:
......@@ -100,9 +105,14 @@ class MultipleChoiceResponse(GenericResponse):
return {self.answer_id:self.correct_choices}
def preprocess_response(self):
'''
Initialize name attributes in <choice> stanzas in the <choicegroup> in this response.
'''
i=0
for response in self.xml.xpath("choicegroup"):
response.set("type", "MultipleChoice")
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))
......@@ -131,6 +141,42 @@ class TrueFalseResponse(MultipleChoiceResponse):
return {self.answer_id : 'incorrect'}
#-----------------------------------------------------------------------------
class OptionResponse(GenericResponse):
'''
Example:
<optionresponse direction="vertical" randomize="yes">
<optioninput options="('Up','Down')" correct="Up"><text>The location of the sky</text></optioninput>
<optioninput options="('Up','Down')" correct="Down"><text>The location of the earth</text></optioninput>
</optionresponse>
TODO: handle direction and randomize
'''
def __init__(self, xml, context):
self.xml = xml
self.answer_fields = xml.findall('optioninput')
if settings.DEBUG:
print '[courseware.capa.responsetypes.OR.init] answer_fields=%s' % (self.answer_fields)
self.context = context
def grade(self, student_answers):
cmap = {}
amap = self.get_answers()
for aid in amap:
if aid in student_answers and student_answers[aid]==amap[aid]:
cmap[aid] = 'correct'
else:
cmap[aid] = 'incorrect'
return cmap
def get_answers(self):
amap = dict([(af.get('id'),af.get('correct')) for af in self.answer_fields])
return amap
#-----------------------------------------------------------------------------
class NumericalResponse(GenericResponse):
def __init__(self, xml, context):
......@@ -219,13 +265,35 @@ def sympy_check2():
self.answer_ids = xml.xpath('//*[@id=$id]//textline/@id',
id=xml.get('id'))
self.context = context
answer_list = xml.xpath('//*[@id=$id]//answer',
id=xml.get('id'))
if len(answer_list):
answer=answer_list[0]
# if <customresponse> has an "expect" attribute then save that
self.expect = xml.get('expect')
self.myid = xml.get('id')
# the <answer>...</answer> stanza should be local to the current <customresponse>. So try looking there first.
self.code = None
answer = None
try:
answer = xml.xpath('//*[@id=$id]//answer',id=xml.get('id'))[0]
except IndexError,err:
# print "xml = ",etree.tostring(xml,pretty_print=True)
# if we have a "cfn" attribute then look for the function specified by cfn, in the problem context
# ie the comparison function is defined in the <script>...</script> stanza instead
cfn = xml.get('cfn')
if cfn:
if settings.DEBUG: print "[courseware.capa.responsetypes] cfn = ",cfn
if cfn in context:
self.code = context[cfn]
else:
raise Exception("Invalid custom response -- no checker code")
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()
......@@ -237,25 +305,113 @@ def sympy_check2():
student_answers is a dict with everything from request.POST, but with the first part
of each key removed (the string before the first "_").
'''
from capa_problem import global_context
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'])
def getkey2(dict,key,default):
"""utilify function: get dict[key] if key exists, or return default"""
if dict.has_key(key):
return dict[key]
return default
idset = sorted(self.answer_ids) # ordered list of answer id's
submission = [student_answers[k] for k in idset] # ordered list of answers
fromjs = [ getkey2(student_answers,k+'_fromjs',None) for k in idset ] # ordered list of fromjs_XXX responses (if exists)
# if there is only one box, and it's empty, then don't evaluate
if len(idset)==1 and not submission[0]:
return {idset[0]:'no_answer_entered'}
gctxt = self.context['global_context']
correct = ['unknown'] * len(idset)
messages = [''] * len(idset)
# put these in the context of the check function evaluator
# note that this doesn't help the "cfn" version - only the exec version
self.context.update({'xml' : self.xml, # our subtree
'response_id' : self.myid, # my ID
'expect': self.expect, # expected answer (if given as attribute)
'submission':submission, # ordered list of student answers from entry boxes in our subtree
'idset':idset, # ordered list of ID's of all entry boxes in our subtree
'fromjs':fromjs, # ordered list of all javascript inputs in our subtree
'answers':student_answers, # dict of student's responses, with keys being entry box IDs
'correct':correct, # the list to be filled in by the check function
'messages':messages, # the list of messages to be filled in by the check function
'testdat':'hello world',
})
# exec the check function
if type(self.code)==str:
try:
exec self.code in self.context['global_context'], self.context
except Exception,err:
print "oops in customresponse (code) error %s" % err
print "context = ",self.context
print traceback.format_exc()
else: # self.code is not a string; assume its a function
# this is an interface to the Tutor2 check functions
fn = self.code
try:
answer_given = submission[0] if (len(idset)==1) else submission
if fn.func_code.co_argcount>=4: # does it want four arguments (the answers dict, myname)?
ret = fn(self.expect,answer_given,student_answers,self.answer_ids[0])
elif fn.func_code.co_argcount>=3: # does it want a third argument (the answers dict)?
ret = fn(self.expect,answer_given,student_answers)
else:
ret = fn(self.expect,answer_given)
except Exception,err:
print "oops in customresponse (cfn) error %s" % err
# print "context = ",self.context
print traceback.format_exc()
if settings.DEBUG: print "[courseware.capa.responsetypes.customresponse.grade] ret = ",ret
if type(ret)==dict:
correct[0] = 'correct' if ret['ok'] else 'incorrect'
msg = ret['msg']
if 1:
# try to clean up message html
msg = '<html>'+msg+'</html>'
msg = etree.tostring(fromstring_bs(msg),pretty_print=True)
msg = msg.replace('&#13;','')
#msg = re.sub('<html>(.*)</html>','\\1',msg,flags=re.M|re.DOTALL) # python 2.7
msg = re.sub('(?ms)<html>(.*)</html>','\\1',msg)
messages[0] = msg
else:
correct[0] = 'correct' if ret else 'incorrect'
# build map giving "correct"ness of the answer(s)
#correct_map = dict(zip(idset, self.context['correct']))
correct_map = {}
for k in range(len(idset)):
correct_map[idset[k]] = correct[k]
correct_map['msg_%s' % idset[k]] = messages[k]
return correct_map
def get_answers(self):
# Since this is explicitly specified in the problem, this will
# be handled by capa_problem
'''
Give correct answer expected for this response.
capa_problem handles correct_answers from entry objects like textline, and that
is what should be used when this response has multiple entry objects.
but for simplicity, if an "expect" attribute was given by the content author
ie <customresponse expect="foo" ...> then return it now.
'''
if len(self.answer_ids)>1:
return {}
if self.expect:
return {self.answer_ids[0] : self.expect}
return {}
#-----------------------------------------------------------------------------
class ExternalResponse(GenericResponse):
'''
"""
Grade the student's input using an external server.
Typically used by coding problems.
'''
"""
def __init__(self, xml, context):
self.xml = xml
self.answer_ids = xml.xpath('//*[@id=$id]//textbox/@id|//*[@id=$id]//textline/@id',
......@@ -471,10 +627,6 @@ class ImageResponse(GenericResponse):
raise Exception,'[capamodule.capa.responsetypes.imageinput] error grading %s (input=%s)' % (err,aid,given)
(gx,gy) = [int(x) for x in m.groups()]
if settings.DEBUG:
print "[capamodule.capa.responsetypes.imageinput] llx,lly,urx,ury=",(llx,lly,urx,ury)
print "[capamodule.capa.responsetypes.imageinput] gx,gy=",(gx,gy)
# answer is correct if (x,y) is within the specified rectangle
if (llx <= gx <= urx) and (lly <= gy <= ury):
correct_map[aid] = 'correct'
......
......@@ -24,7 +24,9 @@ try: # This lets us do __name__ == ='__main__'
from student.models import UserTestGroup
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
......@@ -181,7 +183,7 @@ def course_xml_process(tree):
propogate_downward_tag(tree, "rerandomize")
return tree
def course_file(user):
def course_file(user,coursename=None):
''' Given a user, return course.xml'''
if user.is_authenticated():
......@@ -189,6 +191,11 @@ def course_file(user):
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}
......@@ -210,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,
......@@ -226,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
......@@ -239,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)+")")
......
......@@ -67,7 +67,7 @@ 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:
......@@ -77,7 +77,7 @@ def grade_sheet(student):
- 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)
......@@ -103,7 +103,7 @@ def grade_sheet(student):
scores=[]
if len(problems)>0:
for p in problems:
(correct,total) = get_score(student, p, response_by_id)
(correct,total) = get_score(student, p, response_by_id, coursename=coursename)
if settings.GENERATE_PROFILE_SCORES:
if total > 1:
......@@ -167,7 +167,7 @@ def aggregate_scores(scores, section_name = "summary"):
return all_total, graded_total
def get_score(user, problem, cache):
def get_score(user, problem, cache, coursename=None):
## HACK: assumes max score is fixed per problem
id = problem.get('id')
correct = 0.0
......@@ -196,7 +196,7 @@ def get_score(user, problem, cache):
## 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)
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()
......
......@@ -22,6 +22,11 @@ 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
......@@ -29,6 +34,10 @@ class I4xSystem(object):
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
......@@ -50,6 +59,7 @@ def make_track_function(request):
def f(event_type, event):
return track.views.server_track(request, event_type, event, page='x_module')
return f
def grade_histogram(module_id):
''' Print out a histogram of grades on a given problem.
Part of staff member debug info.
......@@ -83,6 +93,10 @@ 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 = settings.MITX_ROOT_URL + '/modx/'+module_type+'/'+module_id+'/'
system = I4xSystem(track_function = make_track_function(request),
......@@ -104,6 +118,7 @@ def render_x_module(user, request, xml_module, module_object_preload):
state=instance.get_state())
smod.save()
module_object_preload.append(smod)
# Grab content
content = instance.get_html()
init_js = instance.get_init_js()
......
......@@ -21,6 +21,7 @@ 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")
......@@ -115,8 +116,7 @@ class Module(XModule):
if len(explain) == 0:
explain = False
html=render_to_string('problem.html',
{'problem' : content,
context = {'problem' : content,
'id' : self.item_id,
'check_button' : check_button,
'reset_button' : reset_button,
......@@ -125,8 +125,10 @@ class Module(XModule):
'ajax_url' : self.ajax_url,
'attempts_used': self.attempts,
'attempts_allowed': self.max_attempts,
'explain': explain
})
'explain': explain,
}
html=render_to_string('problem.html', context)
if encapsulate:
html = '<div id="main_{id}">'.format(id=self.item_id)+html+"</div>"
......@@ -193,7 +195,12 @@ class Module(XModule):
seed = 1
else:
seed = None
self.lcp=LoncapaProblem(self.filestore.open(self.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):
'''
......@@ -306,7 +313,7 @@ class Module(XModule):
except:
self.lcp = LoncapaProblem(self.filestore.open(self.filename), id=lcp_id, state=old_state)
traceback.print_exc()
raise
raise Exception,"error in capa_module"
return json.dumps({'success':'Unknown Error'})
self.attempts = self.attempts + 1
......
<problem>
<text>
<p>
Why do bicycles benefit from having larger wheels when going up a bump as shown in the picture? <br/>
Assume that for both bicycles:<br/>
1.) The tires have equal air pressure.<br/>
2.) The bicycles never leave the contact with the bump.<br/>
3.) The bicycles have the same mass. The bicycle tires (regardless of size) have the same mass.<br/>
</p>
</text>
<optionresponse texlayout="horizontal" max="10" randomize="yes">
<ul>
<li>
<text>
<p>The bicycles with larger wheels have more time to go over the bump. This decreases the magnitude of the force needed to lift the bicycle.</p>
</text>
<optioninput name="Foil1" location="random" options="('True','False')" correct="True">
</optioninput>
</li>
<li>
<text>
<p>The bicycles with larger wheels always have a smaller vertical displacement regardless of speed.</p>
</text>
<optioninput name="Foil2" location="random" options="('True','False')" correct="False">
</optioninput>
</li>
<li>
<text>
<p>The bicycles with larger wheels experience a force backward with less magnitude for the same amount of time.</p>
</text>
<optioninput name="Foil3" location="random" options="('True','False')" correct="False">
</optioninput>
</li>
<li>
<text>
<p>The bicycles with larger wheels experience a force backward with less magnitude for a greater amount of time.</p>
</text>
<optioninput name="Foil4" location="random" options="('True','False')" correct="True">
</optioninput>
</li>
<li>
<text>
<p>The bicycles with larger wheels have more kinetic energy turned into gravitational potential energy.</p>
</text>
<optioninput name="Foil5" location="random" options="('True','False')" correct="False">
</optioninput>
</li>
<li>
<text>
<p>The bicycles with larger wheels have more rotational kinetic energy, so the horizontal velocity of the biker changes less.</p>
</text>
<optioninput name="Foil6" location="random" options="('True','False')" correct="False">
</optioninput>
</li>
</ul>
<hintgroup showoncorrect="no">
<text>
<br/>
<br/>
</text>
</hintgroup>
</optionresponse>
</problem>
......@@ -63,6 +63,9 @@ class ModelsTest(unittest.TestCase):
exception_happened = True
self.assertTrue(exception_happened)
#-----------------------------------------------------------------------------
# tests of capa_problem inputtypes
class MultiChoiceTest(unittest.TestCase):
def test_MC_grade(self):
multichoice_file = os.path.dirname(__file__)+"/test_files/multichoice.xml"
......@@ -94,6 +97,38 @@ class MultiChoiceTest(unittest.TestCase):
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):
......
......@@ -18,6 +18,7 @@ from module_render import render_module, make_track_function, I4xSystem
from models import StudentModule
from student.models import UserProfile
from util.errors import record_exception
from multicourse import multicourse_settings
import courseware.content_parser as content_parser
import courseware.modules
......@@ -35,11 +36,16 @@ 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]
......@@ -61,6 +67,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,
......@@ -69,7 +78,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)
......@@ -79,7 +88,7 @@ def render_accordion(request,course,chapter,section):
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']:
......@@ -100,8 +109,11 @@ def render_section(request, section):
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:
record_exception(log, "Unable to parse courseware xml")
return render_to_response('courseware-error.html', {})
......@@ -139,13 +151,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:
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
......@@ -153,13 +173,15 @@ 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('/')
request.session['coursename'] = course # keep track of current course being viewed in django's request.session
try:
dom = content_parser.course_file(user)
dom = content_parser.course_file(user,course) # also pass course to it, for course-specific XML path
except:
record_exception(log, "Unable to parse courseware xml")
return render_to_response('courseware-error.html', {})
......@@ -184,6 +206,7 @@ def index(request, course="6.002 Spring 2012", chapter="Using the System", secti
context = {
'csrf': csrf(request)['csrf_token'],
'accordion': render_accordion(request, course, chapter, section)
'COURSE_TITLE':multicourse_settings.get_course_title(course),
}
try:
......@@ -226,9 +249,13 @@ def modx_dispatch(request, module=None, dispatch=None, id=None):
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
try:
xml = content_parser.module_xml(request.user, module, 'id', id)
xml = content_parser.module_xml(request.user, module, 'id', id, coursename)
except:
record_exception(log, "Unable to load module during ajax call")
if 'text/html' in request.accepted_types:
......@@ -267,3 +294,98 @@ def modx_dispatch(request, module=None, dispatch=None, id=None):
s.save()
# Return whatever the module wanted to return to the client/caller
return HttpResponse(ajax_return)
def quickedit(request, id=None):
'''
quick-edit capa problem.
Maybe this should be moved into capa/views.py
Or this should take a "module" argument, and the quickedit moved into capa_module.
'''
print "WARNING: UNDEPLOYABLE CODE. FOR DEV USE ONLY."
print "In deployed use, this will only edit on one server"
print "We need a setting to disable for production where there is"
print "a load balanacer"
if not request.user.is_staff():
return redirect('/')
# get coursename if stored
if 'coursename' in request.session: coursename = request.session['coursename']
else: coursename = None
def get_lcp(coursename,id):
# Grab the XML corresponding to the request from course.xml
module = 'problem'
xml = content_parser.module_xml(request.user, module, 'id', id, coursename)
ajax_url = settings.MITX_ROOT_URL + '/modx/'+module+'/'+id+'/'
# Create the module (instance of capa_module.Module)
system = I4xSystem(track_function = make_track_function(request),
render_function = None,
ajax_url = ajax_url,
filestore = None,
coursename = coursename,
role = 'staff' if request.user.is_staff else 'student', # TODO: generalize this
)
instance=courseware.modules.get_module_class(module)(system,
xml,
id,
state=None)
lcp = instance.lcp
pxml = lcp.tree
pxmls = etree.tostring(pxml,pretty_print=True)
return instance, pxmls
instance, pxmls = get_lcp(coursename,id)
# if there was a POST, then process it
msg = ''
if 'qesubmit' in request.POST:
action = request.POST['qesubmit']
if "Revert" in action:
msg = "Reverted to original"
elif action=='Change Problem':
key = 'quickedit_%s' % id
if not key in request.POST:
msg = "oops, missing code key=%s" % key
else:
newcode = request.POST[key]
# see if code changed
if str(newcode)==str(pxmls) or '<?xml version="1.0"?>\n'+str(newcode)==str(pxmls):
msg = "No changes"
else:
# check new code
isok = False
try:
newxml = etree.fromstring(newcode)
isok = True
except Exception,err:
msg = "Failed to change problem: XML error \"<font color=red>%s</font>\"" % err
if isok:
filename = instance.lcp.fileobject.name
fp = open(filename,'w') # TODO - replace with filestore call?
fp.write(newcode)
fp.close()
msg = "<font color=green>Problem changed!</font> (<tt>%s</tt>)" % filename
instance, pxmls = get_lcp(coursename,id)
lcp = instance.lcp
# get the rendered problem HTML
phtml = instance.get_problem_html()
context = {'id':id,
'msg' : msg,
'lcp' : lcp,
'filename' : lcp.fileobject.name,
'pxmls' : pxmls,
'phtml' : phtml,
'init_js':instance.get_init_js(),
}
result = render_to_response('quickedit.html', context)
return result
# multicourse/multicourse_settings.py
#
# central module for providing fixed settings (course name, number, title)
# for multiple courses. Loads this information from django.conf.settings
#
# Allows backward compatibility with settings configurations without
# multiple courses specified.
#
# The central piece of configuration data is the dict COURSE_SETTINGS, with
# keys being the COURSE_NAME (spaces ok), and the value being a dict of
# parameter,value pairs. The required parameters are:
#
# - number : course number (used in the simplewiki pages)
# - title : humanized descriptive course title
#
# Optional parameters:
#
# - xmlpath : path (relative to data directory) for this course (defaults to "")
#
# If COURSE_SETTINGS does not exist, then fallback to 6.002_Spring_2012 default,
# for now.
from django.conf import settings
#-----------------------------------------------------------------------------
# load course settings
if hasattr(settings,'COURSE_SETTINGS'): # in the future, this could be replaced by reading an XML file
COURSE_SETTINGS = settings.COURSE_SETTINGS
elif hasattr(settings,'COURSE_NAME'): # backward compatibility
COURSE_SETTINGS = {settings.COURSE_NAME: {'number': settings.COURSE_NUMBER,
'title': settings.COURSE_TITLE,
},
}
else: # default to 6.002_Spring_2012
COURSE_SETTINGS = {'6.002_Spring_2012': {'number': '6.002x',
'title': 'Circuits and Electronics',
},
}
#-----------------------------------------------------------------------------
# wrapper functions around course settings
def get_course_settings(coursename):
if not coursename:
if hasattr(settings,'COURSE_DEFAULT'):
coursename = settings.COURSE_DEFAULT
else:
coursename = '6.002_Spring_2012'
if coursename in COURSE_SETTINGS: return COURSE_SETTINGS[coursename]
coursename = coursename.replace(' ','_')
if coursename in COURSE_SETTINGS: return COURSE_SETTINGS[coursename]
return None
def is_valid_course(coursename):
return not (get_course_settings==None)
def get_course_property(coursename,property):
cs = get_course_settings(coursename)
if not cs: return '' # raise exception instead?
if property in cs: return cs[property]
return '' # default
def get_course_xmlpath(coursename):
return get_course_property(coursename,'xmlpath')
def get_course_title(coursename):
return get_course_property(coursename,'title')
def get_course_number(coursename):
return get_course_property(coursename,'number')
# multicourse/views.py
......@@ -9,6 +9,8 @@ from django.utils import simplejson
from django.utils.translation import ugettext_lazy as _
from mitxmako.shortcuts import render_to_response
from multicourse import multicourse_settings
from models import Revision, Article, CreateArticleForm, RevisionFormWithTitle, RevisionForm
import wiki_settings
......@@ -17,6 +19,11 @@ def view(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
......@@ -25,7 +32,7 @@ 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)
......
"""
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
......@@ -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
......@@ -31,7 +32,7 @@ LOG_DIR = ENV_TOKENS['LOG_DIR']
CACHES = ENV_TOKENS['CACHES']
LOGGING = logsettings.get_logger_config(LOG_DIR,
LOGGING = get_logger_config(LOG_DIR,
logging_env=ENV_TOKENS['LOGGING_ENV'],
syslog_addr=(ENV_TOKENS['SYSLOG_SERVER'], 514),
debug=False)
......
......@@ -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
......
......@@ -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
......
......@@ -7,12 +7,13 @@ 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 = False
TEMPLATE_DEBUG = False
LOGGING = logsettings.get_logger_config(ENV_ROOT / "log",
LOGGING = get_logger_config(ENV_ROOT / "log",
logging_env="dev",
tracking_filename="tracking.log",
debug=True)
......
......@@ -13,7 +13,7 @@ Dir structure:
/log # Where we're going to write log files
"""
from dev import *
from envs.dev import *
DATABASES = {
'default': {
......
......@@ -7,11 +7,12 @@ 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
STATIC_GRAB = True
LOGGING = logsettings.get_logger_config(ENV_ROOT / "log",
LOGGING = get_logger_config(ENV_ROOT / "log",
logging_env="dev",
tracking_filename="tracking.log",
debug=False)
......
......@@ -7,7 +7,8 @@ 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
import os
INSTALLED_APPS = [
......@@ -25,7 +26,8 @@ for app in os.listdir(PROJECT_ROOT / 'djangoapps'):
TEST_RUNNER = 'django_nose.NoseTestSuiteRunner'
# Local Directories
COURSES_ROOT = PROJECT_ROOT / "test_data"
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']
......@@ -34,7 +36,7 @@ MAKO_TEMPLATES['main'] = [PROJECT_ROOT / 'templates',
DATA_DIR / 'info',
DATA_DIR / 'problems']
LOGGING = logsettings.get_logger_config(PROJECT_ROOT / "log",
LOGGING = get_logger_config(TEST_ROOT / "log",
logging_env="dev",
tracking_filename="tracking.log",
debug=True)
......
#!/usr/bin/python
from loncapa_check import *
#!/usr/bin/python
#
# File: mitx/lib/loncapa/loncapa_check.py
#
# Python functions which duplicate the standard comparison functions available to LON-CAPA problems.
# Used in translating LON-CAPA problems to i4x problem specification language.
import random
def lc_random(lower,upper,stepsize):
'''
like random.randrange but lower and upper can be non-integer
'''
nstep = int((upper-lower)/(1.0*stepsize))
choices = [lower+x*stepsize for x in range(nstep)]
return random.choice(choices)
......@@ -34,6 +34,9 @@ def render_to_string(template_name, dictionary, context=None, namespace='main'):
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)
......
......@@ -3,7 +3,6 @@ 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
from django.core.mail import send_mail
......@@ -61,3 +60,9 @@ def send_feedback(request):
def info(request):
''' Info page (link from main header) '''
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)
......@@ -29,12 +29,17 @@ INSTALL_DIR_PATH = File.join(DEPLOY_DIR, NORMALIZED_DEPLOY_NAME)
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 djangoapps | tee #{REPORT_DIR}/pep8.report")
sh("pep8 --ignore=E501 djangoapps | tee #{REPORT_DIR}/pep8.report")
end
task :pylint => REPORT_DIR do
......@@ -47,7 +52,8 @@ end
task :test => REPORT_DIR do
ENV['NOSE_XUNIT_FILE'] = File.join(REPORT_DIR, "nosetests.xml")
sh("django-admin.py test --settings=envs.test --pythonpath=. $(ls djangoapps)")
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
......
......@@ -8,6 +8,7 @@ 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
###
......@@ -20,18 +21,10 @@ COURSE_TITLE = "Circuits and Electronics"
COURSE_DEFAULT = '6.002_Spring_2012'
COURSE_LIST = {'6.002_Spring_2012': {'number' : '6.002x',
COURSE_SETTINGS = {'6.002_Spring_2012': {'number' : '6.002x',
'title' : 'Circuits and Electronics',
'datapath': '6002x/',
},
'8.02_Spring_2013': {'number' : '8.02x',
'title' : 'Electricity &amp; Magnetism',
'datapath': '802x/',
},
'8.01_Spring_2013': {'number' : '8.01x',
'title' : 'Mechanics',
'datapath': '801x/',
},
'xmlpath': '6002x/',
}
}
ROOT_URLCONF = 'urls'
......@@ -151,6 +144,7 @@ MIDDLEWARE_CLASSES = (
'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
......@@ -180,6 +174,8 @@ INSTALLED_APPS = (
'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:
......
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -735,14 +735,14 @@ section.index-content section.staff h1 {
margin-top: 25.888px; }
#lean_overlay {
background: #000;
display: none;
height: 100%;
left: 0px;
position: fixed;
z-index: 100;
top: 0px;
left: 0px;
height: 100%;
width: 100%;
background: #000;
display: none; }
z-index: 100; }
div.leanModal_box {
background: #fff;
......@@ -772,8 +772,8 @@ div.leanModal_box a.modal_close {
width: 14px;
z-index: 2; }
div.leanModal_box a.modal_close:hover {
text-decoration: none;
color: #993333; }
color: #993333;
text-decoration: none; }
div.leanModal_box h1 {
border-bottom: 1px solid #eee;
font-size: 24px;
......@@ -786,8 +786,8 @@ div.leanModal_box#enroll {
div.leanModal_box#enroll ol {
padding-top: 25.888px; }
div.leanModal_box#enroll ol li.terms, div.leanModal_box#enroll ol li.honor-code {
width: auto;
float: none; }
float: none;
width: auto; }
div.leanModal_box#enroll ol li div.tip {
display: none; }
div.leanModal_box#enroll ol li:hover div.tip {
......@@ -828,16 +828,16 @@ div.leanModal_box form ol li.terms, div.leanModal_box form ol li.remember {
padding-top: 25.888px;
width: auto; }
div.leanModal_box form ol li.honor-code {
width: auto;
float: none; }
float: none;
width: auto; }
div.leanModal_box form ol li label {
display: block;
font-weight: bold; }
div.leanModal_box form ol li input[type="email"], div.leanModal_box form ol li input[type="number"], div.leanModal_box form ol li input[type="password"], div.leanModal_box form ol li input[type="search"], div.leanModal_box form ol li input[type="tel"], div.leanModal_box form ol li input[type="text"], div.leanModal_box form ol li input[type="url"], div.leanModal_box form ol li input[type="color"], div.leanModal_box form ol li input[type="date"], div.leanModal_box form ol li input[type="datetime"], div.leanModal_box form ol li input[type="datetime-local"], div.leanModal_box form ol li input[type="month"], div.leanModal_box form ol li input[type="time"], div.leanModal_box form ol li input[type="week"], div.leanModal_box form ol li textarea {
width: 100%;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box; }
box-sizing: border-box;
width: 100%; }
div.leanModal_box form ol li input[type="checkbox"] {
margin-right: 10px; }
div.leanModal_box form ol li ul {
......@@ -904,12 +904,12 @@ div#login header h1 {
padding-bottom: 0;
margin-bottom: 6.472px; }
div#login ol li {
width: auto;
float: none; }
float: none;
width: auto; }
div.lost-password {
text-align: left;
margin-top: 25.888px; }
margin-top: 25.888px;
text-align: left; }
div.lost-password a {
color: #999; }
div.lost-password a:hover {
......@@ -927,11 +927,11 @@ div#apply_name_change ul, div#change_email ul, div#unenroll ul, div#deactivate-a
div#apply_name_change ul li, div#change_email ul li, div#unenroll ul li, div#deactivate-account ul li {
margin-bottom: 12.944px; }
div#apply_name_change ul li textarea, div#apply_name_change ul li input[type="email"], div#apply_name_change ul li input[type="number"], div#apply_name_change ul li input[type="password"], div#apply_name_change ul li input[type="search"], div#apply_name_change ul li input[type="tel"], div#apply_name_change ul li input[type="text"], div#apply_name_change ul li input[type="url"], div#apply_name_change ul li input[type="color"], div#apply_name_change ul li input[type="date"], div#apply_name_change ul li input[type="datetime"], div#apply_name_change ul li input[type="datetime-local"], div#apply_name_change ul li input[type="month"], div#apply_name_change ul li input[type="time"], div#apply_name_change ul li input[type="week"], div#change_email ul li textarea, div#change_email ul li input[type="email"], div#change_email ul li input[type="number"], div#change_email ul li input[type="password"], div#change_email ul li input[type="search"], div#change_email ul li input[type="tel"], div#change_email ul li input[type="text"], div#change_email ul li input[type="url"], div#change_email ul li input[type="color"], div#change_email ul li input[type="date"], div#change_email ul li input[type="datetime"], div#change_email ul li input[type="datetime-local"], div#change_email ul li input[type="month"], div#change_email ul li input[type="time"], div#change_email ul li input[type="week"], div#unenroll ul li textarea, div#unenroll ul li input[type="email"], div#unenroll ul li input[type="number"], div#unenroll ul li input[type="password"], div#unenroll ul li input[type="search"], div#unenroll ul li input[type="tel"], div#unenroll ul li input[type="text"], div#unenroll ul li input[type="url"], div#unenroll ul li input[type="color"], div#unenroll ul li input[type="date"], div#unenroll ul li input[type="datetime"], div#unenroll ul li input[type="datetime-local"], div#unenroll ul li input[type="month"], div#unenroll ul li input[type="time"], div#unenroll ul li input[type="week"], div#deactivate-account ul li textarea, div#deactivate-account ul li input[type="email"], div#deactivate-account ul li input[type="number"], div#deactivate-account ul li input[type="password"], div#deactivate-account ul li input[type="search"], div#deactivate-account ul li input[type="tel"], div#deactivate-account ul li input[type="text"], div#deactivate-account ul li input[type="url"], div#deactivate-account ul li input[type="color"], div#deactivate-account ul li input[type="date"], div#deactivate-account ul li input[type="datetime"], div#deactivate-account ul li input[type="datetime-local"], div#deactivate-account ul li input[type="month"], div#deactivate-account ul li input[type="time"], div#deactivate-account ul li input[type="week"] {
display: block;
width: 100%;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box; }
box-sizing: border-box;
display: block;
width: 100%; }
div#apply_name_change ul li textarea, div#change_email ul li textarea, div#unenroll ul li textarea, div#deactivate-account ul li textarea {
height: 60px; }
div#apply_name_change ul li input[type="submit"], div#change_email ul li input[type="submit"], div#unenroll ul li input[type="submit"], div#deactivate-account ul li input[type="submit"] {
......
......@@ -12,10 +12,6 @@
<script type="text/javascript">
$(function() {
${init}
$(".sequence-nav li a").hover(function(){
$(this).siblings().toggleClass("shown");
});
});
</script>
</%block>
......
<html>
<head>
<link rel="stylesheet" href="${ settings.LIB_URL }jquery.treeview.css" type="text/css" media="all" />
<link rel="stylesheet" href="/static/css/codemirror.css" type="text/css" media="all" />
<script type="text/javascript" src="${ settings.LIB_URL }jquery-1.6.2.min.js"></script>
<script type="text/javascript" src="${ settings.LIB_URL }jquery-ui-1.8.16.custom.min.js"></script>
<script type="text/javascript" src="${ settings.LIB_URL }codemirror-compressed.js"></script>
<script type="text/javascript" src="/static/js/schematic.js"></script>
<%include file="mathjax_include.html" />
<script>
function postJSON(url, data, callback) {
$.ajax({type:'POST',
url: url,
dataType: 'json',
data: data,
success: callback,
headers : {'X-CSRFToken':'none'} // getCookie('csrftoken')}
});
}
</script>
</head>
<body>
<!--[if lt IE 9]>
<script src="/static/js/html5shiv.js"></script>
<![endif]-->
<style type="text/css">
.CodeMirror {border-style: solid;
border-width: 1px;}
.CodeMirror-scroll {
height: 500;
width: 100%
}
</style>
## -----------------------------------------------------------------------------
## information and i4x PSL code
<hr width="100%">
<h2>QuickEdit</h2>
<hr width="100%">
<ul>
<li>File = ${filename}</li>
<li>ID = ${id}</li>
</ul>
<form method="post">
<textarea rows="40" cols="160" name="quickedit_${id}" id="quickedit_${id}">${pxmls|h}</textarea>
<br/>
<input type="submit" value="Change Problem" name="qesubmit" />
<input type="submit" value="Revert to original" name="qesubmit" />
</form>
<span>${msg|n}</span>
## -----------------------------------------------------------------------------
## rendered problem display
<script>
// height: auto;
// overflow-y: hidden;
// overflow-x: auto;
$(function(){
var cm = CodeMirror.fromTextArea(document.getElementById("quickedit_${id}"),
{ 'mode': {name: "xml", alignCDATA: true},
lineNumbers: true
});
// $('.my-wymeditor').wymeditor();
});
</script>
<hr width="100%">
<script>
${init_js}
</script>
<style type="text/css">
.staff {display:none;}
}
</style>
<form>
${phtml}
</form>
</body>
</html>
......@@ -2,10 +2,10 @@ section.help.main-content {
padding: lh();
h1 {
margin-top: 0;
border-bottom: 1px solid #ddd;
margin-bottom: lh();
margin-top: 0;
padding-bottom: lh();
border-bottom: 1px solid #ddd;
}
p {
......@@ -17,9 +17,9 @@ section.help.main-content {
}
section.self-help {
float: left;
margin-bottom: lh();
margin-right: flex-gutter();
float: left;
width: flex-grid(6);
ul {
......@@ -36,17 +36,17 @@ section.help.main-content {
width: flex-grid(6);
dl {
margin-bottom: lh();
display: block;
margin-bottom: lh();
dd {
margin-bottom: lh();
}
dt {
font-weight: bold;
float: left;
clear: left;
float: left;
font-weight: bold;
width: flex-grid(2, 6);
}
}
......
......@@ -16,28 +16,28 @@ div.info-wrapper {
list-style: none;
> li {
padding-bottom: lh(.5);
margin-bottom: lh(.5);
@extend .clearfix;
border-bottom: 1px solid #e3e3e3;
margin-bottom: lh(.5);
padding-bottom: lh(.5);
&:first-child {
padding: lh(.5);
margin: 0 (-(lh(.5))) lh();
background: $cream;
border-bottom: 1px solid darken($cream, 10%);
margin: 0 (-(lh(.5))) lh();
padding: lh(.5);
}
h2 {
float: left;
width: flex-grid(2, 9);
margin: 0 flex-gutter() 0 0;
width: flex-grid(2, 9);
}
section.update-description {
float: left;
width: flex-grid(7, 9);
margin-bottom: 0;
width: flex-grid(7, 9);
li {
margin-bottom: lh(.5);
......@@ -55,9 +55,9 @@ div.info-wrapper {
section.handouts {
@extend .sidebar;
border-left: 1px solid #d3d3d3;
@include border-radius(0 4px 4px 0);
border-right: 0;
border-left: 1px solid #d3d3d3;
header {
@extend .bottom-border;
......@@ -69,32 +69,32 @@ div.info-wrapper {
}
p {
color: #666;
font-size: 12px;
margin-bottom: 0;
margin-top: 4px;
font-size: 12px;
color: #666;
}
}
ol {
list-style: none;
background: none;
list-style: none;
li {
@include box-shadow(0 1px 0 #eee);
@extend .clearfix;
background: none;
border-bottom: 1px solid #d3d3d3;
@include box-shadow(0 1px 0 #eee);
@include box-sizing(border-box);
@extend .clearfix;
padding: 7px lh(.75);
background: none;
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('/static/images/treeview-default.gif');
width: 100%;
height: 100%;
max-height: 20px;
display: block;
position: absolute;
height: 100%;
left: lh(.75);
margin-left: 0;
max-height: 20px;
position: absolute;
width: 100%;
&:hover {
opacity: 0.6;
......@@ -140,27 +140,27 @@ div.info-wrapper {
h3 {
border-bottom: 0;
text-transform: uppercase;
font-weight: bold;
color: #999;
@include box-shadow(none);
color: #999;
font-size: 12px;
font-weight: bold;
text-transform: uppercase;
}
p {
font-size: $body-font-size;
letter-spacing: 0;
margin: 0;
text-transform: none;
letter-spacing: 0;
font-size: $body-font-size;
a {
padding-right: 8px;
&:before {
color: #ccc;
content: "•";
@include inline-block();
padding-right: 8px;
color: #ccc;
}
&:first-child {
......@@ -173,10 +173,10 @@ div.info-wrapper {
}
a {
@include transition();
color: lighten($text-color, 10%);
text-decoration: none;
@include inline-block();
text-decoration: none;
@include transition();
&:hover {
color: $mit-red;
......
......@@ -4,14 +4,14 @@ div.profile-wrapper {
section.user-info {
@extend .sidebar;
@include border-radius(0px 4px 4px 0);
border-left: 1px solid #d3d3d3;
@include border-radius(0px 4px 4px 0);
border-right: 0;
header {
padding: lh(.5) lh();
margin: 0 ;
@extend .bottom-border;
margin: 0 ;
padding: lh(.5) lh();
h1 {
font-size: 18px;
......@@ -20,12 +20,12 @@ div.profile-wrapper {
}
a {
color: #999;
font-size: 12px;
position: absolute;
top: 13px;
right: lh(.5);
text-transform: uppercase;
font-size: 12px;
color: #999;
top: 13px;
&:hover {
color: #555;
......@@ -37,14 +37,14 @@ div.profile-wrapper {
list-style: none;
li {
@include transition();
border-bottom: 1px solid #d3d3d3;
@include box-shadow(0 1px 0 #eee);
color: lighten($text-color, 10%);
display: block;
text-decoration: none;
@include box-shadow(0 1px 0 #eee);
padding: 7px lh();
border-bottom: 1px solid #d3d3d3;
position: relative;
text-decoration: none;
@include transition();
div#location_sub, div#language_sub {
font-weight: bold;
......@@ -57,9 +57,9 @@ div.profile-wrapper {
input {
&[type="text"] {
@include box-sizing(border-box);
margin: lh(.5) 0;
width: 100%;
@include box-sizing(border-box);
}
&[type="input"]{
......@@ -80,12 +80,12 @@ div.profile-wrapper {
a.edit-email,
a.name-edit,
a.email-edit {
color: #999;
font-size: 12px;
position: absolute;
top: 9px;
right: lh(.5);
text-transform: uppercase;
font-size: 12px;
color: #999;
top: 9px;
&:hover {
color: #555;
......@@ -93,10 +93,10 @@ div.profile-wrapper {
}
p {
color: #999;
font-size: 12px;
margin-bottom: 0;
margin-top: 4px;
color: #999;
}
a.deactivate {
......@@ -132,10 +132,10 @@ div.profile-wrapper {
padding: 7px lh();
h2 {
margin-top: 0;
font-size: $body-font-size;
font-weight: bold;
margin-top: 0;
text-transform: uppercase;
font-size: $body-font-size;
}
}
}
......@@ -148,14 +148,14 @@ div.profile-wrapper {
@extend .clearfix;
h1 {
margin: 0;
float: left;
margin: 0;
}
}
div#grade-detail-graph {
width: 100%;
min-height: 300px;
width: 100%;
}
> ol {
......
......@@ -3,8 +3,8 @@ div.book-wrapper {
section.book-sidebar {
@extend .sidebar;
@include box-sizing(border-box);
@extend .tran;
@include box-sizing(border-box);
ul#booknav {
font-size: 12px;
......@@ -22,14 +22,14 @@ div.book-wrapper {
padding-left: 30px;
div.hitarea {
margin-left: -22px;
background-image: url('/static/images/treeview-default.gif');
margin-left: -22px;
position: relative;
top: 4px;
&:hover {
opacity: 0.6;
filter: alpha(opacity=60);
opacity: 0.6;
}
}
......@@ -63,13 +63,13 @@ div.book-wrapper {
li {
&.last {
float: left;
display: block;
float: left;
a {
@include box-shadow(inset -1px 0 0 lighten(#f6efd4, 5%));
border-right: 1px solid darken(#f6efd4, 20%);
border-left: 0;
border-right: 1px solid darken(#f6efd4, 20%);
@include box-shadow(inset -1px 0 0 lighten(#f6efd4, 5%));
}
}
......@@ -81,10 +81,10 @@ div.book-wrapper {
}
&.bottom-nav {
margin-top: lh();
margin-bottom: -(lh());
border-bottom: 0;
border-top: 1px solid #EDDFAA;
margin-bottom: -(lh());
margin-top: lh();
}
}
......@@ -110,18 +110,18 @@ div.book-wrapper {
}
h2 {
padding: 0;
visibility: hidden;
width: 10px;
padding: 0;
}
}
ul#booknav {
max-height: 100px;
overflow: hidden;
padding: 0;
visibility: hidden;
width: 10px;
padding: 0;
overflow: hidden;
max-height: 100px;
}
}
......
......@@ -35,10 +35,10 @@ img {
}
#{$all-text-inputs}, textarea {
@include box-shadow(0 -1px 0 #fff);
@include linear-gradient(#eee, #fff);
border: 1px solid #999;
@include box-shadow(0 -1px 0 #fff);
font: $body-font-size $body-font-family;
@include linear-gradient(#eee, #fff);
padding: 4px;
&:focus {
......
.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;
cursor: pointer;
font: normal $body-font-size $body-font-family;
@include linear-gradient(#fff, lighten(#888, 40%));
padding: 4px 8px;
text-decoration: none;
cursor: pointer;
-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;
}
// 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;
......
// Variables
// ---------------------------------------- //
// fonts
// 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;
// JM MOSFET AMPLIFIER
section.tool-wrapper {
@extend .clearfix;
background: #073642;
border-top: 1px solid darken(#002b36, 10%);
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%));
margin: lh() (-(lh())) 0;
color: #839496;
@extend .clearfix;
display: table;
margin: lh() (-(lh())) 0;
div#graph-container {
background: none;
......@@ -29,24 +29,24 @@ section.tool-wrapper {
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%;
@include border-radius(0);
border-bottom: 1px solid darken(#073642, 8%);
li {
margin-bottom: 0;
background: none;
color: #fff;
border: none;
@include border-radius(0);
color: #fff;
margin-bottom: 0;
&.ui-tabs-selected {
border-right: 1px solid darken(#073642, 8%);
border-left: 1px solid darken(#073642, 8%);
background-color: #073642;
border-left: 1px solid darken(#073642, 8%);
border-right: 1px solid darken(#073642, 8%);
&:first-child {
border-left: none;
......@@ -59,10 +59,10 @@ section.tool-wrapper {
a {
border: none;
color: #839496;
font: bold 12px $body-font-family;
text-transform: uppercase;
letter-spacing: 1px;
color: #839496;
text-transform: uppercase;
&:hover {
color: #eee8d5;
......@@ -86,18 +86,18 @@ section.tool-wrapper {
div.graph-controls {
div.music-wrapper {
padding: 0 0 lh();
margin-bottom: lh();
@extend .clearfix;
border-bottom: 1px solid darken(#073642, 10%);
@include box-shadow(0 1px 0 lighten(#073642, 2%));
@extend .clearfix;
margin-bottom: lh();
padding: 0 0 lh();
input#playButton {
display: block;
@include button(simple, lighten( #586e75, 5% ));
font: bold 14px $body-font-family;
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);
......@@ -115,21 +115,21 @@ section.tool-wrapper {
}
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();
padding: 0 0 lh();
margin-bottom: lh();
border-bottom: 1px solid darken(#073642, 10%);
@include box-shadow(0 1px 0 lighten(#073642, 2%));
@extend .clearfix;
padding: 0 0 lh();
}
p {
font-weight: bold;
@include inline-block();
margin: 0;
-webkit-font-smoothing: antialiased;
font-weight: bold;
text-shadow: 0 -1px 0 darken(#073642, 10%);
-webkit-font-smoothing: antialiased;
}
ul {
......@@ -147,19 +147,19 @@ section.tool-wrapper {
}
div#graph-listen {
margin-top: 8px;
margin-right: 20px;
display: block;
text-align: right;
float: left;
margin-bottom: 0;
margin-right: 20px;
margin-top: 8px;
text-align: right;
}
}
label {
@include border-radius(2px);
font-weight: bold;
color: #fff;
font-weight: bold;
padding: 3px;
-webkit-font-smoothing: antialiased;
}
......@@ -188,33 +188,31 @@ section.tool-wrapper {
}
div.schematic-sliders {
div.top-sliders {
padding: 0 0 lh();
margin-bottom: lh();
@extend .clearfix;
border-bottom: 1px solid darken(#073642, 10%);
@include box-shadow(0 1px 0 lighten(#073642, 2%));
@extend .clearfix;
margin-bottom: lh();
padding: 0 0 lh();
select#musicTypeSelect {
@include inline-block();
font: 16px $body-font-family;
@include inline-block();
margin-bottom: 0;
}
p {
font-weight: bold;
@include inline-block();
-webkit-font-smoothing: antialiased;
text-shadow: 0 -1px 0 darken(#073642, 10%);
margin: 0 lh(.5) lh() 0;
font-weight: bold;
text-shadow: 0 -1px 0 darken(#073642, 10%);
-webkit-font-smoothing: antialiased;
}
}
div.slider-label {
margin-bottom: lh(0.5);
font-weight: bold;
margin-bottom: lh(0.5);
text-shadow: 0 -1px 0 darken(#073642, 10%);
-webkit-font-smoothing: antialiased;
}
......@@ -223,10 +221,10 @@ section.tool-wrapper {
margin-bottom: lh(1);
&.ui-slider-horizontal {
height: 0.4em;
background: darken(#002b36, 2%);
border: 1px solid darken(#002b36, 8%);
@include box-shadow(none);
height: 0.4em;
}
.ui-slider-handle {
......@@ -243,5 +241,3 @@ section.tool-wrapper {
}
}
}
......@@ -18,6 +18,8 @@ div.course-wrapper {
section.course-content {
@extend .content;
overflow: hidden;
@include border-top-right-radius(4px);
@include border-bottom-right-radius(4px);
h1 {
margin: 0 0 lh();
......
nav.sequence-nav {
@extend .topbar;
@include box-sizing(border-box);
border-bottom: 1px solid darken($cream, 20%);
margin-bottom: $body-line-height;
position: relative;
top: -1px;
@include border-top-right-radius(4px);
ol {
border-bottom: 1px solid darken($cream, 20%);
border-top: 1px solid darken($cream, 20%);
@include box-sizing(border-box);
display: table;
height: 100%;
padding-right: flex-grid(1, 9);
width: 100%;
......@@ -63,13 +62,10 @@ 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;
......@@ -126,28 +122,22 @@ nav.sequence-nav {
background-image: url('/static/images/sequence-nav/list-icon-current.png');
background-position: center;
}
}
p {
// display: none;
// visibility: hidden;
background: #333;
color: #fff;
display: none;
line-height: lh();
margin: 0px 0 0 -5px;
left: 0px;
opacity: 0;
padding: 6px;
position: absolute;
top: 48px;
text-shadow: 0 -1px 0 #000;
@include transition(all, .6s, $ease-in-out-quart);
white-space: pre-wrap;
@include transition(all, .1s, $ease-in-out-quart);
white-space: pre;
z-index: 99;
&.shown {
margin-top: 4px;
opacity: 1;
}
&:empty {
background: none;
......@@ -168,12 +158,21 @@ nav.sequence-nav {
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;
......@@ -223,6 +222,7 @@ nav.sequence-nav {
&.next {
a {
background-image: url('/static/images/sequence-nav/next-icon.png');
@include border-top-right-radius(4px);
&:hover {
background-color: none;
......
......@@ -26,6 +26,7 @@ section.course-index {
}
&.ui-state-active {
@include background-image(linear-gradient(-90deg, rgb(245,245,245), rgb(225,225,225)));
@extend .active;
}
}
......@@ -33,57 +34,97 @@ 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;
padding: 5px 36px 5px 10px;
&.active {
a {
text-decoration: none;
display: block;
color: #666;
p {
font-weight: bold;
margin-bottom: 0;
span.subtitle {
color: #666;
font-weight: normal;
display: block;
}
}
// &:after {
// content: " ";
// width: 16px;
// height: 16px;
// position: absolute;
// right: -35px;
// top: 7px;
// display: block;
// background-color: #dadada;
// border-top: 1px solid #c3c3c3;
// border-right: 1px solid #c3c3c3;
// z-index: 99;
// @include transform(rotate(45deg));
// }
}
a {
text-decoration: none;
margin-bottom: lh(.5);
&:after {
background: transparent;
border-top: 1px solid rgb(180,180,180);
border-right: 1px solid rgb(180,180,180);
content: "";
display: block;
color: #000;
height: 12px;
margin-top: -6px;
opacity: 0;
position: absolute;
top: 50%;
right: 30px;
@include transform(rotate(45deg));
width: 12px;
}
&:hover {
color: #666;
@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);
}
p {
margin-bottom: 0;
> 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 {
color: #666;
display: block;
font-weight: normal;
}
&:after {
opacity: 1;
right: 15px;
}
}
}
......
......@@ -14,56 +14,53 @@ section.course-content {
}
}
div.video {
}
div.video-subtitles {
@include clearfix();
padding: 6px lh();
margin: 0 (-(lh()));
border-top: 1px solid #e1e1e1;
border-bottom: 1px solid #e1e1e1;
background: #f3f3f3;
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%;
}
}
section.video-controls {
@extend .clearfix;
background: #333;
position: relative;
border: 1px solid #000;
border-top: 0;
color: #ccc;
position: relative;
&:hover {
ul, div {
......@@ -73,12 +70,12 @@ section.course-content {
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);
......@@ -96,54 +93,53 @@ section.course-content {
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 {
background: $mit-red;
border-bottom: 1px solid darken($mit-red, 20%);
border-right: 1px solid darken($mit-red, 20%);
bottom: -5px;
content: " ";
width: 7px;
height: 7px;
display: block;
position: absolute;
bottom: -5px;
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 {
@include border-radius(15px);
@include box-shadow(inset 0 1px 0 lighten($mit-red, 10%));
background: $mit-red url(/static/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%));
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;
}
}
}
ul.vcr {
float: left;
margin-right: lh();
@extend .dullify;
float: left;
list-style: none;
margin-right: lh();
padding: 0;
li {
......@@ -151,17 +147,16 @@ section.course-content {
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;
display: block;
line-height: 46px;
padding: 0 lh(.75);
text-indent: -9999px;
width: 14px;
@include transition();
width: 14px;
&.play {
background: url('/static/images/play-icon.png') center center no-repeat;
......@@ -178,21 +173,20 @@ 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 {
float: right;
@extend .dullify;
float: right;
div.speeds {
float: left;
......@@ -201,10 +195,11 @@ section.course-content {
background: url('/static/images/closed-arrow.png') 10px center no-repeat;
border-left: 1px solid #000;
border-right: 1px solid #000;
display: block;
@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;
......@@ -212,30 +207,29 @@ section.course-content {
@include transition();
-webkit-font-smoothing: antialiased;
width: 110px;
color: #fff;
&.open {
background: url('/static/images/open-arrow.png') 10px center no-repeat;
ol#video_speeds {
opacity: 1;
display: block;
opacity: 1;
}
}
h3 {
color: #999;
float: left;
font-size: 12px;
font-weight: normal;
float: left;
letter-spacing: 1px;
padding: 0 lh(.25) 0 lh(.5);
text-transform: uppercase;
}
p.active {
font-weight: bold;
float: left;
font-weight: bold;
margin-bottom: 0;
padding: 0 lh(.5) 0 0;
}
......@@ -245,14 +239,14 @@ section.course-content {
background-color: #444;
border: 1px solid #000;
@include box-shadow(inset 1px 0 0 #555, 0 3px 0 #444);
left: -1px;
display: none;
left: -1px;
opacity: 0;
position: absolute;
top:0;
@include transition();
width: 100%;
z-index: 10;
@include transition();
opacity: 0;
li {
border-bottom: 1px solid #000;
......@@ -267,20 +261,20 @@ section.course-content {
&:last-child {
border-bottom: 0;
margin-top: 0;
@include box-shadow(none);
margin-top: 0;
}
&:hover {
color: #aaa;
background-color: #666;
color: #aaa;
}
}
}
&:hover {
opacity: 1;
background-color: #444;
opacity: 1;
}
}
}
......@@ -299,34 +293,33 @@ section.course-content {
@include transition();
width: 30px;
&:hover {
background-color: #444;
color: #fff;
text-decoration: none;
background-color: #444;
}
}
a.hide-subtitles {
float: left;
display: block;
padding: 0 lh(.5);
margin-left: 0;
background: url('/static/images/cc.png') center no-repeat;
color: #797979;
line-height: 46px; //height of play pause buttons
width: 30px;
text-indent: -9999px;
display: block;
float: left;
font-weight: 800;
background: url('/static/images/cc.png') center no-repeat;
-webkit-font-smoothing: antialiased;
@include transition();
line-height: 46px; //height of play pause buttons
margin-left: 0;
opacity: 1;
padding: 0 lh(.5);
position: relative;
text-indent: -9999px;
@include transition();
-webkit-font-smoothing: antialiased;
width: 30px;
&:hover {
background-color: #444;
color: #fff;
text-decoration: none;
background-color: #444;
}
&.off {
......@@ -358,10 +351,10 @@ section.course-content {
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;
......@@ -398,43 +391,43 @@ section.course-content {
}
ol.subtitles {
width: 0px;
height: 0;
width: 0px;
}
}
&.fullscreen {
background: rgba(#000, .95);
border: 0;
margin: 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;
overflow: hidden;
&.closed {
ol.subtitles {
width: auto;
height: auto;
right: -(flex-grid(4));
width: auto;
}
}
a.exit {
position: absolute;
top: 20px;
left: 20px;
color: #aaa;
text-transform: uppercase;
letter-spacing: 1px;
font-style: 12px;
display: none;
font-style: 12px;
left: 20px;
letter-spacing: 1px;
position: absolute;
text-transform: uppercase;
top: 20px;
&::after {
content: "✖";
......@@ -453,33 +446,33 @@ section.course-content {
}
object#myytplayer, iframe {
height: 100%;
position: fixed;
top: 0;
bottom: 0;
height: 100%;
left: 0;
overflow: hidden;
position: fixed;
top: 0;
}
section.video-controls {
position: absolute;
bottom: 0;
left: 0;
position: absolute;
width: 100%;
z-index: 9999;
}
}
ol.subtitles {
position: fixed;
top: 0;
right: 0;
background: rgba(#000, .8);
bottom: 0;
height: 100%;
background: rgba(#000, .8);
padding: lh();
max-width: flex-grid(3);
max-height: 100%;
max-width: flex-grid(3);
padding: lh();
position: fixed;
right: 0;
top: 0;
@include transition();
li {
......
......@@ -2,10 +2,10 @@ li.calc-main {
bottom: -126px;
left: 0;
position: fixed;
width: 100%;
@include transition(bottom);
z-index: 99;
-webkit-appearance: none;
width: 100%;
z-index: 99;
&.open {
bottom: -36px;
......@@ -16,19 +16,19 @@ li.calc-main {
}
a.calc {
@include hide-text;
background: url("/static/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);
height: 20px;
@include hide-text;
@include inline-block;
margin-right: 10px;
padding: 8px 12px;
width: 16px;
height: 20px;
position: relative;
top: -36px;
width: 16px;
&:hover {
opacity: .8;
......@@ -41,15 +41,15 @@ li.calc-main {
div#calculator_wrapper {
background: rgba(#111, .9);
position: relative;
top: -36px;
clear: both;
max-height: 90px;
position: relative;
top: -36px;
form {
padding: lh();
@extend .clearfix;
@include box-sizing(border-box);
padding: lh();
input#calculator_button {
background: #111;
......@@ -115,10 +115,10 @@ li.calc-main {
top: 15px;
a {
background: url("/static/images/info-icon.png") center center no-repeat;
height: 17px;
@include hide-text;
width: 17px;
height: 17px;
background: url("/static/images/info-icon.png") center center no-repeat;
}
dl {
......@@ -126,14 +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;
display: none;
@include transition();
width: 500px;
&.shown {
opacity: 1;
......
......@@ -68,11 +68,11 @@ footer {
}
a {
border-bottom: 0;
display: block;
height: 29px;
width: 28px;
text-indent: -9999px;
border-bottom: 0;
width: 28px;
&:hover {
opacity: .8;
......
......@@ -100,12 +100,12 @@ div.header-wrapper {
float: left;
a {
border: none;
color: #fff;
display: block;
font-style: normal;
font-weight: bold;
padding: 10px lh() 8px;
border: none;
font-style: normal;
@media screen and (max-width: 1020px) {
padding: 10px lh(.7) 8px;
......@@ -125,10 +125,10 @@ div.header-wrapper {
ul {
li {
padding: auto;
display: table-cell;
width: 16.6666666667%;
padding: auto;
text-align: center;
width: 16.6666666667%;
}
}
}
......
......@@ -2,11 +2,11 @@ html {
margin-top: 0;
body {
background: #f4f4f4; //#f3f1e5
color: $dark-gray;
font: $body-font-size $body-font-family;
text-align: center;
margin: 0;
background: #f4f4f4; //#f3f1e5
text-align: center;
section.main-content {
@extend .clearfix;
......@@ -17,7 +17,7 @@ html {
@include box-shadow(0 0 4px #dfdfdf);
@include box-sizing(border-box);
margin-top: 3px;
// overflow: hidden;
overflow: hidden;
@media print {
border-bottom: 0;
......@@ -32,12 +32,13 @@ html {
div.qtip {
div.ui-tooltip-content {
border: none;
background: #000;
background: rgba(#000, .8);
border: none;
color: #fff;
font: 12px $body-font-family;
margin-top: -30px;
margin-right: -20px;
margin-top: -30px;
}
}
......
#lean_overlay {
background: #000;
display: none;
height:100%;
left: 0px;
position: fixed;
z-index:100;
top: 0px;
left: 0px;
height:100%;
width:100%;
background: #000;
display: none;
z-index:100;
}
div.leanModal_box {
......@@ -31,8 +31,8 @@ div.leanModal_box {
z-index: 2;
&:hover{
text-decoration: none;
color: $mit-red;
text-decoration: none;
}
}
......@@ -55,8 +55,8 @@ div.leanModal_box {
li {
&.terms, &.honor-code {
width: auto;
float: none;
width: auto;
}
div.tip {
......@@ -118,8 +118,8 @@ div.leanModal_box {
}
&.honor-code {
width: auto;
float: none;
width: auto;
}
label {
......@@ -128,8 +128,8 @@ div.leanModal_box {
}
#{$all-text-inputs}, textarea {
width: 100%;
@include box-sizing(border-box);
width: 100%;
}
input[type="checkbox"] {
......@@ -176,15 +176,15 @@ div#login {
ol {
li {
width: auto;
float: none;
width: auto;
}
}
}
div.lost-password {
text-align: left;
margin-top: lh();
text-align: left;
a {
color: #999;
......@@ -218,9 +218,9 @@ div#deactivate-account {
margin-bottom: lh(.5);
textarea, #{$all-text-inputs} {
@include box-sizing(border-box);
display: block;
width: 100%;
@include box-sizing(border-box);
}
textarea {
......
......@@ -2,5 +2,5 @@
@import "base/reset", "base/font-face", "base/functions";
// pages
@import "index/variables", "index/extends", "index/base", "index/header", "index/footer", "index/index";
@import "marketing/variables", "marketing/extends", "marketing/base", "marketing/header", "marketing/footer", "marketing/index";
@import "layout/leanmodal";
form#wiki_revision {
float: left;
width: flex-grid(6, 9);
margin-right: flex-gutter(9);
width: flex-grid(6, 9);
label {
display: block;
......@@ -12,6 +12,7 @@ form#wiki_revision {
min-height: 550px;
width: 100%;
}
.CodeMirror {
@extend textarea;
@include box-sizing(border-box);
......@@ -32,13 +33,13 @@ 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"] {
......@@ -47,10 +48,10 @@ form#wiki_revision {
}
#wiki_edit_instructions {
color: #666;
float: left;
width: flex-grid(3, 9);
margin-top: lh();
color: #666;
width: flex-grid(3, 9);
&:hover {
color: #333;
......@@ -58,16 +59,14 @@ form#wiki_revision {
.markdown-example {
background-color: #e3e3e3;
text-shadow: 0 1px 0 #fff;
line-height: 1.0;
margin: 5px 0 7px;
padding: {
top: 5px;
right: 2px;
bottom: 5px;
left: 5px;
}
margin: 5px 0 7px;
line-height: 1.0;
text-shadow: 0 1px 0 #fff;
}
}
......@@ -3,20 +3,20 @@ div#wiki_panel {
overflow: auto;
h2 {
padding: lh(.5) lh();
@extend .bottom-border;
font-size: 18px;
margin: 0 ;
@extend .bottom-border;
padding: lh(.5) lh();
}
input[type="button"] {
@extend h3;
@include transition();
color: lighten($text-color, 10%);
font-size: $body-font-size;
margin: 0 !important;
padding: 7px lh();
text-align: left;
@include transition();
width: 100%;
&:hover {
......@@ -28,8 +28,8 @@ div#wiki_panel {
ul {
li {
&.search {
@include box-shadow(0 1px 0 #eee);
border-bottom: 1px solid #d3d3d3;
@include box-shadow(0 1px 0 #eee);
padding: 7px lh();
label {
......@@ -49,15 +49,15 @@ div#wiki_panel {
div#wiki_create_form {
@extend .clearfix;
padding: 15px;
background: #d6d6d6;
border-bottom: 1px solid #bbb;
padding: 15px;
input[type="text"] {
margin-bottom: 6px;
@include box-sizing(border-box);
display: block;
margin-bottom: 6px;
width: 100%;
@include box-sizing(border-box);
}
ul {
......
......@@ -7,15 +7,14 @@ div.wiki-wrapper {
@extend .content;
position: relative;
header {
@extend .topbar;
height:46px;
@include box-shadow(inset 0 1px 0 white);
height:46px;
&:empty {
display: none !important;
border-bottom: 0;
display: none !important;
}
a {
......@@ -23,10 +22,10 @@ div.wiki-wrapper {
}
p {
float: left;
margin-bottom: 0;
color: darken($cream, 55%);
float: left;
line-height: 46px;
margin-bottom: 0;
padding-left: lh();
}
......@@ -48,8 +47,8 @@ div.wiki-wrapper {
@include box-shadow(inset 1px 0 0 lighten(#f6efd4, 5%));
color: darken($cream, 80%);
display: block;
font-weight: normal;
font-size: 12px;
font-weight: normal;
letter-spacing: 1px;
line-height: 46px;
margin: 0;
......@@ -89,15 +88,15 @@ div.wiki-wrapper {
width: flex-grid(2.5, 9);
@media screen and (max-width:900px) {
border-right: 0;
display: block;
width: auto;
border-right: 0;
}
@media print {
border-right: 0;
display: block;
width: auto;
border-right: 0;
}
}
......@@ -106,9 +105,9 @@ div.wiki-wrapper {
}
section.results {
border-left: 1px dashed #ddd;
@include box-sizing(border-box);
display: inline-block;
border-left: 1px dashed #ddd;
float: left;
padding-left: 10px;
width: flex-grid(6.5, 9);
......@@ -123,8 +122,8 @@ div.wiki-wrapper {
@media print {
display: block;
width: auto;
padding: 0;
width: auto;
canvas, img {
page-break-inside: avoid;
......@@ -140,14 +139,15 @@ div.wiki-wrapper {
}
li {
border-bottom: 1px solid #eee;
list-style: none;
margin: 0;
padding: 10px 0;
border-bottom: 1px solid #eee;
&:last-child {
border-bottom: 0;
}
h3 {
font-size: 18px;
font-weight: normal;
......@@ -155,6 +155,5 @@ div.wiki-wrapper {
}
}
}
}
}
......@@ -77,7 +77,7 @@ 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("<p>" + ${ id }titles[i-1] + "</p>");
$('#tt_'+i).append("<p>" + ${ id }titles[i-1] + "</p>");
}
......
......@@ -69,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<id>[^/]*)$', 'courseware.views.quickedit'),)
if settings.ASKBOT_ENABLED:
urlpatterns += (url(r'^%s' % settings.ASKBOT_URL, include('askbot.urls')), \
url(r'^admin/', include(admin.site.urls)), \
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment