home *** CD-ROM | disk | FTP | other *** search
/ Chip 2006 June / CHIP 2006-06.2.iso / program / freeware / Democracy-0.8.2.exe / xulrunner / python / template.py < prev    next >
Encoding:
Python Source  |  2006-04-10  |  20.5 KB  |  526 lines

  1. # template.py Copyright (c) 2005, 2006 Participatory Culture Foundation
  2. #
  3. # Contains runtime template code
  4.  
  5. from templatehelper import quoteattr, escape, evalKey, toUni, clearEvalCache, attrPattern, rawAttrPattern, resourcePattern, generateId, textFunc, textHideFunc, attrFunc, addIDFunc, evalEscapeFunc, evalFunc, includeHideFunc, hideIfEmptyFunc, rawAttrFunc, hideSectionFunc, quoteAndFillFunc
  6. import resource
  7. from xhtmltools import urlencode
  8.  
  9. ###############################################################################
  10. #### Functions used in repeating templates                                 ####
  11. ###############################################################################
  12.  
  13. # These are functions that take in a dictionary to local data, an id,
  14. # and an argument and return text to be added to the template
  15.  
  16. # Simply returns text
  17. def getRepeatText(data, tid, text):
  18.     return text
  19.  
  20. # Returns text if function does not evaluate to true
  21. def getRepeatTextHide(data, tid, args):
  22.     (functionKey,ifKey,parameter,invert, text) = args
  23.     hide = evalKey(functionKey, data, None, True)(evalKey(ifKey, data, None, True), parameter)
  24.     if (not invert and hide) or (invert and not hide):
  25.         return text
  26.     else:
  27.         return ''
  28.  
  29. def getQuoteAttr(data, tid, value):
  30.     return quoteattr(urlencode(toUni(evalKey(value, data, None, True))))
  31.  
  32. def getRawAttr(data, tid, value):
  33.     return quoteattr(toUni(evalKey(value, data, None, True)))
  34.  
  35. # Adds an id attribute to a tag and closes it
  36. def getRepeatAddIdAndClose(data, tid, args):
  37.     return ' id="%s">'%quoteattr(tid)
  38.  
  39. # Evaluates key with data
  40. def getRepeatEvalEscape(data, tid, replace):
  41.     return escape(evalKey(replace,data,None, True))
  42.  
  43. # Evaluates key with data
  44. def getRepeatEval(data, tid, replace):
  45.     return toUni(evalKey(replace,data,None, True))
  46.  
  47. # Returns include iff function does not evaluate to true
  48. def getRepeatIncludeHide(data, tid, args):
  49.     (functionKey,ifKey,parameter,invert, name) = args
  50.     hide = evalKey(functionKey, data, None, True)(evalKey(ifKey, data, None, True), parameter)
  51.     if (not invert and hide) or (invert and not hide):
  52.         f = open(resource.path('templates/%s'%name),'r')
  53.         html = f.read()
  54.         f.close()
  55.         return html
  56.     else:
  57.         return ''
  58.  
  59. def getHideIfEmpty(data, tid, args):
  60.     (self, viewName, name, invert, attrs) = args
  61.     nodeId = generateId()
  62.     view = self.handle.findNamedView(viewName).getView()
  63.     hide = (not invert and view.len() == 0) or (invert and view.len() > 0)
  64.  
  65.     output = ['<%s'%name]
  66.     for key in attrs.keys():
  67.         if not key in ['t:hideIfViewEmpty','t:hideIfViewNotEmpty','style']:
  68.             PyList_Append(output, ' %s=%s'%(key,quoteAndFillAttr(attrs[key],data)))
  69.     PyList_Append(output,' id="')
  70.     PyList_Append(output,quoteattr(nodeId))
  71.     PyList_Append(output,'"')
  72.     if hide:
  73.         PyList_Append(output,' style="display:none">')
  74.     else:
  75.         PyList_Append(output,'>')
  76.     self.handle.addHideIfEmpty(nodeId,viewName, invert)
  77.     return ''.join(output)
  78.  
  79.  
  80. def getHideSection(data, tid, args):
  81.     output = []
  82.     (functionKey,ifKey,parameter,invert, funcList) = args
  83.     hide = evalKey(functionKey, data, None, True)(evalKey(ifKey, data, None, True), parameter)
  84.     if (invert and hide) or (not invert and not hide):
  85.         for count in range(len(funcList)):
  86.             (func, args) = funcList[count]
  87.             output.append(funcTable[func](data,tid,args))
  88.     return ''.join(output)
  89.  
  90. def getQuoteAndFillAttr(data, tid, value):
  91.     return quoteAndFillAttr(value, data)
  92.  
  93. funcTable = [getRepeatText, getRepeatTextHide, getQuoteAttr, getRepeatAddIdAndClose, getRepeatEvalEscape, getRepeatEval, getRepeatIncludeHide, getHideIfEmpty, getRawAttr, getHideSection, getQuoteAndFillAttr]
  94.  
  95. ###############################################################################
  96. #### Public interface                                                      ####
  97. ###############################################################################
  98.  
  99. # Fill the template in the given file in the template directory using
  100. # the information in the dictionary given by 'data'. If the template
  101. # contains dynamic views, call update methods on the provided
  102. # domHandler function object as necessary in the future, passing a
  103. # string that should be executed in the context of the page to update
  104. # it.  Returns a tuple: a string giving the HTML or XML that resulted
  105. # from filling the template, and a "template handle" whose
  106. # unlinkTemplate() method you should call when you no longer want to
  107. # receive Javascript callbacks.
  108. def fillTemplate(filename, data, domHandler, top = True, onlyBody = False):
  109.     filename = filename.replace('/','.').replace('\\','.').replace('-','_')
  110.     mod = __import__("compiled_templates.%s"%filename)
  111.     mod = getattr(mod,filename)
  112.     return mod.fillTemplate(data, domHandler)
  113.  
  114. # As fillTemplate, but no Javascript calls are made, and no template
  115. # handle is returned, only the HTML or XML as a string. Effectively,
  116. # you get a static snapshot of the page at the time the call is made.
  117. def fillStaticTemplate(filename, data):
  118.     # This could be somewhat more efficient
  119.     (tch, handle) = fillTemplate(filename, data, None)
  120.     handle.unlinkTemplate()
  121.     return tch.getOutput()
  122.  
  123. class TemplateError(Exception):
  124.     def __init__(self, message):
  125.         self.message = message
  126.     def __str__(self):
  127.         return self.message
  128.  
  129. ###############################################################################
  130. #### template runtime code                                                 ####
  131. ###############################################################################
  132.  
  133. # Class used internally by Handle to track a t:repeatForSet clause.
  134. class TrackedView:
  135.     def __init__(self, anchorId, anchorType, view, templateFuncs, templateData, parent, name):
  136.         # arguments as Handle.addView(), plus 'parent', a pointer to the Handle
  137.         # that is used to find domHandler and invoke checkHides
  138.         self.anchorId = anchorId
  139.         self.anchorType = anchorType
  140.  
  141.         self.origView = view
  142.         self.view = view.map(IDAssignmentInView(id(self)).mapper)
  143.         self.templateFuncs = templateFuncs
  144.         self.templateData = templateData
  145.         self.parent = parent
  146.         self.name = name
  147.  
  148.     #
  149.     # This is called after the HTML has been rendered to fill in the
  150.     # data for each view and register callbacks to keep it updated
  151.     def initialFillIn(self):
  152.         self.view.beginRead()
  153.         try:
  154.             #print "Filling in %d items" % self.view.len()
  155.             #start = time.clock()
  156.             xmls = []
  157.             for x in self.view:
  158.                 clearEvalCache()
  159.                 xmls.append(self.currentXML(x))
  160.             self.addHTMLAtEnd(''.join(xmls))
  161.             self.view.addChangeCallback(self.onChange)
  162.             self.view.addAddCallback(self.onAdd)
  163.             self.view.addRemoveCallback(self.onRemove)
  164.             #print "done (%f)" % (time.clock()-start)
  165.         finally:
  166.             self.view.endRead()
  167.  
  168.  
  169.     def currentXML(self, item):
  170.         output = []
  171.         data = self.templateData
  172.         data['this'] = item.object
  173.         data['thisView'] = self.name
  174.         for (func, args) in self.templateFuncs:
  175.             output.append(funcTable[func](data,item.tid,args))
  176.         try:
  177. #             print "-----"
  178. #             print str(''.join(output))
  179. #             print "-----"
  180.             return ''.join(output)
  181.         except UnicodeDecodeError:
  182.             ret = ''
  183.             for string in output:
  184.                 try:
  185.                     ret = ret + string
  186.                 except:
  187.                     pass
  188.             return ret
  189.  
  190.     def onChange(self,obj,id):
  191.         clearEvalCache()
  192.         tid = obj.tid
  193.         xmlString = self.currentXML(obj)
  194.         if self.parent.domHandler:
  195.             self.parent.domHandler.changeItem(tid, xmlString)
  196.         self.parent.checkHides()
  197.  
  198.     def onAdd(self, obj, id):
  199.         clearEvalCache()
  200.         if self.parent.domHandler:
  201.             next = self.view.getNextID(id) 
  202.             if next == None:
  203.                 # Adding it at the end of the list. Must add it relative to
  204.                 # the anchor.
  205.                 if self.anchorType == 'parentNode':
  206.                     self.parent.domHandler.addItemAtEnd(self.currentXML(obj), self.anchorId)
  207.                 if self.anchorType == 'nextSibling':
  208.                     self.parent.domHandler.addItemBefore(self.currentXML(obj), self.anchorId)
  209.             else:
  210.                 self.parent.domHandler.addItemBefore(self.currentXML(obj), self.view.getObjectByID(next).tid)
  211.  
  212.         self.parent.checkHides()
  213.  
  214.     def onRemove(self, obj, id):
  215.         clearEvalCache()
  216.         if self.parent.domHandler:
  217.             self.parent.domHandler.removeItem(obj.tid)
  218.         self.parent.checkHides()
  219.  
  220.     # Add the HTML for the item at newIndex in the view to the
  221.     # display. It should only be called by initialFillIn()
  222.     def addHTMLAtEnd(self, xml):
  223.         if self.parent.domHandler:
  224.             self.parent.domHandler.addItemBefore(xml, self.anchorId)
  225.  
  226. # Class used internally by Handle to track a t:updateForView clause.
  227. class UpdateRegion:
  228.     def __init__(self, anchorId, anchorType, view, templateFuncs, templateData, parent, name):
  229.         # arguments as Handle.addView(), plus 'parent', a pointer to the Handle
  230.         # that is used to find domHandler and invoke checkHides
  231.         self.anchorId = anchorId
  232.         self.anchorType = anchorType
  233.  
  234.         self.view = view
  235.         self.templateFuncs = templateFuncs
  236.         self.templateData = templateData
  237.         self.parent = parent
  238.         self.name = name
  239.         self.tid = generateId()
  240.  
  241.     #
  242.     # This is called after the HTML has been rendered to fill in the
  243.     # data for each view and register callbacks to keep it updated
  244.     def initialFillIn(self):
  245.         self.view.beginRead()
  246.         try:
  247.             clearEvalCache()
  248.             if self.parent.domHandler:
  249.                 self.parent.domHandler.addItemBefore(self.currentXML(), self.anchorId)
  250.             self.view.addChangeCallback(self.onChange)
  251.             self.view.addAddCallback(self.onChange)
  252.             self.view.addRemoveCallback(self.onChange)
  253.         finally:
  254.             self.view.endRead()
  255.  
  256.  
  257.     def currentXML(self):
  258.         output = []
  259.         data = self.templateData
  260.         data['this'] = self.view
  261.         data['thisView'] = self.name
  262.         for (func, args) in self.templateFuncs:
  263.             output.append(funcTable[func](data,self.tid,args))
  264.         try:
  265.             return ''.join(output) 
  266.         except UnicodeDecodeError:
  267.             ret = ''
  268.             for string in output:
  269.                 try:
  270.                     ret = ret + string
  271.                 except:
  272.                     pass
  273.             return ret
  274.  
  275.     def onChange(self,obj=None,id=None):
  276.         clearEvalCache()
  277.         xmlString = self.currentXML()
  278.         if self.parent.domHandler:
  279.             self.parent.domHandler.changeItem(self.tid, xmlString)
  280.         self.parent.checkHides()
  281.  
  282. # Class used by Handle to track the dynamically filterable, sortable
  283. # views created by makeNamedView and identified by names. After
  284. # creation, can be looked up with Handle.findNamedView and the filter
  285. # and sort changed with setFilter and setSort.
  286. class NamedView:
  287.     def __init__(self, name, viewKey, viewIndex, viewIndexValue, filterKey, filterFunc, filterParameter, invertFilter, sortKey, sortFunc, invertSort, data):
  288.         self.name = name
  289.         self.data = data
  290.         self.origView = evalKey(viewKey, data, None, True)
  291.  
  292.         if viewIndex is not None:
  293.             self.indexFunc = evalKey(viewIndex, data, None, True)
  294.             self.indexValue = viewIndexValue
  295.             self.indexView = self.origView.filterWithIndex(self.indexFunc,self.indexValue)
  296.         else:
  297.             self.indexView = self.origView
  298.  
  299.         if filterKey is None:
  300.             self.filter = returnTrue
  301.         else:
  302.             self.filter = makeFilter(filterKey, filterFunc, filterParameter, invertFilter,self.data)
  303.         if sortKey is None:
  304.             self.sort = nullSort
  305.         else:
  306.             self.sort = makeSort(sortKey, sortFunc, invertSort, self.data)
  307.  
  308.         self.filterView = self.indexView.filter(lambda x:self.filter(x))
  309.  
  310.         self.view = self.filterView.sort(lambda x, y:self.sort(x,y))
  311.  
  312.     def setFilter(self, fieldKey, funcKey, parameter, invert):
  313.         if not self.filter:
  314.             raise TemplateError, "View '%s' was not declared with a filter, so it is not possible to change the filter parameters" % self.name
  315.         self.filter = makeFilter(fieldKey, funcKey, parameter, invert,self.data)
  316.         self.indexView.recomputeFilter(self.filterView)
  317.  
  318.     def setSort(self, fieldKey, funcKey, invert):
  319.         if not self.sort:
  320.             raise TemplateError, "View '%s' was not declared with a sort, so it is not possible to change the sort parameters." % self.name
  321.         self.sort = makeSort(fieldKey, funcKey, invert, self.data)
  322.         self.indexView.recomputeSort(self.filterView)
  323.  
  324.     def getView(self):
  325.         # Internal use.
  326.         return self.view
  327.  
  328.     def removeViewFromDB(self):
  329.         if self.origView is self.indexView:
  330.             self.origView.removeView(self.filterView)
  331.         else:
  332.             self.origView.removeView(self.indexView)
  333.  
  334. # Object representing a set of registrations for Javascript callbacks when
  335. # the contents of some set of database views change. One of these Handles
  336. # is returned whenever you fill a template; when you no longer want to
  337. # receive Javascript callbacks for a particular filled template, call
  338. # this object's unlinkTemplate() method.
  339. class Handle:
  340.     def __init__(self, domHandler, document = None):
  341.         # 'domHandler' is an object that will receive method calls when
  342.         # dynamic page updates need to be made. 
  343.         self.domHandler = domHandler
  344.         self.document = document
  345.         self.hideConditions = []
  346.         self.namedViews = {}
  347.         self.trackedViews = []
  348.         self.updateRegions = []
  349.         self.subHandles = []
  350.         self.triggerActionURLsOnLoad = []
  351.         self.triggerActionURLsOnUnload = []
  352.         
  353.     def addTriggerActionURLOnLoad(self,url):
  354.         self.triggerActionURLsOnLoad.append(str(url))
  355.  
  356.     def addTriggerActionURLOnUnload(self, url):
  357.         self.triggerActionURLsOnUnload.append(str(url))
  358.  
  359.     def getTriggerActionURLsOnLoad(self):
  360.         return self.triggerActionURLsOnLoad
  361.  
  362.     def getTriggerActionURLsOnUnload(self):
  363.         return self.triggerActionURLsOnUnload
  364.  
  365.     def addHideIfEmpty(self, id, name, invert):
  366.         # Make JS calls to hide and show the node with the give id when
  367.         # the given view becomes true and false, respectively.
  368.         if invert:
  369.             self.hideConditions.append((id, name, invert, not self.viewIsEmpty(name)))
  370.         else:
  371.             self.hideConditions.append((id, name, invert, self.viewIsEmpty(name)))
  372.  
  373.     def viewIsEmpty(self, viewName):
  374.         return self.findNamedView(viewName).getView().len()==0
  375.  
  376.     def checkHides(self):
  377.         # Internal use.
  378.         if self.domHandler:
  379.             for i in range(0,len(self.hideConditions)):
  380.                 (id, name, invert, previous) = self.hideConditions[i]
  381.                 if invert:
  382.                     current = not self.viewIsEmpty(name)
  383.                 else:
  384.                     current = self.viewIsEmpty(name)
  385.                 if (current == True) != (previous == True):
  386.                     self.hideConditions[i] = (id, name, invert, current)
  387.                     if current:
  388.                         self.domHandler.hideItem(id)
  389.                     else:
  390.                         self.domHandler.showItem(id)
  391.                         
  392.     def makeNamedView(self, name, viewKey, viewIndex, viewIndexValue, filterKey, filterFunc, filterParameter, invertFilter, sortKey, sortFunc, invertSort, data):
  393.         if self.namedViews.has_key(name):
  394.             raise TemplateError, "More than one view was declared with the name '%s'. Each view must have a different name." % name
  395.         nv = NamedView(name, viewKey, viewIndex, viewIndexValue, filterKey, filterFunc, filterParameter, invertFilter, sortKey, sortFunc, invertSort, data)
  396.         self.namedViews[name] = nv
  397.         return nv.getView()
  398.  
  399.     def findNamedView(self, name):
  400.         if self.namedViews.has_key(name):
  401.             return self.namedViews[name]
  402.         else:
  403.             for sh in self.subHandles:
  404.                 try:
  405.                     return sh.findNamedView(name)
  406.                 except TemplateError:
  407.                     pass
  408.         raise TemplateError, "A view named '%s' was referenced but not defined." % name
  409.  
  410.     def addView(self, anchorId, anchorType, view, templateFuncs, data, name):
  411.         # Register for JS calls to populate a t:repeatFor. 'view' is the
  412.         # database view to track; 'node' is a DOM node representing the
  413.         # template to fill; 'data' are extra variables to be used in expanding
  414.         # the template. The 'anchor*' arguments tell where in the document
  415.         # to place the expanded template nodes. If 'anchorType' is
  416.         # 'nextSibling', 'anchorId' is the id attribute of the tag immediately
  417.         # following the place the template should be expanded. If it is
  418.         # 'parentNode', the template should be expanded so as to be the final
  419.         # child in the node whose id attribute matches 'anchorId'.
  420.         #
  421.         # We take a private copy of 'node', so don't worry about modifying
  422.         # it subsequent to calling this method.
  423.         tv = TrackedView(anchorId, anchorType, view, templateFuncs, data, self, name)
  424.         self.trackedViews.append(tv)
  425.  
  426.     def addUpdate(self, anchorId, anchorType, view, templateFuncs, data, name):
  427.         ur = UpdateRegion(anchorId, anchorType, view, templateFuncs, data, self, name)
  428.         self.updateRegions.append(ur)
  429.  
  430.     def unlinkTemplate(self):
  431.         # Stop delivering callbacks, allowing the handle to be released.
  432.         self.domHandler = None
  433.         try:
  434.             self.document.unlink()
  435.         except:
  436.             pass
  437.         self.document = None
  438.         self.hideConditions = []
  439.         self.trackedViews = []
  440.         self.updateRegions = []
  441.         for handle in self.subHandles:
  442.             handle.unlinkTemplate()
  443.         for view in self.namedViews.values():
  444.             view.removeViewFromDB()
  445.     
  446.     def initialFillIn(self):
  447.         for tv in self.trackedViews:
  448.             tv.initialFillIn()
  449.         for ur in self.updateRegions:
  450.             ur.initialFillIn()
  451.         for handle in self.subHandles:
  452.             handle.initialFillIn()
  453.  
  454.     def addSubHandle(self, handle):
  455.         self.subHandles.append(handle)
  456.  
  457. # Random utility functions 
  458. def returnFalse(x):
  459.     return False
  460.  
  461. def returnTrue(x):
  462.     return True
  463.  
  464. def identityFunc(x):
  465.     return x
  466.  
  467. def nullSort(x,y):
  468.     return 0
  469.  
  470. # View mapping function used to assign ID attributes to records so
  471. # that we can find them in the page after we generate them if we need
  472. # to update them.
  473. class IDAssignment:
  474.     def __init__(self, x, parentViewName):
  475.         self.object = x
  476.         self.tid = "objid-%s-%d" % (parentViewName, id(self.object))
  477.         
  478. class IDAssignmentInView:
  479.     def __init__(self, name=""):
  480.         self.viewName = name
  481.     def mapper(self, obj):
  482.         return IDAssignment(obj, self.viewName)
  483.  
  484.  
  485. def makeFilter(fieldKey, funcKey, parameter, invert, data):
  486.     func = evalKey(funcKey, data)
  487.     if not invert:
  488.         return lambda x: func(evalKey(fieldKey, x), parameter)
  489.     else:
  490.         return lambda x: not func(evalKey(fieldKey, x), parameter)
  491.  
  492. def makeSort(fieldKey, funcKey, invert,data):
  493.     func = evalKey(funcKey, data)
  494.     if not invert:
  495.         return lambda x,y:  func(evalKey(fieldKey,x), evalKey(fieldKey,y)) 
  496.     else:
  497.         return lambda x,y: -func(evalKey(fieldKey,x), evalKey(fieldKey,y))
  498.  
  499. # Returns a quoted, filled version of attribute text
  500. def quoteAndFillAttr(value,data):
  501.     return ''.join(('"',quoteattr(fillAttr(value,data)),'"'))
  502.  
  503. # Returns a filled version of attribute text
  504. # Important: because we expand resource: URLs here, instead of defining a
  505. # URL handler (which is hard to do in IE), you must link to stylesheets via
  506. # <link .../> rather than <style> @import ... </style> if they are resource:
  507. # URLs.
  508. def fillAttr(value, data):
  509.     match = attrPattern.match(value)
  510.     if match:
  511.         return ''.join((match.group(1), urlencode(toUni(evalKey(match.group(2), data, None, True))), match.group(3)))
  512.     else:
  513.         match = rawAttrPattern.match(value)
  514.         if match:
  515.             return ''.join((match.group(1), toUni(evalKey(match.group(2), data, None, True)), match.group(3)))
  516.         else:
  517.             match = resourcePattern.match(value)
  518.             if match:
  519.                 return resource.url(match.group(1))
  520.             else:
  521.                 return value
  522.  
  523. # This has to be after Handle, so the compiled templates can get
  524. # access to Handle
  525. import compiled_templates
  526.