Code

Plug a number of security holes:
[roundup.git] / roundup / cgi / templating.py
1 from __future__ import nested_scopes
3 """Implements the API used in the HTML templating for the web interface.
4 """
6 todo = """
7 - Most methods should have a "default" arg to supply a value
8   when none appears in the hyperdb or request.
9 - Multilink property additions: change_note and new_upload
10 - Add class.find() too
11 - NumberHTMLProperty should support numeric operations
12 - LinkHTMLProperty should handle comparisons to strings (cf. linked name)
13 - HTMLRequest.default(self, sort, group, filter, columns, **filterspec):
14   '''Set the request's view arguments to the given values when no
15      values are found in the CGI environment.
16   '''
17 - have menu() methods accept filtering arguments
18 """
20 __docformat__ = 'restructuredtext'
23 import sys, cgi, urllib, os, re, os.path, time, errno, mimetypes, csv
24 import calendar, textwrap
26 from roundup import hyperdb, date, support
27 from roundup import i18n
28 from roundup.i18n import _
30 try:
31     import cPickle as pickle
32 except ImportError:
33     import pickle
34 try:
35     import cStringIO as StringIO
36 except ImportError:
37     import StringIO
38 try:
39     from StructuredText.StructuredText import HTML as StructuredText
40 except ImportError:
41     try: # older version
42         import StructuredText
43     except ImportError:
44         StructuredText = None
45 try:
46     from docutils.core import publish_parts as ReStructuredText
47 except ImportError:
48     ReStructuredText = None
50 # bring in the templating support
51 from roundup.cgi.PageTemplates import PageTemplate, GlobalTranslationService
52 from roundup.cgi.PageTemplates.Expressions import getEngine
53 from roundup.cgi.TAL import TALInterpreter
54 from roundup.cgi import TranslationService, ZTUtils
56 ### i18n services
57 # this global translation service is not thread-safe.
58 # it is left here for backward compatibility
59 # until all Web UI translations are done via client.translator object
60 translationService = TranslationService.get_translation()
61 GlobalTranslationService.setGlobalTranslationService(translationService)
63 ### templating
65 class NoTemplate(Exception):
66     pass
68 class Unauthorised(Exception):
69     def __init__(self, action, klass, translator=None):
70         self.action = action
71         self.klass = klass
72         if translator:
73             self._ = translator.gettext
74         else:
75             self._ = TranslationService.get_translation().gettext
76     def __str__(self):
77         return self._('You are not allowed to %(action)s '
78             'items of class %(class)s') % {
79             'action': self.action, 'class': self.klass}
81 def find_template(dir, name, view):
82     """ Find a template in the nominated dir
83     """
84     # find the source
85     if view:
86         filename = '%s.%s'%(name, view)
87     else:
88         filename = name
90     # try old-style
91     src = os.path.join(dir, filename)
92     if os.path.exists(src):
93         return (src, filename)
95     # try with a .html or .xml extension (new-style)
96     for extension in '.html', '.xml':
97         f = filename + extension
98         src = os.path.join(dir, f)
99         if os.path.exists(src):
100             return (src, f)
102     # no view == no generic template is possible
103     if not view:
104         raise NoTemplate, 'Template file "%s" doesn\'t exist'%name
106     # try for a _generic template
107     generic = '_generic.%s'%view
108     src = os.path.join(dir, generic)
109     if os.path.exists(src):
110         return (src, generic)
112     # finally, try _generic.html
113     generic = generic + '.html'
114     src = os.path.join(dir, generic)
115     if os.path.exists(src):
116         return (src, generic)
118     raise NoTemplate, 'No template file exists for templating "%s" '\
119         'with template "%s" (neither "%s" nor "%s")'%(name, view,
120         filename, generic)
122 class Templates:
123     templates = {}
125     def __init__(self, dir):
126         self.dir = dir
128     def precompileTemplates(self):
129         """ Go through a directory and precompile all the templates therein
130         """
131         for filename in os.listdir(self.dir):
132             # skip subdirs
133             if os.path.isdir(filename):
134                 continue
136             # skip files without ".html" or ".xml" extension - .css, .js etc.
137             for extension in '.html', '.xml':
138                 if filename.endswith(extension):
139                     break
140             else:
141                 continue
143             # remove extension
144             filename = filename[:-len(extension)]
146             # load the template
147             if '.' in filename:
148                 name, extension = filename.split('.', 1)
149                 self.get(name, extension)
150             else:
151                 self.get(filename, None)
153     def get(self, name, extension=None):
154         """ Interface to get a template, possibly loading a compiled template.
156             "name" and "extension" indicate the template we're after, which in
157             most cases will be "name.extension". If "extension" is None, then
158             we look for a template just called "name" with no extension.
160             If the file "name.extension" doesn't exist, we look for
161             "_generic.extension" as a fallback.
162         """
163         # default the name to "home"
164         if name is None:
165             name = 'home'
166         elif extension is None and '.' in name:
167             # split name
168             name, extension = name.split('.')
170         # find the source
171         src, filename = find_template(self.dir, name, extension)
173         # has it changed?
174         try:
175             stime = os.stat(src)[os.path.stat.ST_MTIME]
176         except os.error, error:
177             if error.errno != errno.ENOENT:
178                 raise
180         if self.templates.has_key(src) and \
181                 stime <= self.templates[src].mtime:
182             # compiled template is up to date
183             return self.templates[src]
185         # compile the template
186         self.templates[src] = pt = RoundupPageTemplate()
187         # use pt_edit so we can pass the content_type guess too
188         content_type = mimetypes.guess_type(filename)[0] or 'text/html'
189         pt.pt_edit(open(src).read(), content_type)
190         pt.id = filename
191         pt.mtime = stime
192         return pt
194     def __getitem__(self, name):
195         name, extension = os.path.splitext(name)
196         if extension:
197             extension = extension[1:]
198         try:
199             return self.get(name, extension)
200         except NoTemplate, message:
201             raise KeyError, message
203 def context(client, template=None, classname=None, request=None):
204     """Return the rendering context dictionary
206     The dictionary includes following symbols:
208     *context*
209      this is one of three things:
211      1. None - we're viewing a "home" page
212      2. The current class of item being displayed. This is an HTMLClass
213         instance.
214      3. The current item from the database, if we're viewing a specific
215         item, as an HTMLItem instance.
217     *request*
218       Includes information about the current request, including:
220        - the url
221        - the current index information (``filterspec``, ``filter`` args,
222          ``properties``, etc) parsed out of the form.
223        - methods for easy filterspec link generation
224        - *user*, the current user node as an HTMLItem instance
225        - *form*, the current CGI form information as a FieldStorage
227     *config*
228       The current tracker config.
230     *db*
231       The current database, used to access arbitrary database items.
233     *utils*
234       This is a special class that has its base in the TemplatingUtils
235       class in this file. If the tracker interfaces module defines a
236       TemplatingUtils class then it is mixed in, overriding the methods
237       in the base class.
239     *templates*
240       Access to all the tracker templates by name.
241       Used mainly in *use-macro* commands.
243     *template*
244       Current rendering template.
246     *true*
247       Logical True value.
249     *false*
250       Logical False value.
252     *i18n*
253       Internationalization service, providing string translation
254       methods ``gettext`` and ``ngettext``.
256     """
257     # construct the TemplatingUtils class
258     utils = TemplatingUtils
259     if (hasattr(client.instance, 'interfaces') and
260             hasattr(client.instance.interfaces, 'TemplatingUtils')):
261         class utils(client.instance.interfaces.TemplatingUtils, utils):
262             pass
264     # if template, classname and/or request are not passed explicitely,
265     # compute form client
266     if template is None:
267         template = client.template
268     if classname is None:
269         classname = client.classname
270     if request is None:
271         request = HTMLRequest(client)
273     c = {
274          'context': None,
275          'options': {},
276          'nothing': None,
277          'request': request,
278          'db': HTMLDatabase(client),
279          'config': client.instance.config,
280          'tracker': client.instance,
281          'utils': utils(client),
282          'templates': client.instance.templates,
283          'template': template,
284          'true': 1,
285          'false': 0,
286          'i18n': client.translator
287     }
288     # add in the item if there is one
289     if client.nodeid:
290         c['context'] = HTMLItem(client, classname, client.nodeid,
291             anonymous=1)
292     elif client.db.classes.has_key(classname):
293         c['context'] = HTMLClass(client, classname, anonymous=1)
294     return c
296 class RoundupPageTemplate(PageTemplate.PageTemplate):
297     """A Roundup-specific PageTemplate.
299     Interrogate the client to set up Roundup-specific template variables
300     to be available.  See 'context' function for the list of variables.
302     """
304     # 06-jun-2004 [als] i am not sure if this method is used yet
305     def getContext(self, client, classname, request):
306         return context(client, self, classname, request)
308     def render(self, client, classname, request, **options):
309         """Render this Page Template"""
311         if not self._v_cooked:
312             self._cook()
314         __traceback_supplement__ = (PageTemplate.PageTemplateTracebackSupplement, self)
316         if self._v_errors:
317             raise PageTemplate.PTRuntimeError, \
318                 'Page Template %s has errors.'%self.id
320         # figure the context
321         c = context(client, self, classname, request)
322         c.update({'options': options})
324         # and go
325         output = StringIO.StringIO()
326         TALInterpreter.TALInterpreter(self._v_program, self.macros,
327             getEngine().getContext(c), output, tal=1, strictinsert=0)()
328         return output.getvalue()
330     def __repr__(self):
331         return '<Roundup PageTemplate %r>'%self.id
333 class HTMLDatabase:
334     """ Return HTMLClasses for valid class fetches
335     """
336     def __init__(self, client):
337         self._client = client
338         self._ = client._
339         self._db = client.db
341         # we want config to be exposed
342         self.config = client.db.config
344     def __getitem__(self, item, desre=re.compile(r'(?P<cl>[a-zA-Z_]+)(?P<id>[-\d]+)')):
345         # check to see if we're actually accessing an item
346         m = desre.match(item)
347         if m:
348             cl = m.group('cl')
349             self._client.db.getclass(cl)
350             return HTMLItem(self._client, cl, m.group('id'))
351         else:
352             self._client.db.getclass(item)
353             return HTMLClass(self._client, item)
355     def __getattr__(self, attr):
356         try:
357             return self[attr]
358         except KeyError:
359             raise AttributeError, attr
361     def classes(self):
362         l = self._client.db.classes.keys()
363         l.sort()
364         m = []
365         for item in l:
366             m.append(HTMLClass(self._client, item))
367         return m
369 num_re = re.compile('^-?\d+$')
371 def lookupIds(db, prop, ids, fail_ok=0, num_re=num_re, do_lookup=True):
372     """ "fail_ok" should be specified if we wish to pass through bad values
373         (most likely form values that we wish to represent back to the user)
374         "do_lookup" is there for preventing lookup by key-value (if we
375         know that the value passed *is* an id)
376     """
377     cl = db.getclass(prop.classname)
378     l = []
379     for entry in ids:
380         if do_lookup:
381             try:
382                 item = cl.lookup(entry)
383             except (TypeError, KeyError):
384                 pass
385             else:
386                 l.append(item)
387                 continue
388         # if fail_ok, ignore lookup error
389         # otherwise entry must be existing object id rather than key value
390         if fail_ok or num_re.match(entry):
391             l.append(entry)
392     return l
394 def lookupKeys(linkcl, key, ids, num_re=num_re):
395     """ Look up the "key" values for "ids" list - though some may already
396     be key values, not ids.
397     """
398     l = []
399     for entry in ids:
400         if num_re.match(entry):
401             label = linkcl.get(entry, key)
402             # fall back to designator if label is None
403             if label is None: label = '%s%s'%(linkcl.classname, entry)
404             l.append(label)
405         else:
406             l.append(entry)
407     return l
409 def _set_input_default_args(dic):
410     # 'text' is the default value anyway --
411     # but for CSS usage it should be present
412     dic.setdefault('type', 'text')
413     # useful e.g for HTML LABELs:
414     if not dic.has_key('id'):
415         try:
416             if dic['text'] in ('radio', 'checkbox'):
417                 dic['id'] = '%(name)s-%(value)s' % dic
418             else:
419                 dic['id'] = dic['name']
420         except KeyError:
421             pass
423 def input_html4(**attrs):
424     """Generate an 'input' (html4) element with given attributes"""
425     _set_input_default_args(attrs)
426     return '<input %s>'%' '.join(['%s="%s"'%(k,cgi.escape(str(v), True))
427         for k,v in attrs.items()])
429 def input_xhtml(**attrs):
430     """Generate an 'input' (xhtml) element with given attributes"""
431     _set_input_default_args(attrs)
432     return '<input %s/>'%' '.join(['%s="%s"'%(k,cgi.escape(str(v), True))
433         for k,v in attrs.items()])
435 class HTMLInputMixin:
436     """ requires a _client property """
437     def __init__(self):
438         html_version = 'html4'
439         if hasattr(self._client.instance.config, 'HTML_VERSION'):
440             html_version = self._client.instance.config.HTML_VERSION
441         if html_version == 'xhtml':
442             self.input = input_xhtml
443         else:
444             self.input = input_html4
445         # self._context is used for translations.
446         # will be initialized by the first call to .gettext()
447         self._context = None
449     def gettext(self, msgid):
450         """Return the localized translation of msgid"""
451         if self._context is None:
452             self._context = context(self._client)
453         return self._client.translator.translate(domain="roundup",
454             msgid=msgid, context=self._context)
456     _ = gettext
458 class HTMLPermissions:
460     def view_check(self):
461         """ Raise the Unauthorised exception if the user's not permitted to
462             view this class.
463         """
464         if not self.is_view_ok():
465             raise Unauthorised("view", self._classname,
466                 translator=self._client.translator)
468     def edit_check(self):
469         """ Raise the Unauthorised exception if the user's not permitted to
470             edit items of this class.
471         """
472         if not self.is_edit_ok():
473             raise Unauthorised("edit", self._classname,
474                 translator=self._client.translator)
476     def retire_check(self):
477         """ Raise the Unauthorised exception if the user's not permitted to
478             retire items of this class.
479         """
480         if not self.is_retire_ok():
481             raise Unauthorised("retire", self._classname,
482                 translator=self._client.translator)
485 class HTMLClass(HTMLInputMixin, HTMLPermissions):
486     """ Accesses through a class (either through *class* or *db.<classname>*)
487     """
488     def __init__(self, client, classname, anonymous=0):
489         self._client = client
490         self._ = client._
491         self._db = client.db
492         self._anonymous = anonymous
494         # we want classname to be exposed, but _classname gives a
495         # consistent API for extending Class/Item
496         self._classname = self.classname = classname
497         self._klass = self._db.getclass(self.classname)
498         self._props = self._klass.getprops()
500         HTMLInputMixin.__init__(self)
502     def is_edit_ok(self):
503         """ Is the user allowed to Create the current class?
504         """
505         return self._db.security.hasPermission('Create', self._client.userid,
506             self._classname)
508     def is_retire_ok(self):
509         """ Is the user allowed to retire items of the current class?
510         """
511         return self._db.security.hasPermission('Retire', self._client.userid,
512             self._classname)
514     def is_view_ok(self):
515         """ Is the user allowed to View the current class?
516         """
517         return self._db.security.hasPermission('View', self._client.userid,
518             self._classname)
520     def is_only_view_ok(self):
521         """ Is the user only allowed to View (ie. not Create) the current class?
522         """
523         return self.is_view_ok() and not self.is_edit_ok()
525     def __repr__(self):
526         return '<HTMLClass(0x%x) %s>'%(id(self), self.classname)
528     def __getitem__(self, item):
529         """ return an HTMLProperty instance
530         """
532         # we don't exist
533         if item == 'id':
534             return None
536         # get the property
537         try:
538             prop = self._props[item]
539         except KeyError:
540             raise KeyError, 'No such property "%s" on %s'%(item, self.classname)
542         # look up the correct HTMLProperty class
543         form = self._client.form
544         for klass, htmlklass in propclasses:
545             if not isinstance(prop, klass):
546                 continue
547             if isinstance(prop, hyperdb.Multilink):
548                 value = []
549             else:
550                 value = None
551             return htmlklass(self._client, self._classname, None, prop, item,
552                 value, self._anonymous)
554         # no good
555         raise KeyError, item
557     def __getattr__(self, attr):
558         """ convenience access """
559         try:
560             return self[attr]
561         except KeyError:
562             raise AttributeError, attr
564     def designator(self):
565         """ Return this class' designator (classname) """
566         return self._classname
568     def getItem(self, itemid, num_re=num_re):
569         """ Get an item of this class by its item id.
570         """
571         # make sure we're looking at an itemid
572         if not isinstance(itemid, type(1)) and not num_re.match(itemid):
573             itemid = self._klass.lookup(itemid)
575         return HTMLItem(self._client, self.classname, itemid)
577     def properties(self, sort=1):
578         """ Return HTMLProperty for all of this class' properties.
579         """
580         l = []
581         for name, prop in self._props.items():
582             for klass, htmlklass in propclasses:
583                 if isinstance(prop, hyperdb.Multilink):
584                     value = []
585                 else:
586                     value = None
587                 if isinstance(prop, klass):
588                     l.append(htmlklass(self._client, self._classname, '',
589                         prop, name, value, self._anonymous))
590         if sort:
591             l.sort(lambda a,b:cmp(a._name, b._name))
592         return l
594     def list(self, sort_on=None):
595         """ List all items in this class.
596         """
597         # get the list and sort it nicely
598         l = self._klass.list()
599         sortfunc = make_sort_function(self._db, self._classname, sort_on)
600         l.sort(sortfunc)
602         # check perms
603         check = self._client.db.security.hasPermission
604         userid = self._client.userid
606         l = [HTMLItem(self._client, self._classname, id) for id in l
607             if check('View', userid, self._classname, itemid=id)]
609         return l
611     def csv(self):
612         """ Return the items of this class as a chunk of CSV text.
613         """
614         props = self.propnames()
615         s = StringIO.StringIO()
616         writer = csv.writer(s)
617         writer.writerow(props)
618         for nodeid in self._klass.list():
619             l = []
620             for name in props:
621                 value = self._klass.get(nodeid, name)
622                 if value is None:
623                     l.append('')
624                 elif isinstance(value, type([])):
625                     l.append(':'.join(map(str, value)))
626                 else:
627                     l.append(str(self._klass.get(nodeid, name)))
628             writer.writerow(l)
629         return s.getvalue()
631     def propnames(self):
632         """ Return the list of the names of the properties of this class.
633         """
634         idlessprops = self._klass.getprops(protected=0).keys()
635         idlessprops.sort()
636         return ['id'] + idlessprops
638     def filter(self, request=None, filterspec={}, sort=[], group=[]):
639         """ Return a list of items from this class, filtered and sorted
640             by the current requested filterspec/filter/sort/group args
642             "request" takes precedence over the other three arguments.
643         """
644         if request is not None:
645             filterspec = request.filterspec
646             sort = request.sort
647             group = request.group
649         check = self._db.security.hasPermission
650         userid = self._client.userid
652         l = [HTMLItem(self._client, self.classname, id)
653              for id in self._klass.filter(None, filterspec, sort, group)
654              if check('View', userid, self.classname, itemid=id)]
655         return l
657     def classhelp(self, properties=None, label=''"(list)", width='500',
658             height='400', property='', form='itemSynopsis',
659             pagesize=50, inputtype="checkbox", sort=None, filter=None):
660         """Pop up a javascript window with class help
662         This generates a link to a popup window which displays the
663         properties indicated by "properties" of the class named by
664         "classname". The "properties" should be a comma-separated list
665         (eg. 'id,name,description'). Properties defaults to all the
666         properties of a class (excluding id, creator, created and
667         activity).
669         You may optionally override the label displayed, the width,
670         the height, the number of items per page and the field on which
671         the list is sorted (defaults to username if in the displayed
672         properties).
674         With the "filter" arg it is possible to specify a filter for
675         which items are supposed to be displayed. It has to be of
676         the format "<field>=<values>;<field>=<values>;...".
678         The popup window will be resizable and scrollable.
680         If the "property" arg is given, it's passed through to the
681         javascript help_window function.
683         You can use inputtype="radio" to display a radio box instead
684         of the default checkbox (useful for entering Link-properties)
686         If the "form" arg is given, it's passed through to the
687         javascript help_window function. - it's the name of the form
688         the "property" belongs to.
689         """
690         if properties is None:
691             properties = self._klass.getprops(protected=0).keys()
692             properties.sort()
693             properties = ','.join(properties)
694         if sort is None:
695             if 'username' in properties.split( ',' ):
696                 sort = 'username'
697             else:
698                 sort = self._klass.orderprop()
699         sort = '&amp;@sort=' + sort
700         if property:
701             property = '&amp;property=%s'%property
702         if form:
703             form = '&amp;form=%s'%form
704         if inputtype:
705             type= '&amp;type=%s'%inputtype
706         if filter:
707             filterprops = filter.split(';')
708             filtervalues = []
709             names = []
710             for x in filterprops:
711                 (name, values) = x.split('=')
712                 names.append(name)
713                 filtervalues.append('&amp;%s=%s' % (name, urllib.quote(values)))
714             filter = '&amp;@filter=%s%s' % (','.join(names), ''.join(filtervalues))
715         else:
716            filter = ''
717         help_url = "%s?@startwith=0&amp;@template=help&amp;"\
718                    "properties=%s%s%s%s%s&amp;@pagesize=%s%s" % \
719                    (self.classname, properties, property, form, type,
720                    sort, pagesize, filter)
721         onclick = "javascript:help_window('%s', '%s', '%s');return false;" % \
722                   (help_url, width, height)
723         return '<a class="classhelp" href="%s" onclick="%s">%s</a>' % \
724                (help_url, onclick, self._(label))
726     def submit(self, label=''"Submit New Entry", action="new"):
727         """ Generate a submit button (and action hidden element)
729         Generate nothing if we're not editable.
730         """
731         if not self.is_edit_ok():
732             return ''
734         return self.input(type="hidden", name="@action", value=action) + \
735             '\n' + \
736             self.input(type="submit", name="submit_button", value=self._(label))
738     def history(self):
739         if not self.is_view_ok():
740             return self._('[hidden]')
741         return self._('New node - no history')
743     def renderWith(self, name, **kwargs):
744         """ Render this class with the given template.
745         """
746         # create a new request and override the specified args
747         req = HTMLRequest(self._client)
748         req.classname = self.classname
749         req.update(kwargs)
751         # new template, using the specified classname and request
752         pt = self._client.instance.templates.get(self.classname, name)
754         # use our fabricated request
755         args = {
756             'ok_message': self._client.ok_message,
757             'error_message': self._client.error_message
758         }
759         return pt.render(self._client, self.classname, req, **args)
761 class _HTMLItem(HTMLInputMixin, HTMLPermissions):
762     """ Accesses through an *item*
763     """
764     def __init__(self, client, classname, nodeid, anonymous=0):
765         self._client = client
766         self._db = client.db
767         self._classname = classname
768         self._nodeid = nodeid
769         self._klass = self._db.getclass(classname)
770         self._props = self._klass.getprops()
772         # do we prefix the form items with the item's identification?
773         self._anonymous = anonymous
775         HTMLInputMixin.__init__(self)
777     def is_edit_ok(self):
778         """ Is the user allowed to Edit this item?
779         """
780         return self._db.security.hasPermission('Edit', self._client.userid,
781             self._classname, itemid=self._nodeid)
783     def is_retire_ok(self):
784         """ Is the user allowed to Reture this item?
785         """
786         return self._db.security.hasPermission('Retire', self._client.userid,
787             self._classname, itemid=self._nodeid)
789     def is_view_ok(self):
790         """ Is the user allowed to View this item?
791         """
792         if self._db.security.hasPermission('View', self._client.userid,
793                 self._classname, itemid=self._nodeid):
794             return 1
795         return self.is_edit_ok()
797     def is_only_view_ok(self):
798         """ Is the user only allowed to View (ie. not Edit) this item?
799         """
800         return self.is_view_ok() and not self.is_edit_ok()
802     def __repr__(self):
803         return '<HTMLItem(0x%x) %s %s>'%(id(self), self._classname,
804             self._nodeid)
806     def __getitem__(self, item):
807         """ return an HTMLProperty instance
808             this now can handle transitive lookups where item is of the
809             form x.y.z
810         """
811         if item == 'id':
812             return self._nodeid
814         items = item.split('.', 1)
815         has_rest = len(items) > 1
817         # get the property
818         prop = self._props[items[0]]
820         if has_rest and not isinstance(prop, (hyperdb.Link, hyperdb.Multilink)):
821             raise KeyError, item
823         # get the value, handling missing values
824         value = None
825         if int(self._nodeid) > 0:
826             value = self._klass.get(self._nodeid, items[0], None)
827         if value is None:
828             if isinstance(prop, hyperdb.Multilink):
829                 value = []
831         # look up the correct HTMLProperty class
832         htmlprop = None
833         for klass, htmlklass in propclasses:
834             if isinstance(prop, klass):
835                 htmlprop = htmlklass(self._client, self._classname,
836                     self._nodeid, prop, items[0], value, self._anonymous)
837         if htmlprop is not None:
838             if has_rest:
839                 if isinstance(htmlprop, MultilinkHTMLProperty):
840                     return [h[items[1]] for h in htmlprop]
841                 return htmlprop[items[1]]
842             return htmlprop
844         raise KeyError, item
846     def __getattr__(self, attr):
847         """ convenience access to properties """
848         try:
849             return self[attr]
850         except KeyError:
851             raise AttributeError, attr
853     def designator(self):
854         """Return this item's designator (classname + id)."""
855         return '%s%s'%(self._classname, self._nodeid)
857     def is_retired(self):
858         """Is this item retired?"""
859         return self._klass.is_retired(self._nodeid)
861     def submit(self, label=''"Submit Changes", action="edit"):
862         """Generate a submit button.
864         Also sneak in the lastactivity and action hidden elements.
865         """
866         return self.input(type="hidden", name="@lastactivity",
867             value=self.activity.local(0)) + '\n' + \
868             self.input(type="hidden", name="@action", value=action) + '\n' + \
869             self.input(type="submit", name="submit_button", value=self._(label))
871     def journal(self, direction='descending'):
872         """ Return a list of HTMLJournalEntry instances.
873         """
874         # XXX do this
875         return []
877     def history(self, direction='descending', dre=re.compile('^\d+$')):
878         if not self.is_view_ok():
879             return self._('[hidden]')
881         # pre-load the history with the current state
882         current = {}
883         for prop_n in self._props.keys():
884             prop = self[prop_n]
885             if not isinstance(prop, HTMLProperty):
886                 continue
887             current[prop_n] = prop.plain(escape=1)
888             # make link if hrefable
889             if (self._props.has_key(prop_n) and
890                     isinstance(self._props[prop_n], hyperdb.Link)):
891                 classname = self._props[prop_n].classname
892                 try:
893                     template = find_template(self._db.config.TEMPLATES,
894                         classname, 'item')
895                     if template[1].startswith('_generic'):
896                         raise NoTemplate, 'not really...'
897                 except NoTemplate:
898                     pass
899                 else:
900                     id = self._klass.get(self._nodeid, prop_n, None)
901                     current[prop_n] = '<a href="%s%s">%s</a>'%(
902                         classname, id, current[prop_n])
904         # get the journal, sort and reverse
905         history = self._klass.history(self._nodeid)
906         history.sort()
907         history.reverse()
909         timezone = self._db.getUserTimezone()
910         l = []
911         comments = {}
912         for id, evt_date, user, action, args in history:
913             date_s = str(evt_date.local(timezone)).replace("."," ")
914             arg_s = ''
915             if action == 'link' and type(args) == type(()):
916                 if len(args) == 3:
917                     linkcl, linkid, key = args
918                     arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
919                         linkcl, linkid, key)
920                 else:
921                     arg_s = str(args)
923             elif action == 'unlink' and type(args) == type(()):
924                 if len(args) == 3:
925                     linkcl, linkid, key = args
926                     arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
927                         linkcl, linkid, key)
928                 else:
929                     arg_s = str(args)
931             elif type(args) == type({}):
932                 cell = []
933                 for k in args.keys():
934                     # try to get the relevant property and treat it
935                     # specially
936                     try:
937                         prop = self._props[k]
938                     except KeyError:
939                         prop = None
940                     if prop is None:
941                         # property no longer exists
942                         comments['no_exist'] = self._(
943                             "<em>The indicated property no longer exists</em>")
944                         cell.append(self._('<em>%s: %s</em>\n')
945                             % (self._(k), str(args[k])))
946                         continue
948                     if args[k] and (isinstance(prop, hyperdb.Multilink) or
949                             isinstance(prop, hyperdb.Link)):
950                         # figure what the link class is
951                         classname = prop.classname
952                         try:
953                             linkcl = self._db.getclass(classname)
954                         except KeyError:
955                             labelprop = None
956                             comments[classname] = self._(
957                                 "The linked class %(classname)s no longer exists"
958                             ) % locals()
959                         labelprop = linkcl.labelprop(1)
960                         try:
961                             template = find_template(self._db.config.TEMPLATES,
962                                 classname, 'item')
963                             if template[1].startswith('_generic'):
964                                 raise NoTemplate, 'not really...'
965                             hrefable = 1
966                         except NoTemplate:
967                             hrefable = 0
969                     if isinstance(prop, hyperdb.Multilink) and args[k]:
970                         ml = []
971                         for linkid in args[k]:
972                             if isinstance(linkid, type(())):
973                                 sublabel = linkid[0] + ' '
974                                 linkids = linkid[1]
975                             else:
976                                 sublabel = ''
977                                 linkids = [linkid]
978                             subml = []
979                             for linkid in linkids:
980                                 label = classname + linkid
981                                 # if we have a label property, try to use it
982                                 # TODO: test for node existence even when
983                                 # there's no labelprop!
984                                 try:
985                                     if labelprop is not None and \
986                                             labelprop != 'id':
987                                         label = linkcl.get(linkid, labelprop)
988                                         label = cgi.escape(label)
989                                 except IndexError:
990                                     comments['no_link'] = self._(
991                                         "<strike>The linked node"
992                                         " no longer exists</strike>")
993                                     subml.append('<strike>%s</strike>'%label)
994                                 else:
995                                     if hrefable:
996                                         subml.append('<a href="%s%s">%s</a>'%(
997                                             classname, linkid, label))
998                                     elif label is None:
999                                         subml.append('%s%s'%(classname,
1000                                             linkid))
1001                                     else:
1002                                         subml.append(label)
1003                             ml.append(sublabel + ', '.join(subml))
1004                         cell.append('%s:\n  %s'%(self._(k), ', '.join(ml)))
1005                     elif isinstance(prop, hyperdb.Link) and args[k]:
1006                         label = classname + args[k]
1007                         # if we have a label property, try to use it
1008                         # TODO: test for node existence even when
1009                         # there's no labelprop!
1010                         if labelprop is not None and labelprop != 'id':
1011                             try:
1012                                 label = cgi.escape(linkcl.get(args[k],
1013                                     labelprop))
1014                             except IndexError:
1015                                 comments['no_link'] = self._(
1016                                     "<strike>The linked node"
1017                                     " no longer exists</strike>")
1018                                 cell.append(' <strike>%s</strike>,\n'%label)
1019                                 # "flag" this is done .... euwww
1020                                 label = None
1021                         if label is not None:
1022                             if hrefable:
1023                                 old = '<a href="%s%s">%s</a>'%(classname,
1024                                     args[k], label)
1025                             else:
1026                                 old = label;
1027                             cell.append('%s: %s' % (self._(k), old))
1028                             if current.has_key(k):
1029                                 cell[-1] += ' -> %s'%current[k]
1030                                 current[k] = old
1032                     elif isinstance(prop, hyperdb.Date) and args[k]:
1033                         if args[k] is None:
1034                             d = ''
1035                         else:
1036                             d = date.Date(args[k],
1037                                 translator=self._client).local(timezone)
1038                         cell.append('%s: %s'%(self._(k), str(d)))
1039                         if current.has_key(k):
1040                             cell[-1] += ' -> %s' % current[k]
1041                             current[k] = str(d)
1043                     elif isinstance(prop, hyperdb.Interval) and args[k]:
1044                         val = str(date.Interval(args[k],
1045                             translator=self._client))
1046                         cell.append('%s: %s'%(self._(k), val))
1047                         if current.has_key(k):
1048                             cell[-1] += ' -> %s'%current[k]
1049                             current[k] = val
1051                     elif isinstance(prop, hyperdb.String) and args[k]:
1052                         val = cgi.escape(args[k])
1053                         cell.append('%s: %s'%(self._(k), val))
1054                         if current.has_key(k):
1055                             cell[-1] += ' -> %s'%current[k]
1056                             current[k] = val
1058                     elif isinstance(prop, hyperdb.Boolean) and args[k] is not None:
1059                         val = args[k] and ''"Yes" or ''"No"
1060                         cell.append('%s: %s'%(self._(k), val))
1061                         if current.has_key(k):
1062                             cell[-1] += ' -> %s'%current[k]
1063                             current[k] = val
1065                     elif not args[k]:
1066                         if current.has_key(k):
1067                             cell.append('%s: %s'%(self._(k), current[k]))
1068                             current[k] = '(no value)'
1069                         else:
1070                             cell.append(self._('%s: (no value)')%self._(k))
1072                     else:
1073                         cell.append('%s: %s'%(self._(k), str(args[k])))
1074                         if current.has_key(k):
1075                             cell[-1] += ' -> %s'%current[k]
1076                             current[k] = str(args[k])
1078                 arg_s = '<br />'.join(cell)
1079             else:
1080                 # unkown event!!
1081                 comments['unknown'] = self._(
1082                     "<strong><em>This event is not handled"
1083                     " by the history display!</em></strong>")
1084                 arg_s = '<strong><em>' + str(args) + '</em></strong>'
1085             date_s = date_s.replace(' ', '&nbsp;')
1086             # if the user's an itemid, figure the username (older journals
1087             # have the username)
1088             if dre.match(user):
1089                 user = self._db.user.get(user, 'username')
1090             l.append('<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>'%(
1091                 date_s, user, self._(action), arg_s))
1092         if comments:
1093             l.append(self._(
1094                 '<tr><td colspan=4><strong>Note:</strong></td></tr>'))
1095         for entry in comments.values():
1096             l.append('<tr><td colspan=4>%s</td></tr>'%entry)
1098         if direction == 'ascending':
1099             l.reverse()
1101         l[0:0] = ['<table class="history">'
1102              '<tr><th colspan="4" class="header">',
1103              self._('History'),
1104              '</th></tr><tr>',
1105              self._('<th>Date</th>'),
1106              self._('<th>User</th>'),
1107              self._('<th>Action</th>'),
1108              self._('<th>Args</th>'),
1109             '</tr>']
1110         l.append('</table>')
1111         return '\n'.join(l)
1113     def renderQueryForm(self):
1114         """ Render this item, which is a query, as a search form.
1115         """
1116         # create a new request and override the specified args
1117         req = HTMLRequest(self._client)
1118         req.classname = self._klass.get(self._nodeid, 'klass')
1119         name = self._klass.get(self._nodeid, 'name')
1120         req.updateFromURL(self._klass.get(self._nodeid, 'url') +
1121             '&@queryname=%s'%urllib.quote(name))
1123         # new template, using the specified classname and request
1124         pt = self._client.instance.templates.get(req.classname, 'search')
1125         # The context for a search page should be the class, not any
1126         # node.
1127         self._client.nodeid = None
1128         
1129         # use our fabricated request
1130         return pt.render(self._client, req.classname, req)
1132     def download_url(self):
1133         """ Assume that this item is a FileClass and that it has a name
1134         and content. Construct a URL for the download of the content.
1135         """
1136         name = self._klass.get(self._nodeid, 'name')
1137         url = '%s%s/%s'%(self._classname, self._nodeid, name)
1138         return urllib.quote(url)
1140     def copy_url(self, exclude=("messages", "files")):
1141         """Construct a URL for creating a copy of this item
1143         "exclude" is an optional list of properties that should
1144         not be copied to the new object.  By default, this list
1145         includes "messages" and "files" properties.  Note that
1146         "id" property cannot be copied.
1148         """
1149         exclude = ("id", "activity", "actor", "creation", "creator") \
1150             + tuple(exclude)
1151         query = {
1152             "@template": "item",
1153             "@note": self._("Copy of %(class)s %(id)s") % {
1154                 "class": self._(self._classname), "id": self._nodeid},
1155         }
1156         for name in self._props.keys():
1157             if name not in exclude:
1158                 query[name] = self[name].plain()
1159         return self._classname + "?" + "&".join(
1160             ["%s=%s" % (key, urllib.quote(value))
1161                 for key, value in query.items()])
1163 class _HTMLUser(_HTMLItem):
1164     """Add ability to check for permissions on users.
1165     """
1166     _marker = []
1167     def hasPermission(self, permission, classname=_marker,
1168             property=None, itemid=None):
1169         """Determine if the user has the Permission.
1171         The class being tested defaults to the template's class, but may
1172         be overidden for this test by suppling an alternate classname.
1173         """
1174         if classname is self._marker:
1175             classname = self._client.classname
1176         return self._db.security.hasPermission(permission,
1177             self._nodeid, classname, property, itemid)
1179     def hasRole(self, rolename):
1180         """Determine whether the user has the Role."""
1181         roles = self._db.user.get(self._nodeid, 'roles').split(',')
1182         for role in roles:
1183             if role.strip() == rolename: return True
1184         return False
1186 def HTMLItem(client, classname, nodeid, anonymous=0):
1187     if classname == 'user':
1188         return _HTMLUser(client, classname, nodeid, anonymous)
1189     else:
1190         return _HTMLItem(client, classname, nodeid, anonymous)
1192 class HTMLProperty(HTMLInputMixin, HTMLPermissions):
1193     """ String, Number, Date, Interval HTMLProperty
1195         Has useful attributes:
1197          _name  the name of the property
1198          _value the value of the property if any
1200         A wrapper object which may be stringified for the plain() behaviour.
1201     """
1202     def __init__(self, client, classname, nodeid, prop, name, value,
1203             anonymous=0):
1204         self._client = client
1205         self._db = client.db
1206         self._ = client._
1207         self._classname = classname
1208         self._nodeid = nodeid
1209         self._prop = prop
1210         self._value = value
1211         self._anonymous = anonymous
1212         self._name = name
1213         if not anonymous:
1214             self._formname = '%s%s@%s'%(classname, nodeid, name)
1215         else:
1216             self._formname = name
1218         # If no value is already present for this property, see if one
1219         # is specified in the current form.
1220         form = self._client.form
1221         if not self._value and form.has_key(self._formname):
1222             if isinstance(prop, hyperdb.Multilink):
1223                 value = lookupIds(self._db, prop,
1224                                   handleListCGIValue(form[self._formname]),
1225                                   fail_ok=1)
1226             elif isinstance(prop, hyperdb.Link):
1227                 value = form.getfirst(self._formname).strip()
1228                 if value:
1229                     value = lookupIds(self._db, prop, [value],
1230                                       fail_ok=1)[0]
1231                 else:
1232                     value = None
1233             else:
1234                 value = form.getfirst(self._formname).strip() or None
1235             self._value = value
1237         HTMLInputMixin.__init__(self)
1239     def __repr__(self):
1240         return '<HTMLProperty(0x%x) %s %r %r>'%(id(self), self._formname,
1241             self._prop, self._value)
1242     def __str__(self):
1243         return self.plain()
1244     def __cmp__(self, other):
1245         if isinstance(other, HTMLProperty):
1246             return cmp(self._value, other._value)
1247         return cmp(self._value, other)
1249     def __nonzero__(self):
1250         return not not self._value
1252     def isset(self):
1253         """Is my _value not None?"""
1254         return self._value is not None
1256     def is_edit_ok(self):
1257         """Should the user be allowed to use an edit form field for this
1258         property. Check "Create" for new items, or "Edit" for existing
1259         ones.
1260         """
1261         if self._nodeid:
1262             return self._db.security.hasPermission('Edit', self._client.userid,
1263                 self._classname, self._name, self._nodeid)
1264         return self._db.security.hasPermission('Create', self._client.userid,
1265             self._classname, self._name)
1267     def is_view_ok(self):
1268         """ Is the user allowed to View the current class?
1269         """
1270         if self._db.security.hasPermission('View', self._client.userid,
1271                 self._classname, self._name, self._nodeid):
1272             return 1
1273         return self.is_edit_ok()
1275 class StringHTMLProperty(HTMLProperty):
1276     hyper_re = re.compile(r'''(
1277         (?P<url>
1278          (
1279           (ht|f)tp(s?)://                   # protocol
1280           ([\w]+(:\w+)?@)?                  # username/password
1281           ([\w\-]+)                         # hostname
1282           ((\.[\w-]+)+)?                    # .domain.etc
1283          |                                  # ... or ...
1284           ([\w]+(:\w+)?@)?                  # username/password
1285           www\.                             # "www."
1286           ([\w\-]+\.)+                      # hostname
1287           [\w]{2,5}                         # TLD
1288          )
1289          (:[\d]{1,5})?                     # port
1290          (/[\w\-$.+!*(),;:@&=?/~\\#%]*)?   # path etc.
1291         )|
1292         (?P<email>[-+=%/\w\.]+@[\w\.\-]+)|
1293         (?P<item>(?P<class>[A-Za-z_]+)(\s*)(?P<id>\d+))
1294     )''', re.X | re.I)
1295     protocol_re = re.compile('^(ht|f)tp(s?)://', re.I)
1297     def _hyper_repl_item(self,match,replacement):
1298         item = match.group('item')
1299         cls = match.group('class').lower()
1300         id = match.group('id')
1301         try:
1302             # make sure cls is a valid tracker classname
1303             cl = self._db.getclass(cls)
1304             if not cl.hasnode(id):
1305                 return item
1306             return replacement % locals()
1307         except KeyError:
1308             return item
1310     def _hyper_repl(self, match):
1311         if match.group('url'):
1312             u = s = match.group('url')
1313             if not self.protocol_re.search(s):
1314                 u = 'http://' + s
1315             # catch an escaped ">" at the end of the URL
1316             if s.endswith('&gt;'):
1317                 u = s = s[:-4]
1318                 e = '&gt;'
1319             else:
1320                 e = ''
1321             return '<a href="%s">%s</a>%s'%(u, s, e)
1322         elif match.group('email'):
1323             s = match.group('email')
1324             return '<a href="mailto:%s">%s</a>'%(s, s)
1325         else:
1326             return self._hyper_repl_item(match,
1327                 '<a href="%(cls)s%(id)s">%(item)s</a>')
1329     def _hyper_repl_rst(self, match):
1330         if match.group('url'):
1331             s = match.group('url')
1332             return '`%s <%s>`_'%(s, s)
1333         elif match.group('email'):
1334             s = match.group('email')
1335             return '`%s <mailto:%s>`_'%(s, s)
1336         else:
1337             return self._hyper_repl_item(match,'`%(item)s <%(cls)s%(id)s>`_')
1339     def hyperlinked(self):
1340         """ Render a "hyperlinked" version of the text """
1341         return self.plain(hyperlink=1)
1343     def plain(self, escape=0, hyperlink=0):
1344         """Render a "plain" representation of the property
1346         - "escape" turns on/off HTML quoting
1347         - "hyperlink" turns on/off in-text hyperlinking of URLs, email
1348           addresses and designators
1349         """
1350         if not self.is_view_ok():
1351             return self._('[hidden]')
1353         if self._value is None:
1354             return ''
1355         if escape:
1356             s = cgi.escape(str(self._value))
1357         else:
1358             s = str(self._value)
1359         if hyperlink:
1360             # no, we *must* escape this text
1361             if not escape:
1362                 s = cgi.escape(s)
1363             s = self.hyper_re.sub(self._hyper_repl, s)
1364         return s
1366     def wrapped(self, escape=1, hyperlink=1):
1367         """Render a "wrapped" representation of the property.
1369         We wrap long lines at 80 columns on the nearest whitespace. Lines
1370         with no whitespace are not broken to force wrapping.
1372         Note that unlike plain() we default wrapped() to have the escaping
1373         and hyperlinking turned on since that's the most common usage.
1375         - "escape" turns on/off HTML quoting
1376         - "hyperlink" turns on/off in-text hyperlinking of URLs, email
1377           addresses and designators
1378         """
1379         if not self.is_view_ok():
1380             return self._('[hidden]')
1382         if self._value is None:
1383             return ''
1384         s = support.wrap(str(self._value), width=80)
1385         if escape:
1386             s = cgi.escape(s)
1387         if hyperlink:
1388             # no, we *must* escape this text
1389             if not escape:
1390                 s = cgi.escape(s)
1391             s = self.hyper_re.sub(self._hyper_repl, s)
1392         return s
1394     def stext(self, escape=0, hyperlink=1):
1395         """ Render the value of the property as StructuredText.
1397             This requires the StructureText module to be installed separately.
1398         """
1399         if not self.is_view_ok():
1400             return self._('[hidden]')
1402         s = self.plain(escape=escape, hyperlink=hyperlink)
1403         if not StructuredText:
1404             return s
1405         return StructuredText(s,level=1,header=0)
1407     def rst(self, hyperlink=1):
1408         """ Render the value of the property as ReStructuredText.
1410             This requires docutils to be installed separately.
1411         """
1412         if not self.is_view_ok():
1413             return self._('[hidden]')
1415         if not ReStructuredText:
1416             return self.plain(escape=0, hyperlink=hyperlink)
1417         s = self.plain(escape=0, hyperlink=0)
1418         if hyperlink:
1419             s = self.hyper_re.sub(self._hyper_repl_rst, s)
1420         return ReStructuredText(s, writer_name="html")["body"].encode("utf-8",
1421             "replace")
1423     def field(self, **kwargs):
1424         """ Render the property as a field in HTML.
1426             If not editable, just display the value via plain().
1427         """
1428         if not self.is_edit_ok():
1429             return self.plain(escape=1)
1431         value = self._value
1432         if value is None:
1433             value = ''
1435         kwargs.setdefault("size", 30)
1436         kwargs.update({"name": self._formname, "value": value})
1437         return self.input(**kwargs)
1439     def multiline(self, escape=0, rows=5, cols=40, **kwargs):
1440         """ Render a multiline form edit field for the property.
1442             If not editable, just display the plain() value in a <pre> tag.
1443         """
1444         if not self.is_edit_ok():
1445             return '<pre>%s</pre>'%self.plain()
1447         if self._value is None:
1448             value = ''
1449         else:
1450             value = cgi.escape(str(self._value))
1452             value = '&quot;'.join(value.split('"'))
1453         name = self._formname
1454         passthrough_args = ' '.join(['%s="%s"' % (k, cgi.escape(str(v), True))
1455             for k,v in kwargs.items()])
1456         return ('<textarea %(passthrough_args)s name="%(name)s" id="%(name)s"'
1457                 ' rows="%(rows)s" cols="%(cols)s">'
1458                  '%(value)s</textarea>') % locals()
1460     def email(self, escape=1):
1461         """ Render the value of the property as an obscured email address
1462         """
1463         if not self.is_view_ok():
1464             return self._('[hidden]')
1466         if self._value is None:
1467             value = ''
1468         else:
1469             value = str(self._value)
1470         split = value.split('@')
1471         if len(split) == 2:
1472             name, domain = split
1473             domain = ' '.join(domain.split('.')[:-1])
1474             name = name.replace('.', ' ')
1475             value = '%s at %s ...'%(name, domain)
1476         else:
1477             value = value.replace('.', ' ')
1478         if escape:
1479             value = cgi.escape(value)
1480         return value
1482 class PasswordHTMLProperty(HTMLProperty):
1483     def plain(self, escape=0):
1484         """ Render a "plain" representation of the property
1485         """
1486         if not self.is_view_ok():
1487             return self._('[hidden]')
1489         if self._value is None:
1490             return ''
1491         return self._('*encrypted*')
1493     def field(self, size=30):
1494         """ Render a form edit field for the property.
1496             If not editable, just display the value via plain().
1497         """
1498         if not self.is_edit_ok():
1499             return self.plain(escape=1)
1501         return self.input(type="password", name=self._formname, size=size)
1503     def confirm(self, size=30):
1504         """ Render a second form edit field for the property, used for
1505             confirmation that the user typed the password correctly. Generates
1506             a field with name "@confirm@name".
1508             If not editable, display nothing.
1509         """
1510         if not self.is_edit_ok():
1511             return ''
1513         return self.input(type="password",
1514             name="@confirm@%s"%self._formname,
1515             id="%s-confirm"%self._formname,
1516             size=size)
1518 class NumberHTMLProperty(HTMLProperty):
1519     def plain(self, escape=0):
1520         """ Render a "plain" representation of the property
1521         """
1522         if not self.is_view_ok():
1523             return self._('[hidden]')
1525         if self._value is None:
1526             return ''
1528         return str(self._value)
1530     def field(self, size=30):
1531         """ Render a form edit field for the property.
1533             If not editable, just display the value via plain().
1534         """
1535         if not self.is_edit_ok():
1536             return self.plain(escape=1)
1538         value = self._value
1539         if value is None:
1540             value = ''
1542         return self.input(name=self._formname, value=value, size=size)
1544     def __int__(self):
1545         """ Return an int of me
1546         """
1547         return int(self._value)
1549     def __float__(self):
1550         """ Return a float of me
1551         """
1552         return float(self._value)
1555 class BooleanHTMLProperty(HTMLProperty):
1556     def plain(self, escape=0):
1557         """ Render a "plain" representation of the property
1558         """
1559         if not self.is_view_ok():
1560             return self._('[hidden]')
1562         if self._value is None:
1563             return ''
1564         return self._value and self._("Yes") or self._("No")
1566     def field(self):
1567         """ Render a form edit field for the property
1569             If not editable, just display the value via plain().
1570         """
1571         if not self.is_edit_ok():
1572             return self.plain(escape=1)
1574         value = self._value
1575         if isinstance(value, str) or isinstance(value, unicode):
1576             value = value.strip().lower() in ('checked', 'yes', 'true',
1577                 'on', '1')
1579         checked = value and "checked" or ""
1580         if value:
1581             s = self.input(type="radio", name=self._formname, value="yes",
1582                 checked="checked")
1583             s += self._('Yes')
1584             s +=self.input(type="radio", name=self._formname, value="no")
1585             s += self._('No')
1586         else:
1587             s = self.input(type="radio", name=self._formname, value="yes")
1588             s += self._('Yes')
1589             s +=self.input(type="radio", name=self._formname, value="no",
1590                 checked="checked")
1591             s += self._('No')
1592         return s
1594 class DateHTMLProperty(HTMLProperty):
1596     _marker = []
1598     def __init__(self, client, classname, nodeid, prop, name, value,
1599             anonymous=0, offset=None):
1600         HTMLProperty.__init__(self, client, classname, nodeid, prop, name,
1601                 value, anonymous=anonymous)
1602         if self._value and not (isinstance(self._value, str) or
1603                 isinstance(self._value, unicode)):
1604             self._value.setTranslator(self._client.translator)
1605         self._offset = offset
1606         if self._offset is None :
1607             self._offset = self._prop.offset (self._db)
1609     def plain(self, escape=0):
1610         """ Render a "plain" representation of the property
1611         """
1612         if not self.is_view_ok():
1613             return self._('[hidden]')
1615         if self._value is None:
1616             return ''
1617         if self._offset is None:
1618             offset = self._db.getUserTimezone()
1619         else:
1620             offset = self._offset
1621         return str(self._value.local(offset))
1623     def now(self, str_interval=None):
1624         """ Return the current time.
1626             This is useful for defaulting a new value. Returns a
1627             DateHTMLProperty.
1628         """
1629         if not self.is_view_ok():
1630             return self._('[hidden]')
1632         ret = date.Date('.', translator=self._client)
1634         if isinstance(str_interval, basestring):
1635             sign = 1
1636             if str_interval[0] == '-':
1637                 sign = -1
1638                 str_interval = str_interval[1:]
1639             interval = date.Interval(str_interval, translator=self._client)
1640             if sign > 0:
1641                 ret = ret + interval
1642             else:
1643                 ret = ret - interval
1645         return DateHTMLProperty(self._client, self._classname, self._nodeid,
1646             self._prop, self._formname, ret)
1648     def field(self, size=30, default=None, format=_marker, popcal=True):
1649         """Render a form edit field for the property
1651         If not editable, just display the value via plain().
1653         If "popcal" then include the Javascript calendar editor.
1654         Default=yes.
1656         The format string is a standard python strftime format string.
1657         """
1658         if not self.is_edit_ok():
1659             if format is self._marker:
1660                 return self.plain(escape=1)
1661             else:
1662                 return self.pretty(format)
1664         value = self._value
1666         if value is None:
1667             if default is None:
1668                 raw_value = None
1669             else:
1670                 if isinstance(default, basestring):
1671                     raw_value = date.Date(default, translator=self._client)
1672                 elif isinstance(default, date.Date):
1673                     raw_value = default
1674                 elif isinstance(default, DateHTMLProperty):
1675                     raw_value = default._value
1676                 else:
1677                     raise ValueError, self._('default value for '
1678                         'DateHTMLProperty must be either DateHTMLProperty '
1679                         'or string date representation.')
1680         elif isinstance(value, str) or isinstance(value, unicode):
1681             # most likely erroneous input to be passed back to user
1682             if isinstance(value, unicode): value = value.encode('utf8')
1683             return self.input(name=self._formname, value=value, size=size)
1684         else:
1685             raw_value = value
1687         if raw_value is None:
1688             value = ''
1689         elif isinstance(raw_value, str) or isinstance(raw_value, unicode):
1690             if format is self._marker:
1691                 value = raw_value
1692             else:
1693                 value = date.Date(raw_value).pretty(format)
1694         else:
1695             if self._offset is None :
1696                 offset = self._db.getUserTimezone()
1697             else :
1698                 offset = self._offset
1699             value = raw_value.local(offset)
1700             if format is not self._marker:
1701                 value = value.pretty(format)
1703         s = self.input(name=self._formname, value=value, size=size)
1704         if popcal:
1705             s += self.popcal()
1706         return s
1708     def reldate(self, pretty=1):
1709         """ Render the interval between the date and now.
1711             If the "pretty" flag is true, then make the display pretty.
1712         """
1713         if not self.is_view_ok():
1714             return self._('[hidden]')
1716         if not self._value:
1717             return ''
1719         # figure the interval
1720         interval = self._value - date.Date('.', translator=self._client)
1721         if pretty:
1722             return interval.pretty()
1723         return str(interval)
1725     def pretty(self, format=_marker):
1726         """ Render the date in a pretty format (eg. month names, spaces).
1728             The format string is a standard python strftime format string.
1729             Note that if the day is zero, and appears at the start of the
1730             string, then it'll be stripped from the output. This is handy
1731             for the situation when a date only specifies a month and a year.
1732         """
1733         if not self.is_view_ok():
1734             return self._('[hidden]')
1736         if self._offset is None:
1737             offset = self._db.getUserTimezone()
1738         else:
1739             offset = self._offset
1741         if not self._value:
1742             return ''
1743         elif format is not self._marker:
1744             return self._value.local(offset).pretty(format)
1745         else:
1746             return self._value.local(offset).pretty()
1748     def local(self, offset):
1749         """ Return the date/time as a local (timezone offset) date/time.
1750         """
1751         if not self.is_view_ok():
1752             return self._('[hidden]')
1754         return DateHTMLProperty(self._client, self._classname, self._nodeid,
1755             self._prop, self._formname, self._value, offset=offset)
1757     def popcal(self, width=300, height=200, label="(cal)",
1758             form="itemSynopsis"):
1759         """Generate a link to a calendar pop-up window.
1761         item: HTMLProperty e.g.: context.deadline
1762         """
1763         if self.isset():
1764             date = "&date=%s"%self._value
1765         else :
1766             date = ""
1767         return ('<a class="classhelp" href="javascript:help_window('
1768             "'%s?@template=calendar&amp;property=%s&amp;form=%s%s', %d, %d)"
1769             '">%s</a>'%(self._classname, self._name, form, date, width,
1770             height, label))
1772 class IntervalHTMLProperty(HTMLProperty):
1773     def __init__(self, client, classname, nodeid, prop, name, value,
1774             anonymous=0):
1775         HTMLProperty.__init__(self, client, classname, nodeid, prop,
1776             name, value, anonymous)
1777         if self._value and not isinstance(self._value, (str, unicode)):
1778             self._value.setTranslator(self._client.translator)
1780     def plain(self, escape=0):
1781         """ Render a "plain" representation of the property
1782         """
1783         if not self.is_view_ok():
1784             return self._('[hidden]')
1786         if self._value is None:
1787             return ''
1788         return str(self._value)
1790     def pretty(self):
1791         """ Render the interval in a pretty format (eg. "yesterday")
1792         """
1793         if not self.is_view_ok():
1794             return self._('[hidden]')
1796         return self._value.pretty()
1798     def field(self, size=30):
1799         """ Render a form edit field for the property
1801             If not editable, just display the value via plain().
1802         """
1803         if not self.is_edit_ok():
1804             return self.plain(escape=1)
1806         value = self._value
1807         if value is None:
1808             value = ''
1810         return self.input(name=self._formname, value=value, size=size)
1812 class LinkHTMLProperty(HTMLProperty):
1813     """ Link HTMLProperty
1814         Include the above as well as being able to access the class
1815         information. Stringifying the object itself results in the value
1816         from the item being displayed. Accessing attributes of this object
1817         result in the appropriate entry from the class being queried for the
1818         property accessed (so item/assignedto/name would look up the user
1819         entry identified by the assignedto property on item, and then the
1820         name property of that user)
1821     """
1822     def __init__(self, *args, **kw):
1823         HTMLProperty.__init__(self, *args, **kw)
1824         # if we're representing a form value, then the -1 from the form really
1825         # should be a None
1826         if str(self._value) == '-1':
1827             self._value = None
1829     def __getattr__(self, attr):
1830         """ return a new HTMLItem """
1831         if not self._value:
1832             # handle a special page templates lookup
1833             if attr == '__render_with_namespace__':
1834                 def nothing(*args, **kw):
1835                     return ''
1836                 return nothing
1837             msg = self._('Attempt to look up %(attr)s on a missing value')
1838             return MissingValue(msg%locals())
1839         i = HTMLItem(self._client, self._prop.classname, self._value)
1840         return getattr(i, attr)
1842     def plain(self, escape=0):
1843         """ Render a "plain" representation of the property
1844         """
1845         if not self.is_view_ok():
1846             return self._('[hidden]')
1848         if self._value is None:
1849             return ''
1850         linkcl = self._db.classes[self._prop.classname]
1851         k = linkcl.labelprop(1)
1852         if num_re.match(self._value):
1853             try:
1854                 value = str(linkcl.get(self._value, k))
1855             except IndexError:
1856                 value = self._value
1857         else :
1858             value = self._value
1859         if escape:
1860             value = cgi.escape(value)
1861         return value
1863     def field(self, showid=0, size=None):
1864         """ Render a form edit field for the property
1866             If not editable, just display the value via plain().
1867         """
1868         if not self.is_edit_ok():
1869             return self.plain(escape=1)
1871         # edit field
1872         linkcl = self._db.getclass(self._prop.classname)
1873         if self._value is None:
1874             value = ''
1875         else:
1876             k = linkcl.getkey()
1877             if k and num_re.match(self._value):
1878                 value = linkcl.get(self._value, k)
1879             else:
1880                 value = self._value
1881         return self.input(name=self._formname, value=value, size=size)
1883     def menu(self, size=None, height=None, showid=0, additional=[], value=None,
1884             sort_on=None, **conditions):
1885         """ Render a form select list for this property
1887             "size" is used to limit the length of the list labels
1888             "height" is used to set the <select> tag's "size" attribute
1889             "showid" includes the item ids in the list labels
1890             "value" specifies which item is pre-selected
1891             "additional" lists properties which should be included in the
1892                 label
1893             "sort_on" indicates the property to sort the list on as
1894                 (direction, property) where direction is '+' or '-'. A
1895                 single string with the direction prepended may be used.
1896                 For example: ('-', 'order'), '+name'.
1898             The remaining keyword arguments are used as conditions for
1899             filtering the items in the list - they're passed as the
1900             "filterspec" argument to a Class.filter() call.
1902             If not editable, just display the value via plain().
1903         """
1904         if not self.is_edit_ok():
1905             return self.plain(escape=1)
1907         # Since None indicates the default, we need another way to
1908         # indicate "no selection".  We use -1 for this purpose, as
1909         # that is the value we use when submitting a form without the
1910         # value set.
1911         if value is None:
1912             value = self._value
1913         elif value == '-1':
1914             value = None
1916         linkcl = self._db.getclass(self._prop.classname)
1917         l = ['<select name="%s">'%self._formname]
1918         k = linkcl.labelprop(1)
1919         s = ''
1920         if value is None:
1921             s = 'selected="selected" '
1922         l.append(self._('<option %svalue="-1">- no selection -</option>')%s)
1924         if sort_on is not None:
1925             if not isinstance(sort_on, tuple):
1926                 if sort_on[0] in '+-':
1927                     sort_on = (sort_on[0], sort_on[1:])
1928                 else:
1929                     sort_on = ('+', sort_on)
1930         else:
1931             sort_on = ('+', linkcl.orderprop())
1933         options = [opt
1934             for opt in linkcl.filter(None, conditions, sort_on, (None, None))
1935             if self._db.security.hasPermission("View", self._client.userid,
1936                 linkcl.classname, itemid=opt)]
1938         # make sure we list the current value if it's retired
1939         if value and value not in options:
1940             options.insert(0, value)
1942         if additional:
1943             additional_fns = []
1944             props = linkcl.getprops()
1945             for propname in additional:
1946                 prop = props[propname]
1947                 if isinstance(prop, hyperdb.Link):
1948                     cl = self._db.getclass(prop.classname)
1949                     labelprop = cl.labelprop()
1950                     fn = lambda optionid: cl.get(linkcl.get(optionid,
1951                                                             propname),
1952                                                  labelprop)
1953                 else:
1954                     fn = lambda optionid: linkcl.get(optionid, propname)
1955             additional_fns.append(fn)
1956             
1957         for optionid in options:
1958             # get the option value, and if it's None use an empty string
1959             option = linkcl.get(optionid, k) or ''
1961             # figure if this option is selected
1962             s = ''
1963             if value in [optionid, option]:
1964                 s = 'selected="selected" '
1966             # figure the label
1967             if showid:
1968                 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1969             elif not option:
1970                 lab = '%s%s'%(self._prop.classname, optionid)
1971             else:
1972                 lab = option
1974             # truncate if it's too long
1975             if size is not None and len(lab) > size:
1976                 lab = lab[:size-3] + '...'
1977             if additional:
1978                 m = []
1979                 for fn in additional_fns:
1980                     m.append(str(fn(optionid)))
1981                 lab = lab + ' (%s)'%', '.join(m)
1983             # and generate
1984             lab = cgi.escape(self._(lab))
1985             l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
1986         l.append('</select>')
1987         return '\n'.join(l)
1988 #    def checklist(self, ...)
1992 class MultilinkHTMLProperty(HTMLProperty):
1993     """ Multilink HTMLProperty
1995         Also be iterable, returning a wrapper object like the Link case for
1996         each entry in the multilink.
1997     """
1998     def __init__(self, *args, **kwargs):
1999         HTMLProperty.__init__(self, *args, **kwargs)
2000         if self._value:
2001             display_value = lookupIds(self._db, self._prop, self._value,
2002                 fail_ok=1, do_lookup=False)
2003             sortfun = make_sort_function(self._db, self._prop.classname)
2004             # sorting fails if the value contains
2005             # items not yet stored in the database
2006             # ignore these errors to preserve user input
2007             try:
2008                 display_value.sort(sortfun)
2009             except:
2010                 pass
2011             self._value = display_value
2013     def __len__(self):
2014         """ length of the multilink """
2015         return len(self._value)
2017     def __getattr__(self, attr):
2018         """ no extended attribute accesses make sense here """
2019         raise AttributeError, attr
2021     def viewableGenerator(self, values):
2022         """Used to iterate over only the View'able items in a class."""
2023         check = self._db.security.hasPermission
2024         userid = self._client.userid
2025         classname = self._prop.classname
2026         for value in values:
2027             if check('View', userid, classname, itemid=value):
2028                 yield HTMLItem(self._client, classname, value)
2030     def __iter__(self):
2031         """ iterate and return a new HTMLItem
2032         """
2033         return self.viewableGenerator(self._value)
2035     def reverse(self):
2036         """ return the list in reverse order
2037         """
2038         l = self._value[:]
2039         l.reverse()
2040         return self.viewableGenerator(l)
2042     def sorted(self, property):
2043         """ Return this multilink sorted by the given property """
2044         value = list(self.__iter__())
2045         value.sort(lambda a,b:cmp(a[property], b[property]))
2046         return value
2048     def __contains__(self, value):
2049         """ Support the "in" operator. We have to make sure the passed-in
2050             value is a string first, not a HTMLProperty.
2051         """
2052         return str(value) in self._value
2054     def isset(self):
2055         """Is my _value not []?"""
2056         return self._value != []
2058     def plain(self, escape=0):
2059         """ Render a "plain" representation of the property
2060         """
2061         if not self.is_view_ok():
2062             return self._('[hidden]')
2064         linkcl = self._db.classes[self._prop.classname]
2065         k = linkcl.labelprop(1)
2066         labels = []
2067         for v in self._value:
2068             if num_re.match(v):
2069                 try:
2070                     label = linkcl.get(v, k)
2071                 except IndexError:
2072                     label = None
2073                 # fall back to designator if label is None
2074                 if label is None: label = '%s%s'%(self._prop.classname, k)
2075             else:
2076                 label = v
2077             labels.append(label)
2078         value = ', '.join(labels)
2079         if escape:
2080             value = cgi.escape(value)
2081         return value
2083     def field(self, size=30, showid=0):
2084         """ Render a form edit field for the property
2086             If not editable, just display the value via plain().
2087         """
2088         if not self.is_edit_ok():
2089             return self.plain(escape=1)
2091         linkcl = self._db.getclass(self._prop.classname)
2092         value = self._value[:]
2093         # map the id to the label property
2094         if not linkcl.getkey():
2095             showid=1
2096         if not showid:
2097             k = linkcl.labelprop(1)
2098             value = lookupKeys(linkcl, k, value)
2099         value = ','.join(value)
2100         return self.input(name=self._formname, size=size, value=value)
2102     def menu(self, size=None, height=None, showid=0, additional=[],
2103              value=None, sort_on=None, **conditions):
2104         """ Render a form <select> list for this property.
2106             "size" is used to limit the length of the list labels
2107             "height" is used to set the <select> tag's "size" attribute
2108             "showid" includes the item ids in the list labels
2109             "additional" lists properties which should be included in the
2110                 label
2111             "value" specifies which item is pre-selected
2112             "sort_on" indicates the property to sort the list on as
2113                 (direction, property) where direction is '+' or '-'. A
2114                 single string with the direction prepended may be used.
2115                 For example: ('-', 'order'), '+name'.
2117             The remaining keyword arguments are used as conditions for
2118             filtering the items in the list - they're passed as the
2119             "filterspec" argument to a Class.filter() call.
2121             If not editable, just display the value via plain().
2122         """
2123         if not self.is_edit_ok():
2124             return self.plain(escape=1)
2126         if value is None:
2127             value = self._value
2129         linkcl = self._db.getclass(self._prop.classname)
2131         if sort_on is not None:
2132             if not isinstance(sort_on, tuple):
2133                 if sort_on[0] in '+-':
2134                     sort_on = (sort_on[0], sort_on[1:])
2135                 else:
2136                     sort_on = ('+', sort_on)
2137         else:
2138             sort_on = ('+', linkcl.orderprop())
2140         options = [opt
2141             for opt in linkcl.filter(None, conditions, sort_on)
2142             if self._db.security.hasPermission("View", self._client.userid,
2143                 linkcl.classname, itemid=opt)]
2144         
2145         # make sure we list the current values if they're retired
2146         for val in value:
2147             if val not in options:
2148                 options.insert(0, val)
2150         if not height:
2151             height = len(options)
2152             if value:
2153                 # The "no selection" option.
2154                 height += 1
2155             height = min(height, 7)
2156         l = ['<select multiple name="%s" size="%s">'%(self._formname, height)]
2157         k = linkcl.labelprop(1)
2159         if value:
2160             l.append('<option value="%s">- no selection -</option>'
2161                      % ','.join(['-' + v for v in value]))
2163         if additional:
2164             additional_fns = []
2165             props = linkcl.getprops()
2166             for propname in additional:
2167                 prop = props[propname]
2168                 if isinstance(prop, hyperdb.Link):
2169                     cl = self._db.getclass(prop.classname)
2170                     labelprop = cl.labelprop()
2171                     fn = lambda optionid: cl.get(linkcl.get(optionid,
2172                                                             propname),
2173                                                  labelprop)
2174                 else:
2175                     fn = lambda optionid: linkcl.get(optionid, propname)
2176             additional_fns.append(fn)
2177             
2178         for optionid in options:
2179             # get the option value, and if it's None use an empty string
2180             option = linkcl.get(optionid, k) or ''
2182             # figure if this option is selected
2183             s = ''
2184             if optionid in value or option in value:
2185                 s = 'selected="selected" '
2187             # figure the label
2188             if showid:
2189                 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
2190             else:
2191                 lab = option
2192             # truncate if it's too long
2193             if size is not None and len(lab) > size:
2194                 lab = lab[:size-3] + '...'
2195             if additional:
2196                 m = []
2197                 for fn in additional_fns:
2198                     m.append(str(fn(optionid)))
2199                 lab = lab + ' (%s)'%', '.join(m)
2201             # and generate
2202             lab = cgi.escape(self._(lab))
2203             l.append('<option %svalue="%s">%s</option>'%(s, optionid,
2204                 lab))
2205         l.append('</select>')
2206         return '\n'.join(l)
2208 # set the propclasses for HTMLItem
2209 propclasses = (
2210     (hyperdb.String, StringHTMLProperty),
2211     (hyperdb.Number, NumberHTMLProperty),
2212     (hyperdb.Boolean, BooleanHTMLProperty),
2213     (hyperdb.Date, DateHTMLProperty),
2214     (hyperdb.Interval, IntervalHTMLProperty),
2215     (hyperdb.Password, PasswordHTMLProperty),
2216     (hyperdb.Link, LinkHTMLProperty),
2217     (hyperdb.Multilink, MultilinkHTMLProperty),
2220 def make_sort_function(db, classname, sort_on=None):
2221     """Make a sort function for a given class
2222     """
2223     linkcl = db.getclass(classname)
2224     if sort_on is None:
2225         sort_on = linkcl.orderprop()
2226     def sortfunc(a, b):
2227         return cmp(linkcl.get(a, sort_on), linkcl.get(b, sort_on))
2228     return sortfunc
2230 def handleListCGIValue(value):
2231     """ Value is either a single item or a list of items. Each item has a
2232         .value that we're actually interested in.
2233     """
2234     if isinstance(value, type([])):
2235         return [value.value for value in value]
2236     else:
2237         value = value.value.strip()
2238         if not value:
2239             return []
2240         return [v.strip() for v in value.split(',')]
2242 class HTMLRequest(HTMLInputMixin):
2243     """The *request*, holding the CGI form and environment.
2245     - "form" the CGI form as a cgi.FieldStorage
2246     - "env" the CGI environment variables
2247     - "base" the base URL for this instance
2248     - "user" a HTMLItem instance for this user
2249     - "language" as determined by the browser or config
2250     - "classname" the current classname (possibly None)
2251     - "template" the current template (suffix, also possibly None)
2253     Index args:
2255     - "columns" dictionary of the columns to display in an index page
2256     - "show" a convenience access to columns - request/show/colname will
2257       be true if the columns should be displayed, false otherwise
2258     - "sort" index sort column (direction, column name)
2259     - "group" index grouping property (direction, column name)
2260     - "filter" properties to filter the index on
2261     - "filterspec" values to filter the index on
2262     - "search_text" text to perform a full-text search on for an index
2263     """
2264     def __repr__(self):
2265         return '<HTMLRequest %r>'%self.__dict__
2267     def __init__(self, client):
2268         # _client is needed by HTMLInputMixin
2269         self._client = self.client = client
2271         # easier access vars
2272         self.form = client.form
2273         self.env = client.env
2274         self.base = client.base
2275         self.user = HTMLItem(client, 'user', client.userid)
2276         self.language = client.language
2278         # store the current class name and action
2279         self.classname = client.classname
2280         self.nodeid = client.nodeid
2281         self.template = client.template
2283         # the special char to use for special vars
2284         self.special_char = '@'
2286         HTMLInputMixin.__init__(self)
2288         self._post_init()
2290     def current_url(self):
2291         url = self.base
2292         if self.classname:
2293             url += self.classname
2294             if self.nodeid:
2295                 url += self.nodeid
2296         args = {}
2297         if self.template:
2298             args['@template'] = self.template
2299         return self.indexargs_url(url, args)
2301     def _parse_sort(self, var, name):
2302         """ Parse sort/group options. Append to var
2303         """
2304         fields = []
2305         dirs = []
2306         for special in '@:':
2307             idx = 0
2308             key = '%s%s%d'%(special, name, idx)
2309             while key in self.form:
2310                 self.special_char = special
2311                 fields.append(self.form.getfirst(key))
2312                 dirkey = '%s%sdir%d'%(special, name, idx)
2313                 if dirkey in self.form:
2314                     dirs.append(self.form.getfirst(dirkey))
2315                 else:
2316                     dirs.append(None)
2317                 idx += 1
2318                 key = '%s%s%d'%(special, name, idx)
2319             # backward compatible (and query) URL format
2320             key = special + name
2321             dirkey = key + 'dir'
2322             if key in self.form and not fields:
2323                 fields = handleListCGIValue(self.form[key])
2324                 if dirkey in self.form:
2325                     dirs.append(self.form.getfirst(dirkey))
2326             if fields: # only try other special char if nothing found
2327                 break
2328         for f, d in map(None, fields, dirs):
2329             if f.startswith('-'):
2330                 var.append(('-', f[1:]))
2331             elif d:
2332                 var.append(('-', f))
2333             else:
2334                 var.append(('+', f))
2336     def _post_init(self):
2337         """ Set attributes based on self.form
2338         """
2339         # extract the index display information from the form
2340         self.columns = []
2341         for name in ':columns @columns'.split():
2342             if self.form.has_key(name):
2343                 self.special_char = name[0]
2344                 self.columns = handleListCGIValue(self.form[name])
2345                 break
2346         self.show = support.TruthDict(self.columns)
2348         # sorting and grouping
2349         self.sort = []
2350         self.group = []
2351         self._parse_sort(self.sort, 'sort')
2352         self._parse_sort(self.group, 'group')
2354         # filtering
2355         self.filter = []
2356         for name in ':filter @filter'.split():
2357             if self.form.has_key(name):
2358                 self.special_char = name[0]
2359                 self.filter = handleListCGIValue(self.form[name])
2361         self.filterspec = {}
2362         db = self.client.db
2363         if self.classname is not None:
2364             cls = db.getclass (self.classname)
2365             for name in self.filter:
2366                 if not self.form.has_key(name):
2367                     continue
2368                 prop = cls.get_transitive_prop (name)
2369                 fv = self.form[name]
2370                 if (isinstance(prop, hyperdb.Link) or
2371                         isinstance(prop, hyperdb.Multilink)):
2372                     self.filterspec[name] = lookupIds(db, prop,
2373                         handleListCGIValue(fv))
2374                 else:
2375                     if isinstance(fv, type([])):
2376                         self.filterspec[name] = [v.value for v in fv]
2377                     elif name == 'id':
2378                         # special case "id" property
2379                         self.filterspec[name] = handleListCGIValue(fv)
2380                     else:
2381                         self.filterspec[name] = fv.value
2383         # full-text search argument
2384         self.search_text = None
2385         for name in ':search_text @search_text'.split():
2386             if self.form.has_key(name):
2387                 self.special_char = name[0]
2388                 self.search_text = self.form.getfirst(name)
2390         # pagination - size and start index
2391         # figure batch args
2392         self.pagesize = 50
2393         for name in ':pagesize @pagesize'.split():
2394             if self.form.has_key(name):
2395                 self.special_char = name[0]
2396                 self.pagesize = int(self.form.getfirst(name))
2398         self.startwith = 0
2399         for name in ':startwith @startwith'.split():
2400             if self.form.has_key(name):
2401                 self.special_char = name[0]
2402                 self.startwith = int(self.form.getfirst(name))
2404         # dispname
2405         if self.form.has_key('@dispname'):
2406             self.dispname = self.form.getfirst('@dispname')
2407         else:
2408             self.dispname = None
2410     def updateFromURL(self, url):
2411         """ Parse the URL for query args, and update my attributes using the
2412             values.
2413         """
2414         env = {'QUERY_STRING': url}
2415         self.form = cgi.FieldStorage(environ=env)
2417         self._post_init()
2419     def update(self, kwargs):
2420         """ Update my attributes using the keyword args
2421         """
2422         self.__dict__.update(kwargs)
2423         if kwargs.has_key('columns'):
2424             self.show = support.TruthDict(self.columns)
2426     def description(self):
2427         """ Return a description of the request - handle for the page title.
2428         """
2429         s = [self.client.db.config.TRACKER_NAME]
2430         if self.classname:
2431             if self.client.nodeid:
2432                 s.append('- %s%s'%(self.classname, self.client.nodeid))
2433             else:
2434                 if self.template == 'item':
2435                     s.append('- new %s'%self.classname)
2436                 elif self.template == 'index':
2437                     s.append('- %s index'%self.classname)
2438                 else:
2439                     s.append('- %s %s'%(self.classname, self.template))
2440         else:
2441             s.append('- home')
2442         return ' '.join(s)
2444     def __str__(self):
2445         d = {}
2446         d.update(self.__dict__)
2447         f = ''
2448         for k in self.form.keys():
2449             f += '\n      %r=%r'%(k,handleListCGIValue(self.form[k]))
2450         d['form'] = f
2451         e = ''
2452         for k,v in self.env.items():
2453             e += '\n     %r=%r'%(k, v)
2454         d['env'] = e
2455         return """
2456 form: %(form)s
2457 base: %(base)r
2458 classname: %(classname)r
2459 template: %(template)r
2460 columns: %(columns)r
2461 sort: %(sort)r
2462 group: %(group)r
2463 filter: %(filter)r
2464 search_text: %(search_text)r
2465 pagesize: %(pagesize)r
2466 startwith: %(startwith)r
2467 env: %(env)s
2468 """%d
2470     def indexargs_form(self, columns=1, sort=1, group=1, filter=1,
2471             filterspec=1, search_text=1):
2472         """ return the current index args as form elements """
2473         l = []
2474         sc = self.special_char
2475         def add(k, v):
2476             l.append(self.input(type="hidden", name=k, value=v))
2477         if columns and self.columns:
2478             add(sc+'columns', ','.join(self.columns))
2479         if sort:
2480             val = []
2481             for dir, attr in self.sort:
2482                 if dir == '-':
2483                     val.append('-'+attr)
2484                 else:
2485                     val.append(attr)
2486             add(sc+'sort', ','.join (val))
2487         if group:
2488             val = []
2489             for dir, attr in self.group:
2490                 if dir == '-':
2491                     val.append('-'+attr)
2492                 else:
2493                     val.append(attr)
2494             add(sc+'group', ','.join (val))
2495         if filter and self.filter:
2496             add(sc+'filter', ','.join(self.filter))
2497         if self.classname and filterspec:
2498             cls = self.client.db.getclass(self.classname)
2499             for k,v in self.filterspec.items():
2500                 if type(v) == type([]):
2501                     if isinstance(cls.get_transitive_prop(k), hyperdb.String):
2502                         add(k, ' '.join(v))
2503                     else:
2504                         add(k, ','.join(v))
2505                 else:
2506                     add(k, v)
2507         if search_text and self.search_text:
2508             add(sc+'search_text', self.search_text)
2509         add(sc+'pagesize', self.pagesize)
2510         add(sc+'startwith', self.startwith)
2511         return '\n'.join(l)
2513     def indexargs_url(self, url, args):
2514         """ Embed the current index args in a URL
2515         """
2516         q = urllib.quote
2517         sc = self.special_char
2518         l = ['%s=%s'%(k,v) for k,v in args.items()]
2520         # pull out the special values (prefixed by @ or :)
2521         specials = {}
2522         for key in args.keys():
2523             if key[0] in '@:':
2524                 specials[key[1:]] = args[key]
2526         # ok, now handle the specials we received in the request
2527         if self.columns and not specials.has_key('columns'):
2528             l.append(sc+'columns=%s'%(','.join(self.columns)))
2529         if self.sort and not specials.has_key('sort'):
2530             val = []
2531             for dir, attr in self.sort:
2532                 if dir == '-':
2533                     val.append('-'+attr)
2534                 else:
2535                     val.append(attr)
2536             l.append(sc+'sort=%s'%(','.join(val)))
2537         if self.group and not specials.has_key('group'):
2538             val = []
2539             for dir, attr in self.group:
2540                 if dir == '-':
2541                     val.append('-'+attr)
2542                 else:
2543                     val.append(attr)
2544             l.append(sc+'group=%s'%(','.join(val)))
2545         if self.filter and not specials.has_key('filter'):
2546             l.append(sc+'filter=%s'%(','.join(self.filter)))
2547         if self.search_text and not specials.has_key('search_text'):
2548             l.append(sc+'search_text=%s'%q(self.search_text))
2549         if not specials.has_key('pagesize'):
2550             l.append(sc+'pagesize=%s'%self.pagesize)
2551         if not specials.has_key('startwith'):
2552             l.append(sc+'startwith=%s'%self.startwith)
2554         # finally, the remainder of the filter args in the request
2555         if self.classname and self.filterspec:
2556             cls = self.client.db.getclass(self.classname)
2557             for k,v in self.filterspec.items():
2558                 if not args.has_key(k):
2559                     if type(v) == type([]):
2560                         prop = cls.get_transitive_prop(k)
2561                         if k != 'id' and isinstance(prop, hyperdb.String):
2562                             l.append('%s=%s'%(k, '%20'.join([q(i) for i in v])))
2563                         else:
2564                             l.append('%s=%s'%(k, ','.join([q(i) for i in v])))
2565                     else:
2566                         l.append('%s=%s'%(k, q(v)))
2567         return '%s?%s'%(url, '&'.join(l))
2568     indexargs_href = indexargs_url
2570     def base_javascript(self):
2571         return """
2572 <script type="text/javascript">
2573 submitted = false;
2574 function submit_once() {
2575     if (submitted) {
2576         alert("Your request is being processed.\\nPlease be patient.");
2577         event.returnValue = 0;    // work-around for IE
2578         return 0;
2579     }
2580     submitted = true;
2581     return 1;
2584 function help_window(helpurl, width, height) {
2585     HelpWin = window.open('%s' + helpurl, 'RoundupHelpWindow', 'scrollbars=yes,resizable=yes,toolbar=no,height='+height+',width='+width);
2587 </script>
2588 """%self.base
2590     def batch(self):
2591         """ Return a batch object for results from the "current search"
2592         """
2593         filterspec = self.filterspec
2594         sort = self.sort
2595         group = self.group
2597         # get the list of ids we're batching over
2598         klass = self.client.db.getclass(self.classname)
2599         if self.search_text:
2600             matches = self.client.db.indexer.search(
2601                 [w.upper().encode("utf-8", "replace") for w in re.findall(
2602                     r'(?u)\b\w{2,25}\b',
2603                     unicode(self.search_text, "utf-8", "replace")
2604                 )], klass)
2605         else:
2606             matches = None
2608         # filter for visibility
2609         check = self._client.db.security.hasPermission
2610         userid = self._client.userid
2611         l = [id for id in klass.filter(matches, filterspec, sort, group)
2612             if check('View', userid, self.classname, itemid=id)]
2614         # return the batch object, using IDs only
2615         return Batch(self.client, l, self.pagesize, self.startwith,
2616             classname=self.classname)
2618 # extend the standard ZTUtils Batch object to remove dependency on
2619 # Acquisition and add a couple of useful methods
2620 class Batch(ZTUtils.Batch):
2621     """ Use me to turn a list of items, or item ids of a given class, into a
2622         series of batches.
2624         ========= ========================================================
2625         Parameter  Usage
2626         ========= ========================================================
2627         sequence  a list of HTMLItems or item ids
2628         classname if sequence is a list of ids, this is the class of item
2629         size      how big to make the sequence.
2630         start     where to start (0-indexed) in the sequence.
2631         end       where to end (0-indexed) in the sequence.
2632         orphan    if the next batch would contain less items than this
2633                   value, then it is combined with this batch
2634         overlap   the number of items shared between adjacent batches
2635         ========= ========================================================
2637         Attributes: Note that the "start" attribute, unlike the
2638         argument, is a 1-based index (I know, lame).  "first" is the
2639         0-based index.  "length" is the actual number of elements in
2640         the batch.
2642         "sequence_length" is the length of the original, unbatched, sequence.
2643     """
2644     def __init__(self, client, sequence, size, start, end=0, orphan=0,
2645             overlap=0, classname=None):
2646         self.client = client
2647         self.last_index = self.last_item = None
2648         self.current_item = None
2649         self.classname = classname
2650         self.sequence_length = len(sequence)
2651         ZTUtils.Batch.__init__(self, sequence, size, start, end, orphan,
2652             overlap)
2654     # overwrite so we can late-instantiate the HTMLItem instance
2655     def __getitem__(self, index):
2656         if index < 0:
2657             if index + self.end < self.first: raise IndexError, index
2658             return self._sequence[index + self.end]
2660         if index >= self.length:
2661             raise IndexError, index
2663         # move the last_item along - but only if the fetched index changes
2664         # (for some reason, index 0 is fetched twice)
2665         if index != self.last_index:
2666             self.last_item = self.current_item
2667             self.last_index = index
2669         item = self._sequence[index + self.first]
2670         if self.classname:
2671             # map the item ids to instances
2672             item = HTMLItem(self.client, self.classname, item)
2673         self.current_item = item
2674         return item
2676     def propchanged(self, *properties):
2677         """ Detect if one of the properties marked as being a group
2678             property changed in the last iteration fetch
2679         """
2680         # we poke directly at the _value here since MissingValue can screw
2681         # us up and cause Nones to compare strangely
2682         if self.last_item is None:
2683             return 1
2684         for property in properties:
2685             if property == 'id' or isinstance (self.last_item[property], list):
2686                 if (str(self.last_item[property]) !=
2687                     str(self.current_item[property])):
2688                     return 1
2689             else:
2690                 if (self.last_item[property]._value !=
2691                     self.current_item[property]._value):
2692                     return 1
2693         return 0
2695     # override these 'cos we don't have access to acquisition
2696     def previous(self):
2697         if self.start == 1:
2698             return None
2699         return Batch(self.client, self._sequence, self._size,
2700             self.first - self._size + self.overlap, 0, self.orphan,
2701             self.overlap)
2703     def next(self):
2704         try:
2705             self._sequence[self.end]
2706         except IndexError:
2707             return None
2708         return Batch(self.client, self._sequence, self._size,
2709             self.end - self.overlap, 0, self.orphan, self.overlap)
2711 class TemplatingUtils:
2712     """ Utilities for templating
2713     """
2714     def __init__(self, client):
2715         self.client = client
2716     def Batch(self, sequence, size, start, end=0, orphan=0, overlap=0):
2717         return Batch(self.client, sequence, size, start, end, orphan,
2718             overlap)
2720     def url_quote(self, url):
2721         """URL-quote the supplied text."""
2722         return urllib.quote(url)
2724     def html_quote(self, html):
2725         """HTML-quote the supplied text."""
2726         return cgi.escape(html)
2728     def __getattr__(self, name):
2729         """Try the tracker's templating_utils."""
2730         if not hasattr(self.client.instance, 'templating_utils'):
2731             # backwards-compatibility
2732             raise AttributeError, name
2733         if not self.client.instance.templating_utils.has_key(name):
2734             raise AttributeError, name
2735         return self.client.instance.templating_utils[name]
2737     def html_calendar(self, request):
2738         """Generate a HTML calendar.
2740         `request`  the roundup.request object
2741                    - @template : name of the template
2742                    - form      : name of the form to store back the date
2743                    - property  : name of the property of the form to store
2744                                  back the date
2745                    - date      : current date
2746                    - display   : when browsing, specifies year and month
2748         html will simply be a table.
2749         """
2750         date_str  = request.form.getfirst("date", ".")
2751         display   = request.form.getfirst("display", date_str)
2752         template  = request.form.getfirst("@template", "calendar")
2753         form      = request.form.getfirst("form")
2754         property  = request.form.getfirst("property")
2755         curr_date = date.Date(date_str) # to highlight
2756         display   = date.Date(display)  # to show
2757         day       = display.day
2759         # for navigation
2760         date_prev_month = display + date.Interval("-1m")
2761         date_next_month = display + date.Interval("+1m")
2762         date_prev_year  = display + date.Interval("-1y")
2763         date_next_year  = display + date.Interval("+1y")
2765         res = []
2767         base_link = "%s?@template=%s&property=%s&form=%s&date=%s" % \
2768                     (request.classname, template, property, form, curr_date)
2770         # navigation
2771         # month
2772         res.append('<table class="calendar"><tr><td>')
2773         res.append(' <table width="100%" class="calendar_nav"><tr>')
2774         link = "&display=%s"%date_prev_month
2775         res.append('  <td><a href="%s&display=%s">&lt;</a></td>'%(base_link,
2776             date_prev_month))
2777         res.append('  <td>%s</td>'%calendar.month_name[display.month])
2778         res.append('  <td><a href="%s&display=%s">&gt;</a></td>'%(base_link,
2779             date_next_month))
2780         # spacer
2781         res.append('  <td width="100%"></td>')
2782         # year
2783         res.append('  <td><a href="%s&display=%s">&lt;</a></td>'%(base_link,
2784             date_prev_year))
2785         res.append('  <td>%s</td>'%display.year)
2786         res.append('  <td><a href="%s&display=%s">&gt;</a></td>'%(base_link,
2787             date_next_year))
2788         res.append(' </tr></table>')
2789         res.append(' </td></tr>')
2791         # the calendar
2792         res.append(' <tr><td><table class="calendar_display">')
2793         res.append('  <tr class="weekdays">')
2794         for day in calendar.weekheader(3).split():
2795             res.append('   <td>%s</td>'%day)
2796         res.append('  </tr>')
2797         for week in calendar.monthcalendar(display.year, display.month):
2798             res.append('  <tr>')
2799             for day in week:
2800                 link = "javascript:form[field].value = '%d-%02d-%02d'; " \
2801                       "window.close ();"%(display.year, display.month, day)
2802                 if (day == curr_date.day and display.month == curr_date.month
2803                         and display.year == curr_date.year):
2804                     # highlight
2805                     style = "today"
2806                 else :
2807                     style = ""
2808                 if day:
2809                     res.append('   <td class="%s"><a href="%s">%s</a></td>'%(
2810                         style, link, day))
2811                 else :
2812                     res.append('   <td></td>')
2813             res.append('  </tr>')
2814         res.append('</table></td></tr></table>')
2815         return "\n".join(res)
2817 class MissingValue:
2818     def __init__(self, description, **kwargs):
2819         self.__description = description
2820         for key, value in kwargs.items():
2821             self.__dict__[key] = value
2823     def __call__(self, *args, **kwargs): return MissingValue(self.__description)
2824     def __getattr__(self, name):
2825         # This allows assignments which assume all intermediate steps are Null
2826         # objects if they don't exist yet.
2827         #
2828         # For example (with just 'client' defined):
2829         #
2830         # client.db.config.TRACKER_WEB = 'BASE/'
2831         self.__dict__[name] = MissingValue(self.__description)
2832         return getattr(self, name)
2834     def __getitem__(self, key): return self
2835     def __nonzero__(self): return 0
2836     def __str__(self): return '[%s]'%self.__description
2837     def __repr__(self): return '<MissingValue 0x%x "%s">'%(id(self),
2838         self.__description)
2839     def gettext(self, str): return str
2840     _ = gettext
2842 # vim: set et sts=4 sw=4 :