2
README
Normal file
@@ -0,0 +1,2 @@
|
||||
This branch (re-)adds dynamic math and symbolicresponse.
|
||||
Test cases included.
|
||||
9
djangoapps/courseware/admin.py
Normal file
@@ -0,0 +1,9 @@
|
||||
'''
|
||||
django admin pages for courseware model
|
||||
'''
|
||||
|
||||
from courseware.models import *
|
||||
from django.contrib import admin
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
admin.site.register(StudentModule)
|
||||
@@ -26,7 +26,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,OptionResponse
|
||||
from responsetypes import NumericalResponse, FormulaResponse, CustomResponse, SchematicResponse, MultipleChoiceResponse, StudentInputError, TrueFalseResponse, ExternalResponse,ImageResponse,OptionResponse, SymbolicResponse
|
||||
|
||||
import calc
|
||||
import eia
|
||||
@@ -42,6 +42,7 @@ response_types = {'numericalresponse':NumericalResponse,
|
||||
'truefalseresponse':TrueFalseResponse,
|
||||
'imageresponse':ImageResponse,
|
||||
'optionresponse':OptionResponse,
|
||||
'symbolicresponse':SymbolicResponse,
|
||||
}
|
||||
entry_types = ['textline', 'schematic', 'choicegroup','textbox','imageinput','optioninput']
|
||||
solution_types = ['solution'] # extra things displayed after "show answers" is pressed
|
||||
@@ -55,6 +56,7 @@ html_transforms = {'problem': {'tag':'div'},
|
||||
"externalresponse": {'tag':'span'},
|
||||
"schematicresponse": {'tag':'span'},
|
||||
"formularesponse": {'tag':'span'},
|
||||
"symbolicresponse": {'tag':'span'},
|
||||
"multiplechoiceresponse": {'tag':'span'},
|
||||
"text": {'tag':'span'},
|
||||
"math": {'tag':'span'},
|
||||
@@ -70,7 +72,7 @@ global_context={'random':random,
|
||||
# These should be removed from HTML output, including all subelements
|
||||
html_problem_semantics = ["responseparam", "answer", "script"]
|
||||
# These should be removed from HTML output, but keeping subelements
|
||||
html_skip = ["numericalresponse", "customresponse", "schematicresponse", "formularesponse", "text","externalresponse"]
|
||||
html_skip = ["numericalresponse", "customresponse", "schematicresponse", "formularesponse", "text","externalresponse",'symbolicresponse']
|
||||
|
||||
# removed in MC
|
||||
## These should be transformed
|
||||
@@ -109,6 +111,8 @@ class LoncapaProblem(object):
|
||||
self.seed=struct.unpack('i', os.urandom(4))[0]
|
||||
|
||||
## Parse XML file
|
||||
if getattr(system,'DEBUG',False):
|
||||
log.info("[courseware.capa.capa_problem.lcp.init] fileobject = %s" % fileobject)
|
||||
file_text = fileobject.read()
|
||||
self.fileobject = fileobject # save it, so we can use for debugging information later
|
||||
# Convert startouttext and endouttext to proper <text></text>
|
||||
@@ -210,20 +214,26 @@ class LoncapaProblem(object):
|
||||
Problem XML goes to Python execution context. Runs everything in script tags
|
||||
'''
|
||||
random.seed(self.seed)
|
||||
### IKE: Why do we need these two lines?
|
||||
context = {'global_context':global_context} # save global context in here also
|
||||
global_context['context'] = context # and put link to local context in the global one
|
||||
context.update(global_context) # initialize context to have stuff in global_context
|
||||
context['__builtins__'] = globals()['__builtins__'] # put globals there also
|
||||
context['the_lcp'] = self # pass instance of LoncapaProblem in
|
||||
|
||||
#for script in tree.xpath('/problem/script'):
|
||||
for script in tree.findall('.//script'):
|
||||
stype = script.get('type')
|
||||
if stype:
|
||||
if 'javascript' in stype: continue # skip javascript
|
||||
if 'perl' in stype: continue # skip perl
|
||||
# TODO: evaluate only python
|
||||
code = script.text
|
||||
XMLESC = {"'": "'", """: '"'}
|
||||
code = unescape(code,XMLESC)
|
||||
try:
|
||||
exec code in global_context, context
|
||||
exec code in context, context # use "context" for global context; thus defs in code are global within code
|
||||
except Exception,err:
|
||||
print "[courseware.capa.capa_problem.extract_context] error %s" % err
|
||||
print "in doing exec of this code:",code
|
||||
log.exception("[courseware.capa.capa_problem.extract_context] error %s" % err)
|
||||
log.exception("in doing exec of this code: %s" % code)
|
||||
return context
|
||||
|
||||
def get_html(self):
|
||||
|
||||
@@ -197,63 +197,77 @@ def choicegroup(element, value, status, msg=''):
|
||||
type="radio"
|
||||
choices={}
|
||||
for choice in element:
|
||||
assert choice.tag =="choice", "only <choice> tags should be immediate children of a <choicegroup>"
|
||||
choices[choice.get("name")] = etree.tostring(choice[0]) # TODO: what if choice[0] has math tags in it?
|
||||
if not choice.tag=='choice':
|
||||
raise Exception,"[courseware.capa.inputtypes.choicegroup] Error only <choice> tags should be immediate children of a <choicegroup>, found %s instead" % choice.tag
|
||||
ctext = ""
|
||||
ctext += ''.join([etree.tostring(x) for x in choice]) # TODO: what if choice[0] has math tags in it?
|
||||
ctext += choice.text # TODO: fix order?
|
||||
choices[choice.get("name")] = ctext
|
||||
context={'id':eid, 'value':value, 'state':status, 'type':type, 'choices':choices}
|
||||
html=render_to_string("choicegroup.html", context)
|
||||
return etree.XML(html)
|
||||
|
||||
@register_render_function
|
||||
def textline(element, value, state, msg=""):
|
||||
'''
|
||||
Simple text line input, with optional size specification.
|
||||
'''
|
||||
if element.get('math') or element.get('dojs'): # 'dojs' flag is temporary, for backwards compatibility with 8.02x
|
||||
return SimpleInput.xml_tags['textline_dynamath'](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}
|
||||
context = {'id':eid, 'value':value, 'state':state, 'count':count, 'size': size, 'msg': msg}
|
||||
html=render_to_string("textinput.html", context)
|
||||
return etree.XML(html)
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
@register_render_function
|
||||
def js_textline(element, value, status, msg=''):
|
||||
'''
|
||||
Plan: We will inspect element to figure out type
|
||||
'''
|
||||
# TODO: Make a wrapper for <formulainput>
|
||||
# TODO: Make an AJAX loop to confirm equation is okay in real-time as user types
|
||||
## TODO: Code should follow PEP8 (4 spaces per indentation level)
|
||||
'''
|
||||
textline is used for simple one-line inputs, like formularesponse and symbolicresponse.
|
||||
'''
|
||||
eid=element.get('id')
|
||||
count = int(eid.split('_')[-2])-1 # HACK
|
||||
size = element.get('size')
|
||||
dojs = element.get('dojs') # dojs is used for client-side javascript display & return
|
||||
# when dojs=='math', a <span id=display_eid>`{::}`</span>
|
||||
# and a hidden textarea with id=input_eid_fromjs will be output
|
||||
context = {'id':eid, 'value':value, 'state':status, 'count':count, 'size': size,
|
||||
'dojs':dojs,
|
||||
'msg':msg,
|
||||
}
|
||||
html=render_to_string("jstext.html", context)
|
||||
return etree.XML(html)
|
||||
def textline_dynamath(element, value, status, msg=''):
|
||||
'''
|
||||
Text line input with dynamic math display (equation rendered on client in real time during input).
|
||||
'''
|
||||
# TODO: Make a wrapper for <formulainput>
|
||||
# TODO: Make an AJAX loop to confirm equation is okay in real-time as user types
|
||||
## TODO: Code should follow PEP8 (4 spaces per indentation level)
|
||||
'''
|
||||
textline is used for simple one-line inputs, like formularesponse and symbolicresponse.
|
||||
uses a <span id=display_eid>`{::}`</span>
|
||||
and a hidden textarea with id=input_eid_fromjs for the mathjax rendering and return.
|
||||
'''
|
||||
eid=element.get('id')
|
||||
count = int(eid.split('_')[-2])-1 # HACK
|
||||
size = element.get('size')
|
||||
context = {'id':eid, 'value':value, 'state':status, 'count':count, 'size': size,
|
||||
'msg':msg,
|
||||
}
|
||||
html=render_to_string("textinput_dynamath.html", context)
|
||||
return etree.XML(html)
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
## TODO: Make a wrapper for <codeinput>
|
||||
@register_render_function
|
||||
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.
|
||||
'''
|
||||
The textbox is used for code input. The message is the return HTML string from
|
||||
evaluating the code, eg error messages, and output from the code tests.
|
||||
|
||||
TODO: make this use rows and cols attribs, not size
|
||||
'''
|
||||
eid=element.get('id')
|
||||
count = int(eid.split('_')[-2])-1 # HACK
|
||||
size = element.get('size')
|
||||
context = {'id':eid, 'value':value, 'state':status, 'count':count, 'size': size, 'msg':msg}
|
||||
html=render_to_string("textbox.html", context)
|
||||
return etree.XML(html)
|
||||
'''
|
||||
eid=element.get('id')
|
||||
count = int(eid.split('_')[-2])-1 # HACK
|
||||
size = element.get('size')
|
||||
rows = element.get('rows') or '30'
|
||||
cols = element.get('cols') or '80'
|
||||
mode = element.get('mode') or 'python' # mode for CodeMirror, eg "python" or "xml"
|
||||
linenumbers = element.get('linenumbers') # for CodeMirror
|
||||
if not value: value = element.text # if no student input yet, then use the default input given by the problem
|
||||
context = {'id':eid, 'value':value, 'state':status, 'count':count, 'size': size, 'msg':msg,
|
||||
'mode':mode, 'linenumbers':linenumbers,
|
||||
'rows':rows, 'cols':cols,
|
||||
}
|
||||
html=render_to_string("textbox.html", context)
|
||||
return etree.XML(html)
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
@register_render_function
|
||||
|
||||
@@ -8,7 +8,9 @@ Used by capa_problem.py
|
||||
'''
|
||||
|
||||
# standard library imports
|
||||
import inspect
|
||||
import json
|
||||
import logging
|
||||
import math
|
||||
import numbers
|
||||
import numpy
|
||||
@@ -34,6 +36,8 @@ import eia
|
||||
|
||||
from util import contextualize_text
|
||||
|
||||
log = logging.getLogger("mitx.courseware")
|
||||
|
||||
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
|
||||
@@ -140,16 +144,13 @@ class TrueFalseResponse(MultipleChoiceResponse):
|
||||
|
||||
class OptionResponse(GenericResponse):
|
||||
'''
|
||||
Example:
|
||||
|
||||
<optionresponse direction="vertical" randomize="yes">
|
||||
TODO: handle direction and randomize
|
||||
'''
|
||||
snippets = [{'snippet': '''<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>
|
||||
</optionresponse>'''}]
|
||||
|
||||
TODO: handle direction and randomize
|
||||
|
||||
'''
|
||||
def __init__(self, xml, context, system=None):
|
||||
self.xml = xml
|
||||
self.answer_fields = xml.findall('optioninput')
|
||||
@@ -176,6 +177,10 @@ class OptionResponse(GenericResponse):
|
||||
class NumericalResponse(GenericResponse):
|
||||
def __init__(self, xml, context, system=None):
|
||||
self.xml = xml
|
||||
if not xml.get('answer'):
|
||||
msg = "Error in problem specification: numericalresponse missing required answer attribute\n"
|
||||
msg += "See XML source line %s" % getattr(xml,'sourceline','<unavailable>')
|
||||
raise Exception,msg
|
||||
self.correct_answer = contextualize_text(xml.get('answer'), context)
|
||||
try:
|
||||
self.tolerance_xml = xml.xpath('//*[@id=$id]//responseparam[@type="tolerance"]/@default',
|
||||
@@ -212,9 +217,9 @@ class NumericalResponse(GenericResponse):
|
||||
|
||||
class CustomResponse(GenericResponse):
|
||||
'''
|
||||
Custom response. The python code to be run should be in <answer>...</answer>. Example:
|
||||
Custom response. The python code to be run should be in <answer>...</answer>
|
||||
or in a <script>...</script>
|
||||
'''
|
||||
|
||||
snippets = [{'snippet': '''<customresponse>
|
||||
<startouttext/>
|
||||
<br/>
|
||||
@@ -233,12 +238,8 @@ class CustomResponse(GenericResponse):
|
||||
if not(r=="IS*u(t-t0)"):
|
||||
correct[0] ='incorrect'
|
||||
</answer>
|
||||
</customresponse>'''}]
|
||||
|
||||
|
||||
'''Footnote: the check function can also be defined in <script>...</script> Example:
|
||||
|
||||
<script type="loncapa/python"><![CDATA[
|
||||
</customresponse>'''},
|
||||
{'snippet': '''<script type="loncapa/python"><![CDATA[
|
||||
|
||||
def sympy_check2():
|
||||
messages[0] = '%s:%s' % (submission[0],fromjs[0].replace('<','<'))
|
||||
@@ -251,22 +252,26 @@ def sympy_check2():
|
||||
<customresponse cfn="sympy_check2" type="cs" expect="2.27E-39" dojs="math" size="30" answer="2.27E-39">
|
||||
<textline size="40" dojs="math" />
|
||||
<responseparam description="Numerical Tolerance" type="tolerance" default="0.00001" name="tol"/>
|
||||
</customresponse>
|
||||
</customresponse>'''}]
|
||||
|
||||
'''
|
||||
def __init__(self, xml, context, system=None):
|
||||
self.xml = xml
|
||||
self.system = system
|
||||
## CRITICAL TODO: Should cover all entrytypes
|
||||
## NOTE: xpath will look at root of XML tree, not just
|
||||
## what's in xml. @id=id keeps us in the right customresponse.
|
||||
self.answer_ids = xml.xpath('//*[@id=$id]//textline/@id',
|
||||
id=xml.get('id'))
|
||||
self.answer_ids += [x.get('id') for x in xml.findall('textbox')] # also allow textbox inputs
|
||||
self.context = context
|
||||
|
||||
# if <customresponse> has an "expect" attribute then save that
|
||||
self.expect = xml.get('expect')
|
||||
# if <customresponse> has an "expect" (or "answer") attribute then save that
|
||||
self.expect = xml.get('expect') or xml.get('answer')
|
||||
self.myid = xml.get('id')
|
||||
|
||||
if settings.DEBUG:
|
||||
log.info('answer_ids=%s' % self.answer_ids)
|
||||
|
||||
# the <answer>...</answer> stanza should be local to the current <customresponse>. So try looking there first.
|
||||
self.code = None
|
||||
answer = None
|
||||
@@ -279,7 +284,7 @@ def sympy_check2():
|
||||
# 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 settings.DEBUG: log.info("[courseware.capa.responsetypes] cfn = %s" % cfn)
|
||||
if cfn in context:
|
||||
self.code = context[cfn]
|
||||
else:
|
||||
@@ -310,8 +315,16 @@ def sympy_check2():
|
||||
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)
|
||||
try:
|
||||
submission = [student_answers[k] for k in idset] # ordered list of answers
|
||||
except Exception, err:
|
||||
msg = '[courseware.capa.responsetypes.customresponse] error getting student answer from %s' % student_answers
|
||||
msg += '\n idset = %s, error = %s' % (idset,err)
|
||||
log.error(msg)
|
||||
raise Exception,msg
|
||||
|
||||
# global variable in context which holds the Presentation MathML from dynamic math input
|
||||
dynamath = [ student_answers.get(k+'_dynamath',None) for k in idset ] # ordered list of dynamath responses
|
||||
|
||||
# if there is only one box, and it's empty, then don't evaluate
|
||||
if len(idset)==1 and not submission[0]:
|
||||
@@ -329,13 +342,18 @@ def sympy_check2():
|
||||
'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
|
||||
'dynamath':dynamath, # 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
|
||||
'options':self.xml.get('options'), # any options to be passed to the cfn
|
||||
'testdat':'hello world',
|
||||
})
|
||||
|
||||
# pass self.system.debug to cfn
|
||||
# if hasattr(self.system,'debug'): self.context['debug'] = self.system.debug
|
||||
self.context['debug'] = settings.DEBUG
|
||||
|
||||
# exec the check function
|
||||
if type(self.code)==str:
|
||||
try:
|
||||
@@ -348,34 +366,48 @@ def sympy_check2():
|
||||
|
||||
# this is an interface to the Tutor2 check functions
|
||||
fn = self.code
|
||||
ret = None
|
||||
if settings.DEBUG: log.info(" submission = %s" % submission)
|
||||
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)
|
||||
# handle variable number of arguments in check function, for backwards compatibility
|
||||
# with various Tutor2 check functions
|
||||
args = [self.expect,answer_given,student_answers,self.answer_ids[0]]
|
||||
argspec = inspect.getargspec(fn)
|
||||
nargs = len(argspec.args)-len(argspec.defaults or [])
|
||||
kwargs = {}
|
||||
for argname in argspec.args[nargs:]:
|
||||
kwargs[argname] = self.context[argname] if argname in self.context else None
|
||||
|
||||
if settings.DEBUG:
|
||||
log.debug('[courseware.capa.responsetypes.customresponse] answer_given=%s' % answer_given)
|
||||
log.info('nargs=%d, args=%s, kwargs=%s' % (nargs,args,kwargs))
|
||||
|
||||
ret = fn(*args[:nargs],**kwargs)
|
||||
except Exception,err:
|
||||
print "oops in customresponse (cfn) error %s" % err
|
||||
log.error("oops in customresponse (cfn) error %s" % err)
|
||||
# print "context = ",self.context
|
||||
print traceback.format_exc()
|
||||
if settings.DEBUG: print "[courseware.capa.responsetypes.customresponse.get_score] ret = ",ret
|
||||
log.error(traceback.format_exc())
|
||||
raise Exception,"oops in customresponse (cfn) error %s" % err
|
||||
if settings.DEBUG: log.info("[courseware.capa.responsetypes.customresponse.get_score] ret = %s" % ret)
|
||||
if type(ret)==dict:
|
||||
correct[0] = 'correct' if ret['ok'] else 'incorrect'
|
||||
correct = ['correct']*len(idset) if ret['ok'] else ['incorrect']*len(idset)
|
||||
msg = ret['msg']
|
||||
|
||||
if 1:
|
||||
# try to clean up message html
|
||||
msg = '<html>'+msg+'</html>'
|
||||
msg = etree.tostring(fromstring_bs(msg),pretty_print=True)
|
||||
msg = msg.replace('<','<')
|
||||
#msg = msg.replace('<','<')
|
||||
msg = etree.tostring(fromstring_bs(msg,convertEntities=None),pretty_print=True)
|
||||
#msg = etree.tostring(fromstring_bs(msg),pretty_print=True)
|
||||
msg = msg.replace(' ','')
|
||||
#msg = re.sub('<html>(.*)</html>','\\1',msg,flags=re.M|re.DOTALL) # python 2.7
|
||||
msg = re.sub('(?ms)<html>(.*)</html>','\\1',msg)
|
||||
|
||||
messages[0] = msg
|
||||
else:
|
||||
correct[0] = 'correct' if ret else 'incorrect'
|
||||
correct = ['correct']*len(idset) if ret else ['incorrect']*len(idset)
|
||||
|
||||
# build map giving "correct"ness of the answer(s)
|
||||
#correct_map = dict(zip(idset, self.context['correct']))
|
||||
@@ -403,14 +435,78 @@ def sympy_check2():
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
class ExternalResponse(GenericResponse):
|
||||
class SymbolicResponse(CustomResponse):
|
||||
"""
|
||||
Grade the student's input using an external server.
|
||||
Symbolic math response checking, using symmath library.
|
||||
"""
|
||||
snippets = [{'snippet': '''<problem>
|
||||
<text>Compute \[ \exp\left(-i \frac{\theta}{2} \left[ \begin{matrix} 0 & 1 \\ 1 & 0 \end{matrix} \right] \right) \]
|
||||
and give the resulting \(2\times 2\) matrix: <br/>
|
||||
<symbolicresponse answer="">
|
||||
<textline size="40" math="1" />
|
||||
</symbolicresponse>
|
||||
<br/>
|
||||
Your input should be typed in as a list of lists, eg <tt>[[1,2],[3,4]]</tt>.
|
||||
</text>
|
||||
</problem>'''}]
|
||||
def __init__(self, xml, context, system=None):
|
||||
xml.set('cfn','symmath_check')
|
||||
code = "from symmath import *"
|
||||
exec code in context,context
|
||||
CustomResponse.__init__(self,xml,context,system)
|
||||
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
class ExternalResponse(GenericResponse):
|
||||
'''
|
||||
Grade the students input using an external server.
|
||||
|
||||
Typically used by coding problems.
|
||||
"""
|
||||
|
||||
'''
|
||||
snippets = [{'snippet': '''<externalresponse tests="repeat:10,generate">
|
||||
<textbox rows="10" cols="70" mode="python"/>
|
||||
<answer><![CDATA[
|
||||
initial_display = """
|
||||
def inc(x):
|
||||
"""
|
||||
|
||||
answer = """
|
||||
def inc(n):
|
||||
return n+1
|
||||
"""
|
||||
preamble = """
|
||||
import sympy
|
||||
"""
|
||||
test_program = """
|
||||
import random
|
||||
|
||||
def testInc(n = None):
|
||||
if n is None:
|
||||
n = random.randint(2, 20)
|
||||
print 'Test is: inc(%d)'%n
|
||||
return str(inc(n))
|
||||
|
||||
def main():
|
||||
f = os.fdopen(3,'w')
|
||||
test = int(sys.argv[1])
|
||||
rndlist = map(int,os.getenv('rndlist').split(','))
|
||||
random.seed(rndlist[0])
|
||||
if test == 1: f.write(testInc(0))
|
||||
elif test == 2: f.write(testInc(1))
|
||||
else: f.write(testInc())
|
||||
f.close()
|
||||
|
||||
main()
|
||||
"""
|
||||
]]>
|
||||
</answer>
|
||||
</externalresponse>'''}]
|
||||
|
||||
def __init__(self, xml, context, system=None):
|
||||
self.xml = xml
|
||||
self.url = xml.get('url') or "http://eecs1.mit.edu:8889/pyloncapa" # FIXME - hardcoded URL
|
||||
self.answer_ids = xml.xpath('//*[@id=$id]//textbox/@id|//*[@id=$id]//textline/@id',
|
||||
id=xml.get('id'))
|
||||
self.context = context
|
||||
@@ -423,25 +519,66 @@ class ExternalResponse(GenericResponse):
|
||||
else:
|
||||
self.code = answer.text
|
||||
|
||||
self.tests = xml.get('answer')
|
||||
self.tests = xml.get('tests')
|
||||
|
||||
def get_score(self, student_answers):
|
||||
submission = [student_answers[k] for k in sorted(self.answer_ids)]
|
||||
self.context.update({'submission':submission})
|
||||
def do_external_request(self,cmd,extra_payload):
|
||||
'''
|
||||
Perform HTTP request / post to external server.
|
||||
|
||||
cmd = remote command to perform (str)
|
||||
extra_payload = dict of extra stuff to post.
|
||||
|
||||
Return XML tree of response (from response body)
|
||||
'''
|
||||
xmlstr = etree.tostring(self.xml, pretty_print=True)
|
||||
|
||||
payload = {'xml': xmlstr,
|
||||
### Question: Is this correct/what we want? Shouldn't this be a json.dumps?
|
||||
'LONCAPA_student_response': ''.join(submission),
|
||||
'LONCAPA_correct_answer': self.tests,
|
||||
'edX_cmd' : cmd,
|
||||
'edX_tests': self.tests,
|
||||
'processor' : self.code,
|
||||
}
|
||||
payload.update(extra_payload)
|
||||
|
||||
# call external server; TODO: get URL from settings.py
|
||||
r = requests.post("http://eecs1.mit.edu:8889/pyloncapa",data=payload)
|
||||
try:
|
||||
r = requests.post(self.url,data=payload) # call external server
|
||||
except Exception,err:
|
||||
msg = 'Error %s - cannot connect to external server url=%s' % (err,self.url)
|
||||
log.error(msg)
|
||||
raise Exception, msg
|
||||
|
||||
if settings.DEBUG: log.info('response = %s' % r.text)
|
||||
|
||||
if (not r.text ) or (not r.text.strip()):
|
||||
raise Exception,'Error: no response from external server url=%s' % self.url
|
||||
|
||||
try:
|
||||
rxml = etree.fromstring(r.text) # response is XML; prase it
|
||||
except Exception,err:
|
||||
msg = 'Error %s - cannot parse response from external server r.text=%s' % (err,r.text)
|
||||
log.error(msg)
|
||||
raise Exception, msg
|
||||
|
||||
return rxml
|
||||
|
||||
def get_score(self, student_answers):
|
||||
try:
|
||||
submission = [student_answers[k] for k in sorted(self.answer_ids)]
|
||||
except Exception,err:
|
||||
log.error('Error %s: cannot get student answer for %s; student_answers=%s' % (err,self.answer_ids,student_answers))
|
||||
raise Exception,err
|
||||
|
||||
self.context.update({'submission':submission})
|
||||
|
||||
extra_payload = {'edX_student_response': json.dumps(submission)}
|
||||
|
||||
try:
|
||||
rxml = self.do_external_request('get_score',extra_payload)
|
||||
except Exception, err:
|
||||
log.error('Error %s' % err)
|
||||
if settings.DEBUG:
|
||||
correct_map = dict(zip(sorted(self.answer_ids), ['incorrect'] * len(self.answer_ids) ))
|
||||
correct_map['msg_%s' % self.answer_ids[0]] = '<font color="red" size="+2">%s</font>' % str(err).replace('<','<')
|
||||
return correct_map
|
||||
|
||||
rxml = etree.fromstring(r.text) # response is XML; prase it
|
||||
ad = rxml.find('awarddetail').text
|
||||
admap = {'EXACT_ANS':'correct', # TODO: handle other loncapa responses
|
||||
'WRONG_FORMAT': 'incorrect',
|
||||
@@ -453,15 +590,29 @@ class ExternalResponse(GenericResponse):
|
||||
# self.context['correct'] = ['correct','correct']
|
||||
correct_map = dict(zip(sorted(self.answer_ids), self.context['correct']))
|
||||
|
||||
# TODO: separate message for each answer_id?
|
||||
correct_map['msg'] = rxml.find('message').text.replace(' ',' ') # store message in correct_map
|
||||
# store message in correct_map
|
||||
correct_map['msg_%s' % self.answer_ids[0]] = rxml.find('message').text.replace(' ',' ')
|
||||
|
||||
return correct_map
|
||||
|
||||
def get_answers(self):
|
||||
# Since this is explicitly specified in the problem, this will
|
||||
# be handled by capa_problem
|
||||
return {}
|
||||
'''
|
||||
Use external server to get expected answers
|
||||
'''
|
||||
try:
|
||||
rxml = self.do_external_request('get_answers',{})
|
||||
exans = json.loads(rxml.find('expected').text)
|
||||
except Exception,err:
|
||||
log.error('Error %s' % err)
|
||||
if settings.DEBUG:
|
||||
msg = '<font color=red size=+2>%s</font>' % str(err).replace('<','<')
|
||||
exans = [''] * len(self.answer_ids)
|
||||
exans[0] = msg
|
||||
|
||||
if not (len(exans)==len(self.answer_ids)):
|
||||
log.error('Expected %d answers from external server, only got %d!' % (len(self.answer_ids),len(exans)))
|
||||
raise Exception,'Short response from external server'
|
||||
return dict(zip(self.answer_ids,exans))
|
||||
|
||||
class StudentInputError(Exception):
|
||||
pass
|
||||
@@ -469,6 +620,27 @@ class StudentInputError(Exception):
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
class FormulaResponse(GenericResponse):
|
||||
'''
|
||||
Checking of symbolic math response using numerical sampling.
|
||||
'''
|
||||
snippets = [{'snippet': '''<problem>
|
||||
|
||||
<script type="loncapa/python">
|
||||
I = "m*c^2"
|
||||
</script>
|
||||
|
||||
<text>
|
||||
<br/>
|
||||
Give an equation for the relativistic energy of an object with mass m.
|
||||
</text>
|
||||
<formularesponse type="cs" samples="m,c@1,2:3,4#10" answer="$I">
|
||||
<responseparam description="Numerical Tolerance" type="tolerance"
|
||||
default="0.00001" name="tol" />
|
||||
<textline size="40" math="1" />
|
||||
</formularesponse>
|
||||
|
||||
</problem>'''}]
|
||||
|
||||
def __init__(self, xml, context, system=None):
|
||||
self.xml = xml
|
||||
self.correct_answer = contextualize_text(xml.get('answer'), context)
|
||||
@@ -587,15 +759,12 @@ class ImageResponse(GenericResponse):
|
||||
doesn't make sense to me (Ike). Instead, let's have it such that <imageresponse>
|
||||
should contain one or more <imageinput> stanzas. Each <imageinput> should specify
|
||||
a rectangle, given as an attribute, defining the correct answer.
|
||||
|
||||
Example:
|
||||
|
||||
<imageresponse>
|
||||
"""
|
||||
snippets = [{'snippet': '''<imageresponse>
|
||||
<imageinput src="image1.jpg" width="200" height="100" rectangle="(10,10)-(20,30)" />
|
||||
<imageinput src="image2.jpg" width="210" height="130" rectangle="(12,12)-(40,60)" />
|
||||
</imageresponse>
|
||||
</imageresponse>'''}]
|
||||
|
||||
"""
|
||||
def __init__(self, xml, context, system=None):
|
||||
self.xml = xml
|
||||
self.context = context
|
||||
@@ -621,7 +790,7 @@ class ImageResponse(GenericResponse):
|
||||
# parse given answer
|
||||
m = re.match('\[([0-9]+),([0-9]+)]',given.strip().replace(' ',''))
|
||||
if not m:
|
||||
raise Exception,'[capamodule.capa.responsetypes.imageinput] error grading %s (input=%s)' % (err,aid,given)
|
||||
raise Exception,'[capamodule.capa.responsetypes.imageinput] error grading %s (input=%s)' % (aid,given)
|
||||
(gx,gy) = [int(x) for x in m.groups()]
|
||||
|
||||
# answer is correct if (x,y) is within the specified rectangle
|
||||
|
||||
@@ -203,6 +203,10 @@ def course_file(user,coursename=None):
|
||||
else:
|
||||
tree_string = None
|
||||
|
||||
if settings.DEBUG:
|
||||
log.info('[courseware.content_parser.course_file] filename=%s, cache_key=%s' % (filename,cache_key))
|
||||
# print '[courseware.content_parser.course_file] tree_string = ',tree_string
|
||||
|
||||
if not tree_string:
|
||||
tree = course_xml_process(etree.XML(render_to_string(filename, options, namespace = 'course')))
|
||||
tree_string = etree.tostring(tree)
|
||||
@@ -231,7 +235,7 @@ def section_file(user, section, coursename=None, dironly=False):
|
||||
if dironly: return dirname
|
||||
|
||||
if filename not in os.listdir(dirname):
|
||||
print filename+" not in "+str(os.listdir(dirname))
|
||||
log.error(filename+" not in "+str(os.listdir(dirname)))
|
||||
return None
|
||||
|
||||
options = {'dev_content':settings.DEV_CONTENT,
|
||||
@@ -271,9 +275,14 @@ def module_xml(user, module, id_tag, module_id, coursename=None):
|
||||
break
|
||||
|
||||
if len(result_set)>1:
|
||||
print "WARNING: Potentially malformed course file", module, module_id
|
||||
log.error("WARNING: Potentially malformed course file", module, module_id)
|
||||
if len(result_set)==0:
|
||||
if settings.DEBUG:
|
||||
log.error('[courseware.content_parser.module_xml] cannot find %s in course.xml tree' % xpath_search)
|
||||
log.error('tree = %s' % etree.tostring(doc,pretty_print=True))
|
||||
return None
|
||||
if settings.DEBUG:
|
||||
log.info('[courseware.content_parser.module_xml] found %s' % result_set)
|
||||
return etree.tostring(result_set[0])
|
||||
#return result_set[0].serialize()
|
||||
|
||||
|
||||
@@ -35,8 +35,16 @@ class I4xSystem(object):
|
||||
self.filestore = OSFS(settings.DATA_DIR)
|
||||
else:
|
||||
self.filestore = filestore
|
||||
if settings.DEBUG:
|
||||
log.info("[courseware.module_render.I4xSystem] filestore path = %s" % filestore)
|
||||
self.render_function = render_function
|
||||
self.exception404 = Http404
|
||||
self.DEBUG = settings.DEBUG
|
||||
|
||||
def get(self,attr): # uniform access to attributes (like etree)
|
||||
return self.__dict__.get(attr)
|
||||
def set(self,attr,val): # uniform access to attributes (like etree)
|
||||
self.__dict__[attr] = val
|
||||
def __repr__(self):
|
||||
return repr(self.__dict__)
|
||||
def __str__(self):
|
||||
@@ -80,8 +88,7 @@ def grade_histogram(module_id):
|
||||
return []
|
||||
return grades
|
||||
|
||||
|
||||
def get_module(user, request, xml_module, module_object_preload):
|
||||
def get_module(user, request, xml_module, module_object_preload, position=None):
|
||||
module_type=xml_module.tag
|
||||
module_class=courseware.modules.get_module_class(module_type)
|
||||
module_id=xml_module.get('id') #module_class.id_attribute) or ""
|
||||
@@ -110,10 +117,11 @@ def get_module(user, request, xml_module, module_object_preload):
|
||||
ajax_url = settings.MITX_ROOT_URL + '/modx/'+module_type+'/'+module_id+'/'
|
||||
|
||||
system = I4xSystem(track_function = make_track_function(request),
|
||||
render_function = lambda x: render_x_module(user, request, x, module_object_preload),
|
||||
render_function = lambda x: render_x_module(user, request, x, module_object_preload, position),
|
||||
ajax_url = ajax_url,
|
||||
filestore = OSFS(data_root),
|
||||
)
|
||||
system.set('position',position) # pass URL specified position along to module, through I4xSystem
|
||||
instance=module_class(system,
|
||||
etree.tostring(xml_module),
|
||||
module_id,
|
||||
@@ -131,12 +139,30 @@ def get_module(user, request, xml_module, module_object_preload):
|
||||
|
||||
return (instance, smod, module_type)
|
||||
|
||||
def render_x_module(user, request, xml_module, module_object_preload):
|
||||
''' Generic module for extensions. This renders to HTML. '''
|
||||
def render_x_module(user, request, xml_module, module_object_preload, position=None):
|
||||
''' Generic module for extensions. This renders to HTML.
|
||||
|
||||
modules include sequential, vertical, problem, video, html
|
||||
|
||||
Note that modules can recurse. problems, video, html, can be inside sequential or vertical.
|
||||
|
||||
Arguments:
|
||||
|
||||
- user : current django User
|
||||
- request : current django HTTPrequest
|
||||
- xml_module : lxml etree of xml subtree for the current module
|
||||
- module_object_preload : list of StudentModule objects, one of which may match this module type and id
|
||||
- position : extra information from URL for user-specified position within module
|
||||
|
||||
Returns:
|
||||
|
||||
- dict which is context for HTML rendering of the specified module
|
||||
|
||||
'''
|
||||
if xml_module==None :
|
||||
return {"content":""}
|
||||
|
||||
(instance, smod, module_type) = get_module(user, request, xml_module, module_object_preload)
|
||||
(instance, smod, module_type) = get_module(user, request, xml_module, module_object_preload, position)
|
||||
|
||||
# Grab content
|
||||
content = instance.get_html()
|
||||
@@ -156,9 +182,8 @@ def render_x_module(user, request, xml_module, module_object_preload):
|
||||
|
||||
return content
|
||||
|
||||
|
||||
def modx_dispatch(request, module=None, dispatch=None, id=None):
|
||||
''' Generic view for extensions. '''
|
||||
''' Generic view for extensions. This is where AJAX calls go.'''
|
||||
if not request.user.is_authenticated():
|
||||
return redirect('/')
|
||||
|
||||
@@ -191,7 +216,7 @@ def modx_dispatch(request, module=None, dispatch=None, id=None):
|
||||
try:
|
||||
xml = content_parser.module_xml(request.user, module, 'id', id, coursename)
|
||||
except:
|
||||
log.exception("Unable to load module during ajax call")
|
||||
log.exception("Unable to load module during ajax call. module=%s, dispatch=%s, id=%s" % (module, dispatch, id))
|
||||
if accepts(request, 'text/html'):
|
||||
return render_to_response("module-error.html", {})
|
||||
else:
|
||||
@@ -228,4 +253,3 @@ 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)
|
||||
|
||||
|
||||
@@ -25,6 +25,8 @@ from multicourse import multicourse_settings
|
||||
|
||||
log = logging.getLogger("mitx.courseware")
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
class ComplexEncoder(json.JSONEncoder):
|
||||
def default(self, obj):
|
||||
if isinstance(obj, complex):
|
||||
@@ -127,7 +129,7 @@ class Module(XModule):
|
||||
|
||||
html=render_to_string('problem.html', context)
|
||||
if encapsulate:
|
||||
html = '<div id="problem_{id}" class="problem" data-url="{ajax_url}">'.format(id=self.item_id)+html+"</div>"
|
||||
html = '<div id="problem_{id}" class="problem" data-url="{ajax_url}">'.format(id=self.item_id,ajax_url=self.ajax_url)+html+"</div>"
|
||||
|
||||
return html
|
||||
|
||||
@@ -197,9 +199,27 @@ class Module(XModule):
|
||||
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, system=self.system)
|
||||
log.exception('[courseware.capa.capa_module.Module.init] error %s: cannot open file %s' % (err,self.filename))
|
||||
if self.DEBUG:
|
||||
# create a dummy problem instead of failing
|
||||
fp = StringIO.StringIO('<problem><text><font color="red" size="+2">Problem file %s is missing</font></text></problem>' % self.filename)
|
||||
fp.name = "StringIO"
|
||||
else:
|
||||
raise
|
||||
try:
|
||||
self.lcp=LoncapaProblem(fp, self.item_id, state, seed = seed, system=self.system)
|
||||
except Exception,err:
|
||||
msg = '[courseware.capa.capa_module.Module.init] error %s: cannot create LoncapaProblem %s' % (err,self.filename)
|
||||
log.exception(msg)
|
||||
if self.DEBUG:
|
||||
msg = '<p>%s</p>' % msg.replace('<','<')
|
||||
msg += '<p><pre>%s</pre></p>' % traceback.format_exc().replace('<','<')
|
||||
# create a dummy problem with error message instead of failing
|
||||
fp = StringIO.StringIO('<problem><text><font color="red" size="+2">Problem file %s has an error:</font>%s</text></problem>' % (self.filename,msg))
|
||||
fp.name = "StringIO"
|
||||
self.lcp=LoncapaProblem(fp, self.item_id, state, seed = seed, system=self.system)
|
||||
else:
|
||||
raise
|
||||
|
||||
def handle_ajax(self, dispatch, get):
|
||||
'''
|
||||
@@ -328,8 +348,15 @@ class Module(XModule):
|
||||
|
||||
self.tracker('save_problem_check', event_info)
|
||||
|
||||
try:
|
||||
html = self.get_problem_html(encapsulate=False)
|
||||
except Exception,err:
|
||||
log.error('failed to generate html')
|
||||
raise Exception,err
|
||||
|
||||
return json.dumps({'success': success,
|
||||
'contents': self.get_problem_html(encapsulate=False)})
|
||||
'contents': html,
|
||||
})
|
||||
|
||||
def save_problem(self, get):
|
||||
event_info = dict()
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import json
|
||||
import logging
|
||||
|
||||
from mitxmako.shortcuts import render_to_response, render_to_string
|
||||
|
||||
from x_module import XModule, XModuleDescriptor
|
||||
from lxml import etree
|
||||
|
||||
log = logging.getLogger("mitx.courseware")
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
class ModuleDescriptor(XModuleDescriptor):
|
||||
pass
|
||||
|
||||
@@ -28,7 +32,10 @@ class Module(XModule):
|
||||
filename="html/"+self.filename
|
||||
return self.filestore.open(filename).read()
|
||||
except: # For backwards compatibility. TODO: Remove
|
||||
return render_to_string(self.filename, {'id': self.item_id})
|
||||
if self.DEBUG:
|
||||
log.info('[courseware.modules.html_module] filename=%s' % self.filename)
|
||||
#return render_to_string(self.filename, {'id': self.item_id})
|
||||
return render_to_string(self.filename, {'id': self.item_id},namespace='course')
|
||||
|
||||
def __init__(self, system, xml, item_id, state=None):
|
||||
XModule.__init__(self, system, xml, item_id, state)
|
||||
|
||||
@@ -31,7 +31,16 @@ class Module(XModule):
|
||||
self.render()
|
||||
return self.content
|
||||
|
||||
def handle_ajax(self, dispatch, get):
|
||||
def get_init_js(self):
|
||||
self.render()
|
||||
return self.init_js
|
||||
|
||||
def get_destroy_js(self):
|
||||
self.render()
|
||||
return self.destroy_js
|
||||
|
||||
def handle_ajax(self, dispatch, get): # TODO: bounds checking
|
||||
''' get = request.POST instance '''
|
||||
if dispatch=='goto_position':
|
||||
self.position = int(get['position'])
|
||||
return json.dumps({'success':True})
|
||||
@@ -83,4 +92,8 @@ class Module(XModule):
|
||||
state = json.loads(state)
|
||||
if 'position' in state: self.position = int(state['position'])
|
||||
|
||||
# if position is specified in system, then use that instead
|
||||
if system.get('position'):
|
||||
self.position = int(system.get('position'))
|
||||
|
||||
self.rendered = False
|
||||
|
||||
@@ -55,6 +55,7 @@ class XModule(object):
|
||||
self.json = json
|
||||
self.item_id = item_id
|
||||
self.state = state
|
||||
self.DEBUG = False
|
||||
|
||||
self.__xmltree = etree.fromstring(xml) # PRIVATE
|
||||
|
||||
@@ -65,6 +66,7 @@ class XModule(object):
|
||||
self.tracker = system.track_function
|
||||
self.filestore = system.filestore
|
||||
self.render_function = system.render_function
|
||||
self.DEBUG = system.DEBUG
|
||||
self.system = system
|
||||
|
||||
### Functions used in the LMS
|
||||
|
||||
29
djangoapps/courseware/test_files/symbolicresponse.xml
Normal file
@@ -0,0 +1,29 @@
|
||||
<problem>
|
||||
<text>
|
||||
<h2>Example: Symbolic Math Response Problem</h2>
|
||||
|
||||
<p>
|
||||
A symbolic math response problem presents one or more symbolic math
|
||||
input fields for input. Correctness of input is evaluated based on
|
||||
the symbolic properties of the expression entered. The student enters
|
||||
text, but sees a proper symbolic rendition of the entered formula, in
|
||||
real time, next to the input box.
|
||||
</p>
|
||||
|
||||
<p>This is a correct answer which may be entered below: </p>
|
||||
<p><tt>cos(theta)*[[1,0],[0,1]] + i*sin(theta)*[[0,1],[1,0]]</tt></p>
|
||||
|
||||
<script>
|
||||
from symmath import *
|
||||
</script>
|
||||
<text>Compute [mathjax] U = \exp\left( i \theta \left[ \begin{matrix} 0 & 1 \\ 1 & 0 \end{matrix} \right] \right) [/mathjax]
|
||||
and give the resulting \(2 \times 2\) matrix. <br/>
|
||||
Your input should be typed in as a list of lists, eg <tt>[[1,2],[3,4]]</tt>. <br/>
|
||||
[mathjax]U=[/mathjax] <symbolicresponse cfn="symmath_check" answer="[[cos(theta),I*sin(theta)],[I*sin(theta),cos(theta)]]" options="matrix,imaginaryi" id="filenamedogi0VpEBOWedxsymmathresponse_1" state="unsubmitted">
|
||||
<textline size="80" math="1" response_id="2" answer_id="1" id="filenamedogi0VpEBOWedxsymmathresponse_2_1"/>
|
||||
</symbolicresponse>
|
||||
<br/>
|
||||
</text>
|
||||
|
||||
</text>
|
||||
</problem>
|
||||
@@ -1,3 +1,10 @@
|
||||
#
|
||||
# unittests for courseware
|
||||
#
|
||||
# Note: run this using a like like this:
|
||||
#
|
||||
# django-admin.py test --settings=envs.test_ike --pythonpath=. courseware
|
||||
|
||||
import unittest
|
||||
import os
|
||||
|
||||
@@ -127,6 +134,94 @@ class ImageResponseTest(unittest.TestCase):
|
||||
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 SymbolicResponseTest(unittest.TestCase):
|
||||
def test_sr_grade(self):
|
||||
symbolicresponse_file = os.path.dirname(__file__)+"/test_files/symbolicresponse.xml"
|
||||
test_lcp = lcp.LoncapaProblem(open(symbolicresponse_file), '1', system=i4xs)
|
||||
correct_answers = {'1_2_1':'cos(theta)*[[1,0],[0,1]] + i*sin(theta)*[[0,1],[1,0]]',
|
||||
'1_2_1_dynamath': '''
|
||||
<math xmlns="http://www.w3.org/1998/Math/MathML">
|
||||
<mstyle displaystyle="true">
|
||||
<mrow>
|
||||
<mi>cos</mi>
|
||||
<mrow>
|
||||
<mo>(</mo>
|
||||
<mi>θ</mi>
|
||||
<mo>)</mo>
|
||||
</mrow>
|
||||
</mrow>
|
||||
<mo>⋅</mo>
|
||||
<mrow>
|
||||
<mo>[</mo>
|
||||
<mtable>
|
||||
<mtr>
|
||||
<mtd>
|
||||
<mn>1</mn>
|
||||
</mtd>
|
||||
<mtd>
|
||||
<mn>0</mn>
|
||||
</mtd>
|
||||
</mtr>
|
||||
<mtr>
|
||||
<mtd>
|
||||
<mn>0</mn>
|
||||
</mtd>
|
||||
<mtd>
|
||||
<mn>1</mn>
|
||||
</mtd>
|
||||
</mtr>
|
||||
</mtable>
|
||||
<mo>]</mo>
|
||||
</mrow>
|
||||
<mo>+</mo>
|
||||
<mi>i</mi>
|
||||
<mo>⋅</mo>
|
||||
<mrow>
|
||||
<mi>sin</mi>
|
||||
<mrow>
|
||||
<mo>(</mo>
|
||||
<mi>θ</mi>
|
||||
<mo>)</mo>
|
||||
</mrow>
|
||||
</mrow>
|
||||
<mo>⋅</mo>
|
||||
<mrow>
|
||||
<mo>[</mo>
|
||||
<mtable>
|
||||
<mtr>
|
||||
<mtd>
|
||||
<mn>0</mn>
|
||||
</mtd>
|
||||
<mtd>
|
||||
<mn>1</mn>
|
||||
</mtd>
|
||||
</mtr>
|
||||
<mtr>
|
||||
<mtd>
|
||||
<mn>1</mn>
|
||||
</mtd>
|
||||
<mtd>
|
||||
<mn>0</mn>
|
||||
</mtd>
|
||||
</mtr>
|
||||
</mtable>
|
||||
<mo>]</mo>
|
||||
</mrow>
|
||||
</mstyle>
|
||||
</math>
|
||||
''',
|
||||
}
|
||||
wrong_answers = {'1_2_1':'2',
|
||||
'1_2_1_dynamath':'''
|
||||
<math xmlns="http://www.w3.org/1998/Math/MathML">
|
||||
<mstyle displaystyle="true">
|
||||
<mn>2</mn>
|
||||
</mstyle>
|
||||
</math>''',
|
||||
}
|
||||
self.assertEquals(test_lcp.grade_answers(correct_answers)['1_2_1'], 'correct')
|
||||
self.assertEquals(test_lcp.grade_answers(wrong_answers)['1_2_1'], 'incorrect')
|
||||
|
||||
class OptionResponseTest(unittest.TestCase):
|
||||
'''
|
||||
Run this with
|
||||
|
||||
@@ -11,6 +11,7 @@ from django.http import Http404
|
||||
from django.shortcuts import redirect
|
||||
from mitxmako.shortcuts import render_to_response, render_to_string
|
||||
#from django.views.decorators.csrf import ensure_csrf_cookie
|
||||
from django_future.csrf import ensure_csrf_cookie
|
||||
from django.views.decorators.cache import cache_control
|
||||
|
||||
from lxml import etree
|
||||
@@ -145,9 +146,23 @@ def render_section(request, section):
|
||||
return result
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
def index(request, course=None, chapter="Using the System", section="Hints"):
|
||||
''' Displays courseware accordion, and any associated content.
|
||||
def index(request, course=None, chapter="Using the System", section="Hints",position=None):
|
||||
''' Displays courseware accordion, and any associated content.
|
||||
|
||||
Arguments:
|
||||
|
||||
- request : HTTP request
|
||||
- course : coursename (str)
|
||||
- chapter : chapter name (str)
|
||||
- section : section name (str)
|
||||
- position : position in module, eg of <sequential> module (str)
|
||||
|
||||
Returns:
|
||||
|
||||
- HTTPresponse
|
||||
|
||||
'''
|
||||
user = request.user
|
||||
if not settings.COURSEWARE_ENABLED:
|
||||
@@ -176,12 +191,13 @@ def index(request, course=None, chapter="Using the System", section="Hints"):
|
||||
request.session['coursename'] = course # keep track of current course being viewed in django's request.session
|
||||
|
||||
try:
|
||||
# this is the course.xml etree
|
||||
dom = content_parser.course_file(user,course) # also pass course to it, for course-specific XML path
|
||||
except:
|
||||
log.exception("Unable to parse courseware xml")
|
||||
return render_to_response('courseware-error.html', {})
|
||||
|
||||
#dom_module = dom.xpath("//course[@name=$course]/chapter[@name=$chapter]//section[@name=$section]/*[1]",
|
||||
# this is the module's parent's etree
|
||||
dom_module = dom.xpath("//course[@name=$course]/chapter[@name=$chapter]//section[@name=$section]",
|
||||
course=course, chapter=chapter, section=section)
|
||||
|
||||
@@ -197,6 +213,7 @@ def index(request, course=None, chapter="Using the System", section="Hints"):
|
||||
elif module_wrapper.get("src"):
|
||||
module = content_parser.section_file(user=user, section=module_wrapper.get("src"), coursename=course)
|
||||
else:
|
||||
# this is the module's etree
|
||||
module = etree.XML(etree.tostring(module_wrapper[0])) # Copy the element out of the tree
|
||||
|
||||
module_ids = []
|
||||
@@ -217,7 +234,7 @@ def index(request, course=None, chapter="Using the System", section="Hints"):
|
||||
}
|
||||
|
||||
try:
|
||||
module = render_x_module(user, request, module, module_object_preload)
|
||||
module_context = render_x_module(user, request, module, module_object_preload, position)
|
||||
except:
|
||||
log.exception("Unable to load module")
|
||||
context.update({
|
||||
@@ -227,104 +244,48 @@ def index(request, course=None, chapter="Using the System", section="Hints"):
|
||||
return render_to_response('courseware.html', context)
|
||||
|
||||
context.update({
|
||||
'init': module.get('init_js', ''),
|
||||
'content': module['content'],
|
||||
'init': module_context.get('init_js', ''),
|
||||
'content': module_context['content'],
|
||||
})
|
||||
|
||||
result = render_to_response('courseware.html', context)
|
||||
return result
|
||||
|
||||
|
||||
def quickedit(request, id=None):
|
||||
def jump_to(request, probname=None):
|
||||
'''
|
||||
quick-edit capa problem.
|
||||
Jump to viewing a specific problem. The problem is specified by a problem name - currently the filename (minus .xml)
|
||||
of the problem. Maybe this should change to a more generic tag, eg "name" given as an attribute in <problem>.
|
||||
|
||||
We do the jump by (1) reading course.xml to find the first instance of <problem> with the given filename, then
|
||||
(2) finding the parent element of the problem, then (3) rendering that parent element with a specific computed position
|
||||
value (if it is <sequential>).
|
||||
|
||||
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 CONTENT 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
|
||||
coursename = multicourse_settings.get_coursename_from_request(request)
|
||||
xp = multicourse_settings.get_course_xmlpath(coursename) # path to XML for the course
|
||||
|
||||
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 = OSFS(settings.DATA_DIR + xp),
|
||||
#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)
|
||||
# begin by getting course.xml tree
|
||||
xml = content_parser.course_file(request.user,coursename)
|
||||
|
||||
return instance, pxmls
|
||||
# look for problem of given name
|
||||
pxml = xml.xpath('//problem[@filename="%s"]' % probname)
|
||||
if pxml: pxml = pxml[0]
|
||||
|
||||
instance, pxmls = get_lcp(coursename,id)
|
||||
# get the parent element
|
||||
parent = pxml.getparent()
|
||||
|
||||
# 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]
|
||||
# figure out chapter and section names
|
||||
chapter = None
|
||||
section = None
|
||||
branch = parent
|
||||
for k in range(4): # max depth of recursion
|
||||
if branch.tag=='section': section = branch.get('name')
|
||||
if branch.tag=='chapter': chapter = branch.get('name')
|
||||
branch = branch.getparent()
|
||||
|
||||
# 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)
|
||||
position = None
|
||||
if parent.tag=='sequential':
|
||||
position = parent.index(pxml)+1 # position in sequence
|
||||
|
||||
return index(request,course=coursename,chapter=chapter,section=section,position=position)
|
||||
|
||||
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
|
||||
|
||||
@@ -78,3 +78,4 @@ def get_course_title(coursename):
|
||||
def get_course_number(coursename):
|
||||
return get_course_property(coursename,'number')
|
||||
|
||||
|
||||
|
||||
@@ -17,6 +17,8 @@ from multicourse import multicourse_settings
|
||||
|
||||
def mitxhome(request):
|
||||
''' Home page (link from main header). List of courses. '''
|
||||
if settings.DEBUG:
|
||||
print "[djangoapps.multicourse.mitxhome] MITX_ROOT_URL = " + settings.MITX_ROOT_URL
|
||||
if settings.ENABLE_MULTICOURSE:
|
||||
context = {'courseinfo' : multicourse_settings.COURSE_SETTINGS}
|
||||
return render_to_response("mitxhome.html", context)
|
||||
|
||||
@@ -173,7 +173,8 @@ class SSLLoginBackend(ModelBackend):
|
||||
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"
|
||||
if not settings.DEBUG:
|
||||
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
|
||||
|
||||
@@ -35,7 +35,8 @@ PERFSTATS = False
|
||||
|
||||
# Features
|
||||
MITX_FEATURES = {
|
||||
'SAMPLE' : False
|
||||
'SAMPLE' : False,
|
||||
'USE_DJANGO_PIPELINE' : True,
|
||||
}
|
||||
|
||||
# Used for A/B testing
|
||||
|
||||
80
envs/dev_ike.py
Normal file
@@ -0,0 +1,80 @@
|
||||
"""
|
||||
This config file runs the simplest dev environment using sqlite, and db-based
|
||||
sessions. Assumes structure:
|
||||
|
||||
/envroot/
|
||||
/db # This is where it'll write the database file
|
||||
/mitx # The location of this repo
|
||||
/log # Where we're going to write log files
|
||||
"""
|
||||
|
||||
import socket
|
||||
|
||||
if 'eecs1' in socket.gethostname():
|
||||
MITX_ROOT_URL = '/mitx2'
|
||||
|
||||
from envs.common import *
|
||||
from envs.logsettings import get_logger_config
|
||||
from dev import *
|
||||
|
||||
if 'eecs1' in socket.gethostname():
|
||||
MITX_ROOT_URL = '/mitx2'
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# ichuang
|
||||
|
||||
DEBUG = True
|
||||
ENABLE_MULTICOURSE = True # set to False to disable multicourse display (see lib.util.views.mitxhome)
|
||||
QUICKEDIT = True
|
||||
|
||||
MITX_FEATURES['USE_DJANGO_PIPELINE'] = False
|
||||
|
||||
COURSE_SETTINGS = {'6.002_Spring_2012': {'number' : '6.002x',
|
||||
'title' : 'Circuits and Electronics',
|
||||
'xmlpath': '/6002x/',
|
||||
'active' : True,
|
||||
},
|
||||
'8.02_Spring_2013': {'number' : '8.02x',
|
||||
'title' : 'Electricity & Magnetism',
|
||||
'xmlpath': '/802x/',
|
||||
'active' : True,
|
||||
},
|
||||
'8.01_Spring_2013': {'number' : '8.01x',
|
||||
'title' : 'Mechanics',
|
||||
'xmlpath': '/801x/',
|
||||
'active' : False,
|
||||
},
|
||||
'6.189_Spring_2013': {'number' : '6.189x',
|
||||
'title' : 'IAP Python Programming',
|
||||
'xmlpath': '/6189-pytutor/',
|
||||
'active' : True,
|
||||
},
|
||||
'8.01_Summer_2012': {'number' : '8.01x',
|
||||
'title' : 'Mechanics',
|
||||
'xmlpath': '/801x-summer/',
|
||||
'active': True,
|
||||
},
|
||||
'edx4edx': {'number' : 'edX.01',
|
||||
'title' : 'edx4edx: edX Author Course',
|
||||
'xmlpath': '/edx4edx/',
|
||||
'active' : True,
|
||||
},
|
||||
}
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
MIDDLEWARE_CLASSES = MIDDLEWARE_CLASSES + (
|
||||
'ssl_auth.ssl_auth.NginxProxyHeaderMiddleware', # ssl authentication behind nginx proxy
|
||||
)
|
||||
|
||||
AUTHENTICATION_BACKENDS = (
|
||||
'ssl_auth.ssl_auth.SSLLoginBackend',
|
||||
'django.contrib.auth.backends.ModelBackend',
|
||||
)
|
||||
|
||||
INSTALLED_APPS = INSTALLED_APPS + (
|
||||
'ssl_auth',
|
||||
)
|
||||
|
||||
LOGIN_REDIRECT_URL = MITX_ROOT_URL + '/'
|
||||
LOGIN_URL = MITX_ROOT_URL + '/'
|
||||
89
envs/test_ike.py
Normal file
@@ -0,0 +1,89 @@
|
||||
"""
|
||||
This config file runs the simplest dev environment using sqlite, and db-based
|
||||
sessions. Assumes structure:
|
||||
|
||||
/envroot/
|
||||
/db # This is where it'll write the database file
|
||||
/mitx # The location of this repo
|
||||
/log # Where we're going to write log files
|
||||
"""
|
||||
from envs.common import *
|
||||
from envs.logsettings import get_logger_config
|
||||
import os
|
||||
|
||||
DEBUG = True
|
||||
|
||||
INSTALLED_APPS = [
|
||||
app
|
||||
for app
|
||||
in INSTALLED_APPS
|
||||
if not app.startswith('askbot')
|
||||
]
|
||||
|
||||
# Nose Test Runner
|
||||
INSTALLED_APPS += ['django_nose']
|
||||
#NOSE_ARGS = ['--cover-erase', '--with-xunit', '--with-xcoverage', '--cover-html', '--cover-inclusive']
|
||||
NOSE_ARGS = ['--cover-erase', '--with-xunit', '--cover-html', '--cover-inclusive']
|
||||
for app in os.listdir(PROJECT_ROOT / 'djangoapps'):
|
||||
NOSE_ARGS += ['--cover-package', app]
|
||||
TEST_RUNNER = 'django_nose.NoseTestSuiteRunner'
|
||||
|
||||
# Local Directories
|
||||
TEST_ROOT = path("test_root")
|
||||
COURSES_ROOT = TEST_ROOT / "data"
|
||||
DATA_DIR = COURSES_ROOT
|
||||
MAKO_TEMPLATES['course'] = [DATA_DIR]
|
||||
MAKO_TEMPLATES['sections'] = [DATA_DIR / 'sections']
|
||||
MAKO_TEMPLATES['custom_tags'] = [DATA_DIR / 'custom_tags']
|
||||
MAKO_TEMPLATES['main'] = [PROJECT_ROOT / 'templates',
|
||||
DATA_DIR / 'info',
|
||||
DATA_DIR / 'problems']
|
||||
|
||||
LOGGING = get_logger_config(TEST_ROOT / "log",
|
||||
logging_env="dev",
|
||||
tracking_filename="tracking.log",
|
||||
debug=True)
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': PROJECT_ROOT / "db" / "mitx.db",
|
||||
}
|
||||
}
|
||||
|
||||
CACHES = {
|
||||
# This is the cache used for most things. Askbot will not work without a
|
||||
# functioning cache -- it relies on caching to load its settings in places.
|
||||
# In staging/prod envs, the sessions also live here.
|
||||
'default': {
|
||||
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
|
||||
'LOCATION': 'mitx_loc_mem_cache',
|
||||
'KEY_FUNCTION': 'util.memcache.safe_key',
|
||||
},
|
||||
|
||||
# The general cache is what you get if you use our util.cache. It's used for
|
||||
# things like caching the course.xml file for different A/B test groups.
|
||||
# We set it to be a DummyCache to force reloading of course.xml in dev.
|
||||
# In staging environments, we would grab VERSION from data uploaded by the
|
||||
# push process.
|
||||
'general': {
|
||||
'BACKEND': 'django.core.cache.backends.dummy.DummyCache',
|
||||
'KEY_PREFIX': 'general',
|
||||
'VERSION': 4,
|
||||
'KEY_FUNCTION': 'util.memcache.safe_key',
|
||||
}
|
||||
}
|
||||
|
||||
# Dummy secret key for dev
|
||||
SECRET_KEY = '85920908f28904ed733fe576320db18cabd7b6cd'
|
||||
|
||||
############################ FILE UPLOADS (ASKBOT) #############################
|
||||
DEFAULT_FILE_STORAGE = 'django.core.files.storage.FileSystemStorage'
|
||||
MEDIA_ROOT = PROJECT_ROOT / "uploads"
|
||||
MEDIA_URL = "/static/uploads/"
|
||||
STATICFILES_DIRS.append(("uploads", MEDIA_ROOT))
|
||||
FILE_UPLOAD_TEMP_DIR = PROJECT_ROOT / "uploads"
|
||||
FILE_UPLOAD_HANDLERS = (
|
||||
'django.core.files.uploadhandler.MemoryFileUploadHandler',
|
||||
'django.core.files.uploadhandler.TemporaryFileUploadHandler',
|
||||
)
|
||||
1
lib/dogfood/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from check import *
|
||||
61
lib/dogfood/check.py
Normal file
@@ -0,0 +1,61 @@
|
||||
#!/usr/bin/python
|
||||
|
||||
from random import choice
|
||||
import string
|
||||
import traceback
|
||||
|
||||
from django.conf import settings
|
||||
import courseware.capa.capa_problem as lcp
|
||||
from dogfood.views import update_problem
|
||||
|
||||
def GenID(length=8, chars=string.letters + string.digits):
|
||||
return ''.join([choice(chars) for i in range(length)])
|
||||
|
||||
randomid = GenID()
|
||||
|
||||
def check_problem_code(ans,the_lcp,correct_answers,false_answers):
|
||||
"""
|
||||
ans = student's answer
|
||||
the_lcp = LoncapaProblem instance
|
||||
|
||||
returns dict {'ok':is_ok,'msg': message with iframe}
|
||||
"""
|
||||
pfn = "dog%s" % randomid
|
||||
pfn += the_lcp.problem_id.replace('filename','') # add problem ID to dogfood problem name
|
||||
update_problem(pfn,ans,filestore=the_lcp.system.filestore)
|
||||
msg = '<hr width="100%"/>'
|
||||
msg += '<iframe src="%s/dogfood/filename%s" width="95%%" frameborder="1">No iframe support!</iframe>' % (settings.MITX_ROOT_URL,pfn)
|
||||
msg += '<hr width="100%"/>'
|
||||
|
||||
endmsg = """<p><font size="-1" color="purple">Note: if the code text box disappears after clicking on "Check",
|
||||
please type something in the box to make it refresh properly. This is a
|
||||
bug with Chrome; it does not happen with Firefox. It is being fixed.
|
||||
</font></p>"""
|
||||
|
||||
is_ok = True
|
||||
if (not correct_answers) or (not false_answers):
|
||||
ret = {'ok':is_ok,
|
||||
'msg': msg+endmsg,
|
||||
}
|
||||
return ret
|
||||
|
||||
try:
|
||||
# check correctness
|
||||
fp = the_lcp.system.filestore.open('problems/%s.xml' % pfn)
|
||||
test_lcp = lcp.LoncapaProblem(fp, '1', system=the_lcp.system)
|
||||
|
||||
if not (test_lcp.grade_answers(correct_answers)['1_2_1']=='correct'):
|
||||
is_ok = False
|
||||
if (test_lcp.grade_answers(false_answers)['1_2_1']=='correct'):
|
||||
is_ok = False
|
||||
except Exception,err:
|
||||
is_ok = False
|
||||
msg += "<p>Error: %s</p>" % str(err).replace('<','<')
|
||||
msg += "<p><pre>%s</pre></p>" % traceback.format_exc().replace('<','<')
|
||||
|
||||
ret = {'ok':is_ok,
|
||||
'msg': msg+endmsg,
|
||||
}
|
||||
return ret
|
||||
|
||||
|
||||
297
lib/dogfood/views.py
Normal file
@@ -0,0 +1,297 @@
|
||||
'''
|
||||
dogfood.py
|
||||
|
||||
For using mitx / edX / i4x in checking itself.
|
||||
|
||||
df_capa_problem: accepts an XML file for a problem, and renders it.
|
||||
'''
|
||||
import logging
|
||||
import datetime
|
||||
import re
|
||||
import os # FIXME - use OSFS instead
|
||||
|
||||
from fs.osfs import OSFS
|
||||
|
||||
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
|
||||
from django.http import Http404
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import redirect
|
||||
from mitxmako.shortcuts import render_to_response, render_to_string
|
||||
|
||||
import courseware.capa.calc
|
||||
import track.views
|
||||
from lxml import etree
|
||||
|
||||
|
||||
from courseware.module_render import make_track_function, I4xSystem
|
||||
from courseware.models import StudentModule
|
||||
from multicourse import multicourse_settings
|
||||
from student.models import UserProfile
|
||||
from util.cache import cache
|
||||
from util.views import accepts
|
||||
|
||||
import courseware.content_parser as content_parser
|
||||
import courseware.modules
|
||||
|
||||
log = logging.getLogger("mitx.courseware")
|
||||
|
||||
etree.set_default_parser(etree.XMLParser(dtd_validation=False, load_dtd=False,
|
||||
remove_comments = True))
|
||||
|
||||
DOGFOOD_COURSENAME = 'edx_dogfood' # FIXME - should not be here; maybe in settings
|
||||
|
||||
def update_problem(pfn,pxml,coursename=None,overwrite=True,filestore=None):
|
||||
'''
|
||||
update problem with filename pfn, and content (xml) pxml.
|
||||
'''
|
||||
if not filestore:
|
||||
if not coursename: coursename = DOGFOOD_COURSENAME
|
||||
xp = multicourse_settings.get_course_xmlpath(coursename) # path to XML for the course
|
||||
pfn2 = settings.DATA_DIR + xp + 'problems/%s.xml' % pfn
|
||||
fp = open(pfn2,'w')
|
||||
else:
|
||||
pfn2 = 'problems/%s.xml' % pfn
|
||||
fp = filestore.open(pfn2,'w')
|
||||
|
||||
if os.path.exists(pfn2) and not overwrite: return # don't overwrite if already exists and overwrite=False
|
||||
pxmls = pxml if type(pxml) in [str,unicode] else etree.tostring(pxml,pretty_print=True)
|
||||
fp.write(pxmls)
|
||||
fp.close()
|
||||
|
||||
def df_capa_problem(request, id=None):
|
||||
'''
|
||||
dogfood capa problem.
|
||||
|
||||
Accepts XML for a problem, inserts it into the dogfood course.xml.
|
||||
Returns rendered problem.
|
||||
'''
|
||||
# "WARNING: UNDEPLOYABLE CODE. FOR DEV USE ONLY."
|
||||
|
||||
if settings.DEBUG:
|
||||
print '[lib.dogfood.df_capa_problem] id=%s' % id
|
||||
|
||||
if not 'coursename' in request.session:
|
||||
coursename = DOGFOOD_COURSENAME
|
||||
else:
|
||||
coursename = request.session['coursename']
|
||||
|
||||
xp = multicourse_settings.get_course_xmlpath(coursename) # path to XML for the course
|
||||
|
||||
# Grab the XML corresponding to the request from course.xml
|
||||
module = 'problem'
|
||||
|
||||
try:
|
||||
xml = content_parser.module_xml(request.user, module, 'id', id, coursename)
|
||||
except Exception,err:
|
||||
print "[lib.dogfood.df_capa_problem] error in calling content_parser: %s" % err
|
||||
xml = None
|
||||
|
||||
# if problem of given ID does not exist, then create it
|
||||
# do this only if course.xml has a section named "DogfoodProblems"
|
||||
if not xml:
|
||||
m = re.match('filename([A-Za-z0-9_]+)$',id) # extract problem filename from ID given
|
||||
if not m:
|
||||
raise Exception,'[lib.dogfood.df_capa_problem] Illegal problem id %s' % id
|
||||
pfn = m.group(1)
|
||||
print '[lib.dogfood.df_capa_problem] creating new problem pfn=%s' % pfn
|
||||
|
||||
# add problem to course.xml
|
||||
fn = settings.DATA_DIR + xp + 'course.xml'
|
||||
xml = etree.parse(fn)
|
||||
seq = xml.find('chapter/section[@name="DogfoodProblems"]/sequential') # assumes simplistic course.xml structure!
|
||||
if seq==None:
|
||||
raise Exception,"[lib.dogfood.views.df_capa_problem] missing DogfoodProblems section in course.xml!"
|
||||
newprob = etree.Element('problem')
|
||||
newprob.set('type','lecture')
|
||||
newprob.set('showanswer','attempted')
|
||||
newprob.set('rerandomize','never')
|
||||
newprob.set('title',pfn)
|
||||
newprob.set('filename',pfn)
|
||||
newprob.set('name',pfn)
|
||||
seq.append(newprob)
|
||||
fp = open(fn,'w')
|
||||
fp.write(etree.tostring(xml,pretty_print=True)) # write new XML
|
||||
fp.close()
|
||||
|
||||
# now create new problem file
|
||||
# update_problem(pfn,'<problem>\n<text>\nThis is a new problem\n</text>\n</problem>\n',coursename,overwrite=False)
|
||||
|
||||
# reset cache entry
|
||||
user = request.user
|
||||
groups = content_parser.user_groups(user)
|
||||
options = {'dev_content':settings.DEV_CONTENT,
|
||||
'groups' : groups}
|
||||
filename = xp + 'course.xml'
|
||||
cache_key = filename + "_processed?dev_content:" + str(options['dev_content']) + "&groups:" + str(sorted(groups))
|
||||
print '[lib.dogfood.df_capa_problem] cache_key = %s' % cache_key
|
||||
#cache.delete(cache_key)
|
||||
tree = content_parser.course_xml_process(xml) # add ID tags
|
||||
cache.set(cache_key,etree.tostring(tree),60)
|
||||
# settings.DEFAULT_GROUPS.append('dev') # force content_parser.course_file to not use cache
|
||||
|
||||
xml = content_parser.module_xml(request.user, module, 'id', id, coursename)
|
||||
if not xml:
|
||||
print "[lib.dogfood.df_capa_problem] problem xml not found!"
|
||||
|
||||
# add problem ID to list so that is_staff check can be bypassed
|
||||
request.session['dogfood_id'] = id
|
||||
|
||||
# hand over to quickedit to do the rest
|
||||
return quickedit(request,id=id,qetemplate='dogfood.html',coursename=coursename)
|
||||
|
||||
def quickedit(request, id=None, qetemplate='quickedit.html',coursename=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.
|
||||
|
||||
id is passed in from url resolution
|
||||
qetemplate is used by dogfood.views.dj_capa_problem, to override normal template
|
||||
'''
|
||||
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:
|
||||
if not ('dogfood_id' in request.session and request.session['dogfood_id']==id):
|
||||
return redirect('/')
|
||||
|
||||
if id=='course.xml':
|
||||
return quickedit_git_reload(request)
|
||||
|
||||
# get coursename if stored
|
||||
if not coursename:
|
||||
coursename = multicourse_settings.get_coursename_from_request(request)
|
||||
xp = multicourse_settings.get_course_xmlpath(coursename) # path to XML for the course
|
||||
|
||||
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 = OSFS(settings.DATA_DIR + xp),
|
||||
#role = 'staff' if request.user.is_staff else 'student', # TODO: generalize this
|
||||
)
|
||||
instance=courseware.modules.get_module_class(module)(system,
|
||||
xml,
|
||||
id,
|
||||
state=None)
|
||||
log.info('ajax_url = ' + instance.ajax_url)
|
||||
|
||||
# create empty student state for this problem, if not previously existing
|
||||
s = StudentModule.objects.filter(student=request.user,
|
||||
module_id=id)
|
||||
if len(s) == 0 or s is None:
|
||||
smod=StudentModule(student=request.user,
|
||||
module_type = 'problem',
|
||||
module_id=id,
|
||||
state=instance.get_state())
|
||||
smod.save()
|
||||
|
||||
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_html()
|
||||
# phtml = instance.get_problem_html()
|
||||
# init_js = instance.get_init_js()
|
||||
# destory_js = instance.get_destroy_js()
|
||||
|
||||
context = {'id':id,
|
||||
'msg' : msg,
|
||||
'lcp' : lcp,
|
||||
'filename' : lcp.fileobject.name,
|
||||
'pxmls' : pxmls,
|
||||
'phtml' : phtml,
|
||||
"destroy_js":'',
|
||||
'init_js':'',
|
||||
'csrf':csrf(request)['csrf_token'],
|
||||
}
|
||||
|
||||
result = render_to_response(qetemplate, context)
|
||||
return result
|
||||
|
||||
def quickedit_git_reload(request):
|
||||
'''
|
||||
reload course.xml and all courseware files for this course, from the git repo.
|
||||
assumes the git repo has already been setup.
|
||||
staff only.
|
||||
'''
|
||||
if not request.user.is_staff:
|
||||
return redirect('/')
|
||||
|
||||
# get coursename if stored
|
||||
coursename = multicourse_settings.get_coursename_from_request(request)
|
||||
xp = multicourse_settings.get_course_xmlpath(coursename) # path to XML for the course
|
||||
|
||||
msg = ""
|
||||
if 'cancel' in request.POST:
|
||||
return redirect("/courseware")
|
||||
|
||||
if 'gitupdate' in request.POST:
|
||||
import os # FIXME - put at top?
|
||||
cmd = "cd ../data%s; git reset --hard HEAD; git pull origin %s" % (xp,xp.replace('/',''))
|
||||
msg += '<p>cmd: %s</p>' % cmd
|
||||
ret = os.popen(cmd).read()
|
||||
msg += '<p><pre>%s</pre></p>' % ret.replace('<','<')
|
||||
msg += "<p>git update done!</p>"
|
||||
|
||||
context = {'id':id,
|
||||
'msg' : msg,
|
||||
'coursename' : coursename,
|
||||
'csrf':csrf(request)['csrf_token'],
|
||||
}
|
||||
|
||||
result = render_to_response("gitupdate.html", context)
|
||||
return result
|
||||
2
lib/symmath/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from formula import *
|
||||
from symmath_check import *
|
||||
@@ -2,7 +2,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# File: formula.py
|
||||
# Date: 04-May-12
|
||||
# Date: 04-May-12 (creation)
|
||||
# Author: I. Chuang <ichuang@mit.edu>
|
||||
#
|
||||
# flexible python representation of a symbolic mathematical formula.
|
||||
@@ -30,7 +30,7 @@ from lxml import etree
|
||||
import requests
|
||||
from copy import deepcopy
|
||||
|
||||
print "[lib.sympy_check.formula] Warning: Dark code. Needs review before enabling in prod."
|
||||
print "[lib.symmath.formula] Warning: Dark code. Needs review before enabling in prod."
|
||||
|
||||
os.environ['PYTHONIOENCODING'] = 'utf-8'
|
||||
|
||||
@@ -143,11 +143,12 @@ class formula(object):
|
||||
Representation of a mathematical formula object. Accepts mathml math expression for constructing,
|
||||
and can produce sympy translation. The formula may or may not include an assignment (=).
|
||||
'''
|
||||
def __init__(self,expr,asciimath=''):
|
||||
def __init__(self,expr,asciimath='',options=None):
|
||||
self.expr = expr.strip()
|
||||
self.asciimath = asciimath
|
||||
self.the_cmathml = None
|
||||
self.the_sympy = None
|
||||
self.options = options
|
||||
|
||||
def is_presentation_mathml(self):
|
||||
return '<mstyle' in self.expr
|
||||
@@ -234,7 +235,10 @@ class formula(object):
|
||||
if self.the_cmathml: return self.the_cmathml
|
||||
|
||||
# pre-process the presentation mathml before sending it to snuggletex to convert to content mathml
|
||||
xml = self.preprocess_pmathml(self.expr)
|
||||
try:
|
||||
xml = self.preprocess_pmathml(self.expr)
|
||||
except Exception,err:
|
||||
return "<html>Error! Cannot process pmathml</html>"
|
||||
pmathml = etree.tostring(xml,pretty_print=True)
|
||||
self.the_pmathml = pmathml
|
||||
|
||||
@@ -246,7 +250,8 @@ class formula(object):
|
||||
|
||||
def make_sympy(self,xml=None):
|
||||
'''
|
||||
Return sympy expression for the math formula
|
||||
Return sympy expression for the math formula.
|
||||
The math formula is converted to Content MathML then that is parsed.
|
||||
'''
|
||||
|
||||
if self.the_sympy: return self.the_sympy
|
||||
@@ -255,7 +260,11 @@ class formula(object):
|
||||
if not self.is_mathml():
|
||||
return my_sympify(self.expr)
|
||||
if self.is_presentation_mathml():
|
||||
xml = etree.fromstring(str(self.cmathml))
|
||||
try:
|
||||
cmml = self.cmathml
|
||||
xml = etree.fromstring(str(cmml))
|
||||
except Exception,err:
|
||||
raise Exception,'Err %s while converting cmathml to xml; cmml=%s' % (err,cmml)
|
||||
xml = self.fix_greek_in_mathml(xml)
|
||||
self.the_sympy = self.make_sympy(xml[0])
|
||||
else:
|
||||
@@ -274,7 +283,7 @@ class formula(object):
|
||||
# print "divide: arg0=%s, arg1=%s" % (args[0],args[1])
|
||||
return sympy.Mul(args[0],sympy.Pow(args[1],-1))
|
||||
|
||||
def op_plus(*args): return sum(args)
|
||||
def op_plus(*args): return args[0] if len(args)==1 else op_plus(*args[:-1])+args[-1]
|
||||
def op_times(*args): return reduce(operator.mul,args)
|
||||
|
||||
def op_minus(*args):
|
||||
@@ -314,9 +323,9 @@ class formula(object):
|
||||
elif tag=='msup': return '^'.join([parsePresentationMathMLSymbol(y) for y in xml])
|
||||
raise Exception,'[parsePresentationMathMLSymbol] unknown tag %s' % tag
|
||||
|
||||
# parser tree for content MathML
|
||||
# parser tree for Content MathML
|
||||
tag = gettag(xml)
|
||||
print "tag = ",tag
|
||||
# print "tag = ",tag
|
||||
|
||||
# first do compound objects
|
||||
|
||||
@@ -325,7 +334,13 @@ class formula(object):
|
||||
if opstr in opdict:
|
||||
op = opdict[opstr]
|
||||
args = [ self.make_sympy(x) for x in xml[1:]]
|
||||
return op(*args)
|
||||
try:
|
||||
res = op(*args)
|
||||
except Exception,err:
|
||||
self.args = args
|
||||
self.op = op
|
||||
raise Exception,'[formula] error=%s failed to apply %s to args=%s' % (err,opstr,args)
|
||||
return res
|
||||
else:
|
||||
raise Exception,'[formula]: unknown operator tag %s' % (opstr)
|
||||
|
||||
@@ -348,7 +363,7 @@ class formula(object):
|
||||
return float(xml.text)
|
||||
|
||||
elif tag=='ci': # variable (symbol)
|
||||
if len(xml)>0 and (gettag(xml[0])=='msub' or gettag(xml[0])=='msup'):
|
||||
if len(xml)>0 and (gettag(xml[0])=='msub' or gettag(xml[0])=='msup'): # subscript or superscript
|
||||
usym = parsePresentationMathMLSymbol(xml[0])
|
||||
sym = sympy.Symbol(str(usym))
|
||||
else:
|
||||
@@ -356,7 +371,11 @@ class formula(object):
|
||||
if 'hat' in usym:
|
||||
sym = my_sympify(usym)
|
||||
else:
|
||||
sym = sympy.Symbol(str(usym))
|
||||
if usym=='i': print "options=",self.options
|
||||
if usym=='i' and 'imaginary' in self.options: # i = sqrt(-1)
|
||||
sym = sympy.I
|
||||
else:
|
||||
sym = sympy.Symbol(str(usym))
|
||||
return sym
|
||||
|
||||
else: # unknown tag
|
||||
@@ -459,3 +478,78 @@ def test4():
|
||||
</math>
|
||||
'''
|
||||
return formula(xmlstr)
|
||||
|
||||
def test5(): # sum of two matrices
|
||||
xmlstr = u'''
|
||||
<math xmlns="http://www.w3.org/1998/Math/MathML">
|
||||
<mstyle displaystyle="true">
|
||||
<mrow>
|
||||
<mi>cos</mi>
|
||||
<mrow>
|
||||
<mo>(</mo>
|
||||
<mi>θ</mi>
|
||||
<mo>)</mo>
|
||||
</mrow>
|
||||
</mrow>
|
||||
<mo>⋅</mo>
|
||||
<mrow>
|
||||
<mo>[</mo>
|
||||
<mtable>
|
||||
<mtr>
|
||||
<mtd>
|
||||
<mn>1</mn>
|
||||
</mtd>
|
||||
<mtd>
|
||||
<mn>0</mn>
|
||||
</mtd>
|
||||
</mtr>
|
||||
<mtr>
|
||||
<mtd>
|
||||
<mn>0</mn>
|
||||
</mtd>
|
||||
<mtd>
|
||||
<mn>1</mn>
|
||||
</mtd>
|
||||
</mtr>
|
||||
</mtable>
|
||||
<mo>]</mo>
|
||||
</mrow>
|
||||
<mo>+</mo>
|
||||
<mrow>
|
||||
<mo>[</mo>
|
||||
<mtable>
|
||||
<mtr>
|
||||
<mtd>
|
||||
<mn>0</mn>
|
||||
</mtd>
|
||||
<mtd>
|
||||
<mn>1</mn>
|
||||
</mtd>
|
||||
</mtr>
|
||||
<mtr>
|
||||
<mtd>
|
||||
<mn>1</mn>
|
||||
</mtd>
|
||||
<mtd>
|
||||
<mn>0</mn>
|
||||
</mtd>
|
||||
</mtr>
|
||||
</mtable>
|
||||
<mo>]</mo>
|
||||
</mrow>
|
||||
</mstyle>
|
||||
</math>
|
||||
'''
|
||||
return formula(xmlstr)
|
||||
|
||||
def test6(): # imaginary numbers
|
||||
xmlstr = u'''
|
||||
<math xmlns="http://www.w3.org/1998/Math/MathML">
|
||||
<mstyle displaystyle="true">
|
||||
<mn>1</mn>
|
||||
<mo>+</mo>
|
||||
<mi>i</mi>
|
||||
</mstyle>
|
||||
</math>
|
||||
'''
|
||||
return formula(xmlstr,options='imaginaryi')
|
||||
@@ -1,11 +1,10 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# File: sympy_check2.py
|
||||
# Date: 02-May-12
|
||||
# Author: I. Chuang <ichuang@mit.edu>
|
||||
# File: symmath_check.py
|
||||
# Date: 02-May-12 (creation)
|
||||
#
|
||||
# Use sympy to check for expression equality
|
||||
# Symbolic mathematical expression checker for edX. Uses sympy to check for expression equality.
|
||||
#
|
||||
# Takes in math expressions given as Presentation MathML (from ASCIIMathML), converts to Content MathML using SnuggleTeX
|
||||
|
||||
@@ -15,8 +14,14 @@ from formula import *
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# check function interface
|
||||
#
|
||||
# This is one of the main entry points to call.
|
||||
|
||||
def sympy_check(expect,ans,adict={},symtab=None,extra_options=None):
|
||||
def symmath_check_simple(expect,ans,adict={},symtab=None,extra_options=None):
|
||||
'''
|
||||
Check a symbolic mathematical expression using sympy.
|
||||
The input is an ascii string (not MathML) converted to math using sympy.sympify.
|
||||
'''
|
||||
|
||||
options = {'__MATRIX__':False,'__ABC__':False,'__LOWER__':False}
|
||||
if extra_options: options.update(extra_options)
|
||||
@@ -129,30 +134,41 @@ def check(expect,given,numerical=False,matrix=False,normphase=False,abcsym=False
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# Check function interface, which takes pmathml input
|
||||
#
|
||||
# This is one of the main entry points to call.
|
||||
|
||||
def sympy_check2(expect,ans,adict={},abname=''):
|
||||
def symmath_check(expect,ans,dynamath=None,options=None,debug=None):
|
||||
'''
|
||||
Check a symbolic mathematical expression using sympy.
|
||||
The input may be presentation MathML. Uses formula.
|
||||
'''
|
||||
|
||||
msg = ''
|
||||
# msg += '<p/>abname=%s' % abname
|
||||
# msg += '<p/>adict=%s' % (repr(adict).replace('<','<'))
|
||||
|
||||
threshold = 1.0e-3
|
||||
DEBUG = True
|
||||
DEBUG = debug
|
||||
|
||||
# options
|
||||
do_matrix = 'matrix' in (options or '')
|
||||
do_qubit = 'qubit' in (options or '')
|
||||
do_imaginary = 'imaginary' in (options or '')
|
||||
|
||||
# parse expected answer
|
||||
try:
|
||||
fexpect = my_sympify(str(expect))
|
||||
fexpect = my_sympify(str(expect),matrix=do_matrix,do_qubit=do_qubit)
|
||||
except Exception,err:
|
||||
msg += '<p>Error %s in parsing OUR expected answer "%s"</p>' % (err,expect)
|
||||
return {'ok':False,'msg':msg}
|
||||
|
||||
# if expected answer is a number, try parsing provided answer as a number also
|
||||
try:
|
||||
fans = my_sympify(str(ans))
|
||||
fans = my_sympify(str(ans),matrix=do_matrix,do_qubit=do_qubit)
|
||||
except Exception,err:
|
||||
fans = None
|
||||
|
||||
if fexpect.is_number and fans and fans.is_number:
|
||||
if hasattr(fexpect,'is_number') and fexpect.is_number and fans and hasattr(fans,'is_number') and fans.is_number:
|
||||
if abs(abs(fans-fexpect)/fexpect)<threshold:
|
||||
return {'ok':True,'msg':msg}
|
||||
else:
|
||||
@@ -164,10 +180,14 @@ def sympy_check2(expect,ans,adict={},abname=''):
|
||||
return {'ok':True,'msg':msg}
|
||||
|
||||
# convert mathml answer to formula
|
||||
mmlbox = abname+'_fromjs'
|
||||
if mmlbox in adict:
|
||||
mmlans = adict[mmlbox]
|
||||
f = formula(mmlans)
|
||||
try:
|
||||
if dynamath:
|
||||
mmlans = dynamath[0]
|
||||
except Exception,err:
|
||||
mmlans = None
|
||||
if not mmlans:
|
||||
return {'ok':False,'msg':'[symmath_check] failed to get MathML for input; dynamath=%s' % dynamath}
|
||||
f = formula(mmlans,options=options)
|
||||
|
||||
# get sympy representation of the formula
|
||||
# if DEBUG: msg += '<p/> mmlans=%s' % repr(mmlans).replace('<','<')
|
||||
@@ -175,13 +195,20 @@ def sympy_check2(expect,ans,adict={},abname=''):
|
||||
fsym = f.sympy
|
||||
msg += '<p>You entered: %s</p>' % to_latex(f.sympy)
|
||||
except Exception,err:
|
||||
msg += "<p>Error %s in converting to sympy</p>" % str(err).replace('<','<')
|
||||
if DEBUG: msg += "<p><pre>%s</pre></p>" % traceback.format_exc()
|
||||
msg += "<p>Error %s in evaluating your expression '%s' as a valid equation</p>" % (str(err).replace('<','<'),
|
||||
ans)
|
||||
if DEBUG:
|
||||
msg += '<hr>'
|
||||
msg += '<p><font color="blue">DEBUG messages:</p>'
|
||||
msg += "<p><pre>%s</pre></p>" % traceback.format_exc()
|
||||
msg += '<p>cmathml=<pre>%s</pre></p>' % f.cmathml.replace('<','<')
|
||||
msg += '<p>pmathml=<pre>%s</pre></p>' % mmlans.replace('<','<')
|
||||
msg += '<hr>'
|
||||
return {'ok':False,'msg':msg}
|
||||
|
||||
# compare with expected
|
||||
if fexpect.is_number:
|
||||
if fsym.is_number:
|
||||
if hasattr(fexpect,'is_number') and fexpect.is_number:
|
||||
if hasattr(fsym,'is_number') and fsym.is_number:
|
||||
if abs(abs(fsym-fexpect)/fexpect)<threshold:
|
||||
return {'ok':True,'msg':msg}
|
||||
return {'ok':False,'msg':msg}
|
||||
@@ -214,15 +241,21 @@ def sympy_check2(expect,ans,adict={},abname=''):
|
||||
diff = None
|
||||
|
||||
if DEBUG:
|
||||
msg += '<hr>'
|
||||
msg += '<p><font color="blue">DEBUG messages:</p>'
|
||||
msg += "<p>Got: %s</p>" % repr(fsym)
|
||||
# msg += "<p/>Got: %s" % str([type(x) for x in fsym.atoms()]).replace('<','<')
|
||||
msg += "<p>Expecting: %s</p>" % repr(fexpect).replace('**','^').replace('hat(I)','hat(i)')
|
||||
# msg += "<p/>Expecting: %s" % str([type(x) for x in fexpect.atoms()]).replace('<','<')
|
||||
if diff:
|
||||
msg += "<p>Difference: %s</p>" % to_latex(diff)
|
||||
msg += '<hr>'
|
||||
|
||||
return {'ok':False,'msg':msg,'ex':fexpect,'got':fsym}
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# tests
|
||||
|
||||
def sctest1():
|
||||
x = "1/2*(1+(k_e* Q* q)/(m *g *h^2))"
|
||||
y = '''
|
||||
@@ -20,5 +20,6 @@ fs
|
||||
django-jasmine
|
||||
beautifulsoup
|
||||
requests
|
||||
sympy
|
||||
newrelic
|
||||
glob2
|
||||
|
||||
772
static/admin/css/base.css
Normal file
@@ -0,0 +1,772 @@
|
||||
/*
|
||||
DJANGO Admin styles
|
||||
*/
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-size: 12px;
|
||||
font-family: "Lucida Grande","DejaVu Sans","Bitstream Vera Sans",Verdana,Arial,sans-serif;
|
||||
color: #333;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
/* LINKS */
|
||||
|
||||
a:link, a:visited {
|
||||
color: #5b80b2;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #036;
|
||||
}
|
||||
|
||||
a img {
|
||||
border: none;
|
||||
}
|
||||
|
||||
a.section:link, a.section:visited {
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* GLOBAL DEFAULTS */
|
||||
|
||||
p, ol, ul, dl {
|
||||
margin: .2em 0 .8em 0;
|
||||
}
|
||||
|
||||
p {
|
||||
padding: 0;
|
||||
line-height: 140%;
|
||||
}
|
||||
|
||||
h1,h2,h3,h4,h5 {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 18px;
|
||||
color: #666;
|
||||
padding: 0 6px 0 0;
|
||||
margin: 0 0 .2em 0;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 16px;
|
||||
margin: 1em 0 .5em 0;
|
||||
}
|
||||
|
||||
h2.subhead {
|
||||
font-weight: normal;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 14px;
|
||||
margin: .8em 0 .3em 0;
|
||||
color: #666;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 12px;
|
||||
margin: 1em 0 .8em 0;
|
||||
padding-bottom: 3px;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 10px;
|
||||
margin: 1.5em 0 .5em 0;
|
||||
color: #666;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
ul li {
|
||||
list-style-type: square;
|
||||
padding: 1px 0;
|
||||
}
|
||||
|
||||
ul.plainlist {
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
|
||||
ul.plainlist li {
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
li ul {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
li, dt, dd {
|
||||
font-size: 11px;
|
||||
line-height: 14px;
|
||||
}
|
||||
|
||||
dt {
|
||||
font-weight: bold;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
form {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
font-size: 11px;
|
||||
color: #777;
|
||||
margin-left: 2px;
|
||||
padding-left: 10px;
|
||||
border-left: 5px solid #ddd;
|
||||
}
|
||||
|
||||
code, pre {
|
||||
font-family: "Bitstream Vera Sans Mono", Monaco, "Courier New", Courier, monospace;
|
||||
background: inherit;
|
||||
color: #666;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
pre.literal-block {
|
||||
margin: 10px;
|
||||
background: #eee;
|
||||
padding: 6px 8px;
|
||||
}
|
||||
|
||||
code strong {
|
||||
color: #930;
|
||||
}
|
||||
|
||||
hr {
|
||||
clear: both;
|
||||
color: #eee;
|
||||
background-color: #eee;
|
||||
height: 1px;
|
||||
border: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-size: 1px;
|
||||
line-height: 1px;
|
||||
}
|
||||
|
||||
/* TEXT STYLES & MODIFIERS */
|
||||
|
||||
.small {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.tiny {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
p.tiny {
|
||||
margin-top: -2px;
|
||||
}
|
||||
|
||||
.mini {
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
p.mini {
|
||||
margin-top: -3px;
|
||||
}
|
||||
|
||||
.help, p.help {
|
||||
font-size: 10px !important;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
p img, h1 img, h2 img, h3 img, h4 img, td img {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.quiet, a.quiet:link, a.quiet:visited {
|
||||
color: #999 !important;
|
||||
font-weight: normal !important;
|
||||
}
|
||||
|
||||
.quiet strong {
|
||||
font-weight: bold !important;
|
||||
}
|
||||
|
||||
.float-right {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.float-left {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.clear {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.align-left {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.align-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.example {
|
||||
margin: 10px 0;
|
||||
padding: 5px 10px;
|
||||
background: #efefef;
|
||||
}
|
||||
|
||||
.nowrap {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* TABLES */
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
border-color: #ccc;
|
||||
}
|
||||
|
||||
td, th {
|
||||
font-size: 11px;
|
||||
line-height: 13px;
|
||||
border-bottom: 1px solid #eee;
|
||||
vertical-align: top;
|
||||
padding: 5px;
|
||||
font-family: "Lucida Grande", Verdana, Arial, sans-serif;
|
||||
}
|
||||
|
||||
th {
|
||||
text-align: left;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
thead th,
|
||||
tfoot td {
|
||||
color: #666;
|
||||
padding: 2px 5px;
|
||||
font-size: 11px;
|
||||
background: #e1e1e1 url(../img/admin/nav-bg.gif) top left repeat-x;
|
||||
border-left: 1px solid #ddd;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
tfoot td {
|
||||
border-bottom: none;
|
||||
border-top: 1px solid #ddd;
|
||||
}
|
||||
|
||||
thead th:first-child,
|
||||
tfoot td:first-child {
|
||||
border-left: none !important;
|
||||
}
|
||||
|
||||
thead th.optional {
|
||||
font-weight: normal !important;
|
||||
}
|
||||
|
||||
fieldset table {
|
||||
border-right: 1px solid #eee;
|
||||
}
|
||||
|
||||
tr.row-label td {
|
||||
font-size: 9px;
|
||||
padding-top: 2px;
|
||||
padding-bottom: 0;
|
||||
border-bottom: none;
|
||||
color: #666;
|
||||
margin-top: -1px;
|
||||
}
|
||||
|
||||
tr.alt {
|
||||
background: #f6f6f6;
|
||||
}
|
||||
|
||||
.row1 {
|
||||
background: #EDF3FE;
|
||||
}
|
||||
|
||||
.row2 {
|
||||
background: white;
|
||||
}
|
||||
|
||||
/* SORTABLE TABLES */
|
||||
|
||||
thead th a:link, thead th a:visited {
|
||||
color: #666;
|
||||
display: block;
|
||||
}
|
||||
|
||||
table thead th.sorted {
|
||||
background-position: bottom left !important;
|
||||
}
|
||||
|
||||
table thead th.sorted a {
|
||||
padding-right: 13px;
|
||||
}
|
||||
|
||||
table thead th.ascending a {
|
||||
background: url(../img/admin/arrow-up.gif) right .4em no-repeat;
|
||||
}
|
||||
|
||||
table thead th.descending a {
|
||||
background: url(../img/admin/arrow-down.gif) right .4em no-repeat;
|
||||
}
|
||||
|
||||
/* ORDERABLE TABLES */
|
||||
|
||||
table.orderable tbody tr td:hover {
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
table.orderable tbody tr td:first-child {
|
||||
padding-left: 14px;
|
||||
background-image: url(../img/admin/nav-bg-grabber.gif);
|
||||
background-repeat: repeat-y;
|
||||
}
|
||||
|
||||
table.orderable-initalized .order-cell, body>tr>td.order-cell {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* FORM DEFAULTS */
|
||||
|
||||
input, textarea, select, .form-row p {
|
||||
margin: 2px 0;
|
||||
padding: 2px 3px;
|
||||
vertical-align: middle;
|
||||
font-family: "Lucida Grande", Verdana, Arial, sans-serif;
|
||||
font-weight: normal;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
textarea {
|
||||
vertical-align: top !important;
|
||||
}
|
||||
|
||||
input[type=text], input[type=password], textarea, select, .vTextField {
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
|
||||
/* FORM BUTTONS */
|
||||
|
||||
.button, input[type=submit], input[type=button], .submit-row input {
|
||||
background: white url(../img/admin/nav-bg.gif) bottom repeat-x;
|
||||
padding: 3px 5px;
|
||||
color: black;
|
||||
border: 1px solid #bbb;
|
||||
border-color: #ddd #aaa #aaa #ddd;
|
||||
}
|
||||
|
||||
.button:active, input[type=submit]:active, input[type=button]:active {
|
||||
background-image: url(../img/admin/nav-bg-reverse.gif);
|
||||
background-position: top;
|
||||
}
|
||||
|
||||
.button[disabled], input[type=submit][disabled], input[type=button][disabled] {
|
||||
background-image: url(../img/admin/nav-bg.gif);
|
||||
background-position: bottom;
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.button.default, input[type=submit].default, .submit-row input.default {
|
||||
border: 2px solid #5b80b2;
|
||||
background: #7CA0C7 url(../img/admin/default-bg.gif) bottom repeat-x;
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
float: right;
|
||||
}
|
||||
|
||||
.button.default:active, input[type=submit].default:active {
|
||||
background-image: url(../img/admin/default-bg-reverse.gif);
|
||||
background-position: top;
|
||||
}
|
||||
|
||||
.button[disabled].default, input[type=submit][disabled].default, input[type=button][disabled].default {
|
||||
background-image: url(../img/admin/default-bg.gif);
|
||||
background-position: bottom;
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
|
||||
/* MODULES */
|
||||
|
||||
.module {
|
||||
border: 1px solid #ccc;
|
||||
margin-bottom: 5px;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.module p, .module ul, .module h3, .module h4, .module dl, .module pre {
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
.module blockquote {
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
.module ul, .module ol {
|
||||
margin-left: 1.5em;
|
||||
}
|
||||
|
||||
.module h3 {
|
||||
margin-top: .6em;
|
||||
}
|
||||
|
||||
.module h2, .module caption, .inline-group h2 {
|
||||
margin: 0;
|
||||
padding: 2px 5px 3px 5px;
|
||||
font-size: 11px;
|
||||
text-align: left;
|
||||
font-weight: bold;
|
||||
background: #7CA0C7 url(../img/admin/default-bg.gif) top left repeat-x;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.module table {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
/* MESSAGES & ERRORS */
|
||||
|
||||
ul.messagelist {
|
||||
padding: 0 0 5px 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
ul.messagelist li {
|
||||
font-size: 12px;
|
||||
display: block;
|
||||
padding: 4px 5px 4px 25px;
|
||||
margin: 0 0 3px 0;
|
||||
border-bottom: 1px solid #ddd;
|
||||
color: #666;
|
||||
background: #ffc url(../img/admin/icon_success.gif) 5px .3em no-repeat;
|
||||
}
|
||||
|
||||
ul.messagelist li.warning{
|
||||
background-image: url(../img/admin/icon_alert.gif);
|
||||
}
|
||||
|
||||
ul.messagelist li.error{
|
||||
background-image: url(../img/admin/icon_error.gif);
|
||||
}
|
||||
|
||||
.errornote {
|
||||
font-size: 12px !important;
|
||||
display: block;
|
||||
padding: 4px 5px 4px 25px;
|
||||
margin: 0 0 3px 0;
|
||||
border: 1px solid red;
|
||||
color: red;
|
||||
background: #ffc url(../img/admin/icon_error.gif) 5px .3em no-repeat;
|
||||
}
|
||||
|
||||
ul.errorlist {
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.errorlist li {
|
||||
font-size: 12px !important;
|
||||
display: block;
|
||||
padding: 4px 5px 4px 25px;
|
||||
margin: 0 0 3px 0;
|
||||
border: 1px solid red;
|
||||
color: white;
|
||||
background: red url(../img/admin/icon_alert.gif) 5px .3em no-repeat;
|
||||
}
|
||||
|
||||
.errorlist li a {
|
||||
color: white;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
td ul.errorlist {
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
td ul.errorlist li {
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.errors {
|
||||
background: #ffc;
|
||||
}
|
||||
|
||||
.errors input, .errors select, .errors textarea {
|
||||
border: 1px solid red;
|
||||
}
|
||||
|
||||
div.system-message {
|
||||
background: #ffc;
|
||||
margin: 10px;
|
||||
padding: 6px 8px;
|
||||
font-size: .8em;
|
||||
}
|
||||
|
||||
div.system-message p.system-message-title {
|
||||
padding: 4px 5px 4px 25px;
|
||||
margin: 0;
|
||||
color: red;
|
||||
background: #ffc url(../img/admin/icon_error.gif) 5px .3em no-repeat;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 12px;
|
||||
padding: 5px 0 0 12px;
|
||||
}
|
||||
|
||||
/* BREADCRUMBS */
|
||||
|
||||
div.breadcrumbs {
|
||||
background: white url(../img/admin/nav-bg-reverse.gif) 0 -10px repeat-x;
|
||||
padding: 2px 8px 3px 8px;
|
||||
font-size: 11px;
|
||||
color: #999;
|
||||
border-top: 1px solid white;
|
||||
border-bottom: 1px solid #ccc;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
/* ACTION ICONS */
|
||||
|
||||
.addlink {
|
||||
padding-left: 12px;
|
||||
background: url(../img/admin/icon_addlink.gif) 0 .2em no-repeat;
|
||||
}
|
||||
|
||||
.changelink {
|
||||
padding-left: 12px;
|
||||
background: url(../img/admin/icon_changelink.gif) 0 .2em no-repeat;
|
||||
}
|
||||
|
||||
.deletelink {
|
||||
padding-left: 12px;
|
||||
background: url(../img/admin/icon_deletelink.gif) 0 .25em no-repeat;
|
||||
}
|
||||
|
||||
a.deletelink:link, a.deletelink:visited {
|
||||
color: #CC3434;
|
||||
}
|
||||
|
||||
a.deletelink:hover {
|
||||
color: #993333;
|
||||
}
|
||||
|
||||
/* OBJECT TOOLS */
|
||||
|
||||
.object-tools {
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
font-family: Arial,Helvetica,sans-serif;
|
||||
padding-left: 0;
|
||||
float: right;
|
||||
position: relative;
|
||||
margin-top: -2.4em;
|
||||
margin-bottom: -2em;
|
||||
}
|
||||
|
||||
.form-row .object-tools {
|
||||
margin-top: 5px;
|
||||
margin-bottom: 5px;
|
||||
float: none;
|
||||
height: 2em;
|
||||
padding-left: 3.5em;
|
||||
}
|
||||
|
||||
.object-tools li {
|
||||
display: block;
|
||||
float: left;
|
||||
background: url(../img/admin/tool-left.gif) 0 0 no-repeat;
|
||||
padding: 0 0 0 8px;
|
||||
margin-left: 2px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.object-tools li:hover {
|
||||
background: url(../img/admin/tool-left_over.gif) 0 0 no-repeat;
|
||||
}
|
||||
|
||||
.object-tools a:link, .object-tools a:visited {
|
||||
display: block;
|
||||
float: left;
|
||||
color: white;
|
||||
padding: .1em 14px .1em 8px;
|
||||
height: 14px;
|
||||
background: #999 url(../img/admin/tool-right.gif) 100% 0 no-repeat;
|
||||
}
|
||||
|
||||
.object-tools a:hover, .object-tools li:hover a {
|
||||
background: #5b80b2 url(../img/admin/tool-right_over.gif) 100% 0 no-repeat;
|
||||
}
|
||||
|
||||
.object-tools a.viewsitelink, .object-tools a.golink {
|
||||
background: #999 url(../img/admin/tooltag-arrowright.gif) top right no-repeat;
|
||||
padding-right: 28px;
|
||||
}
|
||||
|
||||
.object-tools a.viewsitelink:hover, .object-tools a.golink:hover {
|
||||
background: #5b80b2 url(../img/admin/tooltag-arrowright_over.gif) top right no-repeat;
|
||||
}
|
||||
|
||||
.object-tools a.addlink {
|
||||
background: #999 url(../img/admin/tooltag-add.gif) top right no-repeat;
|
||||
padding-right: 28px;
|
||||
}
|
||||
|
||||
.object-tools a.addlink:hover {
|
||||
background: #5b80b2 url(../img/admin/tooltag-add_over.gif) top right no-repeat;
|
||||
}
|
||||
|
||||
/* OBJECT HISTORY */
|
||||
|
||||
table#change-history {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
table#change-history tbody th {
|
||||
width: 16em;
|
||||
}
|
||||
|
||||
/* PAGE STRUCTURE */
|
||||
|
||||
#container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
min-width: 760px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#content {
|
||||
margin: 10px 15px;
|
||||
}
|
||||
|
||||
#header {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#content-main {
|
||||
float: left;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#content-related {
|
||||
float: right;
|
||||
width: 18em;
|
||||
position: relative;
|
||||
margin-right: -19em;
|
||||
}
|
||||
|
||||
#footer {
|
||||
clear: both;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
/* COLUMN TYPES */
|
||||
|
||||
.colMS {
|
||||
margin-right: 20em !important;
|
||||
}
|
||||
|
||||
.colSM {
|
||||
margin-left: 20em !important;
|
||||
}
|
||||
|
||||
.colSM #content-related {
|
||||
float: left;
|
||||
margin-right: 0;
|
||||
margin-left: -19em;
|
||||
}
|
||||
|
||||
.colSM #content-main {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.popup .colM {
|
||||
width: 95%;
|
||||
}
|
||||
|
||||
.subcol {
|
||||
float: left;
|
||||
width: 46%;
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.dashboard #content {
|
||||
width: 500px;
|
||||
}
|
||||
|
||||
/* HEADER */
|
||||
|
||||
#header {
|
||||
background: #417690;
|
||||
color: #ffc;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#header a:link, #header a:visited {
|
||||
color: white;
|
||||
}
|
||||
|
||||
#header a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
#branding h1 {
|
||||
padding: 0 10px;
|
||||
font-size: 18px;
|
||||
margin: 8px 0;
|
||||
font-weight: normal;
|
||||
color: #f4f379;
|
||||
}
|
||||
|
||||
#branding h2 {
|
||||
padding: 0 10px;
|
||||
font-size: 14px;
|
||||
margin: -8px 0 8px 0;
|
||||
font-weight: normal;
|
||||
color: #ffc;
|
||||
}
|
||||
|
||||
#user-tools {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
padding: 1.2em 10px;
|
||||
font-size: 11px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* SIDEBAR */
|
||||
|
||||
#content-related h3 {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
#content-related h4 {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
#content-related .module h2 {
|
||||
background: #eee url(../img/admin/nav-bg.gif) bottom left repeat-x;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
289
static/admin/css/changelists.css
Normal file
@@ -0,0 +1,289 @@
|
||||
/* CHANGELISTS */
|
||||
|
||||
#changelist {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#changelist table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.change-list .hiddenfields { display:none; }
|
||||
|
||||
.change-list .filtered table {
|
||||
border-right: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.change-list .filtered {
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.change-list .filtered {
|
||||
background: white url(../img/admin/changelist-bg.gif) top right repeat-y !important;
|
||||
}
|
||||
|
||||
.change-list .filtered .results, .change-list .filtered .paginator, .filtered #toolbar, .filtered div.xfull {
|
||||
margin-right: 160px !important;
|
||||
width: auto !important;
|
||||
}
|
||||
|
||||
.change-list .filtered table tbody th {
|
||||
padding-right: 1em;
|
||||
}
|
||||
|
||||
#changelist .toplinks {
|
||||
border-bottom: 1px solid #ccc !important;
|
||||
}
|
||||
|
||||
#changelist .paginator {
|
||||
color: #666;
|
||||
border-top: 1px solid #eee;
|
||||
border-bottom: 1px solid #eee;
|
||||
background: white url(../img/admin/nav-bg.gif) 0 180% repeat-x;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.change-list .filtered .paginator {
|
||||
border-right: 1px solid #ddd;
|
||||
}
|
||||
|
||||
/* CHANGELIST TABLES */
|
||||
|
||||
#changelist table thead th {
|
||||
white-space: nowrap;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
#changelist table thead th.action-checkbox-column {
|
||||
width: 1.5em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#changelist table tbody td, #changelist table tbody th {
|
||||
border-left: 1px solid #ddd;
|
||||
}
|
||||
|
||||
#changelist table tbody td:first-child, #changelist table tbody th:first-child {
|
||||
border-left: 0;
|
||||
border-right: 1px solid #ddd;
|
||||
}
|
||||
|
||||
#changelist table tbody td.action-checkbox {
|
||||
text-align:center;
|
||||
}
|
||||
|
||||
#changelist table tfoot {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* TOOLBAR */
|
||||
|
||||
#changelist #toolbar {
|
||||
padding: 3px;
|
||||
border-bottom: 1px solid #ddd;
|
||||
background: #e1e1e1 url(../img/admin/nav-bg.gif) top left repeat-x;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
#changelist #toolbar form input {
|
||||
font-size: 11px;
|
||||
padding: 1px 2px;
|
||||
}
|
||||
|
||||
#changelist #toolbar form #searchbar {
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
#changelist #changelist-search img {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* FILTER COLUMN */
|
||||
|
||||
#changelist-filter {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
width: 160px;
|
||||
border-left: 1px solid #ddd;
|
||||
background: #efefef;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#changelist-filter h2 {
|
||||
font-size: 11px;
|
||||
padding: 2px 5px;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
#changelist-filter h3 {
|
||||
font-size: 12px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
#changelist-filter ul {
|
||||
padding-left: 0;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
#changelist-filter li {
|
||||
list-style-type: none;
|
||||
margin-left: 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
#changelist-filter a {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
#changelist-filter a:hover {
|
||||
color: #036;
|
||||
}
|
||||
|
||||
#changelist-filter li.selected {
|
||||
border-left: 5px solid #ccc;
|
||||
padding-left: 5px;
|
||||
margin-left: -10px;
|
||||
}
|
||||
|
||||
#changelist-filter li.selected a {
|
||||
color: #5b80b2 !important;
|
||||
}
|
||||
|
||||
/* DATE DRILLDOWN */
|
||||
|
||||
.change-list ul.toplinks {
|
||||
display: block;
|
||||
background: white url(../img/admin/nav-bg-reverse.gif) 0 -10px repeat-x;
|
||||
border-top: 1px solid white;
|
||||
float: left;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.change-list ul.toplinks li {
|
||||
float: left;
|
||||
width: 9em;
|
||||
padding: 3px 6px;
|
||||
font-weight: bold;
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
.change-list ul.toplinks .date-back a {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.change-list ul.toplinks .date-back a:hover {
|
||||
color: #036;
|
||||
}
|
||||
|
||||
/* PAGINATOR */
|
||||
|
||||
.paginator {
|
||||
font-size: 11px;
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
line-height: 22px;
|
||||
margin: 0;
|
||||
border-top: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.paginator a:link, .paginator a:visited {
|
||||
padding: 2px 6px;
|
||||
border: solid 1px #ccc;
|
||||
background: white;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.paginator a.showall {
|
||||
padding: 0 !important;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.paginator a.showall:hover {
|
||||
color: #036 !important;
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.paginator .end {
|
||||
border-width: 2px !important;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.paginator .this-page {
|
||||
padding: 2px 6px;
|
||||
font-weight: bold;
|
||||
font-size: 13px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.paginator a:hover {
|
||||
color: white;
|
||||
background: #5b80b2;
|
||||
border-color: #036;
|
||||
}
|
||||
|
||||
/* ACTIONS */
|
||||
|
||||
.filtered .actions {
|
||||
margin-right: 160px !important;
|
||||
border-right: 1px solid #ddd;
|
||||
}
|
||||
|
||||
#changelist table input {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#changelist table tbody tr.selected {
|
||||
background-color: #FFFFCC;
|
||||
}
|
||||
|
||||
#changelist .actions {
|
||||
color: #999;
|
||||
padding: 3px;
|
||||
border-top: 1px solid #fff;
|
||||
border-bottom: 1px solid #ddd;
|
||||
background: white url(../img/admin/nav-bg-reverse.gif) 0 -10px repeat-x;
|
||||
}
|
||||
|
||||
#changelist .actions.selected {
|
||||
background: #fffccf;
|
||||
border-top: 1px solid #fffee8;
|
||||
border-bottom: 1px solid #edecd6;
|
||||
}
|
||||
|
||||
#changelist .actions span.all,
|
||||
#changelist .actions span.action-counter,
|
||||
#changelist .actions span.clear,
|
||||
#changelist .actions span.question {
|
||||
font-size: 11px;
|
||||
margin: 0 0.5em;
|
||||
display: none;
|
||||
}
|
||||
|
||||
#changelist .actions:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
#changelist .actions select {
|
||||
border: 1px solid #aaa;
|
||||
margin-left: 0.5em;
|
||||
padding: 1px 2px;
|
||||
}
|
||||
|
||||
#changelist .actions label {
|
||||
font-size: 11px;
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
|
||||
#changelist #action-toggle {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#changelist .actions .button {
|
||||
font-size: 11px;
|
||||
padding: 1px 2px;
|
||||
}
|
||||
30
static/admin/css/dashboard.css
Normal file
@@ -0,0 +1,30 @@
|
||||
/* DASHBOARD */
|
||||
|
||||
.dashboard .module table th {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dashboard .module table td {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dashboard .module table td a {
|
||||
display: block;
|
||||
padding-right: .6em;
|
||||
}
|
||||
|
||||
/* RECENT ACTIONS MODULE */
|
||||
|
||||
.module ul.actionlist {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
ul.actionlist li {
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
ul.actionlist li.changelink {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
-o-text-overflow: ellipsis;
|
||||
}
|
||||
360
static/admin/css/forms.css
Normal file
@@ -0,0 +1,360 @@
|
||||
@import url('widgets.css');
|
||||
|
||||
/* FORM ROWS */
|
||||
|
||||
.form-row {
|
||||
overflow: hidden;
|
||||
padding: 8px 12px;
|
||||
font-size: 11px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.form-row img, .form-row input {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
form .form-row p {
|
||||
padding-left: 0;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
/* FORM LABELS */
|
||||
|
||||
form h4 {
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
label {
|
||||
font-weight: normal !important;
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.required label, label.required {
|
||||
font-weight: bold !important;
|
||||
color: #333 !important;
|
||||
}
|
||||
|
||||
/* RADIO BUTTONS */
|
||||
|
||||
form ul.radiolist li {
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
form ul.radiolist label {
|
||||
float: none;
|
||||
display: inline;
|
||||
}
|
||||
|
||||
form ul.inline {
|
||||
margin-left: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
form ul.inline li {
|
||||
float: left;
|
||||
padding-right: 7px;
|
||||
}
|
||||
|
||||
/* ALIGNED FIELDSETS */
|
||||
|
||||
.aligned label {
|
||||
display: block;
|
||||
padding: 3px 10px 0 0;
|
||||
float: left;
|
||||
width: 8em;
|
||||
}
|
||||
|
||||
.aligned ul label {
|
||||
display: inline;
|
||||
float: none;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.colMS .aligned .vLargeTextField, .colMS .aligned .vXMLLargeTextField {
|
||||
width: 350px;
|
||||
}
|
||||
|
||||
form .aligned p, form .aligned ul {
|
||||
margin-left: 7em;
|
||||
padding-left: 30px;
|
||||
}
|
||||
|
||||
form .aligned table p {
|
||||
margin-left: 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
form .aligned p.help {
|
||||
padding-left: 38px;
|
||||
}
|
||||
|
||||
.aligned .vCheckboxLabel {
|
||||
float: none !important;
|
||||
display: inline;
|
||||
padding-left: 4px;
|
||||
}
|
||||
|
||||
.colM .aligned .vLargeTextField, .colM .aligned .vXMLLargeTextField {
|
||||
width: 610px;
|
||||
}
|
||||
|
||||
.checkbox-row p.help {
|
||||
margin-left: 0;
|
||||
padding-left: 0 !important;
|
||||
}
|
||||
|
||||
fieldset .field-box {
|
||||
float: left;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
/* WIDE FIELDSETS */
|
||||
|
||||
.wide label {
|
||||
width: 15em !important;
|
||||
}
|
||||
|
||||
form .wide p {
|
||||
margin-left: 15em;
|
||||
}
|
||||
|
||||
form .wide p.help {
|
||||
padding-left: 38px;
|
||||
}
|
||||
|
||||
.colM fieldset.wide .vLargeTextField, .colM fieldset.wide .vXMLLargeTextField {
|
||||
width: 450px;
|
||||
}
|
||||
|
||||
/* COLLAPSED FIELDSETS */
|
||||
|
||||
fieldset.collapsed * {
|
||||
display: none;
|
||||
}
|
||||
|
||||
fieldset.collapsed h2, fieldset.collapsed {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
fieldset.collapsed h2 {
|
||||
background-image: url(../img/admin/nav-bg.gif);
|
||||
background-position: bottom left;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
fieldset.collapsed .collapse-toggle {
|
||||
background: transparent;
|
||||
display: inline !important;
|
||||
}
|
||||
|
||||
/* MONOSPACE TEXTAREAS */
|
||||
|
||||
fieldset.monospace textarea {
|
||||
font-family: "Bitstream Vera Sans Mono",Monaco,"Courier New",Courier,monospace;
|
||||
}
|
||||
|
||||
/* SUBMIT ROW */
|
||||
|
||||
.submit-row {
|
||||
padding: 5px 7px;
|
||||
text-align: right;
|
||||
background: white url(../img/admin/nav-bg.gif) 0 100% repeat-x;
|
||||
border: 1px solid #ccc;
|
||||
margin: 5px 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.submit-row input {
|
||||
margin: 0 0 0 5px;
|
||||
}
|
||||
|
||||
.submit-row p {
|
||||
margin: 0.3em;
|
||||
}
|
||||
|
||||
.submit-row p.deletelink-box {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.submit-row .deletelink {
|
||||
background: url(../img/admin/icon_deletelink.gif) 0 50% no-repeat;
|
||||
padding-left: 14px;
|
||||
}
|
||||
|
||||
/* CUSTOM FORM FIELDS */
|
||||
|
||||
.vSelectMultipleField {
|
||||
vertical-align: top !important;
|
||||
}
|
||||
|
||||
.vCheckboxField {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.vDateField, .vTimeField {
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.vURLField {
|
||||
width: 30em;
|
||||
}
|
||||
|
||||
.vLargeTextField, .vXMLLargeTextField {
|
||||
width: 48em;
|
||||
}
|
||||
|
||||
.flatpages-flatpage #id_content {
|
||||
height: 40.2em;
|
||||
}
|
||||
|
||||
.module table .vPositiveSmallIntegerField {
|
||||
width: 2.2em;
|
||||
}
|
||||
|
||||
.vTextField {
|
||||
width: 20em;
|
||||
}
|
||||
|
||||
.vIntegerField {
|
||||
width: 5em;
|
||||
}
|
||||
|
||||
.vForeignKeyRawIdAdminField {
|
||||
width: 5em;
|
||||
}
|
||||
|
||||
/* INLINES */
|
||||
|
||||
.inline-group {
|
||||
padding: 0;
|
||||
border: 1px solid #ccc;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.inline-group .aligned label {
|
||||
width: 8em;
|
||||
}
|
||||
|
||||
.inline-related {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.inline-related h3 {
|
||||
margin: 0;
|
||||
color: #666;
|
||||
padding: 3px 5px;
|
||||
font-size: 11px;
|
||||
background: #e1e1e1 url(../img/admin/nav-bg.gif) top left repeat-x;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.inline-related h3 span.delete {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.inline-related h3 span.delete label {
|
||||
margin-left: 2px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.inline-related fieldset {
|
||||
margin: 0;
|
||||
background: #fff;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.inline-related fieldset.module h3 {
|
||||
margin: 0;
|
||||
padding: 2px 5px 3px 5px;
|
||||
font-size: 11px;
|
||||
text-align: left;
|
||||
font-weight: bold;
|
||||
background: #bcd;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.inline-group .tabular fieldset.module {
|
||||
border: none;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.inline-related.tabular fieldset.module table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.last-related fieldset {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.inline-group .tabular tr.has_original td {
|
||||
padding-top: 2em;
|
||||
}
|
||||
|
||||
.inline-group .tabular tr td.original {
|
||||
padding: 2px 0 0 0;
|
||||
width: 0;
|
||||
_position: relative;
|
||||
}
|
||||
|
||||
.inline-group .tabular th.original {
|
||||
width: 0px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.inline-group .tabular td.original p {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
height: 1.1em;
|
||||
padding: 2px 7px;
|
||||
overflow: hidden;
|
||||
font-size: 9px;
|
||||
font-weight: bold;
|
||||
color: #666;
|
||||
_width: 700px;
|
||||
}
|
||||
|
||||
.inline-group ul.tools {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.inline-group ul.tools li {
|
||||
display: inline;
|
||||
padding: 0 5px;
|
||||
}
|
||||
|
||||
.inline-group div.add-row,
|
||||
.inline-group .tabular tr.add-row td {
|
||||
color: #666;
|
||||
padding: 3px 5px;
|
||||
border-bottom: 1px solid #ddd;
|
||||
background: #e1e1e1 url(../img/admin/nav-bg.gif) top left repeat-x;
|
||||
}
|
||||
|
||||
.inline-group .tabular tr.add-row td {
|
||||
padding: 4px 5px 3px;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.inline-group ul.tools a.add,
|
||||
.inline-group div.add-row a,
|
||||
.inline-group .tabular tr.add-row td a {
|
||||
background: url(../img/admin/icon_addlink.gif) 0 50% no-repeat;
|
||||
padding-left: 14px;
|
||||
font-size: 11px;
|
||||
outline: 0; /* Remove dotted border around link */
|
||||
}
|
||||
|
||||
.empty-form {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* IE7 specific bug fixes */
|
||||
|
||||
.submit-row input {
|
||||
float: right;
|
||||
}
|
||||
57
static/admin/css/ie.css
Normal file
@@ -0,0 +1,57 @@
|
||||
/* IE 6 & 7 */
|
||||
|
||||
/* Proper fixed width for dashboard in IE6 */
|
||||
|
||||
.dashboard #content {
|
||||
*width: 768px;
|
||||
}
|
||||
|
||||
.dashboard #content-main {
|
||||
*width: 535px;
|
||||
}
|
||||
|
||||
/* IE 6 ONLY */
|
||||
|
||||
/* Keep header from flowing off the page */
|
||||
|
||||
#container {
|
||||
_position: static;
|
||||
}
|
||||
|
||||
/* Put the right sidebars back on the page */
|
||||
|
||||
.colMS #content-related {
|
||||
_margin-right: 0;
|
||||
_margin-left: 10px;
|
||||
_position: static;
|
||||
}
|
||||
|
||||
/* Put the left sidebars back on the page */
|
||||
|
||||
.colSM #content-related {
|
||||
_margin-right: 10px;
|
||||
_margin-left: -115px;
|
||||
_position: static;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
_height: 1%;
|
||||
}
|
||||
|
||||
/* Fix right margin for changelist filters in IE6 */
|
||||
|
||||
#changelist-filter ul {
|
||||
_margin-right: -10px;
|
||||
}
|
||||
|
||||
/* IE ignores min-height, but treats height as if it were min-height */
|
||||
|
||||
.change-list .filtered {
|
||||
_height: 400px;
|
||||
}
|
||||
|
||||
/* IE doesn't know alpha transparency in PNGs */
|
||||
|
||||
.inline-deletelink {
|
||||
background: transparent url(../img/admin/inline-delete-8bit.png) no-repeat;
|
||||
}
|
||||
54
static/admin/css/login.css
Normal file
@@ -0,0 +1,54 @@
|
||||
/* LOGIN FORM */
|
||||
|
||||
body.login {
|
||||
background: #eee;
|
||||
}
|
||||
|
||||
.login #container {
|
||||
background: white;
|
||||
border: 1px solid #ccc;
|
||||
width: 28em;
|
||||
min-width: 300px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
margin-top: 100px;
|
||||
}
|
||||
|
||||
.login #content-main {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.login form {
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
.login .form-row {
|
||||
padding: 4px 0;
|
||||
float: left;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.login .form-row label {
|
||||
float: left;
|
||||
width: 9em;
|
||||
padding-right: 0.5em;
|
||||
line-height: 2em;
|
||||
text-align: right;
|
||||
font-size: 1em;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.login .form-row #id_username, .login .form-row #id_password {
|
||||
width: 14em;
|
||||
}
|
||||
|
||||
.login span.help {
|
||||
font-size: 10px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.login .submit-row {
|
||||
clear: both;
|
||||
padding: 1em 0 0 9.4em;
|
||||
}
|
||||
|
||||
244
static/admin/css/rtl.css
Normal file
@@ -0,0 +1,244 @@
|
||||
body {
|
||||
direction: rtl;
|
||||
}
|
||||
|
||||
/* LOGIN */
|
||||
|
||||
.login .form-row {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.login .form-row label {
|
||||
float: right;
|
||||
padding-left: 0.5em;
|
||||
padding-right: 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.login .submit-row {
|
||||
clear: both;
|
||||
padding: 1em 9.4em 0 0;
|
||||
}
|
||||
|
||||
/* GLOBAL */
|
||||
|
||||
th {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.module h2, .module caption {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.addlink, .changelink {
|
||||
padding-left: 0px;
|
||||
padding-right: 12px;
|
||||
background-position: 100% 0.2em;
|
||||
}
|
||||
|
||||
.deletelink {
|
||||
padding-left: 0px;
|
||||
padding-right: 12px;
|
||||
background-position: 100% 0.25em;
|
||||
}
|
||||
|
||||
.object-tools {
|
||||
float: left;
|
||||
}
|
||||
|
||||
thead th:first-child,
|
||||
tfoot td:first-child {
|
||||
border-left: 1px solid #ddd !important;
|
||||
}
|
||||
|
||||
/* LAYOUT */
|
||||
|
||||
#user-tools {
|
||||
right: auto;
|
||||
left: 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
div.breadcrumbs {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
#content-main {
|
||||
float: right;
|
||||
}
|
||||
|
||||
#content-related {
|
||||
float: left;
|
||||
margin-left: -19em;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.colMS {
|
||||
margin-left: 20em !important;
|
||||
margin-right: 10px !important;
|
||||
}
|
||||
|
||||
/* SORTABLE TABLES */
|
||||
|
||||
|
||||
table thead th.sorted a {
|
||||
padding-left: 13px;
|
||||
padding-right: 0px;
|
||||
}
|
||||
|
||||
table thead th.ascending a,
|
||||
table thead th.descending a {
|
||||
background-position: left;
|
||||
}
|
||||
|
||||
/* dashboard styles */
|
||||
|
||||
.dashboard .module table td a {
|
||||
padding-left: .6em;
|
||||
padding-right: 12px;
|
||||
}
|
||||
|
||||
/* changelists styles */
|
||||
|
||||
.change-list ul.toplinks li {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.change-list .filtered {
|
||||
background: white url(../img/admin/changelist-bg_rtl.gif) top left repeat-y !important;
|
||||
}
|
||||
|
||||
.change-list .filtered table {
|
||||
border-left: 1px solid #ddd;
|
||||
border-right: 0px none;
|
||||
}
|
||||
|
||||
#changelist-filter {
|
||||
right: auto;
|
||||
left: 0;
|
||||
border-left: 0px none;
|
||||
border-right: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.change-list .filtered .results, .change-list .filtered .paginator, .filtered #toolbar, .filtered div.xfull {
|
||||
margin-right: 0px !important;
|
||||
margin-left: 160px !important;
|
||||
}
|
||||
|
||||
#changelist-filter li.selected {
|
||||
border-left: 0px none;
|
||||
padding-left: 0px;
|
||||
margin-left: 0;
|
||||
border-right: 5px solid #ccc;
|
||||
padding-right: 5px;
|
||||
margin-right: -10px;
|
||||
}
|
||||
|
||||
.filtered .actions {
|
||||
border-left:1px solid #DDDDDD;
|
||||
margin-left:160px !important;
|
||||
border-right: 0 none;
|
||||
margin-right:0 !important;
|
||||
}
|
||||
|
||||
#changelist table tbody td:first-child, #changelist table tbody th:first-child {
|
||||
border-right: 0;
|
||||
border-left: 1px solid #ddd;
|
||||
}
|
||||
|
||||
/* FORMS */
|
||||
|
||||
.aligned label {
|
||||
padding: 0 0 3px 1em;
|
||||
float: right;
|
||||
}
|
||||
|
||||
.submit-row {
|
||||
text-align: left
|
||||
}
|
||||
|
||||
.submit-row p.deletelink-box {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.submit-row .deletelink {
|
||||
background: url(../img/admin/icon_deletelink.gif) 0 50% no-repeat;
|
||||
padding-right: 14px;
|
||||
}
|
||||
|
||||
.vDateField, .vTimeField {
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
form ul.inline li {
|
||||
float: right;
|
||||
padding-right: 0;
|
||||
padding-left: 7px;
|
||||
}
|
||||
|
||||
input[type=submit].default, .submit-row input.default {
|
||||
float: left;
|
||||
}
|
||||
|
||||
fieldset .field-box {
|
||||
float: right;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.errorlist li {
|
||||
background-position: 100% .3em;
|
||||
padding: 4px 25px 4px 5px;
|
||||
}
|
||||
|
||||
.errornote {
|
||||
background-position: 100% .3em;
|
||||
padding: 4px 25px 4px 5px;
|
||||
}
|
||||
|
||||
/* WIDGETS */
|
||||
|
||||
.calendarnav-previous {
|
||||
top: 0;
|
||||
left: auto;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.calendarnav-next {
|
||||
top: 0;
|
||||
right: auto;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.calendar caption, .calendarbox h2 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.selector {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.selector .selector-filter {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.inline-deletelink {
|
||||
float: left;
|
||||
}
|
||||
|
||||
/* MISC */
|
||||
|
||||
.inline-related h2, .inline-group h2 {
|
||||
text-align: right
|
||||
}
|
||||
|
||||
.inline-related h3 span.delete {
|
||||
padding-right: 20px;
|
||||
padding-left: inherit;
|
||||
left: 10px;
|
||||
right: inherit;
|
||||
}
|
||||
|
||||
.inline-related h3 span.delete label {
|
||||
margin-left: inherit;
|
||||
margin-right: 2px;
|
||||
}
|
||||
514
static/admin/css/widgets.css
Normal file
@@ -0,0 +1,514 @@
|
||||
/* SELECTOR (FILTER INTERFACE) */
|
||||
|
||||
.selector {
|
||||
width: 580px;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.selector select {
|
||||
width: 270px;
|
||||
height: 17.2em;
|
||||
}
|
||||
|
||||
.selector-available, .selector-chosen {
|
||||
float: left;
|
||||
width: 270px;
|
||||
text-align: center;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.selector-available h2, .selector-chosen h2 {
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
|
||||
.selector .selector-available h2 {
|
||||
background: white url(../img/admin/nav-bg.gif) bottom left repeat-x;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.selector .selector-filter {
|
||||
background: white;
|
||||
border: 1px solid #ccc;
|
||||
border-width: 0 1px;
|
||||
padding: 3px;
|
||||
color: #999;
|
||||
font-size: 10px;
|
||||
margin: 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.selector .selector-chosen .selector-filter {
|
||||
padding: 4px 5px;
|
||||
}
|
||||
|
||||
.selector .selector-available input {
|
||||
width: 230px;
|
||||
}
|
||||
|
||||
.selector ul.selector-chooser {
|
||||
float: left;
|
||||
width: 22px;
|
||||
height: 50px;
|
||||
background: url(../img/admin/chooser-bg.gif) top center no-repeat;
|
||||
margin: 8em 3px 0 3px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.selector-chooser li {
|
||||
margin: 0;
|
||||
padding: 3px;
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
.selector select {
|
||||
margin-bottom: 5px;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.selector-add, .selector-remove {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: block;
|
||||
text-indent: -3000px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.selector-add {
|
||||
background: url(../img/admin/selector-add.gif) top center no-repeat;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.selector-remove {
|
||||
background: url(../img/admin/selector-remove.gif) top center no-repeat;
|
||||
}
|
||||
|
||||
a.selector-chooseall, a.selector-clearall {
|
||||
display: block;
|
||||
width: 6em;
|
||||
text-align: left;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
font-weight: bold;
|
||||
color: #666;
|
||||
padding: 3px 0 3px 18px;
|
||||
}
|
||||
|
||||
a.selector-chooseall:hover, a.selector-clearall:hover {
|
||||
color: #036;
|
||||
}
|
||||
|
||||
a.selector-chooseall {
|
||||
width: 7em;
|
||||
background: url(../img/admin/selector-addall.gif) left center no-repeat;
|
||||
}
|
||||
|
||||
a.selector-clearall {
|
||||
background: url(../img/admin/selector-removeall.gif) left center no-repeat;
|
||||
}
|
||||
|
||||
|
||||
/* STACKED SELECTORS */
|
||||
|
||||
.stacked {
|
||||
float: left;
|
||||
width: 500px;
|
||||
}
|
||||
|
||||
.stacked select {
|
||||
width: 480px;
|
||||
height: 10.1em;
|
||||
}
|
||||
|
||||
.stacked .selector-available, .stacked .selector-chosen {
|
||||
width: 480px;
|
||||
}
|
||||
|
||||
.stacked .selector-available {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.stacked .selector-available input {
|
||||
width: 442px;
|
||||
}
|
||||
|
||||
.stacked ul.selector-chooser {
|
||||
height: 22px;
|
||||
width: 50px;
|
||||
margin: 0 0 3px 40%;
|
||||
background: url(../img/admin/chooser_stacked-bg.gif) top center no-repeat;
|
||||
}
|
||||
|
||||
.stacked .selector-chooser li {
|
||||
float: left;
|
||||
padding: 3px 3px 3px 5px;
|
||||
}
|
||||
|
||||
.stacked .selector-chooseall, .stacked .selector-clearall {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.stacked .selector-add {
|
||||
background-image: url(../img/admin/selector_stacked-add.gif);
|
||||
}
|
||||
|
||||
.stacked .selector-remove {
|
||||
background-image: url(../img/admin/selector_stacked-remove.gif);
|
||||
}
|
||||
|
||||
|
||||
/* DATE AND TIME */
|
||||
|
||||
p.datetime {
|
||||
line-height: 20px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
color: #666;
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.datetime span {
|
||||
font-size: 11px;
|
||||
color: #ccc;
|
||||
font-weight: normal;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
table p.datetime {
|
||||
font-size: 10px;
|
||||
margin-left: 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
/* FILE UPLOADS */
|
||||
|
||||
p.file-upload {
|
||||
line-height: 20px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
color: #666;
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.file-upload a {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.file-upload .deletelink {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
span.clearable-file-input label {
|
||||
color: #333;
|
||||
font-size: 11px;
|
||||
display: inline;
|
||||
float: none;
|
||||
}
|
||||
|
||||
/* CALENDARS & CLOCKS */
|
||||
|
||||
.calendarbox, .clockbox {
|
||||
margin: 5px auto;
|
||||
font-size: 11px;
|
||||
width: 16em;
|
||||
text-align: center;
|
||||
background: white;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.clockbox {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.calendar {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.calendar table {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border-collapse: collapse;
|
||||
background: white;
|
||||
width: 99%;
|
||||
}
|
||||
|
||||
.calendar caption, .calendarbox h2 {
|
||||
margin: 0;
|
||||
font-size: 11px;
|
||||
text-align: center;
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.calendar th {
|
||||
font-size: 10px;
|
||||
color: #666;
|
||||
padding: 2px 3px;
|
||||
text-align: center;
|
||||
background: #e1e1e1 url(../img/admin/nav-bg.gif) 0 50% repeat-x;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.calendar td {
|
||||
font-size: 11px;
|
||||
text-align: center;
|
||||
padding: 0;
|
||||
border-top: 1px solid #eee;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.calendar td.selected a {
|
||||
background: #C9DBED;
|
||||
}
|
||||
|
||||
.calendar td.nonday {
|
||||
background: #efefef;
|
||||
}
|
||||
|
||||
.calendar td.today a {
|
||||
background: #ffc;
|
||||
}
|
||||
|
||||
.calendar td a, .timelist a {
|
||||
display: block;
|
||||
font-weight: bold;
|
||||
padding: 4px;
|
||||
text-decoration: none;
|
||||
color: #444;
|
||||
}
|
||||
|
||||
.calendar td a:hover, .timelist a:hover {
|
||||
background: #5b80b2;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.calendar td a:active, .timelist a:active {
|
||||
background: #036;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.calendarnav {
|
||||
font-size: 10px;
|
||||
text-align: center;
|
||||
color: #ccc;
|
||||
margin: 0;
|
||||
padding: 1px 3px;
|
||||
}
|
||||
|
||||
.calendarnav a:link, #calendarnav a:visited, #calendarnav a:hover {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.calendar-shortcuts {
|
||||
background: white;
|
||||
font-size: 10px;
|
||||
line-height: 11px;
|
||||
border-top: 1px solid #eee;
|
||||
padding: 3px 0 4px;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.calendarbox .calendarnav-previous, .calendarbox .calendarnav-next {
|
||||
display: block;
|
||||
position: absolute;
|
||||
font-weight: bold;
|
||||
font-size: 12px;
|
||||
background: #C9DBED url(../img/admin/default-bg.gif) bottom left repeat-x;
|
||||
padding: 1px 4px 2px 4px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.calendarnav-previous:hover, .calendarnav-next:hover {
|
||||
background: #036;
|
||||
}
|
||||
|
||||
.calendarnav-previous {
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.calendarnav-next {
|
||||
top: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.calendar-cancel {
|
||||
margin: 0 !important;
|
||||
padding: 0;
|
||||
font-size: 10px;
|
||||
background: #e1e1e1 url(../img/admin/nav-bg.gif) 0 50% repeat-x;
|
||||
border-top: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.calendar-cancel a {
|
||||
padding: 2px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
ul.timelist, .timelist li {
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.timelist a {
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
/* INLINE ORDERER */
|
||||
|
||||
ul.orderer {
|
||||
position: relative;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
ul.orderer li {
|
||||
list-style-type: none;
|
||||
display: block;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: 1px solid #bbb;
|
||||
border-width: 0 1px 1px 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
background: #e2e2e2 url(../img/admin/nav-bg-grabber.gif) repeat-y;
|
||||
}
|
||||
|
||||
ul.orderer li:hover {
|
||||
cursor: move;
|
||||
background-color: #ddd;
|
||||
}
|
||||
|
||||
ul.orderer li a.selector {
|
||||
margin-left: 12px;
|
||||
overflow: hidden;
|
||||
width: 83%;
|
||||
font-size: 10px !important;
|
||||
padding: 0.6em 0;
|
||||
}
|
||||
|
||||
ul.orderer li a:link, ul.orderer li a:visited {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
ul.orderer li .inline-deletelink {
|
||||
position: absolute;
|
||||
right: 4px;
|
||||
margin-top: 0.6em;
|
||||
}
|
||||
|
||||
ul.orderer li.selected {
|
||||
background-color: #f8f8f8;
|
||||
border-right-color: #f8f8f8;
|
||||
}
|
||||
|
||||
ul.orderer li.deleted {
|
||||
background: #bbb url(../img/admin/deleted-overlay.gif);
|
||||
}
|
||||
|
||||
ul.orderer li.deleted a:link, ul.orderer li.deleted a:visited {
|
||||
color: #888;
|
||||
}
|
||||
|
||||
ul.orderer li.deleted .inline-deletelink {
|
||||
background-image: url(../img/admin/inline-restore.png);
|
||||
}
|
||||
|
||||
ul.orderer li.deleted:hover, ul.orderer li.deleted a.selector:hover {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/* EDIT INLINE */
|
||||
|
||||
.inline-deletelink {
|
||||
float: right;
|
||||
text-indent: -9999px;
|
||||
background: transparent url(../img/admin/inline-delete.png) no-repeat;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
border: 0px none;
|
||||
outline: 0; /* Remove dotted border around link */
|
||||
}
|
||||
|
||||
.inline-deletelink:hover {
|
||||
background-position: -15px 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.editinline button.addlink {
|
||||
border: 0px none;
|
||||
color: #5b80b2;
|
||||
font-size: 100%;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.editinline button.addlink:hover {
|
||||
color: #036;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.editinline table .help {
|
||||
text-align: right;
|
||||
float: right;
|
||||
padding-left: 2em;
|
||||
}
|
||||
|
||||
.editinline tfoot .addlink {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.editinline table thead th:last-child {
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
.editinline tr.deleted {
|
||||
background: #ddd url(../img/admin/deleted-overlay.gif);
|
||||
}
|
||||
|
||||
.editinline tr.deleted .inline-deletelink {
|
||||
background-image: url(../img/admin/inline-restore.png);
|
||||
}
|
||||
|
||||
.editinline tr.deleted td:hover {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.editinline tr.deleted td:first-child {
|
||||
background-image: none !important;
|
||||
}
|
||||
|
||||
/* EDIT INLINE - STACKED */
|
||||
|
||||
.editinline-stacked {
|
||||
min-width: 758px;
|
||||
}
|
||||
|
||||
.editinline-stacked .inline-object {
|
||||
margin-left: 210px;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.editinline-stacked .inline-source {
|
||||
float: left;
|
||||
width: 200px;
|
||||
background: #f8f8f8;
|
||||
}
|
||||
|
||||
.editinline-stacked .inline-splitter {
|
||||
float: left;
|
||||
width: 9px;
|
||||
background: #f8f8f8 url(../img/admin/inline-splitter-bg.gif) 50% 50% no-repeat;
|
||||
border-right: 1px solid #ccc;
|
||||
}
|
||||
|
||||
.editinline-stacked .controls {
|
||||
clear: both;
|
||||
background: #e1e1e1 url(../img/admin/nav-bg.gif) top left repeat-x;
|
||||
padding: 3px 4px;
|
||||
font-size: 11px;
|
||||
border-top: 1px solid #ddd;
|
||||
}
|
||||
|
||||
BIN
static/admin/img/admin/arrow-down.gif
Normal file
|
After Width: | Height: | Size: 80 B |
BIN
static/admin/img/admin/arrow-up.gif
Normal file
|
After Width: | Height: | Size: 838 B |
BIN
static/admin/img/admin/changelist-bg.gif
Normal file
|
After Width: | Height: | Size: 58 B |
BIN
static/admin/img/admin/changelist-bg_rtl.gif
Normal file
|
After Width: | Height: | Size: 75 B |
BIN
static/admin/img/admin/chooser-bg.gif
Normal file
|
After Width: | Height: | Size: 199 B |
BIN
static/admin/img/admin/chooser_stacked-bg.gif
Normal file
|
After Width: | Height: | Size: 212 B |
BIN
static/admin/img/admin/default-bg-reverse.gif
Normal file
|
After Width: | Height: | Size: 843 B |
BIN
static/admin/img/admin/default-bg.gif
Normal file
|
After Width: | Height: | Size: 844 B |
BIN
static/admin/img/admin/deleted-overlay.gif
Normal file
|
After Width: | Height: | Size: 45 B |
BIN
static/admin/img/admin/icon-no.gif
Normal file
|
After Width: | Height: | Size: 176 B |
BIN
static/admin/img/admin/icon-unknown.gif
Normal file
|
After Width: | Height: | Size: 130 B |
BIN
static/admin/img/admin/icon-yes.gif
Normal file
|
After Width: | Height: | Size: 299 B |
BIN
static/admin/img/admin/icon_addlink.gif
Normal file
|
After Width: | Height: | Size: 119 B |
BIN
static/admin/img/admin/icon_alert.gif
Normal file
|
After Width: | Height: | Size: 145 B |
BIN
static/admin/img/admin/icon_calendar.gif
Normal file
|
After Width: | Height: | Size: 192 B |
BIN
static/admin/img/admin/icon_changelink.gif
Normal file
|
After Width: | Height: | Size: 119 B |
BIN
static/admin/img/admin/icon_clock.gif
Normal file
|
After Width: | Height: | Size: 390 B |
BIN
static/admin/img/admin/icon_deletelink.gif
Normal file
|
After Width: | Height: | Size: 181 B |
BIN
static/admin/img/admin/icon_error.gif
Normal file
|
After Width: | Height: | Size: 319 B |
BIN
static/admin/img/admin/icon_searchbox.png
Normal file
|
After Width: | Height: | Size: 667 B |
BIN
static/admin/img/admin/icon_success.gif
Normal file
|
After Width: | Height: | Size: 341 B |
BIN
static/admin/img/admin/inline-delete-8bit.png
Normal file
|
After Width: | Height: | Size: 477 B |
BIN
static/admin/img/admin/inline-delete.png
Normal file
|
After Width: | Height: | Size: 781 B |
BIN
static/admin/img/admin/inline-restore-8bit.png
Normal file
|
After Width: | Height: | Size: 447 B |
BIN
static/admin/img/admin/inline-restore.png
Normal file
|
After Width: | Height: | Size: 623 B |
BIN
static/admin/img/admin/inline-splitter-bg.gif
Normal file
|
After Width: | Height: | Size: 102 B |
BIN
static/admin/img/admin/nav-bg-grabber.gif
Normal file
|
After Width: | Height: | Size: 116 B |
BIN
static/admin/img/admin/nav-bg-reverse.gif
Normal file
|
After Width: | Height: | Size: 186 B |
BIN
static/admin/img/admin/nav-bg.gif
Normal file
|
After Width: | Height: | Size: 273 B |
BIN
static/admin/img/admin/selector-add.gif
Normal file
|
After Width: | Height: | Size: 606 B |
BIN
static/admin/img/admin/selector-addall.gif
Normal file
|
After Width: | Height: | Size: 358 B |
BIN
static/admin/img/admin/selector-remove.gif
Normal file
|
After Width: | Height: | Size: 398 B |
BIN
static/admin/img/admin/selector-removeall.gif
Normal file
|
After Width: | Height: | Size: 355 B |
BIN
static/admin/img/admin/selector-search.gif
Normal file
|
After Width: | Height: | Size: 552 B |
BIN
static/admin/img/admin/selector_stacked-add.gif
Normal file
|
After Width: | Height: | Size: 612 B |
BIN
static/admin/img/admin/selector_stacked-remove.gif
Normal file
|
After Width: | Height: | Size: 401 B |
BIN
static/admin/img/admin/tool-left.gif
Normal file
|
After Width: | Height: | Size: 197 B |
BIN
static/admin/img/admin/tool-left_over.gif
Normal file
|
After Width: | Height: | Size: 203 B |
BIN
static/admin/img/admin/tool-right.gif
Normal file
|
After Width: | Height: | Size: 198 B |
BIN
static/admin/img/admin/tool-right_over.gif
Normal file
|
After Width: | Height: | Size: 200 B |
BIN
static/admin/img/admin/tooltag-add.gif
Normal file
|
After Width: | Height: | Size: 932 B |
BIN
static/admin/img/admin/tooltag-add_over.gif
Normal file
|
After Width: | Height: | Size: 336 B |
BIN
static/admin/img/admin/tooltag-arrowright.gif
Normal file
|
After Width: | Height: | Size: 351 B |
BIN
static/admin/img/admin/tooltag-arrowright_over.gif
Normal file
|
After Width: | Height: | Size: 354 B |
BIN
static/admin/img/gis/move_vertex_off.png
Normal file
|
After Width: | Height: | Size: 711 B |
BIN
static/admin/img/gis/move_vertex_on.png
Normal file
|
After Width: | Height: | Size: 506 B |
20
static/admin/js/LICENSE-JQUERY.txt
Normal file
@@ -0,0 +1,20 @@
|
||||
Copyright (c) 2010 John Resig, http://jquery.com/
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
111
static/admin/js/SelectBox.js
Normal file
@@ -0,0 +1,111 @@
|
||||
var SelectBox = {
|
||||
cache: new Object(),
|
||||
init: function(id) {
|
||||
var box = document.getElementById(id);
|
||||
var node;
|
||||
SelectBox.cache[id] = new Array();
|
||||
var cache = SelectBox.cache[id];
|
||||
for (var i = 0; (node = box.options[i]); i++) {
|
||||
cache.push({value: node.value, text: node.text, displayed: 1});
|
||||
}
|
||||
},
|
||||
redisplay: function(id) {
|
||||
// Repopulate HTML select box from cache
|
||||
var box = document.getElementById(id);
|
||||
box.options.length = 0; // clear all options
|
||||
for (var i = 0, j = SelectBox.cache[id].length; i < j; i++) {
|
||||
var node = SelectBox.cache[id][i];
|
||||
if (node.displayed) {
|
||||
box.options[box.options.length] = new Option(node.text, node.value, false, false);
|
||||
}
|
||||
}
|
||||
},
|
||||
filter: function(id, text) {
|
||||
// Redisplay the HTML select box, displaying only the choices containing ALL
|
||||
// the words in text. (It's an AND search.)
|
||||
var tokens = text.toLowerCase().split(/\s+/);
|
||||
var node, token;
|
||||
for (var i = 0; (node = SelectBox.cache[id][i]); i++) {
|
||||
node.displayed = 1;
|
||||
for (var j = 0; (token = tokens[j]); j++) {
|
||||
if (node.text.toLowerCase().indexOf(token) == -1) {
|
||||
node.displayed = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
SelectBox.redisplay(id);
|
||||
},
|
||||
delete_from_cache: function(id, value) {
|
||||
var node, delete_index = null;
|
||||
for (var i = 0; (node = SelectBox.cache[id][i]); i++) {
|
||||
if (node.value == value) {
|
||||
delete_index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
var j = SelectBox.cache[id].length - 1;
|
||||
for (var i = delete_index; i < j; i++) {
|
||||
SelectBox.cache[id][i] = SelectBox.cache[id][i+1];
|
||||
}
|
||||
SelectBox.cache[id].length--;
|
||||
},
|
||||
add_to_cache: function(id, option) {
|
||||
SelectBox.cache[id].push({value: option.value, text: option.text, displayed: 1});
|
||||
},
|
||||
cache_contains: function(id, value) {
|
||||
// Check if an item is contained in the cache
|
||||
var node;
|
||||
for (var i = 0; (node = SelectBox.cache[id][i]); i++) {
|
||||
if (node.value == value) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
move: function(from, to) {
|
||||
var from_box = document.getElementById(from);
|
||||
var to_box = document.getElementById(to);
|
||||
var option;
|
||||
for (var i = 0; (option = from_box.options[i]); i++) {
|
||||
if (option.selected && SelectBox.cache_contains(from, option.value)) {
|
||||
SelectBox.add_to_cache(to, {value: option.value, text: option.text, displayed: 1});
|
||||
SelectBox.delete_from_cache(from, option.value);
|
||||
}
|
||||
}
|
||||
SelectBox.redisplay(from);
|
||||
SelectBox.redisplay(to);
|
||||
},
|
||||
move_all: function(from, to) {
|
||||
var from_box = document.getElementById(from);
|
||||
var to_box = document.getElementById(to);
|
||||
var option;
|
||||
for (var i = 0; (option = from_box.options[i]); i++) {
|
||||
if (SelectBox.cache_contains(from, option.value)) {
|
||||
SelectBox.add_to_cache(to, {value: option.value, text: option.text, displayed: 1});
|
||||
SelectBox.delete_from_cache(from, option.value);
|
||||
}
|
||||
}
|
||||
SelectBox.redisplay(from);
|
||||
SelectBox.redisplay(to);
|
||||
},
|
||||
sort: function(id) {
|
||||
SelectBox.cache[id].sort( function(a, b) {
|
||||
a = a.text.toLowerCase();
|
||||
b = b.text.toLowerCase();
|
||||
try {
|
||||
if (a > b) return 1;
|
||||
if (a < b) return -1;
|
||||
}
|
||||
catch (e) {
|
||||
// silently fail on IE 'unknown' exception
|
||||
}
|
||||
return 0;
|
||||
} );
|
||||
},
|
||||
select_all: function(id) {
|
||||
var box = document.getElementById(id);
|
||||
for (var i = 0; i < box.options.length; i++) {
|
||||
box.options[i].selected = 'selected';
|
||||
}
|
||||
}
|
||||
}
|
||||
130
static/admin/js/SelectFilter2.js
Normal file
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
SelectFilter2 - Turns a multiple-select box into a filter interface.
|
||||
|
||||
Different than SelectFilter because this is coupled to the admin framework.
|
||||
|
||||
Requires core.js, SelectBox.js and addevent.js.
|
||||
*/
|
||||
|
||||
function findForm(node) {
|
||||
// returns the node of the form containing the given node
|
||||
if (node.tagName.toLowerCase() != 'form') {
|
||||
return findForm(node.parentNode);
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
var SelectFilter = {
|
||||
init: function(field_id, field_name, is_stacked, admin_media_prefix) {
|
||||
if (field_id.match(/__prefix__/)){
|
||||
// Don't intialize on empty forms.
|
||||
return;
|
||||
}
|
||||
var from_box = document.getElementById(field_id);
|
||||
from_box.id += '_from'; // change its ID
|
||||
from_box.className = 'filtered';
|
||||
|
||||
var ps = from_box.parentNode.getElementsByTagName('p');
|
||||
for (var i=0; i<ps.length; i++) {
|
||||
if (ps[i].className.indexOf("info") != -1) {
|
||||
// Remove <p class="info">, because it just gets in the way.
|
||||
from_box.parentNode.removeChild(ps[i]);
|
||||
} else if (ps[i].className.indexOf("help") != -1) {
|
||||
// Move help text up to the top so it isn't below the select
|
||||
// boxes or wrapped off on the side to the right of the add
|
||||
// button:
|
||||
from_box.parentNode.insertBefore(ps[i], from_box.parentNode.firstChild);
|
||||
}
|
||||
}
|
||||
|
||||
// <div class="selector"> or <div class="selector stacked">
|
||||
var selector_div = quickElement('div', from_box.parentNode);
|
||||
selector_div.className = is_stacked ? 'selector stacked' : 'selector';
|
||||
|
||||
// <div class="selector-available">
|
||||
var selector_available = quickElement('div', selector_div, '');
|
||||
selector_available.className = 'selector-available';
|
||||
quickElement('h2', selector_available, interpolate(gettext('Available %s'), [field_name]));
|
||||
var filter_p = quickElement('p', selector_available, '');
|
||||
filter_p.className = 'selector-filter';
|
||||
|
||||
var search_filter_label = quickElement('label', filter_p, '', 'for', field_id + "_input", 'style', 'width:16px;padding:2px');
|
||||
|
||||
var search_selector_img = quickElement('img', search_filter_label, '', 'src', admin_media_prefix + 'img/admin/selector-search.gif');
|
||||
search_selector_img.alt = gettext("Filter");
|
||||
|
||||
filter_p.appendChild(document.createTextNode(' '));
|
||||
|
||||
var filter_input = quickElement('input', filter_p, '', 'type', 'text');
|
||||
filter_input.id = field_id + '_input';
|
||||
selector_available.appendChild(from_box);
|
||||
var choose_all = quickElement('a', selector_available, gettext('Choose all'), 'href', 'javascript: (function(){ SelectBox.move_all("' + field_id + '_from", "' + field_id + '_to"); })()');
|
||||
choose_all.className = 'selector-chooseall';
|
||||
|
||||
// <ul class="selector-chooser">
|
||||
var selector_chooser = quickElement('ul', selector_div, '');
|
||||
selector_chooser.className = 'selector-chooser';
|
||||
var add_link = quickElement('a', quickElement('li', selector_chooser, ''), gettext('Add'), 'href', 'javascript: (function(){ SelectBox.move("' + field_id + '_from","' + field_id + '_to");})()');
|
||||
add_link.className = 'selector-add';
|
||||
var remove_link = quickElement('a', quickElement('li', selector_chooser, ''), gettext('Remove'), 'href', 'javascript: (function(){ SelectBox.move("' + field_id + '_to","' + field_id + '_from");})()');
|
||||
remove_link.className = 'selector-remove';
|
||||
|
||||
// <div class="selector-chosen">
|
||||
var selector_chosen = quickElement('div', selector_div, '');
|
||||
selector_chosen.className = 'selector-chosen';
|
||||
quickElement('h2', selector_chosen, interpolate(gettext('Chosen %s'), [field_name]));
|
||||
var selector_filter = quickElement('p', selector_chosen, gettext('Select your choice(s) and click '));
|
||||
selector_filter.className = 'selector-filter';
|
||||
quickElement('img', selector_filter, '', 'src', admin_media_prefix + (is_stacked ? 'img/admin/selector_stacked-add.gif':'img/admin/selector-add.gif'), 'alt', 'Add');
|
||||
var to_box = quickElement('select', selector_chosen, '', 'id', field_id + '_to', 'multiple', 'multiple', 'size', from_box.size, 'name', from_box.getAttribute('name'));
|
||||
to_box.className = 'filtered';
|
||||
var clear_all = quickElement('a', selector_chosen, gettext('Clear all'), 'href', 'javascript: (function() { SelectBox.move_all("' + field_id + '_to", "' + field_id + '_from");})()');
|
||||
clear_all.className = 'selector-clearall';
|
||||
|
||||
from_box.setAttribute('name', from_box.getAttribute('name') + '_old');
|
||||
|
||||
// Set up the JavaScript event handlers for the select box filter interface
|
||||
addEvent(filter_input, 'keyup', function(e) { SelectFilter.filter_key_up(e, field_id); });
|
||||
addEvent(filter_input, 'keydown', function(e) { SelectFilter.filter_key_down(e, field_id); });
|
||||
addEvent(from_box, 'dblclick', function() { SelectBox.move(field_id + '_from', field_id + '_to'); });
|
||||
addEvent(to_box, 'dblclick', function() { SelectBox.move(field_id + '_to', field_id + '_from'); });
|
||||
addEvent(findForm(from_box), 'submit', function() { SelectBox.select_all(field_id + '_to'); });
|
||||
SelectBox.init(field_id + '_from');
|
||||
SelectBox.init(field_id + '_to');
|
||||
// Move selected from_box options to to_box
|
||||
SelectBox.move(field_id + '_from', field_id + '_to');
|
||||
},
|
||||
filter_key_up: function(event, field_id) {
|
||||
from = document.getElementById(field_id + '_from');
|
||||
// don't submit form if user pressed Enter
|
||||
if ((event.which && event.which == 13) || (event.keyCode && event.keyCode == 13)) {
|
||||
from.selectedIndex = 0;
|
||||
SelectBox.move(field_id + '_from', field_id + '_to');
|
||||
from.selectedIndex = 0;
|
||||
return false;
|
||||
}
|
||||
var temp = from.selectedIndex;
|
||||
SelectBox.filter(field_id + '_from', document.getElementById(field_id + '_input').value);
|
||||
from.selectedIndex = temp;
|
||||
return true;
|
||||
},
|
||||
filter_key_down: function(event, field_id) {
|
||||
from = document.getElementById(field_id + '_from');
|
||||
// right arrow -- move across
|
||||
if ((event.which && event.which == 39) || (event.keyCode && event.keyCode == 39)) {
|
||||
var old_index = from.selectedIndex;
|
||||
SelectBox.move(field_id + '_from', field_id + '_to');
|
||||
from.selectedIndex = (old_index == from.length) ? from.length - 1 : old_index;
|
||||
return false;
|
||||
}
|
||||
// down arrow -- wrap around
|
||||
if ((event.which && event.which == 40) || (event.keyCode && event.keyCode == 40)) {
|
||||
from.selectedIndex = (from.length == from.selectedIndex + 1) ? 0 : from.selectedIndex + 1;
|
||||
}
|
||||
// up arrow -- wrap around
|
||||
if ((event.which && event.which == 38) || (event.keyCode && event.keyCode == 38)) {
|
||||
from.selectedIndex = (from.selectedIndex == 0) ? from.length - 1 : from.selectedIndex - 1;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
139
static/admin/js/actions.js
Normal file
@@ -0,0 +1,139 @@
|
||||
(function($) {
|
||||
$.fn.actions = function(opts) {
|
||||
var options = $.extend({}, $.fn.actions.defaults, opts);
|
||||
var actionCheckboxes = $(this);
|
||||
var list_editable_changed = false;
|
||||
checker = function(checked) {
|
||||
if (checked) {
|
||||
showQuestion();
|
||||
} else {
|
||||
reset();
|
||||
}
|
||||
$(actionCheckboxes).attr("checked", checked)
|
||||
.parent().parent().toggleClass(options.selectedClass, checked);
|
||||
}
|
||||
updateCounter = function() {
|
||||
var sel = $(actionCheckboxes).filter(":checked").length;
|
||||
$(options.counterContainer).html(interpolate(
|
||||
ngettext('%(sel)s of %(cnt)s selected', '%(sel)s of %(cnt)s selected', sel), {
|
||||
sel: sel,
|
||||
cnt: _actions_icnt
|
||||
}, true));
|
||||
$(options.allToggle).attr("checked", function() {
|
||||
if (sel == actionCheckboxes.length) {
|
||||
value = true;
|
||||
showQuestion();
|
||||
} else {
|
||||
value = false;
|
||||
clearAcross();
|
||||
}
|
||||
return value;
|
||||
});
|
||||
}
|
||||
showQuestion = function() {
|
||||
$(options.acrossClears).hide();
|
||||
$(options.acrossQuestions).show();
|
||||
$(options.allContainer).hide();
|
||||
}
|
||||
showClear = function() {
|
||||
$(options.acrossClears).show();
|
||||
$(options.acrossQuestions).hide();
|
||||
$(options.actionContainer).toggleClass(options.selectedClass);
|
||||
$(options.allContainer).show();
|
||||
$(options.counterContainer).hide();
|
||||
}
|
||||
reset = function() {
|
||||
$(options.acrossClears).hide();
|
||||
$(options.acrossQuestions).hide();
|
||||
$(options.allContainer).hide();
|
||||
$(options.counterContainer).show();
|
||||
}
|
||||
clearAcross = function() {
|
||||
reset();
|
||||
$(options.acrossInput).val(0);
|
||||
$(options.actionContainer).removeClass(options.selectedClass);
|
||||
}
|
||||
// Show counter by default
|
||||
$(options.counterContainer).show();
|
||||
// Check state of checkboxes and reinit state if needed
|
||||
$(this).filter(":checked").each(function(i) {
|
||||
$(this).parent().parent().toggleClass(options.selectedClass);
|
||||
updateCounter();
|
||||
if ($(options.acrossInput).val() == 1) {
|
||||
showClear();
|
||||
}
|
||||
});
|
||||
$(options.allToggle).show().click(function() {
|
||||
checker($(this).attr("checked"));
|
||||
updateCounter();
|
||||
});
|
||||
$("div.actions span.question a").click(function(event) {
|
||||
event.preventDefault();
|
||||
$(options.acrossInput).val(1);
|
||||
showClear();
|
||||
});
|
||||
$("div.actions span.clear a").click(function(event) {
|
||||
event.preventDefault();
|
||||
$(options.allToggle).attr("checked", false);
|
||||
clearAcross();
|
||||
checker(0);
|
||||
updateCounter();
|
||||
});
|
||||
lastChecked = null;
|
||||
$(actionCheckboxes).click(function(event) {
|
||||
if (!event) { var event = window.event; }
|
||||
var target = event.target ? event.target : event.srcElement;
|
||||
if (lastChecked && $.data(lastChecked) != $.data(target) && event.shiftKey == true) {
|
||||
var inrange = false;
|
||||
$(lastChecked).attr("checked", target.checked)
|
||||
.parent().parent().toggleClass(options.selectedClass, target.checked);
|
||||
$(actionCheckboxes).each(function() {
|
||||
if ($.data(this) == $.data(lastChecked) || $.data(this) == $.data(target)) {
|
||||
inrange = (inrange) ? false : true;
|
||||
}
|
||||
if (inrange) {
|
||||
$(this).attr("checked", target.checked)
|
||||
.parent().parent().toggleClass(options.selectedClass, target.checked);
|
||||
}
|
||||
});
|
||||
}
|
||||
$(target).parent().parent().toggleClass(options.selectedClass, target.checked);
|
||||
lastChecked = target;
|
||||
updateCounter();
|
||||
});
|
||||
$('form#changelist-form table#result_list tr').find('td:gt(0) :input').change(function() {
|
||||
list_editable_changed = true;
|
||||
});
|
||||
$('form#changelist-form button[name="index"]').click(function(event) {
|
||||
if (list_editable_changed) {
|
||||
return confirm(gettext("You have unsaved changes on individual editable fields. If you run an action, your unsaved changes will be lost."));
|
||||
}
|
||||
});
|
||||
$('form#changelist-form input[name="_save"]').click(function(event) {
|
||||
var action_changed = false;
|
||||
$('div.actions select option:selected').each(function() {
|
||||
if ($(this).val()) {
|
||||
action_changed = true;
|
||||
}
|
||||
});
|
||||
if (action_changed) {
|
||||
if (list_editable_changed) {
|
||||
return confirm(gettext("You have selected an action, but you haven't saved your changes to individual fields yet. Please click OK to save. You'll need to re-run the action."));
|
||||
} else {
|
||||
return confirm(gettext("You have selected an action, and you haven't made any changes on individual fields. You're probably looking for the Go button rather than the Save button."));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
/* Setup plugin defaults */
|
||||
$.fn.actions.defaults = {
|
||||
actionContainer: "div.actions",
|
||||
counterContainer: "span.action-counter",
|
||||
allContainer: "div.actions span.all",
|
||||
acrossInput: "div.actions input.select-across",
|
||||
acrossQuestions: "div.actions span.question",
|
||||
acrossClears: "div.actions span.clear",
|
||||
allToggle: "#action-toggle",
|
||||
selectedClass: "selected"
|
||||
}
|
||||
})(django.jQuery);
|
||||
7
static/admin/js/actions.min.js
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
(function(a){a.fn.actions=function(h){var b=a.extend({},a.fn.actions.defaults,h),e=a(this),f=false;checker=function(c){c?showQuestion():reset();a(e).attr("checked",c).parent().parent().toggleClass(b.selectedClass,c)};updateCounter=function(){var c=a(e).filter(":checked").length;a(b.counterContainer).html(interpolate(ngettext("%(sel)s of %(cnt)s selected","%(sel)s of %(cnt)s selected",c),{sel:c,cnt:_actions_icnt},true));a(b.allToggle).attr("checked",function(){if(c==e.length){value=true;showQuestion()}else{value=
|
||||
false;clearAcross()}return value})};showQuestion=function(){a(b.acrossClears).hide();a(b.acrossQuestions).show();a(b.allContainer).hide()};showClear=function(){a(b.acrossClears).show();a(b.acrossQuestions).hide();a(b.actionContainer).toggleClass(b.selectedClass);a(b.allContainer).show();a(b.counterContainer).hide()};reset=function(){a(b.acrossClears).hide();a(b.acrossQuestions).hide();a(b.allContainer).hide();a(b.counterContainer).show()};clearAcross=function(){reset();a(b.acrossInput).val(0);a(b.actionContainer).removeClass(b.selectedClass)};
|
||||
a(b.counterContainer).show();a(this).filter(":checked").each(function(){a(this).parent().parent().toggleClass(b.selectedClass);updateCounter();a(b.acrossInput).val()==1&&showClear()});a(b.allToggle).show().click(function(){checker(a(this).attr("checked"));updateCounter()});a("div.actions span.question a").click(function(c){c.preventDefault();a(b.acrossInput).val(1);showClear()});a("div.actions span.clear a").click(function(c){c.preventDefault();a(b.allToggle).attr("checked",false);clearAcross();checker(0);
|
||||
updateCounter()});lastChecked=null;a(e).click(function(c){if(!c)c=window.event;var d=c.target?c.target:c.srcElement;if(lastChecked&&a.data(lastChecked)!=a.data(d)&&c.shiftKey==true){var g=false;a(lastChecked).attr("checked",d.checked).parent().parent().toggleClass(b.selectedClass,d.checked);a(e).each(function(){if(a.data(this)==a.data(lastChecked)||a.data(this)==a.data(d))g=g?false:true;g&&a(this).attr("checked",d.checked).parent().parent().toggleClass(b.selectedClass,d.checked)})}a(d).parent().parent().toggleClass(b.selectedClass,
|
||||
d.checked);lastChecked=d;updateCounter()});a("form#changelist-form table#result_list tr").find("td:gt(0) :input").change(function(){f=true});a('form#changelist-form button[name="index"]').click(function(){if(f)return confirm(gettext("You have unsaved changes on individual editable fields. If you run an action, your unsaved changes will be lost."))});a('form#changelist-form input[name="_save"]').click(function(){var c=false;a("div.actions select option:selected").each(function(){if(a(this).val())c=
|
||||
true});if(c)return f?confirm(gettext("You have selected an action, but you haven't saved your changes to individual fields yet. Please click OK to save. You'll need to re-run the action.")):confirm(gettext("You have selected an action, and you haven't made any changes on individual fields. You're probably looking for the Go button rather than the Save button."))})};a.fn.actions.defaults={actionContainer:"div.actions",counterContainer:"span.action-counter",allContainer:"div.actions span.all",acrossInput:"div.actions input.select-across",
|
||||
acrossQuestions:"div.actions span.question",acrossClears:"div.actions span.clear",allToggle:"#action-toggle",selectedClass:"selected"}})(django.jQuery);
|
||||
274
static/admin/js/admin/DateTimeShortcuts.js
Normal file
@@ -0,0 +1,274 @@
|
||||
// Inserts shortcut buttons after all of the following:
|
||||
// <input type="text" class="vDateField">
|
||||
// <input type="text" class="vTimeField">
|
||||
|
||||
var DateTimeShortcuts = {
|
||||
calendars: [],
|
||||
calendarInputs: [],
|
||||
clockInputs: [],
|
||||
calendarDivName1: 'calendarbox', // name of calendar <div> that gets toggled
|
||||
calendarDivName2: 'calendarin', // name of <div> that contains calendar
|
||||
calendarLinkName: 'calendarlink',// name of the link that is used to toggle
|
||||
clockDivName: 'clockbox', // name of clock <div> that gets toggled
|
||||
clockLinkName: 'clocklink', // name of the link that is used to toggle
|
||||
shortCutsClass: 'datetimeshortcuts', // class of the clock and cal shortcuts
|
||||
admin_media_prefix: '',
|
||||
init: function() {
|
||||
// Get admin_media_prefix by grabbing it off the window object. It's
|
||||
// set in the admin/base.html template, so if it's not there, someone's
|
||||
// overridden the template. In that case, we'll set a clearly-invalid
|
||||
// value in the hopes that someone will examine HTTP requests and see it.
|
||||
if (window.__admin_media_prefix__ != undefined) {
|
||||
DateTimeShortcuts.admin_media_prefix = window.__admin_media_prefix__;
|
||||
} else {
|
||||
DateTimeShortcuts.admin_media_prefix = '/missing-admin-media-prefix/';
|
||||
}
|
||||
|
||||
var inputs = document.getElementsByTagName('input');
|
||||
for (i=0; i<inputs.length; i++) {
|
||||
var inp = inputs[i];
|
||||
if (inp.getAttribute('type') == 'text' && inp.className.match(/vTimeField/)) {
|
||||
DateTimeShortcuts.addClock(inp);
|
||||
}
|
||||
else if (inp.getAttribute('type') == 'text' && inp.className.match(/vDateField/)) {
|
||||
DateTimeShortcuts.addCalendar(inp);
|
||||
}
|
||||
}
|
||||
},
|
||||
// Add clock widget to a given field
|
||||
addClock: function(inp) {
|
||||
var num = DateTimeShortcuts.clockInputs.length;
|
||||
DateTimeShortcuts.clockInputs[num] = inp;
|
||||
|
||||
// Shortcut links (clock icon and "Now" link)
|
||||
var shortcuts_span = document.createElement('span');
|
||||
shortcuts_span.className = DateTimeShortcuts.shortCutsClass;
|
||||
inp.parentNode.insertBefore(shortcuts_span, inp.nextSibling);
|
||||
var now_link = document.createElement('a');
|
||||
now_link.setAttribute('href', "javascript:DateTimeShortcuts.handleClockQuicklink(" + num + ", new Date().strftime('" + get_format('TIME_INPUT_FORMATS')[0] + "'));");
|
||||
now_link.appendChild(document.createTextNode(gettext('Now')));
|
||||
var clock_link = document.createElement('a');
|
||||
clock_link.setAttribute('href', 'javascript:DateTimeShortcuts.openClock(' + num + ');');
|
||||
clock_link.id = DateTimeShortcuts.clockLinkName + num;
|
||||
quickElement('img', clock_link, '', 'src', DateTimeShortcuts.admin_media_prefix + 'img/admin/icon_clock.gif', 'alt', gettext('Clock'));
|
||||
shortcuts_span.appendChild(document.createTextNode('\240'));
|
||||
shortcuts_span.appendChild(now_link);
|
||||
shortcuts_span.appendChild(document.createTextNode('\240|\240'));
|
||||
shortcuts_span.appendChild(clock_link);
|
||||
|
||||
// Create clock link div
|
||||
//
|
||||
// Markup looks like:
|
||||
// <div id="clockbox1" class="clockbox module">
|
||||
// <h2>Choose a time</h2>
|
||||
// <ul class="timelist">
|
||||
// <li><a href="#">Now</a></li>
|
||||
// <li><a href="#">Midnight</a></li>
|
||||
// <li><a href="#">6 a.m.</a></li>
|
||||
// <li><a href="#">Noon</a></li>
|
||||
// </ul>
|
||||
// <p class="calendar-cancel"><a href="#">Cancel</a></p>
|
||||
// </div>
|
||||
|
||||
var clock_box = document.createElement('div');
|
||||
clock_box.style.display = 'none';
|
||||
clock_box.style.position = 'absolute';
|
||||
clock_box.className = 'clockbox module';
|
||||
clock_box.setAttribute('id', DateTimeShortcuts.clockDivName + num);
|
||||
document.body.appendChild(clock_box);
|
||||
addEvent(clock_box, 'click', DateTimeShortcuts.cancelEventPropagation);
|
||||
|
||||
quickElement('h2', clock_box, gettext('Choose a time'));
|
||||
time_list = quickElement('ul', clock_box, '');
|
||||
time_list.className = 'timelist';
|
||||
time_format = get_format('TIME_INPUT_FORMATS')[0];
|
||||
quickElement("a", quickElement("li", time_list, ""), gettext("Now"), "href", "javascript:DateTimeShortcuts.handleClockQuicklink(" + num + ", new Date().strftime('" + time_format + "'));");
|
||||
quickElement("a", quickElement("li", time_list, ""), gettext("Midnight"), "href", "javascript:DateTimeShortcuts.handleClockQuicklink(" + num + ", new Date(1970,1,1,0,0,0,0).strftime('" + time_format + "'));");
|
||||
quickElement("a", quickElement("li", time_list, ""), gettext("6 a.m."), "href", "javascript:DateTimeShortcuts.handleClockQuicklink(" + num + ", new Date(1970,1,1,6,0,0,0).strftime('" + time_format + "'));");
|
||||
quickElement("a", quickElement("li", time_list, ""), gettext("Noon"), "href", "javascript:DateTimeShortcuts.handleClockQuicklink(" + num + ", new Date(1970,1,1,12,0,0,0).strftime('" + time_format + "'));");
|
||||
|
||||
cancel_p = quickElement('p', clock_box, '');
|
||||
cancel_p.className = 'calendar-cancel';
|
||||
quickElement('a', cancel_p, gettext('Cancel'), 'href', 'javascript:DateTimeShortcuts.dismissClock(' + num + ');');
|
||||
},
|
||||
openClock: function(num) {
|
||||
var clock_box = document.getElementById(DateTimeShortcuts.clockDivName+num)
|
||||
var clock_link = document.getElementById(DateTimeShortcuts.clockLinkName+num)
|
||||
|
||||
// Recalculate the clockbox position
|
||||
// is it left-to-right or right-to-left layout ?
|
||||
if (getStyle(document.body,'direction')!='rtl') {
|
||||
clock_box.style.left = findPosX(clock_link) + 17 + 'px';
|
||||
}
|
||||
else {
|
||||
// since style's width is in em, it'd be tough to calculate
|
||||
// px value of it. let's use an estimated px for now
|
||||
// TODO: IE returns wrong value for findPosX when in rtl mode
|
||||
// (it returns as it was left aligned), needs to be fixed.
|
||||
clock_box.style.left = findPosX(clock_link) - 110 + 'px';
|
||||
}
|
||||
clock_box.style.top = Math.max(0, findPosY(clock_link) - 30) + 'px';
|
||||
|
||||
// Show the clock box
|
||||
clock_box.style.display = 'block';
|
||||
addEvent(window.document, 'click', function() { DateTimeShortcuts.dismissClock(num); return true; });
|
||||
},
|
||||
dismissClock: function(num) {
|
||||
document.getElementById(DateTimeShortcuts.clockDivName + num).style.display = 'none';
|
||||
window.document.onclick = null;
|
||||
},
|
||||
handleClockQuicklink: function(num, val) {
|
||||
DateTimeShortcuts.clockInputs[num].value = val;
|
||||
DateTimeShortcuts.clockInputs[num].focus();
|
||||
DateTimeShortcuts.dismissClock(num);
|
||||
},
|
||||
// Add calendar widget to a given field.
|
||||
addCalendar: function(inp) {
|
||||
var num = DateTimeShortcuts.calendars.length;
|
||||
|
||||
DateTimeShortcuts.calendarInputs[num] = inp;
|
||||
|
||||
// Shortcut links (calendar icon and "Today" link)
|
||||
var shortcuts_span = document.createElement('span');
|
||||
shortcuts_span.className = DateTimeShortcuts.shortCutsClass;
|
||||
inp.parentNode.insertBefore(shortcuts_span, inp.nextSibling);
|
||||
var today_link = document.createElement('a');
|
||||
today_link.setAttribute('href', 'javascript:DateTimeShortcuts.handleCalendarQuickLink(' + num + ', 0);');
|
||||
today_link.appendChild(document.createTextNode(gettext('Today')));
|
||||
var cal_link = document.createElement('a');
|
||||
cal_link.setAttribute('href', 'javascript:DateTimeShortcuts.openCalendar(' + num + ');');
|
||||
cal_link.id = DateTimeShortcuts.calendarLinkName + num;
|
||||
quickElement('img', cal_link, '', 'src', DateTimeShortcuts.admin_media_prefix + 'img/admin/icon_calendar.gif', 'alt', gettext('Calendar'));
|
||||
shortcuts_span.appendChild(document.createTextNode('\240'));
|
||||
shortcuts_span.appendChild(today_link);
|
||||
shortcuts_span.appendChild(document.createTextNode('\240|\240'));
|
||||
shortcuts_span.appendChild(cal_link);
|
||||
|
||||
// Create calendarbox div.
|
||||
//
|
||||
// Markup looks like:
|
||||
//
|
||||
// <div id="calendarbox3" class="calendarbox module">
|
||||
// <h2>
|
||||
// <a href="#" class="link-previous">‹</a>
|
||||
// <a href="#" class="link-next">›</a> February 2003
|
||||
// </h2>
|
||||
// <div class="calendar" id="calendarin3">
|
||||
// <!-- (cal) -->
|
||||
// </div>
|
||||
// <div class="calendar-shortcuts">
|
||||
// <a href="#">Yesterday</a> | <a href="#">Today</a> | <a href="#">Tomorrow</a>
|
||||
// </div>
|
||||
// <p class="calendar-cancel"><a href="#">Cancel</a></p>
|
||||
// </div>
|
||||
var cal_box = document.createElement('div');
|
||||
cal_box.style.display = 'none';
|
||||
cal_box.style.position = 'absolute';
|
||||
cal_box.className = 'calendarbox module';
|
||||
cal_box.setAttribute('id', DateTimeShortcuts.calendarDivName1 + num);
|
||||
document.body.appendChild(cal_box);
|
||||
addEvent(cal_box, 'click', DateTimeShortcuts.cancelEventPropagation);
|
||||
|
||||
// next-prev links
|
||||
var cal_nav = quickElement('div', cal_box, '');
|
||||
var cal_nav_prev = quickElement('a', cal_nav, '<', 'href', 'javascript:DateTimeShortcuts.drawPrev('+num+');');
|
||||
cal_nav_prev.className = 'calendarnav-previous';
|
||||
var cal_nav_next = quickElement('a', cal_nav, '>', 'href', 'javascript:DateTimeShortcuts.drawNext('+num+');');
|
||||
cal_nav_next.className = 'calendarnav-next';
|
||||
|
||||
// main box
|
||||
var cal_main = quickElement('div', cal_box, '', 'id', DateTimeShortcuts.calendarDivName2 + num);
|
||||
cal_main.className = 'calendar';
|
||||
DateTimeShortcuts.calendars[num] = new Calendar(DateTimeShortcuts.calendarDivName2 + num, DateTimeShortcuts.handleCalendarCallback(num));
|
||||
DateTimeShortcuts.calendars[num].drawCurrent();
|
||||
|
||||
// calendar shortcuts
|
||||
var shortcuts = quickElement('div', cal_box, '');
|
||||
shortcuts.className = 'calendar-shortcuts';
|
||||
quickElement('a', shortcuts, gettext('Yesterday'), 'href', 'javascript:DateTimeShortcuts.handleCalendarQuickLink(' + num + ', -1);');
|
||||
shortcuts.appendChild(document.createTextNode('\240|\240'));
|
||||
quickElement('a', shortcuts, gettext('Today'), 'href', 'javascript:DateTimeShortcuts.handleCalendarQuickLink(' + num + ', 0);');
|
||||
shortcuts.appendChild(document.createTextNode('\240|\240'));
|
||||
quickElement('a', shortcuts, gettext('Tomorrow'), 'href', 'javascript:DateTimeShortcuts.handleCalendarQuickLink(' + num + ', +1);');
|
||||
|
||||
// cancel bar
|
||||
var cancel_p = quickElement('p', cal_box, '');
|
||||
cancel_p.className = 'calendar-cancel';
|
||||
quickElement('a', cancel_p, gettext('Cancel'), 'href', 'javascript:DateTimeShortcuts.dismissCalendar(' + num + ');');
|
||||
},
|
||||
openCalendar: function(num) {
|
||||
var cal_box = document.getElementById(DateTimeShortcuts.calendarDivName1+num)
|
||||
var cal_link = document.getElementById(DateTimeShortcuts.calendarLinkName+num)
|
||||
var inp = DateTimeShortcuts.calendarInputs[num];
|
||||
|
||||
// Determine if the current value in the input has a valid date.
|
||||
// If so, draw the calendar with that date's year and month.
|
||||
if (inp.value) {
|
||||
var date_parts = inp.value.split('-');
|
||||
var year = date_parts[0];
|
||||
var month = parseFloat(date_parts[1]);
|
||||
if (year.match(/\d\d\d\d/) && month >= 1 && month <= 12) {
|
||||
DateTimeShortcuts.calendars[num].drawDate(month, year);
|
||||
}
|
||||
}
|
||||
|
||||
// Recalculate the clockbox position
|
||||
// is it left-to-right or right-to-left layout ?
|
||||
if (getStyle(document.body,'direction')!='rtl') {
|
||||
cal_box.style.left = findPosX(cal_link) + 17 + 'px';
|
||||
}
|
||||
else {
|
||||
// since style's width is in em, it'd be tough to calculate
|
||||
// px value of it. let's use an estimated px for now
|
||||
// TODO: IE returns wrong value for findPosX when in rtl mode
|
||||
// (it returns as it was left aligned), needs to be fixed.
|
||||
cal_box.style.left = findPosX(cal_link) - 180 + 'px';
|
||||
}
|
||||
cal_box.style.top = Math.max(0, findPosY(cal_link) - 75) + 'px';
|
||||
|
||||
cal_box.style.display = 'block';
|
||||
addEvent(window.document, 'click', function() { DateTimeShortcuts.dismissCalendar(num); return true; });
|
||||
},
|
||||
dismissCalendar: function(num) {
|
||||
document.getElementById(DateTimeShortcuts.calendarDivName1+num).style.display = 'none';
|
||||
window.document.onclick = null;
|
||||
},
|
||||
drawPrev: function(num) {
|
||||
DateTimeShortcuts.calendars[num].drawPreviousMonth();
|
||||
},
|
||||
drawNext: function(num) {
|
||||
DateTimeShortcuts.calendars[num].drawNextMonth();
|
||||
},
|
||||
handleCalendarCallback: function(num) {
|
||||
format = get_format('DATE_INPUT_FORMATS')[0];
|
||||
// the format needs to be escaped a little
|
||||
format = format.replace('\\', '\\\\');
|
||||
format = format.replace('\r', '\\r');
|
||||
format = format.replace('\n', '\\n');
|
||||
format = format.replace('\t', '\\t');
|
||||
format = format.replace("'", "\\'");
|
||||
return ["function(y, m, d) { DateTimeShortcuts.calendarInputs[",
|
||||
num,
|
||||
"].value = new Date(y, m-1, d).strftime('",
|
||||
format,
|
||||
"');DateTimeShortcuts.calendarInputs[",
|
||||
num,
|
||||
"].focus();document.getElementById(DateTimeShortcuts.calendarDivName1+",
|
||||
num,
|
||||
").style.display='none';}"].join('');
|
||||
},
|
||||
handleCalendarQuickLink: function(num, offset) {
|
||||
var d = new Date();
|
||||
d.setDate(d.getDate() + offset)
|
||||
DateTimeShortcuts.calendarInputs[num].value = d.strftime(get_format('DATE_INPUT_FORMATS')[0]);
|
||||
DateTimeShortcuts.calendarInputs[num].focus();
|
||||
DateTimeShortcuts.dismissCalendar(num);
|
||||
},
|
||||
cancelEventPropagation: function(e) {
|
||||
if (!e) e = window.event;
|
||||
e.cancelBubble = true;
|
||||
if (e.stopPropagation) e.stopPropagation();
|
||||
}
|
||||
}
|
||||
|
||||
addEvent(window, 'load', DateTimeShortcuts.init);
|
||||
96
static/admin/js/admin/RelatedObjectLookups.js
Normal file
@@ -0,0 +1,96 @@
|
||||
// Handles related-objects functionality: lookup link for raw_id_fields
|
||||
// and Add Another links.
|
||||
|
||||
function html_unescape(text) {
|
||||
// Unescape a string that was escaped using django.utils.html.escape.
|
||||
text = text.replace(/</g, '<');
|
||||
text = text.replace(/>/g, '>');
|
||||
text = text.replace(/"/g, '"');
|
||||
text = text.replace(/'/g, "'");
|
||||
text = text.replace(/&/g, '&');
|
||||
return text;
|
||||
}
|
||||
|
||||
// IE doesn't accept periods or dashes in the window name, but the element IDs
|
||||
// we use to generate popup window names may contain them, therefore we map them
|
||||
// to allowed characters in a reversible way so that we can locate the correct
|
||||
// element when the popup window is dismissed.
|
||||
function id_to_windowname(text) {
|
||||
text = text.replace(/\./g, '__dot__');
|
||||
text = text.replace(/\-/g, '__dash__');
|
||||
return text;
|
||||
}
|
||||
|
||||
function windowname_to_id(text) {
|
||||
text = text.replace(/__dot__/g, '.');
|
||||
text = text.replace(/__dash__/g, '-');
|
||||
return text;
|
||||
}
|
||||
|
||||
function showRelatedObjectLookupPopup(triggeringLink) {
|
||||
var name = triggeringLink.id.replace(/^lookup_/, '');
|
||||
name = id_to_windowname(name);
|
||||
var href;
|
||||
if (triggeringLink.href.search(/\?/) >= 0) {
|
||||
href = triggeringLink.href + '&pop=1';
|
||||
} else {
|
||||
href = triggeringLink.href + '?pop=1';
|
||||
}
|
||||
var win = window.open(href, name, 'height=500,width=800,resizable=yes,scrollbars=yes');
|
||||
win.focus();
|
||||
return false;
|
||||
}
|
||||
|
||||
function dismissRelatedLookupPopup(win, chosenId) {
|
||||
var name = windowname_to_id(win.name);
|
||||
var elem = document.getElementById(name);
|
||||
if (elem.className.indexOf('vManyToManyRawIdAdminField') != -1 && elem.value) {
|
||||
elem.value += ',' + chosenId;
|
||||
} else {
|
||||
document.getElementById(name).value = chosenId;
|
||||
}
|
||||
win.close();
|
||||
}
|
||||
|
||||
function showAddAnotherPopup(triggeringLink) {
|
||||
var name = triggeringLink.id.replace(/^add_/, '');
|
||||
name = id_to_windowname(name);
|
||||
href = triggeringLink.href
|
||||
if (href.indexOf('?') == -1) {
|
||||
href += '?_popup=1';
|
||||
} else {
|
||||
href += '&_popup=1';
|
||||
}
|
||||
var win = window.open(href, name, 'height=500,width=800,resizable=yes,scrollbars=yes');
|
||||
win.focus();
|
||||
return false;
|
||||
}
|
||||
|
||||
function dismissAddAnotherPopup(win, newId, newRepr) {
|
||||
// newId and newRepr are expected to have previously been escaped by
|
||||
// django.utils.html.escape.
|
||||
newId = html_unescape(newId);
|
||||
newRepr = html_unescape(newRepr);
|
||||
var name = windowname_to_id(win.name);
|
||||
var elem = document.getElementById(name);
|
||||
if (elem) {
|
||||
if (elem.nodeName == 'SELECT') {
|
||||
var o = new Option(newRepr, newId);
|
||||
elem.options[elem.options.length] = o;
|
||||
o.selected = true;
|
||||
} else if (elem.nodeName == 'INPUT') {
|
||||
if (elem.className.indexOf('vManyToManyRawIdAdminField') != -1 && elem.value) {
|
||||
elem.value += ',' + newId;
|
||||
} else {
|
||||
elem.value = newId;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
var toId = name + "_to";
|
||||
elem = document.getElementById(toId);
|
||||
var o = new Option(newRepr, newId);
|
||||
SelectBox.add_to_cache(toId, o);
|
||||
SelectBox.redisplay(toId);
|
||||
}
|
||||
win.close();
|
||||
}
|
||||
137
static/admin/js/admin/ordering.js
Normal file
@@ -0,0 +1,137 @@
|
||||
addEvent(window, 'load', reorder_init);
|
||||
|
||||
var lis;
|
||||
var top = 0;
|
||||
var left = 0;
|
||||
var height = 30;
|
||||
|
||||
function reorder_init() {
|
||||
lis = document.getElementsBySelector('ul#orderthese li');
|
||||
var input = document.getElementsBySelector('input[name=order_]')[0];
|
||||
setOrder(input.value.split(','));
|
||||
input.disabled = true;
|
||||
draw();
|
||||
// Now initialise the dragging behaviour
|
||||
var limit = (lis.length - 1) * height;
|
||||
for (var i = 0; i < lis.length; i++) {
|
||||
var li = lis[i];
|
||||
var img = document.getElementById('handle'+li.id);
|
||||
li.style.zIndex = 1;
|
||||
Drag.init(img, li, left + 10, left + 10, top + 10, top + 10 + limit);
|
||||
li.onDragStart = startDrag;
|
||||
li.onDragEnd = endDrag;
|
||||
img.style.cursor = 'move';
|
||||
}
|
||||
}
|
||||
|
||||
function submitOrderForm() {
|
||||
var inputOrder = document.getElementsBySelector('input[name=order_]')[0];
|
||||
inputOrder.value = getOrder();
|
||||
inputOrder.disabled=false;
|
||||
}
|
||||
|
||||
function startDrag() {
|
||||
this.style.zIndex = '10';
|
||||
this.className = 'dragging';
|
||||
}
|
||||
|
||||
function endDrag(x, y) {
|
||||
this.style.zIndex = '1';
|
||||
this.className = '';
|
||||
// Work out how far along it has been dropped, using x co-ordinate
|
||||
var oldIndex = this.index;
|
||||
var newIndex = Math.round((y - 10 - top) / height);
|
||||
// 'Snap' to the correct position
|
||||
this.style.top = (10 + top + newIndex * height) + 'px';
|
||||
this.index = newIndex;
|
||||
moveItem(oldIndex, newIndex);
|
||||
}
|
||||
|
||||
function moveItem(oldIndex, newIndex) {
|
||||
// Swaps two items, adjusts the index and left co-ord for all others
|
||||
if (oldIndex == newIndex) {
|
||||
return; // Nothing to swap;
|
||||
}
|
||||
var direction, lo, hi;
|
||||
if (newIndex > oldIndex) {
|
||||
lo = oldIndex;
|
||||
hi = newIndex;
|
||||
direction = -1;
|
||||
} else {
|
||||
direction = 1;
|
||||
hi = oldIndex;
|
||||
lo = newIndex;
|
||||
}
|
||||
var lis2 = new Array(); // We will build the new order in this array
|
||||
for (var i = 0; i < lis.length; i++) {
|
||||
if (i < lo || i > hi) {
|
||||
// Position of items not between the indexes is unaffected
|
||||
lis2[i] = lis[i];
|
||||
continue;
|
||||
} else if (i == newIndex) {
|
||||
lis2[i] = lis[oldIndex];
|
||||
continue;
|
||||
} else {
|
||||
// Item is between the two indexes - move it along 1
|
||||
lis2[i] = lis[i - direction];
|
||||
}
|
||||
}
|
||||
// Re-index everything
|
||||
reIndex(lis2);
|
||||
lis = lis2;
|
||||
draw();
|
||||
// document.getElementById('hiddenOrder').value = getOrder();
|
||||
document.getElementsBySelector('input[name=order_]')[0].value = getOrder();
|
||||
}
|
||||
|
||||
function reIndex(lis) {
|
||||
for (var i = 0; i < lis.length; i++) {
|
||||
lis[i].index = i;
|
||||
}
|
||||
}
|
||||
|
||||
function draw() {
|
||||
for (var i = 0; i < lis.length; i++) {
|
||||
var li = lis[i];
|
||||
li.index = i;
|
||||
li.style.position = 'absolute';
|
||||
li.style.left = (10 + left) + 'px';
|
||||
li.style.top = (10 + top + (i * height)) + 'px';
|
||||
}
|
||||
}
|
||||
|
||||
function getOrder() {
|
||||
var order = new Array(lis.length);
|
||||
for (var i = 0; i < lis.length; i++) {
|
||||
order[i] = lis[i].id.substring(1, 100);
|
||||
}
|
||||
return order.join(',');
|
||||
}
|
||||
|
||||
function setOrder(id_list) {
|
||||
/* Set the current order to match the lsit of IDs */
|
||||
var temp_lis = new Array();
|
||||
for (var i = 0; i < id_list.length; i++) {
|
||||
var id = 'p' + id_list[i];
|
||||
temp_lis[temp_lis.length] = document.getElementById(id);
|
||||
}
|
||||
reIndex(temp_lis);
|
||||
lis = temp_lis;
|
||||
draw();
|
||||
}
|
||||
|
||||
function addEvent(elm, evType, fn, useCapture)
|
||||
// addEvent and removeEvent
|
||||
// cross-browser event handling for IE5+, NS6 and Mozilla
|
||||
// By Scott Andrew
|
||||
{
|
||||
if (elm.addEventListener){
|
||||
elm.addEventListener(evType, fn, useCapture);
|
||||
return true;
|
||||
} else if (elm.attachEvent){
|
||||
var r = elm.attachEvent("on"+evType, fn);
|
||||
return r;
|
||||
} else {
|
||||
elm['on'+evType] = fn;
|
||||
}
|
||||
}
|
||||
156
static/admin/js/calendar.js
Normal file
@@ -0,0 +1,156 @@
|
||||
/*
|
||||
calendar.js - Calendar functions by Adrian Holovaty
|
||||
*/
|
||||
|
||||
function removeChildren(a) { // "a" is reference to an object
|
||||
while (a.hasChildNodes()) a.removeChild(a.lastChild);
|
||||
}
|
||||
|
||||
// quickElement(tagType, parentReference, textInChildNode, [, attribute, attributeValue ...]);
|
||||
function quickElement() {
|
||||
var obj = document.createElement(arguments[0]);
|
||||
if (arguments[2] != '' && arguments[2] != null) {
|
||||
var textNode = document.createTextNode(arguments[2]);
|
||||
obj.appendChild(textNode);
|
||||
}
|
||||
var len = arguments.length;
|
||||
for (var i = 3; i < len; i += 2) {
|
||||
obj.setAttribute(arguments[i], arguments[i+1]);
|
||||
}
|
||||
arguments[1].appendChild(obj);
|
||||
return obj;
|
||||
}
|
||||
|
||||
// CalendarNamespace -- Provides a collection of HTML calendar-related helper functions
|
||||
var CalendarNamespace = {
|
||||
monthsOfYear: gettext('January February March April May June July August September October November December').split(' '),
|
||||
daysOfWeek: gettext('S M T W T F S').split(' '),
|
||||
firstDayOfWeek: parseInt(get_format('FIRST_DAY_OF_WEEK')),
|
||||
isLeapYear: function(year) {
|
||||
return (((year % 4)==0) && ((year % 100)!=0) || ((year % 400)==0));
|
||||
},
|
||||
getDaysInMonth: function(month,year) {
|
||||
var days;
|
||||
if (month==1 || month==3 || month==5 || month==7 || month==8 || month==10 || month==12) {
|
||||
days = 31;
|
||||
}
|
||||
else if (month==4 || month==6 || month==9 || month==11) {
|
||||
days = 30;
|
||||
}
|
||||
else if (month==2 && CalendarNamespace.isLeapYear(year)) {
|
||||
days = 29;
|
||||
}
|
||||
else {
|
||||
days = 28;
|
||||
}
|
||||
return days;
|
||||
},
|
||||
draw: function(month, year, div_id, callback) { // month = 1-12, year = 1-9999
|
||||
var today = new Date();
|
||||
var todayDay = today.getDate();
|
||||
var todayMonth = today.getMonth()+1;
|
||||
var todayYear = today.getFullYear();
|
||||
var todayClass = '';
|
||||
|
||||
month = parseInt(month);
|
||||
year = parseInt(year);
|
||||
var calDiv = document.getElementById(div_id);
|
||||
removeChildren(calDiv);
|
||||
var calTable = document.createElement('table');
|
||||
quickElement('caption', calTable, CalendarNamespace.monthsOfYear[month-1] + ' ' + year);
|
||||
var tableBody = quickElement('tbody', calTable);
|
||||
|
||||
// Draw days-of-week header
|
||||
var tableRow = quickElement('tr', tableBody);
|
||||
for (var i = 0; i < 7; i++) {
|
||||
quickElement('th', tableRow, CalendarNamespace.daysOfWeek[(i + CalendarNamespace.firstDayOfWeek) % 7]);
|
||||
}
|
||||
|
||||
var startingPos = new Date(year, month-1, 1 - CalendarNamespace.firstDayOfWeek).getDay();
|
||||
var days = CalendarNamespace.getDaysInMonth(month, year);
|
||||
|
||||
// Draw blanks before first of month
|
||||
tableRow = quickElement('tr', tableBody);
|
||||
for (var i = 0; i < startingPos; i++) {
|
||||
var _cell = quickElement('td', tableRow, ' ');
|
||||
_cell.style.backgroundColor = '#f3f3f3';
|
||||
}
|
||||
|
||||
// Draw days of month
|
||||
var currentDay = 1;
|
||||
for (var i = startingPos; currentDay <= days; i++) {
|
||||
if (i%7 == 0 && currentDay != 1) {
|
||||
tableRow = quickElement('tr', tableBody);
|
||||
}
|
||||
if ((currentDay==todayDay) && (month==todayMonth) && (year==todayYear)) {
|
||||
todayClass='today';
|
||||
} else {
|
||||
todayClass='';
|
||||
}
|
||||
var cell = quickElement('td', tableRow, '', 'class', todayClass);
|
||||
|
||||
quickElement('a', cell, currentDay, 'href', 'javascript:void(' + callback + '('+year+','+month+','+currentDay+'));');
|
||||
currentDay++;
|
||||
}
|
||||
|
||||
// Draw blanks after end of month (optional, but makes for valid code)
|
||||
while (tableRow.childNodes.length < 7) {
|
||||
var _cell = quickElement('td', tableRow, ' ');
|
||||
_cell.style.backgroundColor = '#f3f3f3';
|
||||
}
|
||||
|
||||
calDiv.appendChild(calTable);
|
||||
}
|
||||
}
|
||||
|
||||
// Calendar -- A calendar instance
|
||||
function Calendar(div_id, callback) {
|
||||
// div_id (string) is the ID of the element in which the calendar will
|
||||
// be displayed
|
||||
// callback (string) is the name of a JavaScript function that will be
|
||||
// called with the parameters (year, month, day) when a day in the
|
||||
// calendar is clicked
|
||||
this.div_id = div_id;
|
||||
this.callback = callback;
|
||||
this.today = new Date();
|
||||
this.currentMonth = this.today.getMonth() + 1;
|
||||
this.currentYear = this.today.getFullYear();
|
||||
}
|
||||
Calendar.prototype = {
|
||||
drawCurrent: function() {
|
||||
CalendarNamespace.draw(this.currentMonth, this.currentYear, this.div_id, this.callback);
|
||||
},
|
||||
drawDate: function(month, year) {
|
||||
this.currentMonth = month;
|
||||
this.currentYear = year;
|
||||
this.drawCurrent();
|
||||
},
|
||||
drawPreviousMonth: function() {
|
||||
if (this.currentMonth == 1) {
|
||||
this.currentMonth = 12;
|
||||
this.currentYear--;
|
||||
}
|
||||
else {
|
||||
this.currentMonth--;
|
||||
}
|
||||
this.drawCurrent();
|
||||
},
|
||||
drawNextMonth: function() {
|
||||
if (this.currentMonth == 12) {
|
||||
this.currentMonth = 1;
|
||||
this.currentYear++;
|
||||
}
|
||||
else {
|
||||
this.currentMonth++;
|
||||
}
|
||||
this.drawCurrent();
|
||||
},
|
||||
drawPreviousYear: function() {
|
||||
this.currentYear--;
|
||||
this.drawCurrent();
|
||||
},
|
||||
drawNextYear: function() {
|
||||
this.currentYear++;
|
||||
this.drawCurrent();
|
||||
}
|
||||
}
|
||||
27
static/admin/js/collapse.js
Normal file
@@ -0,0 +1,27 @@
|
||||
(function($) {
|
||||
$(document).ready(function() {
|
||||
// Add anchor tag for Show/Hide link
|
||||
$("fieldset.collapse").each(function(i, elem) {
|
||||
// Don't hide if fields in this fieldset have errors
|
||||
if ( $(elem).find("div.errors").length == 0 ) {
|
||||
$(elem).addClass("collapsed");
|
||||
$(elem).find("h2").first().append(' (<a id="fieldsetcollapser' +
|
||||
i +'" class="collapse-toggle" href="#">' + gettext("Show") +
|
||||
'</a>)');
|
||||
}
|
||||
});
|
||||
// Add toggle to anchor tag
|
||||
$("fieldset.collapse a.collapse-toggle").toggle(
|
||||
function() { // Show
|
||||
$(this).text(gettext("Hide"));
|
||||
$(this).closest("fieldset").removeClass("collapsed");
|
||||
return false;
|
||||
},
|
||||
function() { // Hide
|
||||
$(this).text(gettext("Show"));
|
||||
$(this).closest("fieldset").addClass("collapsed");
|
||||
return false;
|
||||
}
|
||||
);
|
||||
});
|
||||
})(django.jQuery);
|
||||
2
static/admin/js/collapse.min.js
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
(function(a){a(document).ready(function(){a("fieldset.collapse").each(function(c,b){if(a(b).find("div.errors").length==0){a(b).addClass("collapsed");a(b).find("h2").first().append(' (<a id="fieldsetcollapser'+c+'" class="collapse-toggle" href="#">'+gettext("Show")+"</a>)")}});a("fieldset.collapse a.collapse-toggle").toggle(function(){a(this).text(gettext("Hide"));a(this).closest("fieldset").removeClass("collapsed");return false},function(){a(this).text(gettext("Show"));a(this).closest("fieldset").addClass("collapsed");
|
||||
return false})})})(django.jQuery);
|
||||
47
static/admin/js/compress.py
Normal file
@@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env python
|
||||
import os
|
||||
import optparse
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
here = os.path.dirname(__file__)
|
||||
|
||||
def main():
|
||||
usage = "usage: %prog [file1..fileN]"
|
||||
description = """With no file paths given this script will automatically
|
||||
compress all jQuery-based files of the admin app. Requires the Google Closure
|
||||
Compiler library and Java version 6 or later."""
|
||||
parser = optparse.OptionParser(usage, description=description)
|
||||
parser.add_option("-c", dest="compiler", default="~/bin/compiler.jar",
|
||||
help="path to Closure Compiler jar file")
|
||||
parser.add_option("-v", "--verbose",
|
||||
action="store_true", dest="verbose")
|
||||
parser.add_option("-q", "--quiet",
|
||||
action="store_false", dest="verbose")
|
||||
(options, args) = parser.parse_args()
|
||||
|
||||
compiler = os.path.expanduser(options.compiler)
|
||||
if not os.path.exists(compiler):
|
||||
sys.exit("Google Closure compiler jar file %s not found. Please use the -c option to specify the path." % compiler)
|
||||
|
||||
if not args:
|
||||
if options.verbose:
|
||||
sys.stdout.write("No filenames given; defaulting to admin scripts\n")
|
||||
args = [os.path.join(here, f) for f in [
|
||||
"actions.js", "collapse.js", "inlines.js", "prepopulate.js"]]
|
||||
|
||||
for arg in args:
|
||||
if not arg.endswith(".js"):
|
||||
arg = arg + ".js"
|
||||
to_compress = os.path.expanduser(arg)
|
||||
if os.path.exists(to_compress):
|
||||
to_compress_min = "%s.min.js" % "".join(arg.rsplit(".js"))
|
||||
cmd = "java -jar %s --js %s --js_output_file %s" % (compiler, to_compress, to_compress_min)
|
||||
if options.verbose:
|
||||
sys.stdout.write("Running: %s\n" % cmd)
|
||||
subprocess.call(cmd.split())
|
||||
else:
|
||||
sys.stdout.write("File %s not found. Sure it exists?\n" % to_compress)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
221
static/admin/js/core.js
Normal file
@@ -0,0 +1,221 @@
|
||||
// Core javascript helper functions
|
||||
|
||||
// basic browser identification & version
|
||||
var isOpera = (navigator.userAgent.indexOf("Opera")>=0) && parseFloat(navigator.appVersion);
|
||||
var isIE = ((document.all) && (!isOpera)) && parseFloat(navigator.appVersion.split("MSIE ")[1].split(";")[0]);
|
||||
|
||||
// Cross-browser event handlers.
|
||||
function addEvent(obj, evType, fn) {
|
||||
if (obj.addEventListener) {
|
||||
obj.addEventListener(evType, fn, false);
|
||||
return true;
|
||||
} else if (obj.attachEvent) {
|
||||
var r = obj.attachEvent("on" + evType, fn);
|
||||
return r;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function removeEvent(obj, evType, fn) {
|
||||
if (obj.removeEventListener) {
|
||||
obj.removeEventListener(evType, fn, false);
|
||||
return true;
|
||||
} else if (obj.detachEvent) {
|
||||
obj.detachEvent("on" + evType, fn);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// quickElement(tagType, parentReference, textInChildNode, [, attribute, attributeValue ...]);
|
||||
function quickElement() {
|
||||
var obj = document.createElement(arguments[0]);
|
||||
if (arguments[2] != '' && arguments[2] != null) {
|
||||
var textNode = document.createTextNode(arguments[2]);
|
||||
obj.appendChild(textNode);
|
||||
}
|
||||
var len = arguments.length;
|
||||
for (var i = 3; i < len; i += 2) {
|
||||
obj.setAttribute(arguments[i], arguments[i+1]);
|
||||
}
|
||||
arguments[1].appendChild(obj);
|
||||
return obj;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Cross-browser xmlhttp object
|
||||
// from http://jibbering.com/2002/4/httprequest.html
|
||||
// ----------------------------------------------------------------------------
|
||||
var xmlhttp;
|
||||
/*@cc_on @*/
|
||||
/*@if (@_jscript_version >= 5)
|
||||
try {
|
||||
xmlhttp = new ActiveXObject("Msxml2.XMLHTTP");
|
||||
} catch (e) {
|
||||
try {
|
||||
xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
|
||||
} catch (E) {
|
||||
xmlhttp = false;
|
||||
}
|
||||
}
|
||||
@else
|
||||
xmlhttp = false;
|
||||
@end @*/
|
||||
if (!xmlhttp && typeof XMLHttpRequest != 'undefined') {
|
||||
xmlhttp = new XMLHttpRequest();
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Find-position functions by PPK
|
||||
// See http://www.quirksmode.org/js/findpos.html
|
||||
// ----------------------------------------------------------------------------
|
||||
function findPosX(obj) {
|
||||
var curleft = 0;
|
||||
if (obj.offsetParent) {
|
||||
while (obj.offsetParent) {
|
||||
curleft += obj.offsetLeft - ((isOpera) ? 0 : obj.scrollLeft);
|
||||
obj = obj.offsetParent;
|
||||
}
|
||||
// IE offsetParent does not include the top-level
|
||||
if (isIE && obj.parentElement){
|
||||
curleft += obj.offsetLeft - obj.scrollLeft;
|
||||
}
|
||||
} else if (obj.x) {
|
||||
curleft += obj.x;
|
||||
}
|
||||
return curleft;
|
||||
}
|
||||
|
||||
function findPosY(obj) {
|
||||
var curtop = 0;
|
||||
if (obj.offsetParent) {
|
||||
while (obj.offsetParent) {
|
||||
curtop += obj.offsetTop - ((isOpera) ? 0 : obj.scrollTop);
|
||||
obj = obj.offsetParent;
|
||||
}
|
||||
// IE offsetParent does not include the top-level
|
||||
if (isIE && obj.parentElement){
|
||||
curtop += obj.offsetTop - obj.scrollTop;
|
||||
}
|
||||
} else if (obj.y) {
|
||||
curtop += obj.y;
|
||||
}
|
||||
return curtop;
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Date object extensions
|
||||
// ----------------------------------------------------------------------------
|
||||
Date.prototype.getCorrectYear = function() {
|
||||
// Date.getYear() is unreliable --
|
||||
// see http://www.quirksmode.org/js/introdate.html#year
|
||||
var y = this.getYear() % 100;
|
||||
return (y < 38) ? y + 2000 : y + 1900;
|
||||
}
|
||||
|
||||
Date.prototype.getTwelveHours = function() {
|
||||
hours = this.getHours();
|
||||
if (hours == 0) {
|
||||
return 12;
|
||||
}
|
||||
else {
|
||||
return hours <= 12 ? hours : hours-12
|
||||
}
|
||||
}
|
||||
|
||||
Date.prototype.getTwoDigitMonth = function() {
|
||||
return (this.getMonth() < 9) ? '0' + (this.getMonth()+1) : (this.getMonth()+1);
|
||||
}
|
||||
|
||||
Date.prototype.getTwoDigitDate = function() {
|
||||
return (this.getDate() < 10) ? '0' + this.getDate() : this.getDate();
|
||||
}
|
||||
|
||||
Date.prototype.getTwoDigitTwelveHour = function() {
|
||||
return (this.getTwelveHours() < 10) ? '0' + this.getTwelveHours() : this.getTwelveHours();
|
||||
}
|
||||
|
||||
Date.prototype.getTwoDigitHour = function() {
|
||||
return (this.getHours() < 10) ? '0' + this.getHours() : this.getHours();
|
||||
}
|
||||
|
||||
Date.prototype.getTwoDigitMinute = function() {
|
||||
return (this.getMinutes() < 10) ? '0' + this.getMinutes() : this.getMinutes();
|
||||
}
|
||||
|
||||
Date.prototype.getTwoDigitSecond = function() {
|
||||
return (this.getSeconds() < 10) ? '0' + this.getSeconds() : this.getSeconds();
|
||||
}
|
||||
|
||||
Date.prototype.getISODate = function() {
|
||||
return this.getCorrectYear() + '-' + this.getTwoDigitMonth() + '-' + this.getTwoDigitDate();
|
||||
}
|
||||
|
||||
Date.prototype.getHourMinute = function() {
|
||||
return this.getTwoDigitHour() + ':' + this.getTwoDigitMinute();
|
||||
}
|
||||
|
||||
Date.prototype.getHourMinuteSecond = function() {
|
||||
return this.getTwoDigitHour() + ':' + this.getTwoDigitMinute() + ':' + this.getTwoDigitSecond();
|
||||
}
|
||||
|
||||
Date.prototype.strftime = function(format) {
|
||||
var fields = {
|
||||
c: this.toString(),
|
||||
d: this.getTwoDigitDate(),
|
||||
H: this.getTwoDigitHour(),
|
||||
I: this.getTwoDigitTwelveHour(),
|
||||
m: this.getTwoDigitMonth(),
|
||||
M: this.getTwoDigitMinute(),
|
||||
p: (this.getHours() >= 12) ? 'PM' : 'AM',
|
||||
S: this.getTwoDigitSecond(),
|
||||
w: '0' + this.getDay(),
|
||||
x: this.toLocaleDateString(),
|
||||
X: this.toLocaleTimeString(),
|
||||
y: ('' + this.getFullYear()).substr(2, 4),
|
||||
Y: '' + this.getFullYear(),
|
||||
'%' : '%'
|
||||
};
|
||||
var result = '', i = 0;
|
||||
while (i < format.length) {
|
||||
if (format.charAt(i) === '%') {
|
||||
result = result + fields[format.charAt(i + 1)];
|
||||
++i;
|
||||
}
|
||||
else {
|
||||
result = result + format.charAt(i);
|
||||
}
|
||||
++i;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// String object extensions
|
||||
// ----------------------------------------------------------------------------
|
||||
String.prototype.pad_left = function(pad_length, pad_string) {
|
||||
var new_string = this;
|
||||
for (var i = 0; new_string.length < pad_length; i++) {
|
||||
new_string = pad_string + new_string;
|
||||
}
|
||||
return new_string;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Get the computed style for and element
|
||||
// ----------------------------------------------------------------------------
|
||||
function getStyle(oElm, strCssRule){
|
||||
var strValue = "";
|
||||
if(document.defaultView && document.defaultView.getComputedStyle){
|
||||
strValue = document.defaultView.getComputedStyle(oElm, "").getPropertyValue(strCssRule);
|
||||
}
|
||||
else if(oElm.currentStyle){
|
||||
strCssRule = strCssRule.replace(/\-(\w)/g, function (strMatch, p1){
|
||||
return p1.toUpperCase();
|
||||
});
|
||||
strValue = oElm.currentStyle[strCssRule];
|
||||
}
|
||||
return strValue;
|
||||
}
|
||||
239
static/admin/js/dateparse.js
Normal file
@@ -0,0 +1,239 @@
|
||||
/* 'Magic' date parsing, by Simon Willison (6th October 2003)
|
||||
http://simon.incutio.com/archive/2003/10/06/betterDateInput
|
||||
Adapted for 6newslawrence.com, 28th January 2004
|
||||
*/
|
||||
|
||||
/* Finds the index of the first occurence of item in the array, or -1 if not found */
|
||||
if (typeof Array.prototype.indexOf == 'undefined') {
|
||||
Array.prototype.indexOf = function(item) {
|
||||
var len = this.length;
|
||||
for (var i = 0; i < len; i++) {
|
||||
if (this[i] == item) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
};
|
||||
}
|
||||
/* Returns an array of items judged 'true' by the passed in test function */
|
||||
if (typeof Array.prototype.filter == 'undefined') {
|
||||
Array.prototype.filter = function(test) {
|
||||
var matches = [];
|
||||
var len = this.length;
|
||||
for (var i = 0; i < len; i++) {
|
||||
if (test(this[i])) {
|
||||
matches[matches.length] = this[i];
|
||||
}
|
||||
}
|
||||
return matches;
|
||||
};
|
||||
}
|
||||
|
||||
var monthNames = gettext("January February March April May June July August September October November December").split(" ");
|
||||
var weekdayNames = gettext("Sunday Monday Tuesday Wednesday Thursday Friday Saturday").split(" ");
|
||||
|
||||
/* Takes a string, returns the index of the month matching that string, throws
|
||||
an error if 0 or more than 1 matches
|
||||
*/
|
||||
function parseMonth(month) {
|
||||
var matches = monthNames.filter(function(item) {
|
||||
return new RegExp("^" + month, "i").test(item);
|
||||
});
|
||||
if (matches.length == 0) {
|
||||
throw new Error("Invalid month string");
|
||||
}
|
||||
if (matches.length > 1) {
|
||||
throw new Error("Ambiguous month");
|
||||
}
|
||||
return monthNames.indexOf(matches[0]);
|
||||
}
|
||||
/* Same as parseMonth but for days of the week */
|
||||
function parseWeekday(weekday) {
|
||||
var matches = weekdayNames.filter(function(item) {
|
||||
return new RegExp("^" + weekday, "i").test(item);
|
||||
});
|
||||
if (matches.length == 0) {
|
||||
throw new Error("Invalid day string");
|
||||
}
|
||||
if (matches.length > 1) {
|
||||
throw new Error("Ambiguous weekday");
|
||||
}
|
||||
return weekdayNames.indexOf(matches[0]);
|
||||
}
|
||||
|
||||
/* Array of objects, each has 're', a regular expression and 'handler', a
|
||||
function for creating a date from something that matches the regular
|
||||
expression. Handlers may throw errors if string is unparseable.
|
||||
*/
|
||||
var dateParsePatterns = [
|
||||
// Today
|
||||
{ re: /^tod/i,
|
||||
handler: function() {
|
||||
return new Date();
|
||||
}
|
||||
},
|
||||
// Tomorrow
|
||||
{ re: /^tom/i,
|
||||
handler: function() {
|
||||
var d = new Date();
|
||||
d.setDate(d.getDate() + 1);
|
||||
return d;
|
||||
}
|
||||
},
|
||||
// Yesterday
|
||||
{ re: /^yes/i,
|
||||
handler: function() {
|
||||
var d = new Date();
|
||||
d.setDate(d.getDate() - 1);
|
||||
return d;
|
||||
}
|
||||
},
|
||||
// 4th
|
||||
{ re: /^(\d{1,2})(st|nd|rd|th)?$/i,
|
||||
handler: function(bits) {
|
||||
var d = new Date();
|
||||
d.setDate(parseInt(bits[1], 10));
|
||||
return d;
|
||||
}
|
||||
},
|
||||
// 4th Jan
|
||||
{ re: /^(\d{1,2})(?:st|nd|rd|th)? (\w+)$/i,
|
||||
handler: function(bits) {
|
||||
var d = new Date();
|
||||
d.setDate(1);
|
||||
d.setMonth(parseMonth(bits[2]));
|
||||
d.setDate(parseInt(bits[1], 10));
|
||||
return d;
|
||||
}
|
||||
},
|
||||
// 4th Jan 2003
|
||||
{ re: /^(\d{1,2})(?:st|nd|rd|th)? (\w+),? (\d{4})$/i,
|
||||
handler: function(bits) {
|
||||
var d = new Date();
|
||||
d.setDate(1);
|
||||
d.setYear(bits[3]);
|
||||
d.setMonth(parseMonth(bits[2]));
|
||||
d.setDate(parseInt(bits[1], 10));
|
||||
return d;
|
||||
}
|
||||
},
|
||||
// Jan 4th
|
||||
{ re: /^(\w+) (\d{1,2})(?:st|nd|rd|th)?$/i,
|
||||
handler: function(bits) {
|
||||
var d = new Date();
|
||||
d.setDate(1);
|
||||
d.setMonth(parseMonth(bits[1]));
|
||||
d.setDate(parseInt(bits[2], 10));
|
||||
return d;
|
||||
}
|
||||
},
|
||||
// Jan 4th 2003
|
||||
{ re: /^(\w+) (\d{1,2})(?:st|nd|rd|th)?,? (\d{4})$/i,
|
||||
handler: function(bits) {
|
||||
var d = new Date();
|
||||
d.setDate(1);
|
||||
d.setYear(bits[3]);
|
||||
d.setMonth(parseMonth(bits[1]));
|
||||
d.setDate(parseInt(bits[2], 10));
|
||||
return d;
|
||||
}
|
||||
},
|
||||
// next Tuesday - this is suspect due to weird meaning of "next"
|
||||
{ re: /^next (\w+)$/i,
|
||||
handler: function(bits) {
|
||||
var d = new Date();
|
||||
var day = d.getDay();
|
||||
var newDay = parseWeekday(bits[1]);
|
||||
var addDays = newDay - day;
|
||||
if (newDay <= day) {
|
||||
addDays += 7;
|
||||
}
|
||||
d.setDate(d.getDate() + addDays);
|
||||
return d;
|
||||
}
|
||||
},
|
||||
// last Tuesday
|
||||
{ re: /^last (\w+)$/i,
|
||||
handler: function(bits) {
|
||||
throw new Error("Not yet implemented");
|
||||
}
|
||||
},
|
||||
// mm/dd/yyyy (American style)
|
||||
{ re: /(\d{1,2})\/(\d{1,2})\/(\d{4})/,
|
||||
handler: function(bits) {
|
||||
var d = new Date();
|
||||
d.setDate(1);
|
||||
d.setYear(bits[3]);
|
||||
d.setMonth(parseInt(bits[1], 10) - 1); // Because months indexed from 0
|
||||
d.setDate(parseInt(bits[2], 10));
|
||||
return d;
|
||||
}
|
||||
},
|
||||
// yyyy-mm-dd (ISO style)
|
||||
{ re: /(\d{4})-(\d{1,2})-(\d{1,2})/,
|
||||
handler: function(bits) {
|
||||
var d = new Date();
|
||||
d.setDate(1);
|
||||
d.setYear(parseInt(bits[1]));
|
||||
d.setMonth(parseInt(bits[2], 10) - 1);
|
||||
d.setDate(parseInt(bits[3], 10));
|
||||
return d;
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
function parseDateString(s) {
|
||||
for (var i = 0; i < dateParsePatterns.length; i++) {
|
||||
var re = dateParsePatterns[i].re;
|
||||
var handler = dateParsePatterns[i].handler;
|
||||
var bits = re.exec(s);
|
||||
if (bits) {
|
||||
return handler(bits);
|
||||
}
|
||||
}
|
||||
throw new Error("Invalid date string");
|
||||
}
|
||||
|
||||
function fmt00(x) {
|
||||
// fmt00: Tags leading zero onto numbers 0 - 9.
|
||||
// Particularly useful for displaying results from Date methods.
|
||||
//
|
||||
if (Math.abs(parseInt(x)) < 10){
|
||||
x = "0"+ Math.abs(x);
|
||||
}
|
||||
return x;
|
||||
}
|
||||
|
||||
function parseDateStringISO(s) {
|
||||
try {
|
||||
var d = parseDateString(s);
|
||||
return d.getFullYear() + '-' + (fmt00(d.getMonth() + 1)) + '-' + fmt00(d.getDate())
|
||||
}
|
||||
catch (e) { return s; }
|
||||
}
|
||||
function magicDate(input) {
|
||||
var messagespan = input.id + 'Msg';
|
||||
try {
|
||||
var d = parseDateString(input.value);
|
||||
input.value = d.getFullYear() + '-' + (fmt00(d.getMonth() + 1)) + '-' +
|
||||
fmt00(d.getDate());
|
||||
input.className = '';
|
||||
// Human readable date
|
||||
if (document.getElementById(messagespan)) {
|
||||
document.getElementById(messagespan).firstChild.nodeValue = d.toDateString();
|
||||
document.getElementById(messagespan).className = 'normal';
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
input.className = 'error';
|
||||
var message = e.message;
|
||||
// Fix for IE6 bug
|
||||
if (message.indexOf('is null or not an object') > -1) {
|
||||
message = 'Invalid date string';
|
||||
}
|
||||
if (document.getElementById(messagespan)) {
|
||||
document.getElementById(messagespan).firstChild.nodeValue = message;
|
||||
document.getElementById(messagespan).className = 'error';
|
||||
}
|
||||
}
|
||||
}
|
||||
167
static/admin/js/getElementsBySelector.js
Normal file
@@ -0,0 +1,167 @@
|
||||
/* document.getElementsBySelector(selector)
|
||||
- returns an array of element objects from the current document
|
||||
matching the CSS selector. Selectors can contain element names,
|
||||
class names and ids and can be nested. For example:
|
||||
|
||||
elements = document.getElementsBySelect('div#main p a.external')
|
||||
|
||||
Will return an array of all 'a' elements with 'external' in their
|
||||
class attribute that are contained inside 'p' elements that are
|
||||
contained inside the 'div' element which has id="main"
|
||||
|
||||
New in version 0.4: Support for CSS2 and CSS3 attribute selectors:
|
||||
See http://www.w3.org/TR/css3-selectors/#attribute-selectors
|
||||
|
||||
Version 0.4 - Simon Willison, March 25th 2003
|
||||
-- Works in Phoenix 0.5, Mozilla 1.3, Opera 7, Internet Explorer 6, Internet Explorer 5 on Windows
|
||||
-- Opera 7 fails
|
||||
*/
|
||||
|
||||
function getAllChildren(e) {
|
||||
// Returns all children of element. Workaround required for IE5/Windows. Ugh.
|
||||
return e.all ? e.all : e.getElementsByTagName('*');
|
||||
}
|
||||
|
||||
document.getElementsBySelector = function(selector) {
|
||||
// Attempt to fail gracefully in lesser browsers
|
||||
if (!document.getElementsByTagName) {
|
||||
return new Array();
|
||||
}
|
||||
// Split selector in to tokens
|
||||
var tokens = selector.split(' ');
|
||||
var currentContext = new Array(document);
|
||||
for (var i = 0; i < tokens.length; i++) {
|
||||
token = tokens[i].replace(/^\s+/,'').replace(/\s+$/,'');;
|
||||
if (token.indexOf('#') > -1) {
|
||||
// Token is an ID selector
|
||||
var bits = token.split('#');
|
||||
var tagName = bits[0];
|
||||
var id = bits[1];
|
||||
var element = document.getElementById(id);
|
||||
if (!element || (tagName && element.nodeName.toLowerCase() != tagName)) {
|
||||
// ID not found or tag with that ID not found, return false.
|
||||
return new Array();
|
||||
}
|
||||
// Set currentContext to contain just this element
|
||||
currentContext = new Array(element);
|
||||
continue; // Skip to next token
|
||||
}
|
||||
if (token.indexOf('.') > -1) {
|
||||
// Token contains a class selector
|
||||
var bits = token.split('.');
|
||||
var tagName = bits[0];
|
||||
var className = bits[1];
|
||||
if (!tagName) {
|
||||
tagName = '*';
|
||||
}
|
||||
// Get elements matching tag, filter them for class selector
|
||||
var found = new Array;
|
||||
var foundCount = 0;
|
||||
for (var h = 0; h < currentContext.length; h++) {
|
||||
var elements;
|
||||
if (tagName == '*') {
|
||||
elements = getAllChildren(currentContext[h]);
|
||||
} else {
|
||||
try {
|
||||
elements = currentContext[h].getElementsByTagName(tagName);
|
||||
}
|
||||
catch(e) {
|
||||
elements = [];
|
||||
}
|
||||
}
|
||||
for (var j = 0; j < elements.length; j++) {
|
||||
found[foundCount++] = elements[j];
|
||||
}
|
||||
}
|
||||
currentContext = new Array;
|
||||
var currentContextIndex = 0;
|
||||
for (var k = 0; k < found.length; k++) {
|
||||
if (found[k].className && found[k].className.match(new RegExp('\\b'+className+'\\b'))) {
|
||||
currentContext[currentContextIndex++] = found[k];
|
||||
}
|
||||
}
|
||||
continue; // Skip to next token
|
||||
}
|
||||
// Code to deal with attribute selectors
|
||||
if (token.match(/^(\w*)\[(\w+)([=~\|\^\$\*]?)=?"?([^\]"]*)"?\]$/)) {
|
||||
var tagName = RegExp.$1;
|
||||
var attrName = RegExp.$2;
|
||||
var attrOperator = RegExp.$3;
|
||||
var attrValue = RegExp.$4;
|
||||
if (!tagName) {
|
||||
tagName = '*';
|
||||
}
|
||||
// Grab all of the tagName elements within current context
|
||||
var found = new Array;
|
||||
var foundCount = 0;
|
||||
for (var h = 0; h < currentContext.length; h++) {
|
||||
var elements;
|
||||
if (tagName == '*') {
|
||||
elements = getAllChildren(currentContext[h]);
|
||||
} else {
|
||||
elements = currentContext[h].getElementsByTagName(tagName);
|
||||
}
|
||||
for (var j = 0; j < elements.length; j++) {
|
||||
found[foundCount++] = elements[j];
|
||||
}
|
||||
}
|
||||
currentContext = new Array;
|
||||
var currentContextIndex = 0;
|
||||
var checkFunction; // This function will be used to filter the elements
|
||||
switch (attrOperator) {
|
||||
case '=': // Equality
|
||||
checkFunction = function(e) { return (e.getAttribute(attrName) == attrValue); };
|
||||
break;
|
||||
case '~': // Match one of space seperated words
|
||||
checkFunction = function(e) { return (e.getAttribute(attrName).match(new RegExp('\\b'+attrValue+'\\b'))); };
|
||||
break;
|
||||
case '|': // Match start with value followed by optional hyphen
|
||||
checkFunction = function(e) { return (e.getAttribute(attrName).match(new RegExp('^'+attrValue+'-?'))); };
|
||||
break;
|
||||
case '^': // Match starts with value
|
||||
checkFunction = function(e) { return (e.getAttribute(attrName).indexOf(attrValue) == 0); };
|
||||
break;
|
||||
case '$': // Match ends with value - fails with "Warning" in Opera 7
|
||||
checkFunction = function(e) { return (e.getAttribute(attrName).lastIndexOf(attrValue) == e.getAttribute(attrName).length - attrValue.length); };
|
||||
break;
|
||||
case '*': // Match ends with value
|
||||
checkFunction = function(e) { return (e.getAttribute(attrName).indexOf(attrValue) > -1); };
|
||||
break;
|
||||
default :
|
||||
// Just test for existence of attribute
|
||||
checkFunction = function(e) { return e.getAttribute(attrName); };
|
||||
}
|
||||
currentContext = new Array;
|
||||
var currentContextIndex = 0;
|
||||
for (var k = 0; k < found.length; k++) {
|
||||
if (checkFunction(found[k])) {
|
||||
currentContext[currentContextIndex++] = found[k];
|
||||
}
|
||||
}
|
||||
// alert('Attribute Selector: '+tagName+' '+attrName+' '+attrOperator+' '+attrValue);
|
||||
continue; // Skip to next token
|
||||
}
|
||||
// If we get here, token is JUST an element (not a class or ID selector)
|
||||
tagName = token;
|
||||
var found = new Array;
|
||||
var foundCount = 0;
|
||||
for (var h = 0; h < currentContext.length; h++) {
|
||||
var elements = currentContext[h].getElementsByTagName(tagName);
|
||||
for (var j = 0; j < elements.length; j++) {
|
||||
found[foundCount++] = elements[j];
|
||||
}
|
||||
}
|
||||
currentContext = found;
|
||||
}
|
||||
return currentContext;
|
||||
}
|
||||
|
||||
/* That revolting regular expression explained
|
||||
/^(\w+)\[(\w+)([=~\|\^\$\*]?)=?"?([^\]"]*)"?\]$/
|
||||
\---/ \---/\-------------/ \-------/
|
||||
| | | |
|
||||
| | | The value
|
||||
| | ~,|,^,$,* or =
|
||||
| Attribute
|
||||
Tag
|
||||
*/
|
||||
136
static/admin/js/inlines.js
Normal file
@@ -0,0 +1,136 @@
|
||||
/**
|
||||
* Django admin inlines
|
||||
*
|
||||
* Based on jQuery Formset 1.1
|
||||
* @author Stanislaus Madueke (stan DOT madueke AT gmail DOT com)
|
||||
* @requires jQuery 1.2.6 or later
|
||||
*
|
||||
* Copyright (c) 2009, Stanislaus Madueke
|
||||
* All rights reserved.
|
||||
*
|
||||
* Spiced up with Code from Zain Memon's GSoC project 2009
|
||||
* and modified for Django by Jannis Leidel
|
||||
*
|
||||
* Licensed under the New BSD License
|
||||
* See: http://www.opensource.org/licenses/bsd-license.php
|
||||
*/
|
||||
(function($) {
|
||||
$.fn.formset = function(opts) {
|
||||
var options = $.extend({}, $.fn.formset.defaults, opts);
|
||||
var updateElementIndex = function(el, prefix, ndx) {
|
||||
var id_regex = new RegExp("(" + prefix + "-(\\d+|__prefix__))");
|
||||
var replacement = prefix + "-" + ndx;
|
||||
if ($(el).attr("for")) {
|
||||
$(el).attr("for", $(el).attr("for").replace(id_regex, replacement));
|
||||
}
|
||||
if (el.id) {
|
||||
el.id = el.id.replace(id_regex, replacement);
|
||||
}
|
||||
if (el.name) {
|
||||
el.name = el.name.replace(id_regex, replacement);
|
||||
}
|
||||
};
|
||||
var totalForms = $("#id_" + options.prefix + "-TOTAL_FORMS").attr("autocomplete", "off");
|
||||
var nextIndex = parseInt(totalForms.val());
|
||||
var maxForms = $("#id_" + options.prefix + "-MAX_NUM_FORMS").attr("autocomplete", "off");
|
||||
// only show the add button if we are allowed to add more items,
|
||||
// note that max_num = None translates to a blank string.
|
||||
var showAddButton = maxForms.val() == '' || (maxForms.val()-totalForms.val()) > 0;
|
||||
$(this).each(function(i) {
|
||||
$(this).not("." + options.emptyCssClass).addClass(options.formCssClass);
|
||||
});
|
||||
if ($(this).length && showAddButton) {
|
||||
var addButton;
|
||||
if ($(this).attr("tagName") == "TR") {
|
||||
// If forms are laid out as table rows, insert the
|
||||
// "add" button in a new table row:
|
||||
var numCols = this.eq(0).children().length;
|
||||
$(this).parent().append('<tr class="' + options.addCssClass + '"><td colspan="' + numCols + '"><a href="javascript:void(0)">' + options.addText + "</a></tr>");
|
||||
addButton = $(this).parent().find("tr:last a");
|
||||
} else {
|
||||
// Otherwise, insert it immediately after the last form:
|
||||
$(this).filter(":last").after('<div class="' + options.addCssClass + '"><a href="javascript:void(0)">' + options.addText + "</a></div>");
|
||||
addButton = $(this).filter(":last").next().find("a");
|
||||
}
|
||||
addButton.click(function() {
|
||||
var totalForms = $("#id_" + options.prefix + "-TOTAL_FORMS");
|
||||
var template = $("#" + options.prefix + "-empty");
|
||||
var row = template.clone(true);
|
||||
row.removeClass(options.emptyCssClass)
|
||||
.addClass(options.formCssClass)
|
||||
.attr("id", options.prefix + "-" + nextIndex);
|
||||
if (row.is("tr")) {
|
||||
// If the forms are laid out in table rows, insert
|
||||
// the remove button into the last table cell:
|
||||
row.children(":last").append('<div><a class="' + options.deleteCssClass +'" href="javascript:void(0)">' + options.deleteText + "</a></div>");
|
||||
} else if (row.is("ul") || row.is("ol")) {
|
||||
// If they're laid out as an ordered/unordered list,
|
||||
// insert an <li> after the last list item:
|
||||
row.append('<li><a class="' + options.deleteCssClass +'" href="javascript:void(0)">' + options.deleteText + "</a></li>");
|
||||
} else {
|
||||
// Otherwise, just insert the remove button as the
|
||||
// last child element of the form's container:
|
||||
row.children(":first").append('<span><a class="' + options.deleteCssClass + '" href="javascript:void(0)">' + options.deleteText + "</a></span>");
|
||||
}
|
||||
row.find("*").each(function() {
|
||||
updateElementIndex(this, options.prefix, totalForms.val());
|
||||
});
|
||||
// Insert the new form when it has been fully edited
|
||||
row.insertBefore($(template));
|
||||
// Update number of total forms
|
||||
$(totalForms).val(parseInt(totalForms.val()) + 1);
|
||||
nextIndex += 1;
|
||||
// Hide add button in case we've hit the max, except we want to add infinitely
|
||||
if ((maxForms.val() != '') && (maxForms.val()-totalForms.val()) <= 0) {
|
||||
addButton.parent().hide();
|
||||
}
|
||||
// The delete button of each row triggers a bunch of other things
|
||||
row.find("a." + options.deleteCssClass).click(function() {
|
||||
// Remove the parent form containing this button:
|
||||
var row = $(this).parents("." + options.formCssClass);
|
||||
row.remove();
|
||||
nextIndex -= 1;
|
||||
// If a post-delete callback was provided, call it with the deleted form:
|
||||
if (options.removed) {
|
||||
options.removed(row);
|
||||
}
|
||||
// Update the TOTAL_FORMS form count.
|
||||
var forms = $("." + options.formCssClass);
|
||||
$("#id_" + options.prefix + "-TOTAL_FORMS").val(forms.length);
|
||||
// Show add button again once we drop below max
|
||||
if ((maxForms.val() == '') || (maxForms.val()-forms.length) > 0) {
|
||||
addButton.parent().show();
|
||||
}
|
||||
// Also, update names and ids for all remaining form controls
|
||||
// so they remain in sequence:
|
||||
for (var i=0, formCount=forms.length; i<formCount; i++)
|
||||
{
|
||||
updateElementIndex($(forms).get(i), options.prefix, i);
|
||||
$(forms.get(i)).find("*").each(function() {
|
||||
updateElementIndex(this, options.prefix, i);
|
||||
});
|
||||
}
|
||||
return false;
|
||||
});
|
||||
// If a post-add callback was supplied, call it with the added form:
|
||||
if (options.added) {
|
||||
options.added(row);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
return this;
|
||||
}
|
||||
/* Setup plugin defaults */
|
||||
$.fn.formset.defaults = {
|
||||
prefix: "form", // The form prefix for your django formset
|
||||
addText: "add another", // Text for the add link
|
||||
deleteText: "remove", // Text for the delete link
|
||||
addCssClass: "add-row", // CSS class applied to the add link
|
||||
deleteCssClass: "delete-row", // CSS class applied to the delete link
|
||||
emptyCssClass: "empty-row", // CSS class applied to the empty row
|
||||
formCssClass: "dynamic-form", // CSS class applied to each form in a formset
|
||||
added: null, // Function called each time a new form is added
|
||||
removed: null // Function called each time a form is deleted
|
||||
}
|
||||
})(django.jQuery);
|
||||
5
static/admin/js/inlines.min.js
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
(function(b){b.fn.formset=function(g){var a=b.extend({},b.fn.formset.defaults,g),k=function(c,f,d){var e=new RegExp("("+f+"-(\\d+|__prefix__))");f=f+"-"+d;b(c).attr("for")&&b(c).attr("for",b(c).attr("for").replace(e,f));if(c.id)c.id=c.id.replace(e,f);if(c.name)c.name=c.name.replace(e,f)};g=b("#id_"+a.prefix+"-TOTAL_FORMS").attr("autocomplete","off");var l=parseInt(g.val()),h=b("#id_"+a.prefix+"-MAX_NUM_FORMS").attr("autocomplete","off");g=h.val()==""||h.val()-g.val()>0;b(this).each(function(){b(this).not("."+
|
||||
a.emptyCssClass).addClass(a.formCssClass)});if(b(this).length&&g){var j;if(b(this).attr("tagName")=="TR"){g=this.eq(0).children().length;b(this).parent().append('<tr class="'+a.addCssClass+'"><td colspan="'+g+'"><a href="javascript:void(0)">'+a.addText+"</a></tr>");j=b(this).parent().find("tr:last a")}else{b(this).filter(":last").after('<div class="'+a.addCssClass+'"><a href="javascript:void(0)">'+a.addText+"</a></div>");j=b(this).filter(":last").next().find("a")}j.click(function(){var c=b("#id_"+
|
||||
a.prefix+"-TOTAL_FORMS"),f=b("#"+a.prefix+"-empty"),d=f.clone(true);d.removeClass(a.emptyCssClass).addClass(a.formCssClass).attr("id",a.prefix+"-"+l);if(d.is("tr"))d.children(":last").append('<div><a class="'+a.deleteCssClass+'" href="javascript:void(0)">'+a.deleteText+"</a></div>");else d.is("ul")||d.is("ol")?d.append('<li><a class="'+a.deleteCssClass+'" href="javascript:void(0)">'+a.deleteText+"</a></li>"):d.children(":first").append('<span><a class="'+a.deleteCssClass+'" href="javascript:void(0)">'+
|
||||
a.deleteText+"</a></span>");d.find("*").each(function(){k(this,a.prefix,c.val())});d.insertBefore(b(f));b(c).val(parseInt(c.val())+1);l+=1;h.val()!=""&&h.val()-c.val()<=0&&j.parent().hide();d.find("a."+a.deleteCssClass).click(function(){var e=b(this).parents("."+a.formCssClass);e.remove();l-=1;a.removed&&a.removed(e);e=b("."+a.formCssClass);b("#id_"+a.prefix+"-TOTAL_FORMS").val(e.length);if(h.val()==""||h.val()-e.length>0)j.parent().show();for(var i=0,m=e.length;i<m;i++){k(b(e).get(i),a.prefix,i);
|
||||
b(e.get(i)).find("*").each(function(){k(this,a.prefix,i)})}return false});a.added&&a.added(d);return false})}return this};b.fn.formset.defaults={prefix:"form",addText:"add another",deleteText:"remove",addCssClass:"add-row",deleteCssClass:"delete-row",emptyCssClass:"empty-row",formCssClass:"dynamic-form",added:null,removed:null}})(django.jQuery);
|
||||
4
static/admin/js/jquery.init.js
Normal file
@@ -0,0 +1,4 @@
|
||||
// Puts the included jQuery into our own namespace
|
||||
var django = {
|
||||
"jQuery": jQuery.noConflict(true)
|
||||
};
|
||||