Commit 2af0dd34 by Calen Pennington

Merge pull request #67 from MITx/dogfood

Dogfood
parents 818f243d 0e06132d
This branch (re-)adds dynamic math and symbolicresponse.
Test cases included.
'''
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 ...@@ -26,7 +26,7 @@ from mako.template import Template
from util import contextualize_text from util import contextualize_text
import inputtypes 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 calc
import eia import eia
...@@ -42,6 +42,7 @@ response_types = {'numericalresponse':NumericalResponse, ...@@ -42,6 +42,7 @@ response_types = {'numericalresponse':NumericalResponse,
'truefalseresponse':TrueFalseResponse, 'truefalseresponse':TrueFalseResponse,
'imageresponse':ImageResponse, 'imageresponse':ImageResponse,
'optionresponse':OptionResponse, 'optionresponse':OptionResponse,
'symbolicresponse':SymbolicResponse,
} }
entry_types = ['textline', 'schematic', 'choicegroup','textbox','imageinput','optioninput'] entry_types = ['textline', 'schematic', 'choicegroup','textbox','imageinput','optioninput']
solution_types = ['solution'] # extra things displayed after "show answers" is pressed solution_types = ['solution'] # extra things displayed after "show answers" is pressed
...@@ -55,6 +56,7 @@ html_transforms = {'problem': {'tag':'div'}, ...@@ -55,6 +56,7 @@ html_transforms = {'problem': {'tag':'div'},
"externalresponse": {'tag':'span'}, "externalresponse": {'tag':'span'},
"schematicresponse": {'tag':'span'}, "schematicresponse": {'tag':'span'},
"formularesponse": {'tag':'span'}, "formularesponse": {'tag':'span'},
"symbolicresponse": {'tag':'span'},
"multiplechoiceresponse": {'tag':'span'}, "multiplechoiceresponse": {'tag':'span'},
"text": {'tag':'span'}, "text": {'tag':'span'},
"math": {'tag':'span'}, "math": {'tag':'span'},
...@@ -70,7 +72,7 @@ global_context={'random':random, ...@@ -70,7 +72,7 @@ global_context={'random':random,
# These should be removed from HTML output, including all subelements # These should be removed from HTML output, including all subelements
html_problem_semantics = ["responseparam", "answer", "script"] html_problem_semantics = ["responseparam", "answer", "script"]
# These should be removed from HTML output, but keeping subelements # 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 # removed in MC
## These should be transformed ## These should be transformed
...@@ -109,6 +111,8 @@ class LoncapaProblem(object): ...@@ -109,6 +111,8 @@ class LoncapaProblem(object):
self.seed=struct.unpack('i', os.urandom(4))[0] self.seed=struct.unpack('i', os.urandom(4))[0]
## Parse XML file ## Parse XML file
if getattr(system,'DEBUG',False):
log.info("[courseware.capa.capa_problem.lcp.init] fileobject = %s" % fileobject)
file_text = fileobject.read() file_text = fileobject.read()
self.fileobject = fileobject # save it, so we can use for debugging information later self.fileobject = fileobject # save it, so we can use for debugging information later
# Convert startouttext and endouttext to proper <text></text> # Convert startouttext and endouttext to proper <text></text>
...@@ -210,20 +214,26 @@ class LoncapaProblem(object): ...@@ -210,20 +214,26 @@ class LoncapaProblem(object):
Problem XML goes to Python execution context. Runs everything in script tags Problem XML goes to Python execution context. Runs everything in script tags
''' '''
random.seed(self.seed) random.seed(self.seed)
### IKE: Why do we need these two lines?
context = {'global_context':global_context} # save global context in here also 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.xpath('/problem/script'):
for script in tree.findall('.//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 code = script.text
XMLESC = {"&apos;": "'", "&quot;": '"'} XMLESC = {"&apos;": "'", "&quot;": '"'}
code = unescape(code,XMLESC) code = unescape(code,XMLESC)
try: 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: except Exception,err:
print "[courseware.capa.capa_problem.extract_context] error %s" % err log.exception("[courseware.capa.capa_problem.extract_context] error %s" % err)
print "in doing exec of this code:",code log.exception("in doing exec of this code: %s" % code)
return context return context
def get_html(self): def get_html(self):
......
...@@ -197,45 +197,52 @@ def choicegroup(element, value, status, msg=''): ...@@ -197,45 +197,52 @@ def choicegroup(element, value, status, msg=''):
type="radio" type="radio"
choices={} choices={}
for choice in element: for choice in element:
assert choice.tag =="choice", "only <choice> tags should be immediate children of a <choicegroup>" if not choice.tag=='choice':
choices[choice.get("name")] = etree.tostring(choice[0]) # TODO: what if choice[0] has math tags in it? 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} context={'id':eid, 'value':value, 'state':status, 'type':type, 'choices':choices}
html=render_to_string("choicegroup.html", context) html=render_to_string("choicegroup.html", context)
return etree.XML(html) return etree.XML(html)
@register_render_function @register_render_function
def textline(element, value, state, msg=""): 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') eid=element.get('id')
count = int(eid.split('_')[-2])-1 # HACK count = int(eid.split('_')[-2])-1 # HACK
size = element.get('size') 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) html=render_to_string("textinput.html", context)
return etree.XML(html) return etree.XML(html)
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
@register_render_function @register_render_function
def js_textline(element, value, status, msg=''): def textline_dynamath(element, value, status, msg=''):
''' '''
Plan: We will inspect element to figure out type Text line input with dynamic math display (equation rendered on client in real time during input).
''' '''
# TODO: Make a wrapper for <formulainput> # TODO: Make a wrapper for <formulainput>
# TODO: Make an AJAX loop to confirm equation is okay in real-time as user types # 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) ## TODO: Code should follow PEP8 (4 spaces per indentation level)
''' '''
textline is used for simple one-line inputs, like formularesponse and symbolicresponse. 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') eid=element.get('id')
count = int(eid.split('_')[-2])-1 # HACK count = int(eid.split('_')[-2])-1 # HACK
size = element.get('size') 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, context = {'id':eid, 'value':value, 'state':status, 'count':count, 'size': size,
'dojs':dojs,
'msg':msg, 'msg':msg,
} }
html=render_to_string("jstext.html", context) html=render_to_string("textinput_dynamath.html", context)
return etree.XML(html) return etree.XML(html)
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
...@@ -246,12 +253,19 @@ def textbox(element, value, status, msg=''): ...@@ -246,12 +253,19 @@ def textbox(element, value, status, msg=''):
The textbox is used for code input. The message is the return HTML string from 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. 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') eid=element.get('id')
count = int(eid.split('_')[-2])-1 # HACK count = int(eid.split('_')[-2])-1 # HACK
size = element.get('size') size = element.get('size')
context = {'id':eid, 'value':value, 'state':status, 'count':count, 'size': size, 'msg':msg} 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) html=render_to_string("textbox.html", context)
return etree.XML(html) return etree.XML(html)
......
...@@ -203,6 +203,10 @@ def course_file(user,coursename=None): ...@@ -203,6 +203,10 @@ def course_file(user,coursename=None):
else: else:
tree_string = None 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: if not tree_string:
tree = course_xml_process(etree.XML(render_to_string(filename, options, namespace = 'course'))) tree = course_xml_process(etree.XML(render_to_string(filename, options, namespace = 'course')))
tree_string = etree.tostring(tree) tree_string = etree.tostring(tree)
...@@ -231,7 +235,7 @@ def section_file(user, section, coursename=None, dironly=False): ...@@ -231,7 +235,7 @@ def section_file(user, section, coursename=None, dironly=False):
if dironly: return dirname if dironly: return dirname
if filename not in os.listdir(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 return None
options = {'dev_content':settings.DEV_CONTENT, options = {'dev_content':settings.DEV_CONTENT,
...@@ -271,9 +275,14 @@ def module_xml(user, module, id_tag, module_id, coursename=None): ...@@ -271,9 +275,14 @@ def module_xml(user, module, id_tag, module_id, coursename=None):
break break
if len(result_set)>1: 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 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 return None
if settings.DEBUG:
log.info('[courseware.content_parser.module_xml] found %s' % result_set)
return etree.tostring(result_set[0]) return etree.tostring(result_set[0])
#return result_set[0].serialize() #return result_set[0].serialize()
......
...@@ -35,8 +35,16 @@ class I4xSystem(object): ...@@ -35,8 +35,16 @@ class I4xSystem(object):
self.filestore = OSFS(settings.DATA_DIR) self.filestore = OSFS(settings.DATA_DIR)
else: else:
self.filestore = filestore self.filestore = filestore
if settings.DEBUG:
log.info("[courseware.module_render.I4xSystem] filestore path = %s" % filestore)
self.render_function = render_function self.render_function = render_function
self.exception404 = Http404 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): def __repr__(self):
return repr(self.__dict__) return repr(self.__dict__)
def __str__(self): def __str__(self):
...@@ -80,8 +88,7 @@ def grade_histogram(module_id): ...@@ -80,8 +88,7 @@ def grade_histogram(module_id):
return [] return []
return grades return grades
def get_module(user, request, xml_module, module_object_preload, position=None):
def get_module(user, request, xml_module, module_object_preload):
module_type=xml_module.tag module_type=xml_module.tag
module_class=courseware.modules.get_module_class(module_type) module_class=courseware.modules.get_module_class(module_type)
module_id=xml_module.get('id') #module_class.id_attribute) or "" 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): ...@@ -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+'/' ajax_url = settings.MITX_ROOT_URL + '/modx/'+module_type+'/'+module_id+'/'
system = I4xSystem(track_function = make_track_function(request), 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, ajax_url = ajax_url,
filestore = OSFS(data_root), filestore = OSFS(data_root),
) )
system.set('position',position) # pass URL specified position along to module, through I4xSystem
instance=module_class(system, instance=module_class(system,
etree.tostring(xml_module), etree.tostring(xml_module),
module_id, module_id,
...@@ -131,12 +139,30 @@ def get_module(user, request, xml_module, module_object_preload): ...@@ -131,12 +139,30 @@ def get_module(user, request, xml_module, module_object_preload):
return (instance, smod, module_type) return (instance, smod, module_type)
def render_x_module(user, request, xml_module, module_object_preload): def render_x_module(user, request, xml_module, module_object_preload, position=None):
''' Generic module for extensions. This renders to HTML. ''' ''' 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 : if xml_module==None :
return {"content":""} 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 # Grab content
content = instance.get_html() content = instance.get_html()
...@@ -156,9 +182,8 @@ def render_x_module(user, request, xml_module, module_object_preload): ...@@ -156,9 +182,8 @@ def render_x_module(user, request, xml_module, module_object_preload):
return content return content
def modx_dispatch(request, module=None, dispatch=None, id=None): 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(): if not request.user.is_authenticated():
return redirect('/') return redirect('/')
...@@ -191,7 +216,7 @@ def modx_dispatch(request, module=None, dispatch=None, id=None): ...@@ -191,7 +216,7 @@ def modx_dispatch(request, module=None, dispatch=None, id=None):
try: try:
xml = content_parser.module_xml(request.user, module, 'id', id, coursename) xml = content_parser.module_xml(request.user, module, 'id', id, coursename)
except: 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'): if accepts(request, 'text/html'):
return render_to_response("module-error.html", {}) return render_to_response("module-error.html", {})
else: else:
...@@ -228,4 +253,3 @@ def modx_dispatch(request, module=None, dispatch=None, id=None): ...@@ -228,4 +253,3 @@ def modx_dispatch(request, module=None, dispatch=None, id=None):
s.save() s.save()
# Return whatever the module wanted to return to the client/caller # Return whatever the module wanted to return to the client/caller
return HttpResponse(ajax_return) return HttpResponse(ajax_return)
...@@ -25,6 +25,8 @@ from multicourse import multicourse_settings ...@@ -25,6 +25,8 @@ from multicourse import multicourse_settings
log = logging.getLogger("mitx.courseware") log = logging.getLogger("mitx.courseware")
#-----------------------------------------------------------------------------
class ComplexEncoder(json.JSONEncoder): class ComplexEncoder(json.JSONEncoder):
def default(self, obj): def default(self, obj):
if isinstance(obj, complex): if isinstance(obj, complex):
...@@ -127,7 +129,7 @@ class Module(XModule): ...@@ -127,7 +129,7 @@ class Module(XModule):
html=render_to_string('problem.html', context) html=render_to_string('problem.html', context)
if encapsulate: 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 return html
...@@ -197,9 +199,27 @@ class Module(XModule): ...@@ -197,9 +199,27 @@ class Module(XModule):
try: try:
fp = self.filestore.open(self.filename) fp = self.filestore.open(self.filename)
except Exception,err: except Exception,err:
print '[courseware.capa.capa_module.Module.init] error %s: cannot open file %s' % (err,self.filename) log.exception('[courseware.capa.capa_module.Module.init] error %s: cannot open file %s' % (err,self.filename))
raise Exception,err 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('<','&lt;')
msg += '<p><pre>%s</pre></p>' % traceback.format_exc().replace('<','&lt;')
# 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) self.lcp=LoncapaProblem(fp, self.item_id, state, seed = seed, system=self.system)
else:
raise
def handle_ajax(self, dispatch, get): def handle_ajax(self, dispatch, get):
''' '''
...@@ -328,8 +348,15 @@ class Module(XModule): ...@@ -328,8 +348,15 @@ class Module(XModule):
self.tracker('save_problem_check', event_info) 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, return json.dumps({'success': success,
'contents': self.get_problem_html(encapsulate=False)}) 'contents': html,
})
def save_problem(self, get): def save_problem(self, get):
event_info = dict() event_info = dict()
......
import json import json
import logging
from mitxmako.shortcuts import render_to_response, render_to_string from mitxmako.shortcuts import render_to_response, render_to_string
from x_module import XModule, XModuleDescriptor from x_module import XModule, XModuleDescriptor
from lxml import etree from lxml import etree
log = logging.getLogger("mitx.courseware")
#-----------------------------------------------------------------------------
class ModuleDescriptor(XModuleDescriptor): class ModuleDescriptor(XModuleDescriptor):
pass pass
...@@ -28,7 +32,10 @@ class Module(XModule): ...@@ -28,7 +32,10 @@ class Module(XModule):
filename="html/"+self.filename filename="html/"+self.filename
return self.filestore.open(filename).read() return self.filestore.open(filename).read()
except: # For backwards compatibility. TODO: Remove 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): def __init__(self, system, xml, item_id, state=None):
XModule.__init__(self, system, xml, item_id, state) XModule.__init__(self, system, xml, item_id, state)
......
...@@ -31,7 +31,16 @@ class Module(XModule): ...@@ -31,7 +31,16 @@ class Module(XModule):
self.render() self.render()
return self.content 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': if dispatch=='goto_position':
self.position = int(get['position']) self.position = int(get['position'])
return json.dumps({'success':True}) return json.dumps({'success':True})
...@@ -83,4 +92,8 @@ class Module(XModule): ...@@ -83,4 +92,8 @@ class Module(XModule):
state = json.loads(state) state = json.loads(state)
if 'position' in state: self.position = int(state['position']) 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 self.rendered = False
...@@ -55,6 +55,7 @@ class XModule(object): ...@@ -55,6 +55,7 @@ class XModule(object):
self.json = json self.json = json
self.item_id = item_id self.item_id = item_id
self.state = state self.state = state
self.DEBUG = False
self.__xmltree = etree.fromstring(xml) # PRIVATE self.__xmltree = etree.fromstring(xml) # PRIVATE
...@@ -65,6 +66,7 @@ class XModule(object): ...@@ -65,6 +66,7 @@ class XModule(object):
self.tracker = system.track_function self.tracker = system.track_function
self.filestore = system.filestore self.filestore = system.filestore
self.render_function = system.render_function self.render_function = system.render_function
self.DEBUG = system.DEBUG
self.system = system self.system = system
### Functions used in the LMS ### Functions used in the LMS
......
<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 &amp; 1 \\ 1 &amp; 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>
#
# unittests for courseware
#
# Note: run this using a like like this:
#
# django-admin.py test --settings=envs.test_ike --pythonpath=. courseware
import unittest import unittest
import os import os
...@@ -127,6 +134,94 @@ class ImageResponseTest(unittest.TestCase): ...@@ -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_1'], 'correct')
self.assertEquals(test_lcp.grade_answers(test_answers)['1_2_2'], 'incorrect') 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>&#x3B8;</mi>
<mo>)</mo>
</mrow>
</mrow>
<mo>&#x22C5;</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>&#x22C5;</mo>
<mrow>
<mi>sin</mi>
<mrow>
<mo>(</mo>
<mi>&#x3B8;</mi>
<mo>)</mo>
</mrow>
</mrow>
<mo>&#x22C5;</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): class OptionResponseTest(unittest.TestCase):
''' '''
Run this with Run this with
......
...@@ -11,6 +11,7 @@ from django.http import Http404 ...@@ -11,6 +11,7 @@ from django.http import Http404
from django.shortcuts import redirect from django.shortcuts import redirect
from mitxmako.shortcuts import render_to_response, render_to_string from mitxmako.shortcuts import render_to_response, render_to_string
#from django.views.decorators.csrf import ensure_csrf_cookie #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 django.views.decorators.cache import cache_control
from lxml import etree from lxml import etree
...@@ -145,9 +146,23 @@ def render_section(request, section): ...@@ -145,9 +146,23 @@ def render_section(request, section):
return result return result
@ensure_csrf_cookie
@cache_control(no_cache=True, no_store=True, must_revalidate=True) @cache_control(no_cache=True, no_store=True, must_revalidate=True)
def index(request, course=None, chapter="Using the System", section="Hints"): def index(request, course=None, chapter="Using the System", section="Hints",position=None):
''' Displays courseware accordion, and any associated content. ''' 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 user = request.user
if not settings.COURSEWARE_ENABLED: if not settings.COURSEWARE_ENABLED:
...@@ -176,12 +191,13 @@ def index(request, course=None, chapter="Using the System", section="Hints"): ...@@ -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 request.session['coursename'] = course # keep track of current course being viewed in django's request.session
try: try:
# this is the course.xml etree
dom = content_parser.course_file(user,course) # also pass course to it, for course-specific XML path dom = content_parser.course_file(user,course) # also pass course to it, for course-specific XML path
except: except:
log.exception("Unable to parse courseware xml") log.exception("Unable to parse courseware xml")
return render_to_response('courseware-error.html', {}) 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]", dom_module = dom.xpath("//course[@name=$course]/chapter[@name=$chapter]//section[@name=$section]",
course=course, chapter=chapter, section=section) course=course, chapter=chapter, section=section)
...@@ -197,6 +213,7 @@ def index(request, course=None, chapter="Using the System", section="Hints"): ...@@ -197,6 +213,7 @@ def index(request, course=None, chapter="Using the System", section="Hints"):
elif module_wrapper.get("src"): elif module_wrapper.get("src"):
module = content_parser.section_file(user=user, section=module_wrapper.get("src"), coursename=course) module = content_parser.section_file(user=user, section=module_wrapper.get("src"), coursename=course)
else: else:
# this is the module's etree
module = etree.XML(etree.tostring(module_wrapper[0])) # Copy the element out of the tree module = etree.XML(etree.tostring(module_wrapper[0])) # Copy the element out of the tree
module_ids = [] module_ids = []
...@@ -217,7 +234,7 @@ def index(request, course=None, chapter="Using the System", section="Hints"): ...@@ -217,7 +234,7 @@ def index(request, course=None, chapter="Using the System", section="Hints"):
} }
try: try:
module = render_x_module(user, request, module, module_object_preload) module_context = render_x_module(user, request, module, module_object_preload, position)
except: except:
log.exception("Unable to load module") log.exception("Unable to load module")
context.update({ context.update({
...@@ -227,104 +244,48 @@ def index(request, course=None, chapter="Using the System", section="Hints"): ...@@ -227,104 +244,48 @@ def index(request, course=None, chapter="Using the System", section="Hints"):
return render_to_response('courseware.html', context) return render_to_response('courseware.html', context)
context.update({ context.update({
'init': module.get('init_js', ''), 'init': module_context.get('init_js', ''),
'content': module['content'], 'content': module_context['content'],
}) })
result = render_to_response('courseware.html', context) result = render_to_response('courseware.html', context)
return result return result
def jump_to(request, probname=None):
def quickedit(request, id=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>.
Maybe this should be moved into capa/views.py We do the jump by (1) reading course.xml to find the first instance of <problem> with the given filename, then
Or this should take a "module" argument, and the quickedit moved into capa_module. (2) finding the parent element of the problem, then (3) rendering that parent element with a specific computed position
''' value (if it is <sequential>).
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 # get coursename if stored
coursename = multicourse_settings.get_coursename_from_request(request) 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)
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 # begin by getting course.xml tree
if str(newcode)==str(pxmls) or '<?xml version="1.0"?>\n'+str(newcode)==str(pxmls): xml = content_parser.course_file(request.user,coursename)
msg = "No changes"
else: # look for problem of given name
# check new code pxml = xml.xpath('//problem[@filename="%s"]' % probname)
isok = False if pxml: pxml = pxml[0]
try:
newxml = etree.fromstring(newcode) # get the parent element
isok = True parent = pxml.getparent()
except Exception,err:
msg = "Failed to change problem: XML error \"<font color=red>%s</font>\"" % err # figure out chapter and section names
chapter = None
if isok: section = None
filename = instance.lcp.fileobject.name branch = parent
fp = open(filename,'w') # TODO - replace with filestore call? for k in range(4): # max depth of recursion
fp.write(newcode) if branch.tag=='section': section = branch.get('name')
fp.close() if branch.tag=='chapter': chapter = branch.get('name')
msg = "<font color=green>Problem changed!</font> (<tt>%s</tt>)" % filename branch = branch.getparent()
instance, pxmls = get_lcp(coursename,id)
position = None
lcp = instance.lcp if parent.tag=='sequential':
position = parent.index(pxml)+1 # position in sequence
# get the rendered problem HTML
phtml = instance.get_problem_html() return index(request,course=coursename,chapter=chapter,section=section,position=position)
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): ...@@ -78,3 +78,4 @@ def get_course_title(coursename):
def get_course_number(coursename): def get_course_number(coursename):
return get_course_property(coursename,'number') return get_course_property(coursename,'number')
...@@ -17,6 +17,8 @@ from multicourse import multicourse_settings ...@@ -17,6 +17,8 @@ from multicourse import multicourse_settings
def mitxhome(request): def mitxhome(request):
''' Home page (link from main header). List of courses. ''' ''' 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: if settings.ENABLE_MULTICOURSE:
context = {'courseinfo' : multicourse_settings.COURSE_SETTINGS} context = {'courseinfo' : multicourse_settings.COURSE_SETTINGS}
return render_to_response("mitxhome.html", context) return render_to_response("mitxhome.html", context)
......
...@@ -173,6 +173,7 @@ class SSLLoginBackend(ModelBackend): ...@@ -173,6 +173,7 @@ class SSLLoginBackend(ModelBackend):
try: try:
user = User.objects.get(username=username) # if user already exists don't create it user = User.objects.get(username=username) # if user already exists don't create it
except User.DoesNotExist: except User.DoesNotExist:
if not settings.DEBUG:
raise "User does not exist. Not creating user; potential schema consistency issues" raise "User does not exist. Not creating user; potential schema consistency issues"
#raise ImproperlyConfigured("[SSLLoginBackend] creating %s" % repr(info)) #raise ImproperlyConfigured("[SSLLoginBackend] creating %s" % repr(info))
user = User(username=username, password=GenPasswd()) # create new User user = User(username=username, password=GenPasswd()) # create new User
......
...@@ -35,7 +35,8 @@ PERFSTATS = False ...@@ -35,7 +35,8 @@ PERFSTATS = False
# Features # Features
MITX_FEATURES = { MITX_FEATURES = {
'SAMPLE' : False 'SAMPLE' : False,
'USE_DJANGO_PIPELINE' : True,
} }
# Used for A/B testing # Used for A/B testing
......
"""
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 &amp; 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 + '/'
"""
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',
)
from check import *
#!/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('<','&#60;')
msg += "<p><pre>%s</pre></p>" % traceback.format_exc().replace('<','&#60;')
ret = {'ok':is_ok,
'msg': msg+endmsg,
}
return ret
from formula import *
from symmath_check import *
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
# File: formula.py # File: formula.py
# Date: 04-May-12 # Date: 04-May-12 (creation)
# Author: I. Chuang <ichuang@mit.edu> # Author: I. Chuang <ichuang@mit.edu>
# #
# flexible python representation of a symbolic mathematical formula. # flexible python representation of a symbolic mathematical formula.
...@@ -30,7 +30,7 @@ from lxml import etree ...@@ -30,7 +30,7 @@ from lxml import etree
import requests import requests
from copy import deepcopy 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' os.environ['PYTHONIOENCODING'] = 'utf-8'
...@@ -143,11 +143,12 @@ class formula(object): ...@@ -143,11 +143,12 @@ class formula(object):
Representation of a mathematical formula object. Accepts mathml math expression for constructing, 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 (=). 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.expr = expr.strip()
self.asciimath = asciimath self.asciimath = asciimath
self.the_cmathml = None self.the_cmathml = None
self.the_sympy = None self.the_sympy = None
self.options = options
def is_presentation_mathml(self): def is_presentation_mathml(self):
return '<mstyle' in self.expr return '<mstyle' in self.expr
...@@ -234,7 +235,10 @@ class formula(object): ...@@ -234,7 +235,10 @@ class formula(object):
if self.the_cmathml: return self.the_cmathml if self.the_cmathml: return self.the_cmathml
# pre-process the presentation mathml before sending it to snuggletex to convert to content mathml # pre-process the presentation mathml before sending it to snuggletex to convert to content mathml
try:
xml = self.preprocess_pmathml(self.expr) xml = self.preprocess_pmathml(self.expr)
except Exception,err:
return "<html>Error! Cannot process pmathml</html>"
pmathml = etree.tostring(xml,pretty_print=True) pmathml = etree.tostring(xml,pretty_print=True)
self.the_pmathml = pmathml self.the_pmathml = pmathml
...@@ -246,7 +250,8 @@ class formula(object): ...@@ -246,7 +250,8 @@ class formula(object):
def make_sympy(self,xml=None): 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 if self.the_sympy: return self.the_sympy
...@@ -255,7 +260,11 @@ class formula(object): ...@@ -255,7 +260,11 @@ class formula(object):
if not self.is_mathml(): if not self.is_mathml():
return my_sympify(self.expr) return my_sympify(self.expr)
if self.is_presentation_mathml(): 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) xml = self.fix_greek_in_mathml(xml)
self.the_sympy = self.make_sympy(xml[0]) self.the_sympy = self.make_sympy(xml[0])
else: else:
...@@ -274,7 +283,7 @@ class formula(object): ...@@ -274,7 +283,7 @@ class formula(object):
# print "divide: arg0=%s, arg1=%s" % (args[0],args[1]) # print "divide: arg0=%s, arg1=%s" % (args[0],args[1])
return sympy.Mul(args[0],sympy.Pow(args[1],-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_times(*args): return reduce(operator.mul,args)
def op_minus(*args): def op_minus(*args):
...@@ -314,9 +323,9 @@ class formula(object): ...@@ -314,9 +323,9 @@ class formula(object):
elif tag=='msup': return '^'.join([parsePresentationMathMLSymbol(y) for y in xml]) elif tag=='msup': return '^'.join([parsePresentationMathMLSymbol(y) for y in xml])
raise Exception,'[parsePresentationMathMLSymbol] unknown tag %s' % tag raise Exception,'[parsePresentationMathMLSymbol] unknown tag %s' % tag
# parser tree for content MathML # parser tree for Content MathML
tag = gettag(xml) tag = gettag(xml)
print "tag = ",tag # print "tag = ",tag
# first do compound objects # first do compound objects
...@@ -325,7 +334,13 @@ class formula(object): ...@@ -325,7 +334,13 @@ class formula(object):
if opstr in opdict: if opstr in opdict:
op = opdict[opstr] op = opdict[opstr]
args = [ self.make_sympy(x) for x in xml[1:]] 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: else:
raise Exception,'[formula]: unknown operator tag %s' % (opstr) raise Exception,'[formula]: unknown operator tag %s' % (opstr)
...@@ -348,7 +363,7 @@ class formula(object): ...@@ -348,7 +363,7 @@ class formula(object):
return float(xml.text) return float(xml.text)
elif tag=='ci': # variable (symbol) 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]) usym = parsePresentationMathMLSymbol(xml[0])
sym = sympy.Symbol(str(usym)) sym = sympy.Symbol(str(usym))
else: else:
...@@ -356,6 +371,10 @@ class formula(object): ...@@ -356,6 +371,10 @@ class formula(object):
if 'hat' in usym: if 'hat' in usym:
sym = my_sympify(usym) sym = my_sympify(usym)
else: else:
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)) sym = sympy.Symbol(str(usym))
return sym return sym
...@@ -459,3 +478,78 @@ def test4(): ...@@ -459,3 +478,78 @@ def test4():
</math> </math>
''' '''
return formula(xmlstr) 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>&#x3B8;</mi>
<mo>)</mo>
</mrow>
</mrow>
<mo>&#x22C5;</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')
#!/usr/bin/python #!/usr/bin/python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
# File: sympy_check2.py # File: symmath_check.py
# Date: 02-May-12 # Date: 02-May-12 (creation)
# Author: I. Chuang <ichuang@mit.edu>
# #
# 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 # Takes in math expressions given as Presentation MathML (from ASCIIMathML), converts to Content MathML using SnuggleTeX
...@@ -15,8 +14,14 @@ from formula import * ...@@ -15,8 +14,14 @@ from formula import *
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
# check function interface # 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} options = {'__MATRIX__':False,'__ABC__':False,'__LOWER__':False}
if extra_options: options.update(extra_options) if extra_options: options.update(extra_options)
...@@ -129,30 +134,41 @@ def check(expect,given,numerical=False,matrix=False,normphase=False,abcsym=False ...@@ -129,30 +134,41 @@ def check(expect,given,numerical=False,matrix=False,normphase=False,abcsym=False
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
# Check function interface, which takes pmathml input # 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 = ''
# msg += '<p/>abname=%s' % abname # msg += '<p/>abname=%s' % abname
# msg += '<p/>adict=%s' % (repr(adict).replace('<','&lt;')) # msg += '<p/>adict=%s' % (repr(adict).replace('<','&lt;'))
threshold = 1.0e-3 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 # parse expected answer
try: try:
fexpect = my_sympify(str(expect)) fexpect = my_sympify(str(expect),matrix=do_matrix,do_qubit=do_qubit)
except Exception,err: except Exception,err:
msg += '<p>Error %s in parsing OUR expected answer "%s"</p>' % (err,expect) msg += '<p>Error %s in parsing OUR expected answer "%s"</p>' % (err,expect)
return {'ok':False,'msg':msg} return {'ok':False,'msg':msg}
# if expected answer is a number, try parsing provided answer as a number also # if expected answer is a number, try parsing provided answer as a number also
try: try:
fans = my_sympify(str(ans)) fans = my_sympify(str(ans),matrix=do_matrix,do_qubit=do_qubit)
except Exception,err: except Exception,err:
fans = None 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: if abs(abs(fans-fexpect)/fexpect)<threshold:
return {'ok':True,'msg':msg} return {'ok':True,'msg':msg}
else: else:
...@@ -164,10 +180,14 @@ def sympy_check2(expect,ans,adict={},abname=''): ...@@ -164,10 +180,14 @@ def sympy_check2(expect,ans,adict={},abname=''):
return {'ok':True,'msg':msg} return {'ok':True,'msg':msg}
# convert mathml answer to formula # convert mathml answer to formula
mmlbox = abname+'_fromjs' try:
if mmlbox in adict: if dynamath:
mmlans = adict[mmlbox] mmlans = dynamath[0]
f = formula(mmlans) 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 # get sympy representation of the formula
# if DEBUG: msg += '<p/> mmlans=%s' % repr(mmlans).replace('<','&lt;') # if DEBUG: msg += '<p/> mmlans=%s' % repr(mmlans).replace('<','&lt;')
...@@ -175,13 +195,20 @@ def sympy_check2(expect,ans,adict={},abname=''): ...@@ -175,13 +195,20 @@ def sympy_check2(expect,ans,adict={},abname=''):
fsym = f.sympy fsym = f.sympy
msg += '<p>You entered: %s</p>' % to_latex(f.sympy) msg += '<p>You entered: %s</p>' % to_latex(f.sympy)
except Exception,err: except Exception,err:
msg += "<p>Error %s in converting to sympy</p>" % str(err).replace('<','&lt;') msg += "<p>Error %s in evaluating your expression '%s' as a valid equation</p>" % (str(err).replace('<','&lt;'),
if DEBUG: msg += "<p><pre>%s</pre></p>" % traceback.format_exc() 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('<','&lt;')
msg += '<p>pmathml=<pre>%s</pre></p>' % mmlans.replace('<','&lt;')
msg += '<hr>'
return {'ok':False,'msg':msg} return {'ok':False,'msg':msg}
# compare with expected # compare with expected
if fexpect.is_number: if hasattr(fexpect,'is_number') and fexpect.is_number:
if fsym.is_number: if hasattr(fsym,'is_number') and fsym.is_number:
if abs(abs(fsym-fexpect)/fexpect)<threshold: if abs(abs(fsym-fexpect)/fexpect)<threshold:
return {'ok':True,'msg':msg} return {'ok':True,'msg':msg}
return {'ok':False,'msg':msg} return {'ok':False,'msg':msg}
...@@ -214,15 +241,21 @@ def sympy_check2(expect,ans,adict={},abname=''): ...@@ -214,15 +241,21 @@ def sympy_check2(expect,ans,adict={},abname=''):
diff = None diff = None
if DEBUG: if DEBUG:
msg += '<hr>'
msg += '<p><font color="blue">DEBUG messages:</p>'
msg += "<p>Got: %s</p>" % repr(fsym) msg += "<p>Got: %s</p>" % repr(fsym)
# msg += "<p/>Got: %s" % str([type(x) for x in fsym.atoms()]).replace('<','&lt;') # msg += "<p/>Got: %s" % str([type(x) for x in fsym.atoms()]).replace('<','&lt;')
msg += "<p>Expecting: %s</p>" % repr(fexpect).replace('**','^').replace('hat(I)','hat(i)') msg += "<p>Expecting: %s</p>" % repr(fexpect).replace('**','^').replace('hat(I)','hat(i)')
# msg += "<p/>Expecting: %s" % str([type(x) for x in fexpect.atoms()]).replace('<','&lt;') # msg += "<p/>Expecting: %s" % str([type(x) for x in fexpect.atoms()]).replace('<','&lt;')
if diff: if diff:
msg += "<p>Difference: %s</p>" % to_latex(diff) msg += "<p>Difference: %s</p>" % to_latex(diff)
msg += '<hr>'
return {'ok':False,'msg':msg,'ex':fexpect,'got':fsym} return {'ok':False,'msg':msg,'ex':fexpect,'got':fsym}
#-----------------------------------------------------------------------------
# tests
def sctest1(): def sctest1():
x = "1/2*(1+(k_e* Q* q)/(m *g *h^2))" x = "1/2*(1+(k_e* Q* q)/(m *g *h^2))"
y = ''' y = '''
......
/* 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;
}
/* 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;
}
\ No newline at end of file
@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;
}
\ No newline at end of file
/* 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;
}
\ No newline at end of file
/* 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;
}
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;
}
/* 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;
}
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.
\ No newline at end of file
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';
}
}
}
/*
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;
}
}
(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);
(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);
// 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(/&lt;/g, '<');
text = text.replace(/&gt;/g, '>');
text = text.replace(/&quot;/g, '"');
text = text.replace(/&#39;/g, "'");
text = text.replace(/&amp;/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();
}
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;
}
}
/*
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();
}
}
(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);
(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);
#!/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()
// 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;
}
/* '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';
}
}
}
/* 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
*/
/**
* 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);
(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);
// Puts the included jQuery into our own namespace
var django = {
"jQuery": jQuery.noConflict(true)
};
This source diff could not be displayed because it is too large. You can view the blob instead.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment