Code

eea18ce7497ba5d1b0bf2f54fd592d1754ce5bf6
[roundup.git] / roundup / cgi / templating.py
1 import sys, cgi, urllib, os, re, os.path, time, errno
3 from roundup import hyperdb, date
4 from roundup.i18n import _
6 try:
7     import cPickle as pickle
8 except ImportError:
9     import pickle
10 try:
11     import cStringIO as StringIO
12 except ImportError:
13     import StringIO
14 try:
15     import StructuredText
16 except ImportError:
17     StructuredText = None
19 # bring in the templating support
20 from roundup.cgi.PageTemplates import PageTemplate
21 from roundup.cgi.PageTemplates.Expressions import getEngine
22 from roundup.cgi.TAL.TALInterpreter import TALInterpreter
23 from roundup.cgi import ZTUtils
25 # XXX WAH pagetemplates aren't pickleable :(
26 #def getTemplate(dir, name, classname=None, request=None):
27 #    ''' Interface to get a template, possibly loading a compiled template.
28 #    '''
29 #    # source
30 #    src = os.path.join(dir, name)
31 #
32 #    # see if we can get a compile from the template"c" directory (most
33 #    # likely is "htmlc"
34 #    split = list(os.path.split(dir))
35 #    split[-1] = split[-1] + 'c'
36 #    cdir = os.path.join(*split)
37 #    split.append(name)
38 #    cpl = os.path.join(*split)
39 #
40 #    # ok, now see if the source is newer than the compiled (or if the
41 #    # compiled even exists)
42 #    MTIME = os.path.stat.ST_MTIME
43 #    if (not os.path.exists(cpl) or os.stat(cpl)[MTIME] < os.stat(src)[MTIME]):
44 #        # nope, we need to compile
45 #        pt = RoundupPageTemplate()
46 #        pt.write(open(src).read())
47 #        pt.id = name
48 #
49 #        # save off the compiled template
50 #        if not os.path.exists(cdir):
51 #            os.makedirs(cdir)
52 #        f = open(cpl, 'wb')
53 #        pickle.dump(pt, f)
54 #        f.close()
55 #    else:
56 #        # yay, use the compiled template
57 #        f = open(cpl, 'rb')
58 #        pt = pickle.load(f)
59 #    return pt
61 templates = {}
63 def getTemplate(dir, name, extension, classname=None, request=None):
64     ''' Interface to get a template, possibly loading a compiled template.
66         "name" and "extension" indicate the template we're after, which in
67         most cases will be "name.extension". If "extension" is None, then
68         we look for a template just called "name" with no extension.
70         If the file "name.extension" doesn't exist, we look for
71         "_generic.extension" as a fallback.
72     '''
73     # default the name to "home"
74     if name is None:
75         name = 'home'
77     # find the source, figure the time it was last modified
78     if extension:
79         filename = '%s.%s'%(name, extension)
80     else:
81         filename = name
82     src = os.path.join(dir, filename)
83     try:
84         stime = os.stat(src)[os.path.stat.ST_MTIME]
85     except os.error, error:
86         if error.errno != errno.ENOENT or not extension:
87             raise
88         # try for a generic template
89         filename = '_generic.%s'%extension
90         src = os.path.join(dir, filename)
91         stime = os.stat(src)[os.path.stat.ST_MTIME]
93     key = (dir, filename)
94     if templates.has_key(key) and stime < templates[key].mtime:
95         # compiled template is up to date
96         return templates[key]
98     # compile the template
99     templates[key] = pt = RoundupPageTemplate()
100     pt.write(open(src).read())
101     pt.id = filename
102     pt.mtime = time.time()
103     return pt
105 class RoundupPageTemplate(PageTemplate.PageTemplate):
106     ''' A Roundup-specific PageTemplate.
108         Interrogate the client to set up the various template variables to
109         be available:
111         *class*
112           The current class of node being displayed as an HTMLClass
113           instance.
114         *item*
115           The current node from the database, if we're viewing a specific
116           node, as an HTMLItem instance. If it doesn't exist, then we're
117           on a new item page.
118         (*classname*)
119           this is one of two things:
121           1. the *item* is also available under its classname, so a *user*
122              node would also be available under the name *user*. This is
123              also an HTMLItem instance.
124           2. if there's no *item* then the current class is available
125              through this name, thus "user/name" and "user/name/menu" will
126              still work - the latter will pull information from the form
127              if it can.
128         *form*
129           The current CGI form information as a mapping of form argument
130           name to value
131         *request*
132           Includes information about the current request, including:
133            - the url
134            - the current index information (``filterspec``, ``filter`` args,
135              ``properties``, etc) parsed out of the form. 
136            - methods for easy filterspec link generation
137            - *user*, the current user node as an HTMLItem instance
138         *instance*
139           The current instance
140         *db*
141           The current database, through which db.config may be reached.
143         Maybe also:
145         *modules*
146           python modules made available (XXX: not sure what's actually in
147           there tho)
148     '''
149     def getContext(self, client, classname, request):
150         c = {
151              'klass': HTMLClass(client, classname),
152              'options': {},
153              'nothing': None,
154              'request': request,
155              'content': client.content,
156              'db': HTMLDatabase(client),
157              'instance': client.instance
158         }
159         # add in the item if there is one
160         if client.nodeid:
161             c['item'] = HTMLItem(client, classname, client.nodeid)
162             c[classname] = c['item']
163         else:
164             c[classname] = c['klass']
165         return c
167     def render(self, client, classname, request, **options):
168         """Render this Page Template"""
170         if not self._v_cooked:
171             self._cook()
173         __traceback_supplement__ = (PageTemplate.PageTemplateTracebackSupplement, self)
175         if self._v_errors:
176             raise PTRuntimeError, 'Page Template %s has errors.' % self.id
178         # figure the context
179         classname = classname or client.classname
180         request = request or HTMLRequest(client)
181         c = self.getContext(client, classname, request)
182         c.update({'options': options})
184         # and go
185         output = StringIO.StringIO()
186         TALInterpreter(self._v_program, self._v_macros,
187             getEngine().getContext(c), output, tal=1, strictinsert=0)()
188         return output.getvalue()
190 class HTMLDatabase:
191     ''' Return HTMLClasses for valid class fetches
192     '''
193     def __init__(self, client):
194         self.client = client
195         self.config = client.db.config
196     def __getattr__(self, attr):
197         try:
198             self.client.db.getclass(attr)
199         except KeyError:
200             raise AttributeError, attr
201         return HTMLClass(self.client, attr)
202     def classes(self):
203         l = self.client.db.classes.keys()
204         l.sort()
205         return [HTMLClass(self.client, cn) for cn in l]
206         
207 class HTMLClass:
208     ''' Accesses through a class (either through *class* or *db.<classname>*)
209     '''
210     def __init__(self, client, classname):
211         self.client = client
212         self.db = client.db
213         self.classname = classname
214         if classname is not None:
215             self.klass = self.db.getclass(self.classname)
216             self.props = self.klass.getprops()
218     def __repr__(self):
219         return '<HTMLClass(0x%x) %s>'%(id(self), self.classname)
221     def __getitem__(self, item):
222         ''' return an HTMLProperty instance
223         '''
224         #print 'getitem', (self, item)
225         if item == 'creator':
226             return HTMLUser(self.client, 'user', client.userid)
228         if not self.props.has_key(item):
229             raise KeyError, item
230         prop = self.props[item]
232         # look up the correct HTMLProperty class
233         for klass, htmlklass in propclasses:
234             if isinstance(prop, hyperdb.Multilink):
235                 value = []
236             else:
237                 value = None
238             if isinstance(prop, klass):
239                 return htmlklass(self.client, '', prop, item, value)
241         # no good
242         raise KeyError, item
244     def __getattr__(self, attr):
245         ''' convenience access '''
246         try:
247             return self[attr]
248         except KeyError:
249             raise AttributeError, attr
251     def properties(self):
252         ''' Return HTMLProperty for all props
253         '''
254         l = []
255         for name, prop in self.props.items():
256             for klass, htmlklass in propclasses:
257                 if isinstance(prop, hyperdb.Multilink):
258                     value = []
259                 else:
260                     value = None
261                 if isinstance(prop, klass):
262                     l.append(htmlklass(self.client, '', prop, name, value))
263         return l
265     def list(self):
266         if self.classname == 'user':
267             klass = HTMLUser
268         else:
269             klass = HTMLItem
270         l = [klass(self.client, self.classname, x) for x in self.klass.list()]
271         return l
273     def csv(self):
274         ''' Return the items of this class as a chunk of CSV text.
275         '''
276         # get the CSV module
277         try:
278             import csv
279         except ImportError:
280             return 'Sorry, you need the csv module to use this function.\n'\
281                 'Get it from: http://www.object-craft.com.au/projects/csv/'
283         props = self.propnames()
284         p = csv.parser()
285         s = StringIO.StringIO()
286         s.write(p.join(props) + '\n')
287         for nodeid in self.klass.list():
288             l = []
289             for name in props:
290                 value = self.klass.get(nodeid, name)
291                 if value is None:
292                     l.append('')
293                 elif isinstance(value, type([])):
294                     l.append(':'.join(map(str, value)))
295                 else:
296                     l.append(str(self.klass.get(nodeid, name)))
297             s.write(p.join(l) + '\n')
298         return s.getvalue()
300     def propnames(self):
301         ''' Return the list of the names of the properties of this class.
302         '''
303         idlessprops = self.klass.getprops(protected=0).keys()
304         idlessprops.sort()
305         return ['id'] + idlessprops
307     def filter(self, request=None):
308         ''' Return a list of items from this class, filtered and sorted
309             by the current requested filterspec/filter/sort/group args
310         '''
311         if request is not None:
312             filterspec = request.filterspec
313             sort = request.sort
314             group = request.group
315         if self.classname == 'user':
316             klass = HTMLUser
317         else:
318             klass = HTMLItem
319         l = [klass(self.client, self.classname, x)
320              for x in self.klass.filter(None, filterspec, sort, group)]
321         return l
323     def classhelp(self, properties, label='?', width='400', height='400'):
324         '''pop up a javascript window with class help
326            This generates a link to a popup window which displays the 
327            properties indicated by "properties" of the class named by
328            "classname". The "properties" should be a comma-separated list
329            (eg. 'id,name,description').
331            You may optionally override the label displayed, the width and
332            height. The popup window will be resizable and scrollable.
333         '''
334         return '<a href="javascript:help_window(\'%s?:template=help&' \
335             ':contentonly=1&properties=%s\', \'%s\', \'%s\')"><b>'\
336             '(%s)</b></a>'%(self.classname, properties, width, height, label)
338     def submit(self, label="Submit New Entry"):
339         ''' Generate a submit button (and action hidden element)
340         '''
341         return '  <input type="hidden" name=":action" value="new">\n'\
342         '  <input type="submit" name="submit" value="%s">'%label
344     def history(self):
345         return 'New node - no history'
347     def renderWith(self, name, **kwargs):
348         ''' Render this class with the given template.
349         '''
350         # create a new request and override the specified args
351         req = HTMLRequest(self.client)
352         req.classname = self.classname
353         req.update(kwargs)
355         # new template, using the specified classname and request
356         pt = getTemplate(self.db.config.TEMPLATES, self.classname, name)
358         # XXX handle PT rendering errors here nicely
359         try:
360             # use our fabricated request
361             return pt.render(self.client, self.classname, req)
362         except PageTemplate.PTRuntimeError, message:
363             return '<strong>%s</strong><ol>%s</ol>'%(message,
364                 cgi.escape('<li>'.join(pt._v_errors)))
366 class HTMLItem:
367     ''' Accesses through an *item*
368     '''
369     def __init__(self, client, classname, nodeid):
370         self.client = client
371         self.db = client.db
372         self.classname = classname
373         self.nodeid = nodeid
374         self.klass = self.db.getclass(classname)
375         self.props = self.klass.getprops()
377     def __repr__(self):
378         return '<HTMLItem(0x%x) %s %s>'%(id(self), self.classname, self.nodeid)
380     def __getitem__(self, item):
381         ''' return an HTMLProperty instance
382         '''
383         #print 'getitem', (self, item)
384         if item == 'id':
385             return self.nodeid
386         if not self.props.has_key(item):
387             raise KeyError, item
388         prop = self.props[item]
390         # get the value, handling missing values
391         value = self.klass.get(self.nodeid, item, None)
392         if value is None:
393             if isinstance(self.props[item], hyperdb.Multilink):
394                 value = []
396         # look up the correct HTMLProperty class
397         for klass, htmlklass in propclasses:
398             if isinstance(prop, klass):
399                 return htmlklass(self.client, self.nodeid, prop, item, value)
401         raise KeyErorr, item
403     def __getattr__(self, attr):
404         ''' convenience access to properties '''
405         try:
406             return self[attr]
407         except KeyError:
408             raise AttributeError, attr
409     
410     def submit(self, label="Submit Changes"):
411         ''' Generate a submit button (and action hidden element)
412         '''
413         return '  <input type="hidden" name=":action" value="edit">\n'\
414         '  <input type="submit" name="submit" value="%s">'%label
416     # XXX this probably should just return the history items, not the HTML
417     def history(self, direction='descending'):
418         l = ['<table width=100% border=0 cellspacing=0 cellpadding=2>',
419             '<tr class="list-header">',
420             _('<th align=left><span class="list-item">Date</span></th>'),
421             _('<th align=left><span class="list-item">User</span></th>'),
422             _('<th align=left><span class="list-item">Action</span></th>'),
423             _('<th align=left><span class="list-item">Args</span></th>'),
424             '</tr>']
425         comments = {}
426         history = self.klass.history(self.nodeid)
427         history.sort()
428         if direction == 'descending':
429             history.reverse()
430         for id, evt_date, user, action, args in history:
431             date_s = str(evt_date).replace("."," ")
432             arg_s = ''
433             if action == 'link' and type(args) == type(()):
434                 if len(args) == 3:
435                     linkcl, linkid, key = args
436                     arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
437                         linkcl, linkid, key)
438                 else:
439                     arg_s = str(args)
441             elif action == 'unlink' and type(args) == type(()):
442                 if len(args) == 3:
443                     linkcl, linkid, key = args
444                     arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
445                         linkcl, linkid, key)
446                 else:
447                     arg_s = str(args)
449             elif type(args) == type({}):
450                 cell = []
451                 for k in args.keys():
452                     # try to get the relevant property and treat it
453                     # specially
454                     try:
455                         prop = self.props[k]
456                     except KeyError:
457                         prop = None
458                     if prop is not None:
459                         if args[k] and (isinstance(prop, hyperdb.Multilink) or
460                                 isinstance(prop, hyperdb.Link)):
461                             # figure what the link class is
462                             classname = prop.classname
463                             try:
464                                 linkcl = self.db.getclass(classname)
465                             except KeyError:
466                                 labelprop = None
467                                 comments[classname] = _('''The linked class
468                                     %(classname)s no longer exists''')%locals()
469                             labelprop = linkcl.labelprop(1)
470                             hrefable = os.path.exists(
471                                 os.path.join(self.db.config.TEMPLATES,
472                                 classname+'.item'))
474                         if isinstance(prop, hyperdb.Multilink) and \
475                                 len(args[k]) > 0:
476                             ml = []
477                             for linkid in args[k]:
478                                 if isinstance(linkid, type(())):
479                                     sublabel = linkid[0] + ' '
480                                     linkids = linkid[1]
481                                 else:
482                                     sublabel = ''
483                                     linkids = [linkid]
484                                 subml = []
485                                 for linkid in linkids:
486                                     label = classname + linkid
487                                     # if we have a label property, try to use it
488                                     # TODO: test for node existence even when
489                                     # there's no labelprop!
490                                     try:
491                                         if labelprop is not None:
492                                             label = linkcl.get(linkid, labelprop)
493                                     except IndexError:
494                                         comments['no_link'] = _('''<strike>The
495                                             linked node no longer
496                                             exists</strike>''')
497                                         subml.append('<strike>%s</strike>'%label)
498                                     else:
499                                         if hrefable:
500                                             subml.append('<a href="%s%s">%s</a>'%(
501                                                 classname, linkid, label))
502                                 ml.append(sublabel + ', '.join(subml))
503                             cell.append('%s:\n  %s'%(k, ', '.join(ml)))
504                         elif isinstance(prop, hyperdb.Link) and args[k]:
505                             label = classname + args[k]
506                             # if we have a label property, try to use it
507                             # TODO: test for node existence even when
508                             # there's no labelprop!
509                             if labelprop is not None:
510                                 try:
511                                     label = linkcl.get(args[k], labelprop)
512                                 except IndexError:
513                                     comments['no_link'] = _('''<strike>The
514                                         linked node no longer
515                                         exists</strike>''')
516                                     cell.append(' <strike>%s</strike>,\n'%label)
517                                     # "flag" this is done .... euwww
518                                     label = None
519                             if label is not None:
520                                 if hrefable:
521                                     cell.append('%s: <a href="%s%s">%s</a>\n'%(k,
522                                         classname, args[k], label))
523                                 else:
524                                     cell.append('%s: %s' % (k,label))
526                         elif isinstance(prop, hyperdb.Date) and args[k]:
527                             d = date.Date(args[k])
528                             cell.append('%s: %s'%(k, str(d)))
530                         elif isinstance(prop, hyperdb.Interval) and args[k]:
531                             d = date.Interval(args[k])
532                             cell.append('%s: %s'%(k, str(d)))
534                         elif isinstance(prop, hyperdb.String) and args[k]:
535                             cell.append('%s: %s'%(k, cgi.escape(args[k])))
537                         elif not args[k]:
538                             cell.append('%s: (no value)\n'%k)
540                         else:
541                             cell.append('%s: %s\n'%(k, str(args[k])))
542                     else:
543                         # property no longer exists
544                         comments['no_exist'] = _('''<em>The indicated property
545                             no longer exists</em>''')
546                         cell.append('<em>%s: %s</em>\n'%(k, str(args[k])))
547                 arg_s = '<br />'.join(cell)
548             else:
549                 # unkown event!!
550                 comments['unknown'] = _('''<strong><em>This event is not
551                     handled by the history display!</em></strong>''')
552                 arg_s = '<strong><em>' + str(args) + '</em></strong>'
553             date_s = date_s.replace(' ', '&nbsp;')
554             l.append('<tr><td nowrap valign=top>%s</td><td valign=top>%s</td>'
555                 '<td valign=top>%s</td><td valign=top>%s</td></tr>'%(date_s,
556                 user, action, arg_s))
557         if comments:
558             l.append(_('<tr><td colspan=4><strong>Note:</strong></td></tr>'))
559         for entry in comments.values():
560             l.append('<tr><td colspan=4>%s</td></tr>'%entry)
561         l.append('</table>')
562         return '\n'.join(l)
564     def remove(self):
565         # XXX do what?
566         return ''
568 class HTMLUser(HTMLItem):
569     ''' Accesses through the *user* (a special case of item)
570     '''
571     def __init__(self, client, classname, nodeid):
572         HTMLItem.__init__(self, client, 'user', nodeid)
573         self.default_classname = client.classname
575         # used for security checks
576         self.security = client.db.security
577     _marker = []
578     def hasPermission(self, role, classname=_marker):
579         ''' Determine if the user has the Role.
581             The class being tested defaults to the template's class, but may
582             be overidden for this test by suppling an alternate classname.
583         '''
584         if classname is self._marker:
585             classname = self.default_classname
586         return self.security.hasPermission(role, self.nodeid, classname)
588 class HTMLProperty:
589     ''' String, Number, Date, Interval HTMLProperty
591         A wrapper object which may be stringified for the plain() behaviour.
592     '''
593     def __init__(self, client, nodeid, prop, name, value):
594         self.client = client
595         self.db = client.db
596         self.nodeid = nodeid
597         self.prop = prop
598         self.name = name
599         self.value = value
600     def __repr__(self):
601         return '<HTMLProperty(0x%x) %s %r %r>'%(id(self), self.name, self.prop, self.value)
602     def __str__(self):
603         return self.plain()
604     def __cmp__(self, other):
605         if isinstance(other, HTMLProperty):
606             return cmp(self.value, other.value)
607         return cmp(self.value, other)
609 class StringHTMLProperty(HTMLProperty):
610     def plain(self, escape=0):
611         if self.value is None:
612             return ''
613         if escape:
614             return cgi.escape(str(self.value))
615         return str(self.value)
617     def stext(self, escape=0):
618         s = self.plain(escape=escape)
619         if not StructuredText:
620             return s
621         return StructuredText(s,level=1,header=0)
623     def field(self, size = 30):
624         if self.value is None:
625             value = ''
626         else:
627             value = cgi.escape(str(self.value))
628             value = '&quot;'.join(value.split('"'))
629         return '<input name="%s" value="%s" size="%s">'%(self.name, value, size)
631     def multiline(self, escape=0, rows=5, cols=40):
632         if self.value is None:
633             value = ''
634         else:
635             value = cgi.escape(str(self.value))
636             value = '&quot;'.join(value.split('"'))
637         return '<textarea name="%s" rows="%s" cols="%s">%s</textarea>'%(
638             self.name, rows, cols, value)
640     def email(self, escape=1):
641         ''' fudge email '''
642         if self.value is None: value = ''
643         else: value = str(self.value)
644         value = value.replace('@', ' at ')
645         value = value.replace('.', ' ')
646         if escape:
647             value = cgi.escape(value)
648         return value
650 class PasswordHTMLProperty(HTMLProperty):
651     def plain(self):
652         if self.value is None:
653             return ''
654         return _('*encrypted*')
656     def field(self, size = 30):
657         return '<input type="password" name="%s" size="%s">'%(self.name, size)
659 class NumberHTMLProperty(HTMLProperty):
660     def plain(self):
661         return str(self.value)
663     def field(self, size = 30):
664         if self.value is None:
665             value = ''
666         else:
667             value = cgi.escape(str(self.value))
668             value = '&quot;'.join(value.split('"'))
669         return '<input name="%s" value="%s" size="%s">'%(self.name, value, size)
671 class BooleanHTMLProperty(HTMLProperty):
672     def plain(self):
673         if self.value is None:
674             return ''
675         return self.value and "Yes" or "No"
677     def field(self):
678         checked = self.value and "checked" or ""
679         s = '<input type="radio" name="%s" value="yes" %s>Yes'%(self.name,
680             checked)
681         if checked:
682             checked = ""
683         else:
684             checked = "checked"
685         s += '<input type="radio" name="%s" value="no" %s>No'%(self.name,
686             checked)
687         return s
689 class DateHTMLProperty(HTMLProperty):
690     def plain(self):
691         if self.value is None:
692             return ''
693         return str(self.value)
695     def field(self, size = 30):
696         if self.value is None:
697             value = ''
698         else:
699             value = cgi.escape(str(self.value))
700             value = '&quot;'.join(value.split('"'))
701         return '<input name="%s" value="%s" size="%s">'%(self.name, value, size)
703     def reldate(self, pretty=1):
704         if not self.value:
705             return ''
707         # figure the interval
708         interval = date.Date('.') - self.value
709         if pretty:
710             return interval.pretty()
711         return str(interval)
713 class IntervalHTMLProperty(HTMLProperty):
714     def plain(self):
715         if self.value is None:
716             return ''
717         return str(self.value)
719     def pretty(self):
720         return self.value.pretty()
722     def field(self, size = 30):
723         if self.value is None:
724             value = ''
725         else:
726             value = cgi.escape(str(self.value))
727             value = '&quot;'.join(value.split('"'))
728         return '<input name="%s" value="%s" size="%s">'%(self.name, value, size)
730 class LinkHTMLProperty(HTMLProperty):
731     ''' Link HTMLProperty
732         Include the above as well as being able to access the class
733         information. Stringifying the object itself results in the value
734         from the item being displayed. Accessing attributes of this object
735         result in the appropriate entry from the class being queried for the
736         property accessed (so item/assignedto/name would look up the user
737         entry identified by the assignedto property on item, and then the
738         name property of that user)
739     '''
740     def __getattr__(self, attr):
741         ''' return a new HTMLItem '''
742         #print 'getattr', (self, attr, self.value)
743         if not self.value:
744             raise AttributeError, "Can't access missing value"
745         if self.prop.classname == 'user':
746             klass = HTMLItem
747         else:
748             klass = HTMLUser
749         i = klass(self.client, self.prop.classname, self.value)
750         return getattr(i, attr)
752     def plain(self, escape=0):
753         if self.value is None:
754             return _('[unselected]')
755         linkcl = self.db.classes[self.prop.classname]
756         k = linkcl.labelprop(1)
757         value = str(linkcl.get(self.value, k))
758         if escape:
759             value = cgi.escape(value)
760         return value
762     # XXX most of the stuff from here down is of dubious utility - it's easy
763     # enough to do in the template by hand (and in some cases, it's shorter
764     # and clearer...
766     def field(self):
767         linkcl = self.db.getclass(self.prop.classname)
768         if linkcl.getprops().has_key('order'):  
769             sort_on = 'order'  
770         else:  
771             sort_on = linkcl.labelprop()  
772         options = linkcl.filter(None, {}, [sort_on], []) 
773         # TODO: make this a field display, not a menu one!
774         l = ['<select name="%s">'%property]
775         k = linkcl.labelprop(1)
776         if value is None:
777             s = 'selected '
778         else:
779             s = ''
780         l.append(_('<option %svalue="-1">- no selection -</option>')%s)
781         for optionid in options:
782             option = linkcl.get(optionid, k)
783             s = ''
784             if optionid == value:
785                 s = 'selected '
786             if showid:
787                 lab = '%s%s: %s'%(self.prop.classname, optionid, option)
788             else:
789                 lab = option
790             if size is not None and len(lab) > size:
791                 lab = lab[:size-3] + '...'
792             lab = cgi.escape(lab)
793             l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
794         l.append('</select>')
795         return '\n'.join(l)
797     def download(self, showid=0):
798         linkname = self.prop.classname
799         linkcl = self.db.getclass(linkname)
800         k = linkcl.labelprop(1)
801         linkvalue = cgi.escape(str(linkcl.get(self.value, k)))
802         if showid:
803             label = value
804             title = ' title="%s"'%linkvalue
805             # note ... this should be urllib.quote(linkcl.get(value, k))
806         else:
807             label = linkvalue
808             title = ''
809         return '<a href="%s%s/%s"%s>%s</a>'%(linkname, self.value,
810             linkvalue, title, label)
812     def menu(self, size=None, height=None, showid=0, additional=[],
813             **conditions):
814         value = self.value
816         # sort function
817         sortfunc = make_sort_function(self.db, self.prop.classname)
819         # force the value to be a single choice
820         if isinstance(value, type('')):
821             value = value[0]
822         linkcl = self.db.getclass(self.prop.classname)
823         l = ['<select name="%s">'%self.name]
824         k = linkcl.labelprop(1)
825         s = ''
826         if value is None:
827             s = 'selected '
828         l.append(_('<option %svalue="-1">- no selection -</option>')%s)
829         if linkcl.getprops().has_key('order'):  
830             sort_on = ('+', 'order')
831         else:  
832             sort_on = ('+', linkcl.labelprop())
833         options = linkcl.filter(None, conditions, sort_on, (None, None))
834         for optionid in options:
835             option = linkcl.get(optionid, k)
836             s = ''
837             if value in [optionid, option]:
838                 s = 'selected '
839             if showid:
840                 lab = '%s%s: %s'%(self.prop.classname, optionid, option)
841             else:
842                 lab = option
843             if size is not None and len(lab) > size:
844                 lab = lab[:size-3] + '...'
845             if additional:
846                 m = []
847                 for propname in additional:
848                     m.append(linkcl.get(optionid, propname))
849                 lab = lab + ' (%s)'%', '.join(map(str, m))
850             lab = cgi.escape(lab)
851             l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
852         l.append('</select>')
853         return '\n'.join(l)
855 #    def checklist(self, ...)
857 class MultilinkHTMLProperty(HTMLProperty):
858     ''' Multilink HTMLProperty
860         Also be iterable, returning a wrapper object like the Link case for
861         each entry in the multilink.
862     '''
863     def __len__(self):
864         ''' length of the multilink '''
865         return len(self.value)
867     def __getattr__(self, attr):
868         ''' no extended attribute accesses make sense here '''
869         raise AttributeError, attr
871     def __getitem__(self, num):
872         ''' iterate and return a new HTMLItem
873         '''
874         #print 'getitem', (self, num)
875         value = self.value[num]
876         if self.prop.classname == 'user':
877             klass = HTMLUser
878         else:
879             klass = HTMLItem
880         return klass(self.client, self.prop.classname, value)
882     def reverse(self):
883         ''' return the list in reverse order
884         '''
885         l = self.value[:]
886         l.reverse()
887         if self.prop.classname == 'user':
888             klass = HTMLUser
889         else:
890             klass = HTMLItem
891         return [klass(self.client, self.prop.classname, value) for value in l]
893     def plain(self, escape=0):
894         linkcl = self.db.classes[self.prop.classname]
895         k = linkcl.labelprop(1)
896         labels = []
897         for v in self.value:
898             labels.append(linkcl.get(v, k))
899         value = ', '.join(labels)
900         if escape:
901             value = cgi.escape(value)
902         return value
904     # XXX most of the stuff from here down is of dubious utility - it's easy
905     # enough to do in the template by hand (and in some cases, it's shorter
906     # and clearer...
908     def field(self, size=30, showid=0):
909         sortfunc = make_sort_function(self.db, self.prop.classname)
910         linkcl = self.db.getclass(self.prop.classname)
911         value = self.value[:]
912         if value:
913             value.sort(sortfunc)
914         # map the id to the label property
915         if not showid:
916             k = linkcl.labelprop(1)
917             value = [linkcl.get(v, k) for v in value]
918         value = cgi.escape(','.join(value))
919         return '<input name="%s" size="%s" value="%s">'%(self.name, size, value)
921     def menu(self, size=None, height=None, showid=0, additional=[],
922             **conditions):
923         value = self.value
925         # sort function
926         sortfunc = make_sort_function(self.db, self.prop.classname)
928         linkcl = self.db.getclass(self.prop.classname)
929         if linkcl.getprops().has_key('order'):  
930             sort_on = ('+', 'order')
931         else:  
932             sort_on = ('+', linkcl.labelprop())
933         options = linkcl.filter(None, conditions, sort_on, (None,None)) 
934         height = height or min(len(options), 7)
935         l = ['<select multiple name="%s" size="%s">'%(self.name, height)]
936         k = linkcl.labelprop(1)
937         for optionid in options:
938             option = linkcl.get(optionid, k)
939             s = ''
940             if optionid in value or option in value:
941                 s = 'selected '
942             if showid:
943                 lab = '%s%s: %s'%(self.prop.classname, optionid, option)
944             else:
945                 lab = option
946             if size is not None and len(lab) > size:
947                 lab = lab[:size-3] + '...'
948             if additional:
949                 m = []
950                 for propname in additional:
951                     m.append(linkcl.get(optionid, propname))
952                 lab = lab + ' (%s)'%', '.join(m)
953             lab = cgi.escape(lab)
954             l.append('<option %svalue="%s">%s</option>'%(s, optionid,
955                 lab))
956         l.append('</select>')
957         return '\n'.join(l)
959 # set the propclasses for HTMLItem
960 propclasses = (
961     (hyperdb.String, StringHTMLProperty),
962     (hyperdb.Number, NumberHTMLProperty),
963     (hyperdb.Boolean, BooleanHTMLProperty),
964     (hyperdb.Date, DateHTMLProperty),
965     (hyperdb.Interval, IntervalHTMLProperty),
966     (hyperdb.Password, PasswordHTMLProperty),
967     (hyperdb.Link, LinkHTMLProperty),
968     (hyperdb.Multilink, MultilinkHTMLProperty),
971 def make_sort_function(db, classname):
972     '''Make a sort function for a given class
973     '''
974     linkcl = db.getclass(classname)
975     if linkcl.getprops().has_key('order'):
976         sort_on = 'order'
977     else:
978         sort_on = linkcl.labelprop()
979     def sortfunc(a, b, linkcl=linkcl, sort_on=sort_on):
980         return cmp(linkcl.get(a, sort_on), linkcl.get(b, sort_on))
981     return sortfunc
983 def handleListCGIValue(value):
984     ''' Value is either a single item or a list of items. Each item has a
985         .value that we're actually interested in.
986     '''
987     if isinstance(value, type([])):
988         return [value.value for value in value]
989     else:
990         return value.value.split(',')
992 class ShowDict:
993     ''' A convenience access to the :columns index parameters
994     '''
995     def __init__(self, columns):
996         self.columns = {}
997         for col in columns:
998             self.columns[col] = 1
999     def __getitem__(self, name):
1000         return self.columns.has_key(name)
1002 class HTMLRequest:
1003     ''' The *request*, holding the CGI form and environment.
1005         "form" the CGI form as a cgi.FieldStorage
1006         "env" the CGI environment variables
1007         "url" the current URL path for this request
1008         "base" the base URL for this instance
1009         "user" a HTMLUser instance for this user
1010         "classname" the current classname (possibly None)
1011         "template" the current template (suffix, also possibly None)
1013         Index args:
1014         "columns" dictionary of the columns to display in an index page
1015         "show" a convenience access to columns - request/show/colname will
1016                be true if the columns should be displayed, false otherwise
1017         "sort" index sort column (direction, column name)
1018         "group" index grouping property (direction, column name)
1019         "filter" properties to filter the index on
1020         "filterspec" values to filter the index on
1021         "search_text" text to perform a full-text search on for an index
1023     '''
1024     def __init__(self, client):
1025         self.client = client
1027         # easier access vars
1028         self.form = client.form
1029         self.env = client.env
1030         self.base = client.base
1031         self.url = client.url
1032         self.user = HTMLUser(client, 'user', client.userid)
1034         # store the current class name and action
1035         self.classname = client.classname
1036         self.template = client.template
1038         # extract the index display information from the form
1039         self.columns = []
1040         if self.form.has_key(':columns'):
1041             self.columns = handleListCGIValue(self.form[':columns'])
1042         self.show = ShowDict(self.columns)
1044         # sorting
1045         self.sort = (None, None)
1046         if self.form.has_key(':sort'):
1047             sort = self.form[':sort'].value
1048             if sort.startswith('-'):
1049                 self.sort = ('-', sort[1:])
1050             else:
1051                 self.sort = ('+', sort)
1052         if self.form.has_key(':sortdir'):
1053             self.sort = ('-', self.sort[1])
1055         # grouping
1056         self.group = (None, None)
1057         if self.form.has_key(':group'):
1058             group = self.form[':group'].value
1059             if group.startswith('-'):
1060                 self.group = ('-', group[1:])
1061             else:
1062                 self.group = ('+', group)
1063         if self.form.has_key(':groupdir'):
1064             self.group = ('-', self.group[1])
1066         # filtering
1067         self.filter = []
1068         if self.form.has_key(':filter'):
1069             self.filter = handleListCGIValue(self.form[':filter'])
1070         self.filterspec = {}
1071         if self.classname is not None:
1072             props = self.client.db.getclass(self.classname).getprops()
1073             for name in self.filter:
1074                 if self.form.has_key(name):
1075                     prop = props[name]
1076                     fv = self.form[name]
1077                     if (isinstance(prop, hyperdb.Link) or
1078                             isinstance(prop, hyperdb.Multilink)):
1079                         self.filterspec[name] = handleListCGIValue(fv)
1080                     else:
1081                         self.filterspec[name] = fv.value
1083         # full-text search argument
1084         self.search_text = None
1085         if self.form.has_key(':search_text'):
1086             self.search_text = self.form[':search_text'].value
1088         # pagination - size and start index
1089         # figure batch args
1090         if self.form.has_key(':pagesize'):
1091             self.pagesize = int(self.form[':pagesize'].value)
1092         else:
1093             self.pagesize = 50
1094         if self.form.has_key(':startwith'):
1095             self.startwith = int(self.form[':startwith'].value)
1096         else:
1097             self.startwith = 0
1099     def update(self, kwargs):
1100         self.__dict__.update(kwargs)
1101         if kwargs.has_key('columns'):
1102             self.show = ShowDict(self.columns)
1104     def description(self):
1105         ''' Return a description of the request - handle for the page title.
1106         '''
1107         s = [self.client.db.config.INSTANCE_NAME]
1108         if self.classname:
1109             if self.client.nodeid:
1110                 s.append('- %s%s'%(self.classname, self.client.nodeid))
1111             else:
1112                 s.append('- index of '+self.classname)
1113         else:
1114             s.append('- home')
1115         return ' '.join(s)
1117     def __str__(self):
1118         d = {}
1119         d.update(self.__dict__)
1120         f = ''
1121         for k in self.form.keys():
1122             f += '\n      %r=%r'%(k,handleListCGIValue(self.form[k]))
1123         d['form'] = f
1124         e = ''
1125         for k,v in self.env.items():
1126             e += '\n     %r=%r'%(k, v)
1127         d['env'] = e
1128         return '''
1129 form: %(form)s
1130 url: %(url)r
1131 base: %(base)r
1132 classname: %(classname)r
1133 template: %(template)r
1134 columns: %(columns)r
1135 sort: %(sort)r
1136 group: %(group)r
1137 filter: %(filter)r
1138 search_text: %(search_text)r
1139 pagesize: %(pagesize)r
1140 startwith: %(startwith)r
1141 env: %(env)s
1142 '''%d
1144     def indexargs_form(self, columns=1, sort=1, group=1, filter=1,
1145             filterspec=1):
1146         ''' return the current index args as form elements '''
1147         l = []
1148         s = '<input type="hidden" name="%s" value="%s">'
1149         if columns and self.columns:
1150             l.append(s%(':columns', ','.join(self.columns)))
1151         if sort and self.sort[1] is not None:
1152             if self.sort[0] == '-':
1153                 val = '-'+self.sort[1]
1154             else:
1155                 val = self.sort[1]
1156             l.append(s%(':sort', val))
1157         if group and self.group[1] is not None:
1158             if self.group[0] == '-':
1159                 val = '-'+self.group[1]
1160             else:
1161                 val = self.group[1]
1162             l.append(s%(':group', val))
1163         if filter and self.filter:
1164             l.append(s%(':filter', ','.join(self.filter)))
1165         if filterspec:
1166             for k,v in self.filterspec.items():
1167                 l.append(s%(k, ','.join(v)))
1168         if self.search_text:
1169             l.append(s%(':search_text', self.search_text))
1170         l.append(s%(':pagesize', self.pagesize))
1171         l.append(s%(':startwith', self.startwith))
1172         return '\n'.join(l)
1174     def indexargs_href(self, url, args):
1175         ''' embed the current index args in a URL '''
1176         l = ['%s=%s'%(k,v) for k,v in args.items()]
1177         if self.columns and not args.has_key(':columns'):
1178             l.append(':columns=%s'%(','.join(self.columns)))
1179         if self.sort[1] is not None and not args.has_key(':sort'):
1180             if self.sort[0] == '-':
1181                 val = '-'+self.sort[1]
1182             else:
1183                 val = self.sort[1]
1184             l.append(':sort=%s'%val)
1185         if self.group[1] is not None and not args.has_key(':group'):
1186             if self.group[0] == '-':
1187                 val = '-'+self.group[1]
1188             else:
1189                 val = self.group[1]
1190             l.append(':group=%s'%val)
1191         if self.filter and not args.has_key(':columns'):
1192             l.append(':filter=%s'%(','.join(self.filter)))
1193         for k,v in self.filterspec.items():
1194             if not args.has_key(k):
1195                 l.append('%s=%s'%(k, ','.join(v)))
1196         if self.search_text and not args.has_key(':search_text'):
1197             l.append(':search_text=%s'%self.search_text)
1198         if not args.has_key(':pagesize'):
1199             l.append(':pagesize=%s'%self.pagesize)
1200         if not args.has_key(':startwith'):
1201             l.append(':startwith=%s'%self.startwith)
1202         return '%s?%s'%(url, '&'.join(l))
1204     def base_javascript(self):
1205         return '''
1206 <script language="javascript">
1207 submitted = false;
1208 function submit_once() {
1209     if (submitted) {
1210         alert("Your request is being processed.\\nPlease be patient.");
1211         return 0;
1212     }
1213     submitted = true;
1214     return 1;
1217 function help_window(helpurl, width, height) {
1218     HelpWin = window.open('%s/' + helpurl, 'RoundupHelpWindow', 'scrollbars=yes,resizable=yes,toolbar=no,height='+height+',width='+width);
1220 </script>
1221 '''%self.base
1223     def batch(self):
1224         ''' Return a batch object for results from the "current search"
1225         '''
1226         filterspec = self.filterspec
1227         sort = self.sort
1228         group = self.group
1230         # get the list of ids we're batching over
1231         klass = self.client.db.getclass(self.classname)
1232         if self.search_text:
1233             matches = self.client.db.indexer.search(
1234                 re.findall(r'\b\w{2,25}\b', self.search_text), klass)
1235         else:
1236             matches = None
1237         l = klass.filter(matches, filterspec, sort, group)
1239         # return the batch object
1240         return Batch(self.client, self.classname, l, self.pagesize,
1241             self.startwith)
1244 # extend the standard ZTUtils Batch object to remove dependency on
1245 # Acquisition and add a couple of useful methods
1246 class Batch(ZTUtils.Batch):
1247     def __init__(self, client, classname, l, size, start, end=0, orphan=0, overlap=0):
1248         self.client = client
1249         self.classname = classname
1250         self.last_index = self.last_item = None
1251         self.current_item = None
1252         ZTUtils.Batch.__init__(self, l, size, start, end, orphan, overlap)
1254     # overwrite so we can late-instantiate the HTMLItem instance
1255     def __getitem__(self, index):
1256         if index < 0:
1257             if index + self.end < self.first: raise IndexError, index
1258             return self._sequence[index + self.end]
1259         
1260         if index >= self.length: raise IndexError, index
1262         # move the last_item along - but only if the fetched index changes
1263         # (for some reason, index 0 is fetched twice)
1264         if index != self.last_index:
1265             self.last_item = self.current_item
1266             self.last_index = index
1268         # wrap the return in an HTMLItem
1269         if self.classname == 'user':
1270             klass = HTMLUser
1271         else:
1272             klass = HTMLItem
1273         self.current_item = klass(self.client, self.classname,
1274             self._sequence[index+self.first])
1275         return self.current_item
1277     def propchanged(self, property):
1278         ''' Detect if the property marked as being the group property
1279             changed in the last iteration fetch
1280         '''
1281         if (self.last_item is None or
1282                 self.last_item[property] != self.current_item[property]):
1283             return 1
1284         return 0
1286     # override these 'cos we don't have access to acquisition
1287     def previous(self):
1288         if self.start == 1:
1289             return None
1290         return Batch(self.client, self.classname, self._sequence, self._size,
1291             self.first - self._size + self.overlap, 0, self.orphan,
1292             self.overlap)
1294     def next(self):
1295         try:
1296             self._sequence[self.end]
1297         except IndexError:
1298             return None
1299         return Batch(self.client, self.classname, self._sequence, self._size,
1300             self.end - self.overlap, 0, self.orphan, self.overlap)
1302     def length(self):
1303         self.sequence_length = l = len(self._sequence)
1304         return l