Code

Fix issue2550553.
[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         check = self._client.db.security.hasPermission
619         for nodeid in self._klass.list():
620             l = []
621             for name in props:
622                 # check permission to view this property on this item
623                 if not check('View', self._client.userid, itemid=nodeid,
624                         classname=self._klass.classname, property=name):
625                     raise Unauthorised('view', self._klass.classname,
626                         translator=self._client.translator)
627                 value = self._klass.get(nodeid, name)
628                 if value is None:
629                     l.append('')
630                 elif isinstance(value, type([])):
631                     l.append(':'.join(map(str, value)))
632                 else:
633                     l.append(str(self._klass.get(nodeid, name)))
634             writer.writerow(l)
635         return s.getvalue()
637     def propnames(self):
638         """ Return the list of the names of the properties of this class.
639         """
640         idlessprops = self._klass.getprops(protected=0).keys()
641         idlessprops.sort()
642         return ['id'] + idlessprops
644     def filter(self, request=None, filterspec={}, sort=[], group=[]):
645         """ Return a list of items from this class, filtered and sorted
646             by the current requested filterspec/filter/sort/group args
648             "request" takes precedence over the other three arguments.
649         """
650         if request is not None:
651             filterspec = request.filterspec
652             sort = request.sort
653             group = request.group
655         check = self._db.security.hasPermission
656         userid = self._client.userid
658         l = [HTMLItem(self._client, self.classname, id)
659              for id in self._klass.filter(None, filterspec, sort, group)
660              if check('View', userid, self.classname, itemid=id)]
661         return l
663     def classhelp(self, properties=None, label=''"(list)", width='500',
664             height='400', property='', form='itemSynopsis',
665             pagesize=50, inputtype="checkbox", sort=None, filter=None):
666         """Pop up a javascript window with class help
668         This generates a link to a popup window which displays the
669         properties indicated by "properties" of the class named by
670         "classname". The "properties" should be a comma-separated list
671         (eg. 'id,name,description'). Properties defaults to all the
672         properties of a class (excluding id, creator, created and
673         activity).
675         You may optionally override the label displayed, the width,
676         the height, the number of items per page and the field on which
677         the list is sorted (defaults to username if in the displayed
678         properties).
680         With the "filter" arg it is possible to specify a filter for
681         which items are supposed to be displayed. It has to be of
682         the format "<field>=<values>;<field>=<values>;...".
684         The popup window will be resizable and scrollable.
686         If the "property" arg is given, it's passed through to the
687         javascript help_window function.
689         You can use inputtype="radio" to display a radio box instead
690         of the default checkbox (useful for entering Link-properties)
692         If the "form" arg is given, it's passed through to the
693         javascript help_window function. - it's the name of the form
694         the "property" belongs to.
695         """
696         if properties is None:
697             properties = self._klass.getprops(protected=0).keys()
698             properties.sort()
699             properties = ','.join(properties)
700         if sort is None:
701             if 'username' in properties.split( ',' ):
702                 sort = 'username'
703             else:
704                 sort = self._klass.orderprop()
705         sort = '&amp;@sort=' + sort
706         if property:
707             property = '&amp;property=%s'%property
708         if form:
709             form = '&amp;form=%s'%form
710         if inputtype:
711             type= '&amp;type=%s'%inputtype
712         if filter:
713             filterprops = filter.split(';')
714             filtervalues = []
715             names = []
716             for x in filterprops:
717                 (name, values) = x.split('=')
718                 names.append(name)
719                 filtervalues.append('&amp;%s=%s' % (name, urllib.quote(values)))
720             filter = '&amp;@filter=%s%s' % (','.join(names), ''.join(filtervalues))
721         else:
722            filter = ''
723         help_url = "%s?@startwith=0&amp;@template=help&amp;"\
724                    "properties=%s%s%s%s%s&amp;@pagesize=%s%s" % \
725                    (self.classname, properties, property, form, type,
726                    sort, pagesize, filter)
727         onclick = "javascript:help_window('%s', '%s', '%s');return false;" % \
728                   (help_url, width, height)
729         return '<a class="classhelp" href="%s" onclick="%s">%s</a>' % \
730                (help_url, onclick, self._(label))
732     def submit(self, label=''"Submit New Entry", action="new"):
733         """ Generate a submit button (and action hidden element)
735         Generate nothing if we're not editable.
736         """
737         if not self.is_edit_ok():
738             return ''
740         return self.input(type="hidden", name="@action", value=action) + \
741             '\n' + \
742             self.input(type="submit", name="submit_button", value=self._(label))
744     def history(self):
745         if not self.is_view_ok():
746             return self._('[hidden]')
747         return self._('New node - no history')
749     def renderWith(self, name, **kwargs):
750         """ Render this class with the given template.
751         """
752         # create a new request and override the specified args
753         req = HTMLRequest(self._client)
754         req.classname = self.classname
755         req.update(kwargs)
757         # new template, using the specified classname and request
758         pt = self._client.instance.templates.get(self.classname, name)
760         # use our fabricated request
761         args = {
762             'ok_message': self._client.ok_message,
763             'error_message': self._client.error_message
764         }
765         return pt.render(self._client, self.classname, req, **args)
767 class _HTMLItem(HTMLInputMixin, HTMLPermissions):
768     """ Accesses through an *item*
769     """
770     def __init__(self, client, classname, nodeid, anonymous=0):
771         self._client = client
772         self._db = client.db
773         self._classname = classname
774         self._nodeid = nodeid
775         self._klass = self._db.getclass(classname)
776         self._props = self._klass.getprops()
778         # do we prefix the form items with the item's identification?
779         self._anonymous = anonymous
781         HTMLInputMixin.__init__(self)
783     def is_edit_ok(self):
784         """ Is the user allowed to Edit this item?
785         """
786         return self._db.security.hasPermission('Edit', self._client.userid,
787             self._classname, itemid=self._nodeid)
789     def is_retire_ok(self):
790         """ Is the user allowed to Reture this item?
791         """
792         return self._db.security.hasPermission('Retire', self._client.userid,
793             self._classname, itemid=self._nodeid)
795     def is_view_ok(self):
796         """ Is the user allowed to View this item?
797         """
798         if self._db.security.hasPermission('View', self._client.userid,
799                 self._classname, itemid=self._nodeid):
800             return 1
801         return self.is_edit_ok()
803     def is_only_view_ok(self):
804         """ Is the user only allowed to View (ie. not Edit) this item?
805         """
806         return self.is_view_ok() and not self.is_edit_ok()
808     def __repr__(self):
809         return '<HTMLItem(0x%x) %s %s>'%(id(self), self._classname,
810             self._nodeid)
812     def __getitem__(self, item):
813         """ return an HTMLProperty instance
814             this now can handle transitive lookups where item is of the
815             form x.y.z
816         """
817         if item == 'id':
818             return self._nodeid
820         items = item.split('.', 1)
821         has_rest = len(items) > 1
823         # get the property
824         prop = self._props[items[0]]
826         if has_rest and not isinstance(prop, (hyperdb.Link, hyperdb.Multilink)):
827             raise KeyError, item
829         # get the value, handling missing values
830         value = None
831         if int(self._nodeid) > 0:
832             value = self._klass.get(self._nodeid, items[0], None)
833         if value is None:
834             if isinstance(prop, hyperdb.Multilink):
835                 value = []
837         # look up the correct HTMLProperty class
838         htmlprop = None
839         for klass, htmlklass in propclasses:
840             if isinstance(prop, klass):
841                 htmlprop = htmlklass(self._client, self._classname,
842                     self._nodeid, prop, items[0], value, self._anonymous)
843         if htmlprop is not None:
844             if has_rest:
845                 if isinstance(htmlprop, MultilinkHTMLProperty):
846                     return [h[items[1]] for h in htmlprop]
847                 return htmlprop[items[1]]
848             return htmlprop
850         raise KeyError, item
852     def __getattr__(self, attr):
853         """ convenience access to properties """
854         try:
855             return self[attr]
856         except KeyError:
857             raise AttributeError, attr
859     def designator(self):
860         """Return this item's designator (classname + id)."""
861         return '%s%s'%(self._classname, self._nodeid)
863     def is_retired(self):
864         """Is this item retired?"""
865         return self._klass.is_retired(self._nodeid)
867     def submit(self, label=''"Submit Changes", action="edit"):
868         """Generate a submit button.
870         Also sneak in the lastactivity and action hidden elements.
871         """
872         return self.input(type="hidden", name="@lastactivity",
873             value=self.activity.local(0)) + '\n' + \
874             self.input(type="hidden", name="@action", value=action) + '\n' + \
875             self.input(type="submit", name="submit_button", value=self._(label))
877     def journal(self, direction='descending'):
878         """ Return a list of HTMLJournalEntry instances.
879         """
880         # XXX do this
881         return []
883     def history(self, direction='descending', dre=re.compile('^\d+$')):
884         if not self.is_view_ok():
885             return self._('[hidden]')
887         # pre-load the history with the current state
888         current = {}
889         for prop_n in self._props.keys():
890             prop = self[prop_n]
891             if not isinstance(prop, HTMLProperty):
892                 continue
893             current[prop_n] = prop.plain(escape=1)
894             # make link if hrefable
895             if (self._props.has_key(prop_n) and
896                     isinstance(self._props[prop_n], hyperdb.Link)):
897                 classname = self._props[prop_n].classname
898                 try:
899                     template = find_template(self._db.config.TEMPLATES,
900                         classname, 'item')
901                     if template[1].startswith('_generic'):
902                         raise NoTemplate, 'not really...'
903                 except NoTemplate:
904                     pass
905                 else:
906                     id = self._klass.get(self._nodeid, prop_n, None)
907                     current[prop_n] = '<a href="%s%s">%s</a>'%(
908                         classname, id, current[prop_n])
910         # get the journal, sort and reverse
911         history = self._klass.history(self._nodeid)
912         history.sort()
913         history.reverse()
915         timezone = self._db.getUserTimezone()
916         l = []
917         comments = {}
918         for id, evt_date, user, action, args in history:
919             date_s = str(evt_date.local(timezone)).replace("."," ")
920             arg_s = ''
921             if action == 'link' and type(args) == type(()):
922                 if len(args) == 3:
923                     linkcl, linkid, key = args
924                     arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
925                         linkcl, linkid, key)
926                 else:
927                     arg_s = str(args)
929             elif action == 'unlink' and type(args) == type(()):
930                 if len(args) == 3:
931                     linkcl, linkid, key = args
932                     arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
933                         linkcl, linkid, key)
934                 else:
935                     arg_s = str(args)
937             elif type(args) == type({}):
938                 cell = []
939                 for k in args.keys():
940                     # try to get the relevant property and treat it
941                     # specially
942                     try:
943                         prop = self._props[k]
944                     except KeyError:
945                         prop = None
946                     if prop is None:
947                         # property no longer exists
948                         comments['no_exist'] = self._(
949                             "<em>The indicated property no longer exists</em>")
950                         cell.append(self._('<em>%s: %s</em>\n')
951                             % (self._(k), str(args[k])))
952                         continue
954                     if args[k] and (isinstance(prop, hyperdb.Multilink) or
955                             isinstance(prop, hyperdb.Link)):
956                         # figure what the link class is
957                         classname = prop.classname
958                         try:
959                             linkcl = self._db.getclass(classname)
960                         except KeyError:
961                             labelprop = None
962                             comments[classname] = self._(
963                                 "The linked class %(classname)s no longer exists"
964                             ) % locals()
965                         labelprop = linkcl.labelprop(1)
966                         try:
967                             template = find_template(self._db.config.TEMPLATES,
968                                 classname, 'item')
969                             if template[1].startswith('_generic'):
970                                 raise NoTemplate, 'not really...'
971                             hrefable = 1
972                         except NoTemplate:
973                             hrefable = 0
975                     if isinstance(prop, hyperdb.Multilink) and args[k]:
976                         ml = []
977                         for linkid in args[k]:
978                             if isinstance(linkid, type(())):
979                                 sublabel = linkid[0] + ' '
980                                 linkids = linkid[1]
981                             else:
982                                 sublabel = ''
983                                 linkids = [linkid]
984                             subml = []
985                             for linkid in linkids:
986                                 label = classname + linkid
987                                 # if we have a label property, try to use it
988                                 # TODO: test for node existence even when
989                                 # there's no labelprop!
990                                 try:
991                                     if labelprop is not None and \
992                                             labelprop != 'id':
993                                         label = linkcl.get(linkid, labelprop)
994                                         label = cgi.escape(label)
995                                 except IndexError:
996                                     comments['no_link'] = self._(
997                                         "<strike>The linked node"
998                                         " no longer exists</strike>")
999                                     subml.append('<strike>%s</strike>'%label)
1000                                 else:
1001                                     if hrefable:
1002                                         subml.append('<a href="%s%s">%s</a>'%(
1003                                             classname, linkid, label))
1004                                     elif label is None:
1005                                         subml.append('%s%s'%(classname,
1006                                             linkid))
1007                                     else:
1008                                         subml.append(label)
1009                             ml.append(sublabel + ', '.join(subml))
1010                         cell.append('%s:\n  %s'%(self._(k), ', '.join(ml)))
1011                     elif isinstance(prop, hyperdb.Link) and args[k]:
1012                         label = classname + args[k]
1013                         # if we have a label property, try to use it
1014                         # TODO: test for node existence even when
1015                         # there's no labelprop!
1016                         if labelprop is not None and labelprop != 'id':
1017                             try:
1018                                 label = cgi.escape(linkcl.get(args[k],
1019                                     labelprop))
1020                             except IndexError:
1021                                 comments['no_link'] = self._(
1022                                     "<strike>The linked node"
1023                                     " no longer exists</strike>")
1024                                 cell.append(' <strike>%s</strike>,\n'%label)
1025                                 # "flag" this is done .... euwww
1026                                 label = None
1027                         if label is not None:
1028                             if hrefable:
1029                                 old = '<a href="%s%s">%s</a>'%(classname,
1030                                     args[k], label)
1031                             else:
1032                                 old = label;
1033                             cell.append('%s: %s' % (self._(k), old))
1034                             if current.has_key(k):
1035                                 cell[-1] += ' -> %s'%current[k]
1036                                 current[k] = old
1038                     elif isinstance(prop, hyperdb.Date) and args[k]:
1039                         if args[k] is None:
1040                             d = ''
1041                         else:
1042                             d = date.Date(args[k],
1043                                 translator=self._client).local(timezone)
1044                         cell.append('%s: %s'%(self._(k), str(d)))
1045                         if current.has_key(k):
1046                             cell[-1] += ' -> %s' % current[k]
1047                             current[k] = str(d)
1049                     elif isinstance(prop, hyperdb.Interval) and args[k]:
1050                         val = str(date.Interval(args[k],
1051                             translator=self._client))
1052                         cell.append('%s: %s'%(self._(k), val))
1053                         if current.has_key(k):
1054                             cell[-1] += ' -> %s'%current[k]
1055                             current[k] = val
1057                     elif isinstance(prop, hyperdb.String) and args[k]:
1058                         val = cgi.escape(args[k])
1059                         cell.append('%s: %s'%(self._(k), val))
1060                         if current.has_key(k):
1061                             cell[-1] += ' -> %s'%current[k]
1062                             current[k] = val
1064                     elif isinstance(prop, hyperdb.Boolean) and args[k] is not None:
1065                         val = args[k] and ''"Yes" or ''"No"
1066                         cell.append('%s: %s'%(self._(k), val))
1067                         if current.has_key(k):
1068                             cell[-1] += ' -> %s'%current[k]
1069                             current[k] = val
1071                     elif not args[k]:
1072                         if current.has_key(k):
1073                             cell.append('%s: %s'%(self._(k), current[k]))
1074                             current[k] = '(no value)'
1075                         else:
1076                             cell.append(self._('%s: (no value)')%self._(k))
1078                     else:
1079                         cell.append('%s: %s'%(self._(k), str(args[k])))
1080                         if current.has_key(k):
1081                             cell[-1] += ' -> %s'%current[k]
1082                             current[k] = str(args[k])
1084                 arg_s = '<br />'.join(cell)
1085             else:
1086                 # unkown event!!
1087                 comments['unknown'] = self._(
1088                     "<strong><em>This event is not handled"
1089                     " by the history display!</em></strong>")
1090                 arg_s = '<strong><em>' + str(args) + '</em></strong>'
1091             date_s = date_s.replace(' ', '&nbsp;')
1092             # if the user's an itemid, figure the username (older journals
1093             # have the username)
1094             if dre.match(user):
1095                 user = self._db.user.get(user, 'username')
1096             l.append('<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>'%(
1097                 date_s, user, self._(action), arg_s))
1098         if comments:
1099             l.append(self._(
1100                 '<tr><td colspan=4><strong>Note:</strong></td></tr>'))
1101         for entry in comments.values():
1102             l.append('<tr><td colspan=4>%s</td></tr>'%entry)
1104         if direction == 'ascending':
1105             l.reverse()
1107         l[0:0] = ['<table class="history">'
1108              '<tr><th colspan="4" class="header">',
1109              self._('History'),
1110              '</th></tr><tr>',
1111              self._('<th>Date</th>'),
1112              self._('<th>User</th>'),
1113              self._('<th>Action</th>'),
1114              self._('<th>Args</th>'),
1115             '</tr>']
1116         l.append('</table>')
1117         return '\n'.join(l)
1119     def renderQueryForm(self):
1120         """ Render this item, which is a query, as a search form.
1121         """
1122         # create a new request and override the specified args
1123         req = HTMLRequest(self._client)
1124         req.classname = self._klass.get(self._nodeid, 'klass')
1125         name = self._klass.get(self._nodeid, 'name')
1126         req.updateFromURL(self._klass.get(self._nodeid, 'url') +
1127             '&@queryname=%s'%urllib.quote(name))
1129         # new template, using the specified classname and request
1130         pt = self._client.instance.templates.get(req.classname, 'search')
1131         # The context for a search page should be the class, not any
1132         # node.
1133         self._client.nodeid = None
1135         # use our fabricated request
1136         return pt.render(self._client, req.classname, req)
1138     def download_url(self):
1139         """ Assume that this item is a FileClass and that it has a name
1140         and content. Construct a URL for the download of the content.
1141         """
1142         name = self._klass.get(self._nodeid, 'name')
1143         url = '%s%s/%s'%(self._classname, self._nodeid, name)
1144         return urllib.quote(url)
1146     def copy_url(self, exclude=("messages", "files")):
1147         """Construct a URL for creating a copy of this item
1149         "exclude" is an optional list of properties that should
1150         not be copied to the new object.  By default, this list
1151         includes "messages" and "files" properties.  Note that
1152         "id" property cannot be copied.
1154         """
1155         exclude = ("id", "activity", "actor", "creation", "creator") \
1156             + tuple(exclude)
1157         query = {
1158             "@template": "item",
1159             "@note": self._("Copy of %(class)s %(id)s") % {
1160                 "class": self._(self._classname), "id": self._nodeid},
1161         }
1162         for name in self._props.keys():
1163             if name not in exclude:
1164                 query[name] = self[name].plain()
1165         return self._classname + "?" + "&".join(
1166             ["%s=%s" % (key, urllib.quote(value))
1167                 for key, value in query.items()])
1169 class _HTMLUser(_HTMLItem):
1170     """Add ability to check for permissions on users.
1171     """
1172     _marker = []
1173     def hasPermission(self, permission, classname=_marker,
1174             property=None, itemid=None):
1175         """Determine if the user has the Permission.
1177         The class being tested defaults to the template's class, but may
1178         be overidden for this test by suppling an alternate classname.
1179         """
1180         if classname is self._marker:
1181             classname = self._client.classname
1182         return self._db.security.hasPermission(permission,
1183             self._nodeid, classname, property, itemid)
1185     def hasRole(self, rolename):
1186         """Determine whether the user has the Role."""
1187         roles = self._db.user.get(self._nodeid, 'roles').split(',')
1188         for role in roles:
1189             if role.strip() == rolename: return True
1190         return False
1192 def HTMLItem(client, classname, nodeid, anonymous=0):
1193     if classname == 'user':
1194         return _HTMLUser(client, classname, nodeid, anonymous)
1195     else:
1196         return _HTMLItem(client, classname, nodeid, anonymous)
1198 class HTMLProperty(HTMLInputMixin, HTMLPermissions):
1199     """ String, Number, Date, Interval HTMLProperty
1201         Has useful attributes:
1203          _name  the name of the property
1204          _value the value of the property if any
1206         A wrapper object which may be stringified for the plain() behaviour.
1207     """
1208     def __init__(self, client, classname, nodeid, prop, name, value,
1209             anonymous=0):
1210         self._client = client
1211         self._db = client.db
1212         self._ = client._
1213         self._classname = classname
1214         self._nodeid = nodeid
1215         self._prop = prop
1216         self._value = value
1217         self._anonymous = anonymous
1218         self._name = name
1219         if not anonymous:
1220             self._formname = '%s%s@%s'%(classname, nodeid, name)
1221         else:
1222             self._formname = name
1224         # If no value is already present for this property, see if one
1225         # is specified in the current form.
1226         form = self._client.form
1227         if not self._value and form.has_key(self._formname):
1228             if isinstance(prop, hyperdb.Multilink):
1229                 value = lookupIds(self._db, prop,
1230                                   handleListCGIValue(form[self._formname]),
1231                                   fail_ok=1)
1232             elif isinstance(prop, hyperdb.Link):
1233                 value = form.getfirst(self._formname).strip()
1234                 if value:
1235                     value = lookupIds(self._db, prop, [value],
1236                                       fail_ok=1)[0]
1237                 else:
1238                     value = None
1239             else:
1240                 value = form.getfirst(self._formname).strip() or None
1241             self._value = value
1243         HTMLInputMixin.__init__(self)
1245     def __repr__(self):
1246         return '<HTMLProperty(0x%x) %s %r %r>'%(id(self), self._formname,
1247             self._prop, self._value)
1248     def __str__(self):
1249         return self.plain()
1250     def __cmp__(self, other):
1251         if isinstance(other, HTMLProperty):
1252             return cmp(self._value, other._value)
1253         return cmp(self._value, other)
1255     def __nonzero__(self):
1256         return not not self._value
1258     def isset(self):
1259         """Is my _value not None?"""
1260         return self._value is not None
1262     def is_edit_ok(self):
1263         """Should the user be allowed to use an edit form field for this
1264         property. Check "Create" for new items, or "Edit" for existing
1265         ones.
1266         """
1267         if self._nodeid:
1268             return self._db.security.hasPermission('Edit', self._client.userid,
1269                 self._classname, self._name, self._nodeid)
1270         return self._db.security.hasPermission('Create', self._client.userid,
1271             self._classname, self._name) or \
1272             self._db.security.hasPermission('Register', self._client.userid,
1273                                             self._classname, self._name)
1275     def is_view_ok(self):
1276         """ Is the user allowed to View the current class?
1277         """
1278         if self._db.security.hasPermission('View', self._client.userid,
1279                 self._classname, self._name, self._nodeid):
1280             return 1
1281         return self.is_edit_ok()
1283 class StringHTMLProperty(HTMLProperty):
1284     hyper_re = re.compile(r'''(
1285         (?P<url>
1286          (
1287           (ht|f)tp(s?)://                   # protocol
1288           ([\w]+(:\w+)?@)?                  # username/password
1289           ([\w\-]+)                         # hostname
1290           ((\.[\w-]+)+)?                    # .domain.etc
1291          |                                  # ... or ...
1292           ([\w]+(:\w+)?@)?                  # username/password
1293           www\.                             # "www."
1294           ([\w\-]+\.)+                      # hostname
1295           [\w]{2,5}                         # TLD
1296          )
1297          (:[\d]{1,5})?                     # port
1298          (/[\w\-$.+!*(),;:@&=?/~\\#%]*)?   # path etc.
1299         )|
1300         (?P<email>[-+=%/\w\.]+@[\w\.\-]+)|
1301         (?P<item>(?P<class>[A-Za-z_]+)(\s*)(?P<id>\d+))
1302     )''', re.X | re.I)
1303     protocol_re = re.compile('^(ht|f)tp(s?)://', re.I)
1305     def _hyper_repl_item(self,match,replacement):
1306         item = match.group('item')
1307         cls = match.group('class').lower()
1308         id = match.group('id')
1309         try:
1310             # make sure cls is a valid tracker classname
1311             cl = self._db.getclass(cls)
1312             if not cl.hasnode(id):
1313                 return item
1314             return replacement % locals()
1315         except KeyError:
1316             return item
1318     def _hyper_repl(self, match):
1319         if match.group('url'):
1320             u = s = match.group('url')
1321             if not self.protocol_re.search(s):
1322                 u = 'http://' + s
1323             # catch an escaped ">" at the end of the URL
1324             if s.endswith('&gt;'):
1325                 u = s = s[:-4]
1326                 e = '&gt;'
1327             else:
1328                 e = ''
1329             return '<a href="%s">%s</a>%s'%(u, s, e)
1330         elif match.group('email'):
1331             s = match.group('email')
1332             return '<a href="mailto:%s">%s</a>'%(s, s)
1333         else:
1334             return self._hyper_repl_item(match,
1335                 '<a href="%(cls)s%(id)s">%(item)s</a>')
1337     def _hyper_repl_rst(self, match):
1338         if match.group('url'):
1339             s = match.group('url')
1340             return '`%s <%s>`_'%(s, s)
1341         elif match.group('email'):
1342             s = match.group('email')
1343             return '`%s <mailto:%s>`_'%(s, s)
1344         else:
1345             return self._hyper_repl_item(match,'`%(item)s <%(cls)s%(id)s>`_')
1347     def hyperlinked(self):
1348         """ Render a "hyperlinked" version of the text """
1349         return self.plain(hyperlink=1)
1351     def plain(self, escape=0, hyperlink=0):
1352         """Render a "plain" representation of the property
1354         - "escape" turns on/off HTML quoting
1355         - "hyperlink" turns on/off in-text hyperlinking of URLs, email
1356           addresses and designators
1357         """
1358         if not self.is_view_ok():
1359             return self._('[hidden]')
1361         if self._value is None:
1362             return ''
1363         if escape:
1364             s = cgi.escape(str(self._value))
1365         else:
1366             s = str(self._value)
1367         if hyperlink:
1368             # no, we *must* escape this text
1369             if not escape:
1370                 s = cgi.escape(s)
1371             s = self.hyper_re.sub(self._hyper_repl, s)
1372         return s
1374     def wrapped(self, escape=1, hyperlink=1):
1375         """Render a "wrapped" representation of the property.
1377         We wrap long lines at 80 columns on the nearest whitespace. Lines
1378         with no whitespace are not broken to force wrapping.
1380         Note that unlike plain() we default wrapped() to have the escaping
1381         and hyperlinking turned on since that's the most common usage.
1383         - "escape" turns on/off HTML quoting
1384         - "hyperlink" turns on/off in-text hyperlinking of URLs, email
1385           addresses and designators
1386         """
1387         if not self.is_view_ok():
1388             return self._('[hidden]')
1390         if self._value is None:
1391             return ''
1392         s = support.wrap(str(self._value), width=80)
1393         if escape:
1394             s = cgi.escape(s)
1395         if hyperlink:
1396             # no, we *must* escape this text
1397             if not escape:
1398                 s = cgi.escape(s)
1399             s = self.hyper_re.sub(self._hyper_repl, s)
1400         return s
1402     def stext(self, escape=0, hyperlink=1):
1403         """ Render the value of the property as StructuredText.
1405             This requires the StructureText module to be installed separately.
1406         """
1407         if not self.is_view_ok():
1408             return self._('[hidden]')
1410         s = self.plain(escape=escape, hyperlink=hyperlink)
1411         if not StructuredText:
1412             return s
1413         return StructuredText(s,level=1,header=0)
1415     def rst(self, hyperlink=1):
1416         """ Render the value of the property as ReStructuredText.
1418             This requires docutils to be installed separately.
1419         """
1420         if not self.is_view_ok():
1421             return self._('[hidden]')
1423         if not ReStructuredText:
1424             return self.plain(escape=0, hyperlink=hyperlink)
1425         s = self.plain(escape=0, hyperlink=0)
1426         if hyperlink:
1427             s = self.hyper_re.sub(self._hyper_repl_rst, s)
1428         return ReStructuredText(s, writer_name="html")["html_body"].encode("utf-8",
1429             "replace")
1431     def field(self, **kwargs):
1432         """ Render the property as a field in HTML.
1434             If not editable, just display the value via plain().
1435         """
1436         if not self.is_edit_ok():
1437             return self.plain(escape=1)
1439         value = self._value
1440         if value is None:
1441             value = ''
1443         kwargs.setdefault("size", 30)
1444         kwargs.update({"name": self._formname, "value": value})
1445         return self.input(**kwargs)
1447     def multiline(self, escape=0, rows=5, cols=40, **kwargs):
1448         """ Render a multiline form edit field for the property.
1450             If not editable, just display the plain() value in a <pre> tag.
1451         """
1452         if not self.is_edit_ok():
1453             return '<pre>%s</pre>'%self.plain()
1455         if self._value is None:
1456             value = ''
1457         else:
1458             value = cgi.escape(str(self._value))
1460             value = '&quot;'.join(value.split('"'))
1461         name = self._formname
1462         passthrough_args = ' '.join(['%s="%s"' % (k, cgi.escape(str(v), True))
1463             for k,v in kwargs.items()])
1464         return ('<textarea %(passthrough_args)s name="%(name)s" id="%(name)s"'
1465                 ' rows="%(rows)s" cols="%(cols)s">'
1466                  '%(value)s</textarea>') % locals()
1468     def email(self, escape=1):
1469         """ Render the value of the property as an obscured email address
1470         """
1471         if not self.is_view_ok():
1472             return self._('[hidden]')
1474         if self._value is None:
1475             value = ''
1476         else:
1477             value = str(self._value)
1478         split = value.split('@')
1479         if len(split) == 2:
1480             name, domain = split
1481             domain = ' '.join(domain.split('.')[:-1])
1482             name = name.replace('.', ' ')
1483             value = '%s at %s ...'%(name, domain)
1484         else:
1485             value = value.replace('.', ' ')
1486         if escape:
1487             value = cgi.escape(value)
1488         return value
1490 class PasswordHTMLProperty(HTMLProperty):
1491     def plain(self, escape=0):
1492         """ Render a "plain" representation of the property
1493         """
1494         if not self.is_view_ok():
1495             return self._('[hidden]')
1497         if self._value is None:
1498             return ''
1499         return self._('*encrypted*')
1501     def field(self, size=30):
1502         """ Render a form edit field for the property.
1504             If not editable, just display the value via plain().
1505         """
1506         if not self.is_edit_ok():
1507             return self.plain(escape=1)
1509         return self.input(type="password", name=self._formname, size=size)
1511     def confirm(self, size=30):
1512         """ Render a second form edit field for the property, used for
1513             confirmation that the user typed the password correctly. Generates
1514             a field with name "@confirm@name".
1516             If not editable, display nothing.
1517         """
1518         if not self.is_edit_ok():
1519             return ''
1521         return self.input(type="password",
1522             name="@confirm@%s"%self._formname,
1523             id="%s-confirm"%self._formname,
1524             size=size)
1526 class NumberHTMLProperty(HTMLProperty):
1527     def plain(self, escape=0):
1528         """ Render a "plain" representation of the property
1529         """
1530         if not self.is_view_ok():
1531             return self._('[hidden]')
1533         if self._value is None:
1534             return ''
1536         return str(self._value)
1538     def field(self, size=30):
1539         """ Render a form edit field for the property.
1541             If not editable, just display the value via plain().
1542         """
1543         if not self.is_edit_ok():
1544             return self.plain(escape=1)
1546         value = self._value
1547         if value is None:
1548             value = ''
1550         return self.input(name=self._formname, value=value, size=size)
1552     def __int__(self):
1553         """ Return an int of me
1554         """
1555         return int(self._value)
1557     def __float__(self):
1558         """ Return a float of me
1559         """
1560         return float(self._value)
1563 class BooleanHTMLProperty(HTMLProperty):
1564     def plain(self, escape=0):
1565         """ Render a "plain" representation of the property
1566         """
1567         if not self.is_view_ok():
1568             return self._('[hidden]')
1570         if self._value is None:
1571             return ''
1572         return self._value and self._("Yes") or self._("No")
1574     def field(self):
1575         """ Render a form edit field for the property
1577             If not editable, just display the value via plain().
1578         """
1579         if not self.is_edit_ok():
1580             return self.plain(escape=1)
1582         value = self._value
1583         if isinstance(value, str) or isinstance(value, unicode):
1584             value = value.strip().lower() in ('checked', 'yes', 'true',
1585                 'on', '1')
1587         checked = value and "checked" or ""
1588         if value:
1589             s = self.input(type="radio", name=self._formname, value="yes",
1590                 checked="checked")
1591             s += self._('Yes')
1592             s +=self.input(type="radio", name=self._formname, value="no")
1593             s += self._('No')
1594         else:
1595             s = self.input(type="radio", name=self._formname, value="yes")
1596             s += self._('Yes')
1597             s +=self.input(type="radio", name=self._formname, value="no",
1598                 checked="checked")
1599             s += self._('No')
1600         return s
1602 class DateHTMLProperty(HTMLProperty):
1604     _marker = []
1606     def __init__(self, client, classname, nodeid, prop, name, value,
1607             anonymous=0, offset=None):
1608         HTMLProperty.__init__(self, client, classname, nodeid, prop, name,
1609                 value, anonymous=anonymous)
1610         if self._value and not (isinstance(self._value, str) or
1611                 isinstance(self._value, unicode)):
1612             self._value.setTranslator(self._client.translator)
1613         self._offset = offset
1614         if self._offset is None :
1615             self._offset = self._prop.offset (self._db)
1617     def plain(self, escape=0):
1618         """ Render a "plain" representation of the property
1619         """
1620         if not self.is_view_ok():
1621             return self._('[hidden]')
1623         if self._value is None:
1624             return ''
1625         if self._offset is None:
1626             offset = self._db.getUserTimezone()
1627         else:
1628             offset = self._offset
1629         return str(self._value.local(offset))
1631     def now(self, str_interval=None):
1632         """ Return the current time.
1634             This is useful for defaulting a new value. Returns a
1635             DateHTMLProperty.
1636         """
1637         if not self.is_view_ok():
1638             return self._('[hidden]')
1640         ret = date.Date('.', translator=self._client)
1642         if isinstance(str_interval, basestring):
1643             sign = 1
1644             if str_interval[0] == '-':
1645                 sign = -1
1646                 str_interval = str_interval[1:]
1647             interval = date.Interval(str_interval, translator=self._client)
1648             if sign > 0:
1649                 ret = ret + interval
1650             else:
1651                 ret = ret - interval
1653         return DateHTMLProperty(self._client, self._classname, self._nodeid,
1654             self._prop, self._formname, ret)
1656     def field(self, size=30, default=None, format=_marker, popcal=True):
1657         """Render a form edit field for the property
1659         If not editable, just display the value via plain().
1661         If "popcal" then include the Javascript calendar editor.
1662         Default=yes.
1664         The format string is a standard python strftime format string.
1665         """
1666         if not self.is_edit_ok():
1667             if format is self._marker:
1668                 return self.plain(escape=1)
1669             else:
1670                 return self.pretty(format)
1672         value = self._value
1674         if value is None:
1675             if default is None:
1676                 raw_value = None
1677             else:
1678                 if isinstance(default, basestring):
1679                     raw_value = date.Date(default, translator=self._client)
1680                 elif isinstance(default, date.Date):
1681                     raw_value = default
1682                 elif isinstance(default, DateHTMLProperty):
1683                     raw_value = default._value
1684                 else:
1685                     raise ValueError, self._('default value for '
1686                         'DateHTMLProperty must be either DateHTMLProperty '
1687                         'or string date representation.')
1688         elif isinstance(value, str) or isinstance(value, unicode):
1689             # most likely erroneous input to be passed back to user
1690             if isinstance(value, unicode): value = value.encode('utf8')
1691             return self.input(name=self._formname, value=value, size=size)
1692         else:
1693             raw_value = value
1695         if raw_value is None:
1696             value = ''
1697         elif isinstance(raw_value, str) or isinstance(raw_value, unicode):
1698             if format is self._marker:
1699                 value = raw_value
1700             else:
1701                 value = date.Date(raw_value).pretty(format)
1702         else:
1703             if self._offset is None :
1704                 offset = self._db.getUserTimezone()
1705             else :
1706                 offset = self._offset
1707             value = raw_value.local(offset)
1708             if format is not self._marker:
1709                 value = value.pretty(format)
1711         s = self.input(name=self._formname, value=value, size=size)
1712         if popcal:
1713             s += self.popcal()
1714         return s
1716     def reldate(self, pretty=1):
1717         """ Render the interval between the date and now.
1719             If the "pretty" flag is true, then make the display pretty.
1720         """
1721         if not self.is_view_ok():
1722             return self._('[hidden]')
1724         if not self._value:
1725             return ''
1727         # figure the interval
1728         interval = self._value - date.Date('.', translator=self._client)
1729         if pretty:
1730             return interval.pretty()
1731         return str(interval)
1733     def pretty(self, format=_marker):
1734         """ Render the date in a pretty format (eg. month names, spaces).
1736             The format string is a standard python strftime format string.
1737             Note that if the day is zero, and appears at the start of the
1738             string, then it'll be stripped from the output. This is handy
1739             for the situation when a date only specifies a month and a year.
1740         """
1741         if not self.is_view_ok():
1742             return self._('[hidden]')
1744         if self._offset is None:
1745             offset = self._db.getUserTimezone()
1746         else:
1747             offset = self._offset
1749         if not self._value:
1750             return ''
1751         elif format is not self._marker:
1752             return self._value.local(offset).pretty(format)
1753         else:
1754             return self._value.local(offset).pretty()
1756     def local(self, offset):
1757         """ Return the date/time as a local (timezone offset) date/time.
1758         """
1759         if not self.is_view_ok():
1760             return self._('[hidden]')
1762         return DateHTMLProperty(self._client, self._classname, self._nodeid,
1763             self._prop, self._formname, self._value, offset=offset)
1765     def popcal(self, width=300, height=200, label="(cal)",
1766             form="itemSynopsis"):
1767         """Generate a link to a calendar pop-up window.
1769         item: HTMLProperty e.g.: context.deadline
1770         """
1771         if self.isset():
1772             date = "&date=%s"%self._value
1773         else :
1774             date = ""
1775         return ('<a class="classhelp" href="javascript:help_window('
1776             "'%s?@template=calendar&amp;property=%s&amp;form=%s%s', %d, %d)"
1777             '">%s</a>'%(self._classname, self._name, form, date, width,
1778             height, label))
1780 class IntervalHTMLProperty(HTMLProperty):
1781     def __init__(self, client, classname, nodeid, prop, name, value,
1782             anonymous=0):
1783         HTMLProperty.__init__(self, client, classname, nodeid, prop,
1784             name, value, anonymous)
1785         if self._value and not isinstance(self._value, (str, unicode)):
1786             self._value.setTranslator(self._client.translator)
1788     def plain(self, escape=0):
1789         """ Render a "plain" representation of the property
1790         """
1791         if not self.is_view_ok():
1792             return self._('[hidden]')
1794         if self._value is None:
1795             return ''
1796         return str(self._value)
1798     def pretty(self):
1799         """ Render the interval in a pretty format (eg. "yesterday")
1800         """
1801         if not self.is_view_ok():
1802             return self._('[hidden]')
1804         return self._value.pretty()
1806     def field(self, size=30):
1807         """ Render a form edit field for the property
1809             If not editable, just display the value via plain().
1810         """
1811         if not self.is_edit_ok():
1812             return self.plain(escape=1)
1814         value = self._value
1815         if value is None:
1816             value = ''
1818         return self.input(name=self._formname, value=value, size=size)
1820 class LinkHTMLProperty(HTMLProperty):
1821     """ Link HTMLProperty
1822         Include the above as well as being able to access the class
1823         information. Stringifying the object itself results in the value
1824         from the item being displayed. Accessing attributes of this object
1825         result in the appropriate entry from the class being queried for the
1826         property accessed (so item/assignedto/name would look up the user
1827         entry identified by the assignedto property on item, and then the
1828         name property of that user)
1829     """
1830     def __init__(self, *args, **kw):
1831         HTMLProperty.__init__(self, *args, **kw)
1832         # if we're representing a form value, then the -1 from the form really
1833         # should be a None
1834         if str(self._value) == '-1':
1835             self._value = None
1837     def __getattr__(self, attr):
1838         """ return a new HTMLItem """
1839         if not self._value:
1840             # handle a special page templates lookup
1841             if attr == '__render_with_namespace__':
1842                 def nothing(*args, **kw):
1843                     return ''
1844                 return nothing
1845             msg = self._('Attempt to look up %(attr)s on a missing value')
1846             return MissingValue(msg%locals())
1847         i = HTMLItem(self._client, self._prop.classname, self._value)
1848         return getattr(i, attr)
1850     def plain(self, escape=0):
1851         """ Render a "plain" representation of the property
1852         """
1853         if not self.is_view_ok():
1854             return self._('[hidden]')
1856         if self._value is None:
1857             return ''
1858         linkcl = self._db.classes[self._prop.classname]
1859         k = linkcl.labelprop(1)
1860         if num_re.match(self._value):
1861             try:
1862                 value = str(linkcl.get(self._value, k))
1863             except IndexError:
1864                 value = self._value
1865         else :
1866             value = self._value
1867         if escape:
1868             value = cgi.escape(value)
1869         return value
1871     def field(self, showid=0, size=None):
1872         """ Render a form edit field for the property
1874             If not editable, just display the value via plain().
1875         """
1876         if not self.is_edit_ok():
1877             return self.plain(escape=1)
1879         # edit field
1880         linkcl = self._db.getclass(self._prop.classname)
1881         if self._value is None:
1882             value = ''
1883         else:
1884             k = linkcl.getkey()
1885             if k and num_re.match(self._value):
1886                 value = linkcl.get(self._value, k)
1887             else:
1888                 value = self._value
1889         return self.input(name=self._formname, value=value, size=size)
1891     def menu(self, size=None, height=None, showid=0, additional=[], value=None,
1892             sort_on=None, **conditions):
1893         """ Render a form select list for this property
1895             "size" is used to limit the length of the list labels
1896             "height" is used to set the <select> tag's "size" attribute
1897             "showid" includes the item ids in the list labels
1898             "value" specifies which item is pre-selected
1899             "additional" lists properties which should be included in the
1900                 label
1901             "sort_on" indicates the property to sort the list on as
1902                 (direction, property) where direction is '+' or '-'. A
1903                 single string with the direction prepended may be used.
1904                 For example: ('-', 'order'), '+name'.
1906             The remaining keyword arguments are used as conditions for
1907             filtering the items in the list - they're passed as the
1908             "filterspec" argument to a Class.filter() call.
1910             If not editable, just display the value via plain().
1911         """
1912         if not self.is_edit_ok():
1913             return self.plain(escape=1)
1915         # Since None indicates the default, we need another way to
1916         # indicate "no selection".  We use -1 for this purpose, as
1917         # that is the value we use when submitting a form without the
1918         # value set.
1919         if value is None:
1920             value = self._value
1921         elif value == '-1':
1922             value = None
1924         linkcl = self._db.getclass(self._prop.classname)
1925         l = ['<select name="%s">'%self._formname]
1926         k = linkcl.labelprop(1)
1927         s = ''
1928         if value is None:
1929             s = 'selected="selected" '
1930         l.append(self._('<option %svalue="-1">- no selection -</option>')%s)
1932         if sort_on is not None:
1933             if not isinstance(sort_on, tuple):
1934                 if sort_on[0] in '+-':
1935                     sort_on = (sort_on[0], sort_on[1:])
1936                 else:
1937                     sort_on = ('+', sort_on)
1938         else:
1939             sort_on = ('+', linkcl.orderprop())
1941         options = [opt
1942             for opt in linkcl.filter(None, conditions, sort_on, (None, None))
1943             if self._db.security.hasPermission("View", self._client.userid,
1944                 linkcl.classname, itemid=opt)]
1946         # make sure we list the current value if it's retired
1947         if value and value not in options:
1948             options.insert(0, value)
1950         if additional:
1951             additional_fns = []
1952             props = linkcl.getprops()
1953             for propname in additional:
1954                 prop = props[propname]
1955                 if isinstance(prop, hyperdb.Link):
1956                     cl = self._db.getclass(prop.classname)
1957                     labelprop = cl.labelprop()
1958                     fn = lambda optionid: cl.get(linkcl.get(optionid,
1959                                                             propname),
1960                                                  labelprop)
1961                 else:
1962                     fn = lambda optionid: linkcl.get(optionid, propname)
1963             additional_fns.append(fn)
1965         for optionid in options:
1966             # get the option value, and if it's None use an empty string
1967             option = linkcl.get(optionid, k) or ''
1969             # figure if this option is selected
1970             s = ''
1971             if value in [optionid, option]:
1972                 s = 'selected="selected" '
1974             # figure the label
1975             if showid:
1976                 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1977             elif not option:
1978                 lab = '%s%s'%(self._prop.classname, optionid)
1979             else:
1980                 lab = option
1982             # truncate if it's too long
1983             if size is not None and len(lab) > size:
1984                 lab = lab[:size-3] + '...'
1985             if additional:
1986                 m = []
1987                 for fn in additional_fns:
1988                     m.append(str(fn(optionid)))
1989                 lab = lab + ' (%s)'%', '.join(m)
1991             # and generate
1992             lab = cgi.escape(self._(lab))
1993             l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
1994         l.append('</select>')
1995         return '\n'.join(l)
1996 #    def checklist(self, ...)
2000 class MultilinkHTMLProperty(HTMLProperty):
2001     """ Multilink HTMLProperty
2003         Also be iterable, returning a wrapper object like the Link case for
2004         each entry in the multilink.
2005     """
2006     def __init__(self, *args, **kwargs):
2007         HTMLProperty.__init__(self, *args, **kwargs)
2008         if self._value:
2009             display_value = lookupIds(self._db, self._prop, self._value,
2010                 fail_ok=1, do_lookup=False)
2011             sortfun = make_sort_function(self._db, self._prop.classname)
2012             # sorting fails if the value contains
2013             # items not yet stored in the database
2014             # ignore these errors to preserve user input
2015             try:
2016                 display_value.sort(sortfun)
2017             except:
2018                 pass
2019             self._value = display_value
2021     def __len__(self):
2022         """ length of the multilink """
2023         return len(self._value)
2025     def __getattr__(self, attr):
2026         """ no extended attribute accesses make sense here """
2027         raise AttributeError, attr
2029     def viewableGenerator(self, values):
2030         """Used to iterate over only the View'able items in a class."""
2031         check = self._db.security.hasPermission
2032         userid = self._client.userid
2033         classname = self._prop.classname
2034         for value in values:
2035             if check('View', userid, classname, itemid=value):
2036                 yield HTMLItem(self._client, classname, value)
2038     def __iter__(self):
2039         """ iterate and return a new HTMLItem
2040         """
2041         return self.viewableGenerator(self._value)
2043     def reverse(self):
2044         """ return the list in reverse order
2045         """
2046         l = self._value[:]
2047         l.reverse()
2048         return self.viewableGenerator(l)
2050     def sorted(self, property):
2051         """ Return this multilink sorted by the given property """
2052         value = list(self.__iter__())
2053         value.sort(lambda a,b:cmp(a[property], b[property]))
2054         return value
2056     def __contains__(self, value):
2057         """ Support the "in" operator. We have to make sure the passed-in
2058             value is a string first, not a HTMLProperty.
2059         """
2060         return str(value) in self._value
2062     def isset(self):
2063         """Is my _value not []?"""
2064         return self._value != []
2066     def plain(self, escape=0):
2067         """ Render a "plain" representation of the property
2068         """
2069         if not self.is_view_ok():
2070             return self._('[hidden]')
2072         linkcl = self._db.classes[self._prop.classname]
2073         k = linkcl.labelprop(1)
2074         labels = []
2075         for v in self._value:
2076             if num_re.match(v):
2077                 try:
2078                     label = linkcl.get(v, k)
2079                 except IndexError:
2080                     label = None
2081                 # fall back to designator if label is None
2082                 if label is None: label = '%s%s'%(self._prop.classname, k)
2083             else:
2084                 label = v
2085             labels.append(label)
2086         value = ', '.join(labels)
2087         if escape:
2088             value = cgi.escape(value)
2089         return value
2091     def field(self, size=30, showid=0):
2092         """ Render a form edit field for the property
2094             If not editable, just display the value via plain().
2095         """
2096         if not self.is_edit_ok():
2097             return self.plain(escape=1)
2099         linkcl = self._db.getclass(self._prop.classname)
2100         value = self._value[:]
2101         # map the id to the label property
2102         if not linkcl.getkey():
2103             showid=1
2104         if not showid:
2105             k = linkcl.labelprop(1)
2106             value = lookupKeys(linkcl, k, value)
2107         value = ','.join(value)
2108         return self.input(name=self._formname, size=size, value=value)
2110     def menu(self, size=None, height=None, showid=0, additional=[],
2111              value=None, sort_on=None, **conditions):
2112         """ Render a form <select> list for this property.
2114             "size" is used to limit the length of the list labels
2115             "height" is used to set the <select> tag's "size" attribute
2116             "showid" includes the item ids in the list labels
2117             "additional" lists properties which should be included in the
2118                 label
2119             "value" specifies which item is pre-selected
2120             "sort_on" indicates the property to sort the list on as
2121                 (direction, property) where direction is '+' or '-'. A
2122                 single string with the direction prepended may be used.
2123                 For example: ('-', 'order'), '+name'.
2125             The remaining keyword arguments are used as conditions for
2126             filtering the items in the list - they're passed as the
2127             "filterspec" argument to a Class.filter() call.
2129             If not editable, just display the value via plain().
2130         """
2131         if not self.is_edit_ok():
2132             return self.plain(escape=1)
2134         if value is None:
2135             value = self._value
2137         linkcl = self._db.getclass(self._prop.classname)
2139         if sort_on is not None:
2140             if not isinstance(sort_on, tuple):
2141                 if sort_on[0] in '+-':
2142                     sort_on = (sort_on[0], sort_on[1:])
2143                 else:
2144                     sort_on = ('+', sort_on)
2145         else:
2146             sort_on = ('+', linkcl.orderprop())
2148         options = [opt
2149             for opt in linkcl.filter(None, conditions, sort_on)
2150             if self._db.security.hasPermission("View", self._client.userid,
2151                 linkcl.classname, itemid=opt)]
2153         # make sure we list the current values if they're retired
2154         for val in value:
2155             if val not in options:
2156                 options.insert(0, val)
2158         if not height:
2159             height = len(options)
2160             if value:
2161                 # The "no selection" option.
2162                 height += 1
2163             height = min(height, 7)
2164         l = ['<select multiple name="%s" size="%s">'%(self._formname, height)]
2165         k = linkcl.labelprop(1)
2167         if value:
2168             l.append('<option value="%s">- no selection -</option>'
2169                      % ','.join(['-' + v for v in value]))
2171         if additional:
2172             additional_fns = []
2173             props = linkcl.getprops()
2174             for propname in additional:
2175                 prop = props[propname]
2176                 if isinstance(prop, hyperdb.Link):
2177                     cl = self._db.getclass(prop.classname)
2178                     labelprop = cl.labelprop()
2179                     fn = lambda optionid: cl.get(linkcl.get(optionid,
2180                                                             propname),
2181                                                  labelprop)
2182                 else:
2183                     fn = lambda optionid: linkcl.get(optionid, propname)
2184             additional_fns.append(fn)
2186         for optionid in options:
2187             # get the option value, and if it's None use an empty string
2188             option = linkcl.get(optionid, k) or ''
2190             # figure if this option is selected
2191             s = ''
2192             if optionid in value or option in value:
2193                 s = 'selected="selected" '
2195             # figure the label
2196             if showid:
2197                 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
2198             else:
2199                 lab = option
2200             # truncate if it's too long
2201             if size is not None and len(lab) > size:
2202                 lab = lab[:size-3] + '...'
2203             if additional:
2204                 m = []
2205                 for fn in additional_fns:
2206                     m.append(str(fn(optionid)))
2207                 lab = lab + ' (%s)'%', '.join(m)
2209             # and generate
2210             lab = cgi.escape(self._(lab))
2211             l.append('<option %svalue="%s">%s</option>'%(s, optionid,
2212                 lab))
2213         l.append('</select>')
2214         return '\n'.join(l)
2216 # set the propclasses for HTMLItem
2217 propclasses = (
2218     (hyperdb.String, StringHTMLProperty),
2219     (hyperdb.Number, NumberHTMLProperty),
2220     (hyperdb.Boolean, BooleanHTMLProperty),
2221     (hyperdb.Date, DateHTMLProperty),
2222     (hyperdb.Interval, IntervalHTMLProperty),
2223     (hyperdb.Password, PasswordHTMLProperty),
2224     (hyperdb.Link, LinkHTMLProperty),
2225     (hyperdb.Multilink, MultilinkHTMLProperty),
2228 def make_sort_function(db, classname, sort_on=None):
2229     """Make a sort function for a given class
2230     """
2231     linkcl = db.getclass(classname)
2232     if sort_on is None:
2233         sort_on = linkcl.orderprop()
2234     def sortfunc(a, b):
2235         return cmp(linkcl.get(a, sort_on), linkcl.get(b, sort_on))
2236     return sortfunc
2238 def handleListCGIValue(value):
2239     """ Value is either a single item or a list of items. Each item has a
2240         .value that we're actually interested in.
2241     """
2242     if isinstance(value, type([])):
2243         return [value.value for value in value]
2244     else:
2245         value = value.value.strip()
2246         if not value:
2247             return []
2248         return [v.strip() for v in value.split(',')]
2250 class HTMLRequest(HTMLInputMixin):
2251     """The *request*, holding the CGI form and environment.
2253     - "form" the CGI form as a cgi.FieldStorage
2254     - "env" the CGI environment variables
2255     - "base" the base URL for this instance
2256     - "user" a HTMLItem instance for this user
2257     - "language" as determined by the browser or config
2258     - "classname" the current classname (possibly None)
2259     - "template" the current template (suffix, also possibly None)
2261     Index args:
2263     - "columns" dictionary of the columns to display in an index page
2264     - "show" a convenience access to columns - request/show/colname will
2265       be true if the columns should be displayed, false otherwise
2266     - "sort" index sort column (direction, column name)
2267     - "group" index grouping property (direction, column name)
2268     - "filter" properties to filter the index on
2269     - "filterspec" values to filter the index on
2270     - "search_text" text to perform a full-text search on for an index
2271     """
2272     def __repr__(self):
2273         return '<HTMLRequest %r>'%self.__dict__
2275     def __init__(self, client):
2276         # _client is needed by HTMLInputMixin
2277         self._client = self.client = client
2279         # easier access vars
2280         self.form = client.form
2281         self.env = client.env
2282         self.base = client.base
2283         self.user = HTMLItem(client, 'user', client.userid)
2284         self.language = client.language
2286         # store the current class name and action
2287         self.classname = client.classname
2288         self.nodeid = client.nodeid
2289         self.template = client.template
2291         # the special char to use for special vars
2292         self.special_char = '@'
2294         HTMLInputMixin.__init__(self)
2296         self._post_init()
2298     def current_url(self):
2299         url = self.base
2300         if self.classname:
2301             url += self.classname
2302             if self.nodeid:
2303                 url += self.nodeid
2304         args = {}
2305         if self.template:
2306             args['@template'] = self.template
2307         return self.indexargs_url(url, args)
2309     def _parse_sort(self, var, name):
2310         """ Parse sort/group options. Append to var
2311         """
2312         fields = []
2313         dirs = []
2314         for special in '@:':
2315             idx = 0
2316             key = '%s%s%d'%(special, name, idx)
2317             while key in self.form:
2318                 self.special_char = special
2319                 fields.append(self.form.getfirst(key))
2320                 dirkey = '%s%sdir%d'%(special, name, idx)
2321                 if dirkey in self.form:
2322                     dirs.append(self.form.getfirst(dirkey))
2323                 else:
2324                     dirs.append(None)
2325                 idx += 1
2326                 key = '%s%s%d'%(special, name, idx)
2327             # backward compatible (and query) URL format
2328             key = special + name
2329             dirkey = key + 'dir'
2330             if key in self.form and not fields:
2331                 fields = handleListCGIValue(self.form[key])
2332                 if dirkey in self.form:
2333                     dirs.append(self.form.getfirst(dirkey))
2334             if fields: # only try other special char if nothing found
2335                 break
2336         for f, d in map(None, fields, dirs):
2337             if f.startswith('-'):
2338                 var.append(('-', f[1:]))
2339             elif d:
2340                 var.append(('-', f))
2341             else:
2342                 var.append(('+', f))
2344     def _post_init(self):
2345         """ Set attributes based on self.form
2346         """
2347         # extract the index display information from the form
2348         self.columns = []
2349         for name in ':columns @columns'.split():
2350             if self.form.has_key(name):
2351                 self.special_char = name[0]
2352                 self.columns = handleListCGIValue(self.form[name])
2353                 break
2354         self.show = support.TruthDict(self.columns)
2356         # sorting and grouping
2357         self.sort = []
2358         self.group = []
2359         self._parse_sort(self.sort, 'sort')
2360         self._parse_sort(self.group, 'group')
2362         # filtering
2363         self.filter = []
2364         for name in ':filter @filter'.split():
2365             if self.form.has_key(name):
2366                 self.special_char = name[0]
2367                 self.filter = handleListCGIValue(self.form[name])
2369         self.filterspec = {}
2370         db = self.client.db
2371         if self.classname is not None:
2372             cls = db.getclass (self.classname)
2373             for name in self.filter:
2374                 if not self.form.has_key(name):
2375                     continue
2376                 prop = cls.get_transitive_prop (name)
2377                 fv = self.form[name]
2378                 if (isinstance(prop, hyperdb.Link) or
2379                         isinstance(prop, hyperdb.Multilink)):
2380                     self.filterspec[name] = lookupIds(db, prop,
2381                         handleListCGIValue(fv))
2382                 else:
2383                     if isinstance(fv, type([])):
2384                         self.filterspec[name] = [v.value for v in fv]
2385                     elif name == 'id':
2386                         # special case "id" property
2387                         self.filterspec[name] = handleListCGIValue(fv)
2388                     else:
2389                         self.filterspec[name] = fv.value
2391         # full-text search argument
2392         self.search_text = None
2393         for name in ':search_text @search_text'.split():
2394             if self.form.has_key(name):
2395                 self.special_char = name[0]
2396                 self.search_text = self.form.getfirst(name)
2398         # pagination - size and start index
2399         # figure batch args
2400         self.pagesize = 50
2401         for name in ':pagesize @pagesize'.split():
2402             if self.form.has_key(name):
2403                 self.special_char = name[0]
2404                 try:
2405                     self.pagesize = int(self.form.getfirst(name))
2406                 except ValueError:
2407                     # not an integer - ignore
2408                     pass
2410         self.startwith = 0
2411         for name in ':startwith @startwith'.split():
2412             if self.form.has_key(name):
2413                 self.special_char = name[0]
2414                 try:
2415                     self.startwith = int(self.form.getfirst(name))
2416                 except ValueError:
2417                     # not an integer - ignore
2418                     pass
2420         # dispname
2421         if self.form.has_key('@dispname'):
2422             self.dispname = self.form.getfirst('@dispname')
2423         else:
2424             self.dispname = None
2426     def updateFromURL(self, url):
2427         """ Parse the URL for query args, and update my attributes using the
2428             values.
2429         """
2430         env = {'QUERY_STRING': url}
2431         self.form = cgi.FieldStorage(environ=env)
2433         self._post_init()
2435     def update(self, kwargs):
2436         """ Update my attributes using the keyword args
2437         """
2438         self.__dict__.update(kwargs)
2439         if kwargs.has_key('columns'):
2440             self.show = support.TruthDict(self.columns)
2442     def description(self):
2443         """ Return a description of the request - handle for the page title.
2444         """
2445         s = [self.client.db.config.TRACKER_NAME]
2446         if self.classname:
2447             if self.client.nodeid:
2448                 s.append('- %s%s'%(self.classname, self.client.nodeid))
2449             else:
2450                 if self.template == 'item':
2451                     s.append('- new %s'%self.classname)
2452                 elif self.template == 'index':
2453                     s.append('- %s index'%self.classname)
2454                 else:
2455                     s.append('- %s %s'%(self.classname, self.template))
2456         else:
2457             s.append('- home')
2458         return ' '.join(s)
2460     def __str__(self):
2461         d = {}
2462         d.update(self.__dict__)
2463         f = ''
2464         for k in self.form.keys():
2465             f += '\n      %r=%r'%(k,handleListCGIValue(self.form[k]))
2466         d['form'] = f
2467         e = ''
2468         for k,v in self.env.items():
2469             e += '\n     %r=%r'%(k, v)
2470         d['env'] = e
2471         return """
2472 form: %(form)s
2473 base: %(base)r
2474 classname: %(classname)r
2475 template: %(template)r
2476 columns: %(columns)r
2477 sort: %(sort)r
2478 group: %(group)r
2479 filter: %(filter)r
2480 search_text: %(search_text)r
2481 pagesize: %(pagesize)r
2482 startwith: %(startwith)r
2483 env: %(env)s
2484 """%d
2486     def indexargs_form(self, columns=1, sort=1, group=1, filter=1,
2487             filterspec=1, search_text=1):
2488         """ return the current index args as form elements """
2489         l = []
2490         sc = self.special_char
2491         def add(k, v):
2492             l.append(self.input(type="hidden", name=k, value=v))
2493         if columns and self.columns:
2494             add(sc+'columns', ','.join(self.columns))
2495         if sort:
2496             val = []
2497             for dir, attr in self.sort:
2498                 if dir == '-':
2499                     val.append('-'+attr)
2500                 else:
2501                     val.append(attr)
2502             add(sc+'sort', ','.join (val))
2503         if group:
2504             val = []
2505             for dir, attr in self.group:
2506                 if dir == '-':
2507                     val.append('-'+attr)
2508                 else:
2509                     val.append(attr)
2510             add(sc+'group', ','.join (val))
2511         if filter and self.filter:
2512             add(sc+'filter', ','.join(self.filter))
2513         if self.classname and filterspec:
2514             cls = self.client.db.getclass(self.classname)
2515             for k,v in self.filterspec.items():
2516                 if type(v) == type([]):
2517                     if isinstance(cls.get_transitive_prop(k), hyperdb.String):
2518                         add(k, ' '.join(v))
2519                     else:
2520                         add(k, ','.join(v))
2521                 else:
2522                     add(k, v)
2523         if search_text and self.search_text:
2524             add(sc+'search_text', self.search_text)
2525         add(sc+'pagesize', self.pagesize)
2526         add(sc+'startwith', self.startwith)
2527         return '\n'.join(l)
2529     def indexargs_url(self, url, args):
2530         """ Embed the current index args in a URL
2531         """
2532         q = urllib.quote
2533         sc = self.special_char
2534         l = ['%s=%s'%(k,v) for k,v in args.items()]
2536         # pull out the special values (prefixed by @ or :)
2537         specials = {}
2538         for key in args.keys():
2539             if key[0] in '@:':
2540                 specials[key[1:]] = args[key]
2542         # ok, now handle the specials we received in the request
2543         if self.columns and not specials.has_key('columns'):
2544             l.append(sc+'columns=%s'%(','.join(self.columns)))
2545         if self.sort and not specials.has_key('sort'):
2546             val = []
2547             for dir, attr in self.sort:
2548                 if dir == '-':
2549                     val.append('-'+attr)
2550                 else:
2551                     val.append(attr)
2552             l.append(sc+'sort=%s'%(','.join(val)))
2553         if self.group and not specials.has_key('group'):
2554             val = []
2555             for dir, attr in self.group:
2556                 if dir == '-':
2557                     val.append('-'+attr)
2558                 else:
2559                     val.append(attr)
2560             l.append(sc+'group=%s'%(','.join(val)))
2561         if self.filter and not specials.has_key('filter'):
2562             l.append(sc+'filter=%s'%(','.join(self.filter)))
2563         if self.search_text and not specials.has_key('search_text'):
2564             l.append(sc+'search_text=%s'%q(self.search_text))
2565         if not specials.has_key('pagesize'):
2566             l.append(sc+'pagesize=%s'%self.pagesize)
2567         if not specials.has_key('startwith'):
2568             l.append(sc+'startwith=%s'%self.startwith)
2570         # finally, the remainder of the filter args in the request
2571         if self.classname and self.filterspec:
2572             cls = self.client.db.getclass(self.classname)
2573             for k,v in self.filterspec.items():
2574                 if not args.has_key(k):
2575                     if type(v) == type([]):
2576                         prop = cls.get_transitive_prop(k)
2577                         if k != 'id' and isinstance(prop, hyperdb.String):
2578                             l.append('%s=%s'%(k, '%20'.join([q(i) for i in v])))
2579                         else:
2580                             l.append('%s=%s'%(k, ','.join([q(i) for i in v])))
2581                     else:
2582                         l.append('%s=%s'%(k, q(v)))
2583         return '%s?%s'%(url, '&'.join(l))
2584     indexargs_href = indexargs_url
2586     def base_javascript(self):
2587         return """
2588 <script type="text/javascript">
2589 submitted = false;
2590 function submit_once() {
2591     if (submitted) {
2592         alert("Your request is being processed.\\nPlease be patient.");
2593         event.returnValue = 0;    // work-around for IE
2594         return 0;
2595     }
2596     submitted = true;
2597     return 1;
2600 function help_window(helpurl, width, height) {
2601     HelpWin = window.open('%s' + helpurl, 'RoundupHelpWindow', 'scrollbars=yes,resizable=yes,toolbar=no,height='+height+',width='+width);
2603 </script>
2604 """%self.base
2606     def batch(self):
2607         """ Return a batch object for results from the "current search"
2608         """
2609         filterspec = self.filterspec
2610         sort = self.sort
2611         group = self.group
2613         # get the list of ids we're batching over
2614         klass = self.client.db.getclass(self.classname)
2615         if self.search_text:
2616             matches = self.client.db.indexer.search(
2617                 [w.upper().encode("utf-8", "replace") for w in re.findall(
2618                     r'(?u)\b\w{2,25}\b',
2619                     unicode(self.search_text, "utf-8", "replace")
2620                 )], klass)
2621         else:
2622             matches = None
2624         # filter for visibility
2625         check = self._client.db.security.hasPermission
2626         userid = self._client.userid
2627         l = [id for id in klass.filter(matches, filterspec, sort, group)
2628             if check('View', userid, self.classname, itemid=id)]
2630         # return the batch object, using IDs only
2631         return Batch(self.client, l, self.pagesize, self.startwith,
2632             classname=self.classname)
2634 # extend the standard ZTUtils Batch object to remove dependency on
2635 # Acquisition and add a couple of useful methods
2636 class Batch(ZTUtils.Batch):
2637     """ Use me to turn a list of items, or item ids of a given class, into a
2638         series of batches.
2640         ========= ========================================================
2641         Parameter  Usage
2642         ========= ========================================================
2643         sequence  a list of HTMLItems or item ids
2644         classname if sequence is a list of ids, this is the class of item
2645         size      how big to make the sequence.
2646         start     where to start (0-indexed) in the sequence.
2647         end       where to end (0-indexed) in the sequence.
2648         orphan    if the next batch would contain less items than this
2649                   value, then it is combined with this batch
2650         overlap   the number of items shared between adjacent batches
2651         ========= ========================================================
2653         Attributes: Note that the "start" attribute, unlike the
2654         argument, is a 1-based index (I know, lame).  "first" is the
2655         0-based index.  "length" is the actual number of elements in
2656         the batch.
2658         "sequence_length" is the length of the original, unbatched, sequence.
2659     """
2660     def __init__(self, client, sequence, size, start, end=0, orphan=0,
2661             overlap=0, classname=None):
2662         self.client = client
2663         self.last_index = self.last_item = None
2664         self.current_item = None
2665         self.classname = classname
2666         self.sequence_length = len(sequence)
2667         ZTUtils.Batch.__init__(self, sequence, size, start, end, orphan,
2668             overlap)
2670     # overwrite so we can late-instantiate the HTMLItem instance
2671     def __getitem__(self, index):
2672         if index < 0:
2673             if index + self.end < self.first: raise IndexError, index
2674             return self._sequence[index + self.end]
2676         if index >= self.length:
2677             raise IndexError, index
2679         # move the last_item along - but only if the fetched index changes
2680         # (for some reason, index 0 is fetched twice)
2681         if index != self.last_index:
2682             self.last_item = self.current_item
2683             self.last_index = index
2685         item = self._sequence[index + self.first]
2686         if self.classname:
2687             # map the item ids to instances
2688             item = HTMLItem(self.client, self.classname, item)
2689         self.current_item = item
2690         return item
2692     def propchanged(self, *properties):
2693         """ Detect if one of the properties marked as being a group
2694             property changed in the last iteration fetch
2695         """
2696         # we poke directly at the _value here since MissingValue can screw
2697         # us up and cause Nones to compare strangely
2698         if self.last_item is None:
2699             return 1
2700         for property in properties:
2701             if property == 'id' or isinstance (self.last_item[property], list):
2702                 if (str(self.last_item[property]) !=
2703                     str(self.current_item[property])):
2704                     return 1
2705             else:
2706                 if (self.last_item[property]._value !=
2707                     self.current_item[property]._value):
2708                     return 1
2709         return 0
2711     # override these 'cos we don't have access to acquisition
2712     def previous(self):
2713         if self.start == 1:
2714             return None
2715         return Batch(self.client, self._sequence, self._size,
2716             self.first - self._size + self.overlap, 0, self.orphan,
2717             self.overlap)
2719     def next(self):
2720         try:
2721             self._sequence[self.end]
2722         except IndexError:
2723             return None
2724         return Batch(self.client, self._sequence, self._size,
2725             self.end - self.overlap, 0, self.orphan, self.overlap)
2727 class TemplatingUtils:
2728     """ Utilities for templating
2729     """
2730     def __init__(self, client):
2731         self.client = client
2732     def Batch(self, sequence, size, start, end=0, orphan=0, overlap=0):
2733         return Batch(self.client, sequence, size, start, end, orphan,
2734             overlap)
2736     def url_quote(self, url):
2737         """URL-quote the supplied text."""
2738         return urllib.quote(url)
2740     def html_quote(self, html):
2741         """HTML-quote the supplied text."""
2742         return cgi.escape(html)
2744     def __getattr__(self, name):
2745         """Try the tracker's templating_utils."""
2746         if not hasattr(self.client.instance, 'templating_utils'):
2747             # backwards-compatibility
2748             raise AttributeError, name
2749         if not self.client.instance.templating_utils.has_key(name):
2750             raise AttributeError, name
2751         return self.client.instance.templating_utils[name]
2753     def html_calendar(self, request):
2754         """Generate a HTML calendar.
2756         `request`  the roundup.request object
2757                    - @template : name of the template
2758                    - form      : name of the form to store back the date
2759                    - property  : name of the property of the form to store
2760                                  back the date
2761                    - date      : current date
2762                    - display   : when browsing, specifies year and month
2764         html will simply be a table.
2765         """
2766         date_str  = request.form.getfirst("date", ".")
2767         display   = request.form.getfirst("display", date_str)
2768         template  = request.form.getfirst("@template", "calendar")
2769         form      = request.form.getfirst("form")
2770         property  = request.form.getfirst("property")
2771         curr_date = date.Date(date_str) # to highlight
2772         display   = date.Date(display)  # to show
2773         day       = display.day
2775         # for navigation
2776         date_prev_month = display + date.Interval("-1m")
2777         date_next_month = display + date.Interval("+1m")
2778         date_prev_year  = display + date.Interval("-1y")
2779         date_next_year  = display + date.Interval("+1y")
2781         res = []
2783         base_link = "%s?@template=%s&property=%s&form=%s&date=%s" % \
2784                     (request.classname, template, property, form, curr_date)
2786         # navigation
2787         # month
2788         res.append('<table class="calendar"><tr><td>')
2789         res.append(' <table width="100%" class="calendar_nav"><tr>')
2790         link = "&display=%s"%date_prev_month
2791         res.append('  <td><a href="%s&display=%s">&lt;</a></td>'%(base_link,
2792             date_prev_month))
2793         res.append('  <td>%s</td>'%calendar.month_name[display.month])
2794         res.append('  <td><a href="%s&display=%s">&gt;</a></td>'%(base_link,
2795             date_next_month))
2796         # spacer
2797         res.append('  <td width="100%"></td>')
2798         # year
2799         res.append('  <td><a href="%s&display=%s">&lt;</a></td>'%(base_link,
2800             date_prev_year))
2801         res.append('  <td>%s</td>'%display.year)
2802         res.append('  <td><a href="%s&display=%s">&gt;</a></td>'%(base_link,
2803             date_next_year))
2804         res.append(' </tr></table>')
2805         res.append(' </td></tr>')
2807         # the calendar
2808         res.append(' <tr><td><table class="calendar_display">')
2809         res.append('  <tr class="weekdays">')
2810         for day in calendar.weekheader(3).split():
2811             res.append('   <td>%s</td>'%day)
2812         res.append('  </tr>')
2813         for week in calendar.monthcalendar(display.year, display.month):
2814             res.append('  <tr>')
2815             for day in week:
2816                 link = "javascript:form[field].value = '%d-%02d-%02d'; " \
2817                       "window.close ();"%(display.year, display.month, day)
2818                 if (day == curr_date.day and display.month == curr_date.month
2819                         and display.year == curr_date.year):
2820                     # highlight
2821                     style = "today"
2822                 else :
2823                     style = ""
2824                 if day:
2825                     res.append('   <td class="%s"><a href="%s">%s</a></td>'%(
2826                         style, link, day))
2827                 else :
2828                     res.append('   <td></td>')
2829             res.append('  </tr>')
2830         res.append('</table></td></tr></table>')
2831         return "\n".join(res)
2833 class MissingValue:
2834     def __init__(self, description, **kwargs):
2835         self.__description = description
2836         for key, value in kwargs.items():
2837             self.__dict__[key] = value
2839     def __call__(self, *args, **kwargs): return MissingValue(self.__description)
2840     def __getattr__(self, name):
2841         # This allows assignments which assume all intermediate steps are Null
2842         # objects if they don't exist yet.
2843         #
2844         # For example (with just 'client' defined):
2845         #
2846         # client.db.config.TRACKER_WEB = 'BASE/'
2847         self.__dict__[name] = MissingValue(self.__description)
2848         return getattr(self, name)
2850     def __getitem__(self, key): return self
2851     def __nonzero__(self): return 0
2852     def __str__(self): return '[%s]'%self.__description
2853     def __repr__(self): return '<MissingValue 0x%x "%s">'%(id(self),
2854         self.__description)
2855     def gettext(self, str): return str
2856     _ = gettext
2858 # vim: set et sts=4 sw=4 :