Code

don't show entire history by default (fixes http://bugs.debian.org/cgi-bin/bugreport...
[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             limit=None):
885         if not self.is_view_ok():
886             return self._('[hidden]')
888         # pre-load the history with the current state
889         current = {}
890         for prop_n in self._props.keys():
891             prop = self[prop_n]
892             if not isinstance(prop, HTMLProperty):
893                 continue
894             current[prop_n] = prop.plain(escape=1)
895             # make link if hrefable
896             if (self._props.has_key(prop_n) and
897                     isinstance(self._props[prop_n], hyperdb.Link)):
898                 classname = self._props[prop_n].classname
899                 try:
900                     template = find_template(self._db.config.TEMPLATES,
901                         classname, 'item')
902                     if template[1].startswith('_generic'):
903                         raise NoTemplate, 'not really...'
904                 except NoTemplate:
905                     pass
906                 else:
907                     id = self._klass.get(self._nodeid, prop_n, None)
908                     current[prop_n] = '<a href="%s%s">%s</a>'%(
909                         classname, id, current[prop_n])
911         # get the journal, sort and reverse
912         history = self._klass.history(self._nodeid)
913         history.sort()
914         history.reverse()
916         # restrict the volume
917         if limit:
918             history = history[:limit]
920         timezone = self._db.getUserTimezone()
921         l = []
922         comments = {}
923         for id, evt_date, user, action, args in history:
924             date_s = str(evt_date.local(timezone)).replace("."," ")
925             arg_s = ''
926             if action == 'link' and type(args) == type(()):
927                 if len(args) == 3:
928                     linkcl, linkid, key = args
929                     arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
930                         linkcl, linkid, key)
931                 else:
932                     arg_s = str(args)
934             elif action == 'unlink' and type(args) == type(()):
935                 if len(args) == 3:
936                     linkcl, linkid, key = args
937                     arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
938                         linkcl, linkid, key)
939                 else:
940                     arg_s = str(args)
942             elif type(args) == type({}):
943                 cell = []
944                 for k in args.keys():
945                     # try to get the relevant property and treat it
946                     # specially
947                     try:
948                         prop = self._props[k]
949                     except KeyError:
950                         prop = None
951                     if prop is None:
952                         # property no longer exists
953                         comments['no_exist'] = self._(
954                             "<em>The indicated property no longer exists</em>")
955                         cell.append(self._('<em>%s: %s</em>\n')
956                             % (self._(k), str(args[k])))
957                         continue
959                     if args[k] and (isinstance(prop, hyperdb.Multilink) or
960                             isinstance(prop, hyperdb.Link)):
961                         # figure what the link class is
962                         classname = prop.classname
963                         try:
964                             linkcl = self._db.getclass(classname)
965                         except KeyError:
966                             labelprop = None
967                             comments[classname] = self._(
968                                 "The linked class %(classname)s no longer exists"
969                             ) % locals()
970                         labelprop = linkcl.labelprop(1)
971                         try:
972                             template = find_template(self._db.config.TEMPLATES,
973                                 classname, 'item')
974                             if template[1].startswith('_generic'):
975                                 raise NoTemplate, 'not really...'
976                             hrefable = 1
977                         except NoTemplate:
978                             hrefable = 0
980                     if isinstance(prop, hyperdb.Multilink) and args[k]:
981                         ml = []
982                         for linkid in args[k]:
983                             if isinstance(linkid, type(())):
984                                 sublabel = linkid[0] + ' '
985                                 linkids = linkid[1]
986                             else:
987                                 sublabel = ''
988                                 linkids = [linkid]
989                             subml = []
990                             for linkid in linkids:
991                                 label = classname + linkid
992                                 # if we have a label property, try to use it
993                                 # TODO: test for node existence even when
994                                 # there's no labelprop!
995                                 try:
996                                     if labelprop is not None and \
997                                             labelprop != 'id':
998                                         label = linkcl.get(linkid, labelprop)
999                                         label = cgi.escape(label)
1000                                 except IndexError:
1001                                     comments['no_link'] = self._(
1002                                         "<strike>The linked node"
1003                                         " no longer exists</strike>")
1004                                     subml.append('<strike>%s</strike>'%label)
1005                                 else:
1006                                     if hrefable:
1007                                         subml.append('<a href="%s%s">%s</a>'%(
1008                                             classname, linkid, label))
1009                                     elif label is None:
1010                                         subml.append('%s%s'%(classname,
1011                                             linkid))
1012                                     else:
1013                                         subml.append(label)
1014                             ml.append(sublabel + ', '.join(subml))
1015                         cell.append('%s:\n  %s'%(self._(k), ', '.join(ml)))
1016                     elif isinstance(prop, hyperdb.Link) and args[k]:
1017                         label = classname + args[k]
1018                         # if we have a label property, try to use it
1019                         # TODO: test for node existence even when
1020                         # there's no labelprop!
1021                         if labelprop is not None and labelprop != 'id':
1022                             try:
1023                                 label = cgi.escape(linkcl.get(args[k],
1024                                     labelprop))
1025                             except IndexError:
1026                                 comments['no_link'] = self._(
1027                                     "<strike>The linked node"
1028                                     " no longer exists</strike>")
1029                                 cell.append(' <strike>%s</strike>,\n'%label)
1030                                 # "flag" this is done .... euwww
1031                                 label = None
1032                         if label is not None:
1033                             if hrefable:
1034                                 old = '<a href="%s%s">%s</a>'%(classname,
1035                                     args[k], label)
1036                             else:
1037                                 old = label;
1038                             cell.append('%s: %s' % (self._(k), old))
1039                             if current.has_key(k):
1040                                 cell[-1] += ' -> %s'%current[k]
1041                                 current[k] = old
1043                     elif isinstance(prop, hyperdb.Date) and args[k]:
1044                         if args[k] is None:
1045                             d = ''
1046                         else:
1047                             d = date.Date(args[k],
1048                                 translator=self._client).local(timezone)
1049                         cell.append('%s: %s'%(self._(k), str(d)))
1050                         if current.has_key(k):
1051                             cell[-1] += ' -> %s' % current[k]
1052                             current[k] = str(d)
1054                     elif isinstance(prop, hyperdb.Interval) and args[k]:
1055                         val = str(date.Interval(args[k],
1056                             translator=self._client))
1057                         cell.append('%s: %s'%(self._(k), val))
1058                         if current.has_key(k):
1059                             cell[-1] += ' -> %s'%current[k]
1060                             current[k] = val
1062                     elif isinstance(prop, hyperdb.String) and args[k]:
1063                         val = cgi.escape(args[k])
1064                         cell.append('%s: %s'%(self._(k), val))
1065                         if current.has_key(k):
1066                             cell[-1] += ' -> %s'%current[k]
1067                             current[k] = val
1069                     elif isinstance(prop, hyperdb.Boolean) and args[k] is not None:
1070                         val = args[k] and ''"Yes" or ''"No"
1071                         cell.append('%s: %s'%(self._(k), val))
1072                         if current.has_key(k):
1073                             cell[-1] += ' -> %s'%current[k]
1074                             current[k] = val
1076                     elif not args[k]:
1077                         if current.has_key(k):
1078                             cell.append('%s: %s'%(self._(k), current[k]))
1079                             current[k] = '(no value)'
1080                         else:
1081                             cell.append(self._('%s: (no value)')%self._(k))
1083                     else:
1084                         cell.append('%s: %s'%(self._(k), str(args[k])))
1085                         if current.has_key(k):
1086                             cell[-1] += ' -> %s'%current[k]
1087                             current[k] = str(args[k])
1089                 arg_s = '<br />'.join(cell)
1090             else:
1091                 # unkown event!!
1092                 comments['unknown'] = self._(
1093                     "<strong><em>This event is not handled"
1094                     " by the history display!</em></strong>")
1095                 arg_s = '<strong><em>' + str(args) + '</em></strong>'
1096             date_s = date_s.replace(' ', '&nbsp;')
1097             # if the user's an itemid, figure the username (older journals
1098             # have the username)
1099             if dre.match(user):
1100                 user = self._db.user.get(user, 'username')
1101             l.append('<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>'%(
1102                 date_s, user, self._(action), arg_s))
1103         if comments:
1104             l.append(self._(
1105                 '<tr><td colspan=4><strong>Note:</strong></td></tr>'))
1106         for entry in comments.values():
1107             l.append('<tr><td colspan=4>%s</td></tr>'%entry)
1109         if direction == 'ascending':
1110             l.reverse()
1112         l[0:0] = ['<table class="history">'
1113              '<tr><th colspan="4" class="header">',
1114              self._('History'),
1115              '</th></tr><tr>',
1116              self._('<th>Date</th>'),
1117              self._('<th>User</th>'),
1118              self._('<th>Action</th>'),
1119              self._('<th>Args</th>'),
1120             '</tr>']
1121         l.append('</table>')
1122         return '\n'.join(l)
1124     def renderQueryForm(self):
1125         """ Render this item, which is a query, as a search form.
1126         """
1127         # create a new request and override the specified args
1128         req = HTMLRequest(self._client)
1129         req.classname = self._klass.get(self._nodeid, 'klass')
1130         name = self._klass.get(self._nodeid, 'name')
1131         req.updateFromURL(self._klass.get(self._nodeid, 'url') +
1132             '&@queryname=%s'%urllib.quote(name))
1134         # new template, using the specified classname and request
1135         pt = self._client.instance.templates.get(req.classname, 'search')
1136         # The context for a search page should be the class, not any
1137         # node.
1138         self._client.nodeid = None
1140         # use our fabricated request
1141         return pt.render(self._client, req.classname, req)
1143     def download_url(self):
1144         """ Assume that this item is a FileClass and that it has a name
1145         and content. Construct a URL for the download of the content.
1146         """
1147         name = self._klass.get(self._nodeid, 'name')
1148         url = '%s%s/%s'%(self._classname, self._nodeid, name)
1149         return urllib.quote(url)
1151     def copy_url(self, exclude=("messages", "files")):
1152         """Construct a URL for creating a copy of this item
1154         "exclude" is an optional list of properties that should
1155         not be copied to the new object.  By default, this list
1156         includes "messages" and "files" properties.  Note that
1157         "id" property cannot be copied.
1159         """
1160         exclude = ("id", "activity", "actor", "creation", "creator") \
1161             + tuple(exclude)
1162         query = {
1163             "@template": "item",
1164             "@note": self._("Copy of %(class)s %(id)s") % {
1165                 "class": self._(self._classname), "id": self._nodeid},
1166         }
1167         for name in self._props.keys():
1168             if name not in exclude:
1169                 query[name] = self[name].plain()
1170         return self._classname + "?" + "&".join(
1171             ["%s=%s" % (key, urllib.quote(value))
1172                 for key, value in query.items()])
1174 class _HTMLUser(_HTMLItem):
1175     """Add ability to check for permissions on users.
1176     """
1177     _marker = []
1178     def hasPermission(self, permission, classname=_marker,
1179             property=None, itemid=None):
1180         """Determine if the user has the Permission.
1182         The class being tested defaults to the template's class, but may
1183         be overidden for this test by suppling an alternate classname.
1184         """
1185         if classname is self._marker:
1186             classname = self._client.classname
1187         return self._db.security.hasPermission(permission,
1188             self._nodeid, classname, property, itemid)
1190     def hasRole(self, rolename):
1191         """Determine whether the user has the Role."""
1192         roles = self._db.user.get(self._nodeid, 'roles').split(',')
1193         for role in roles:
1194             if role.strip() == rolename: return True
1195         return False
1197 def HTMLItem(client, classname, nodeid, anonymous=0):
1198     if classname == 'user':
1199         return _HTMLUser(client, classname, nodeid, anonymous)
1200     else:
1201         return _HTMLItem(client, classname, nodeid, anonymous)
1203 class HTMLProperty(HTMLInputMixin, HTMLPermissions):
1204     """ String, Number, Date, Interval HTMLProperty
1206         Has useful attributes:
1208          _name  the name of the property
1209          _value the value of the property if any
1211         A wrapper object which may be stringified for the plain() behaviour.
1212     """
1213     def __init__(self, client, classname, nodeid, prop, name, value,
1214             anonymous=0):
1215         self._client = client
1216         self._db = client.db
1217         self._ = client._
1218         self._classname = classname
1219         self._nodeid = nodeid
1220         self._prop = prop
1221         self._value = value
1222         self._anonymous = anonymous
1223         self._name = name
1224         if not anonymous:
1225             self._formname = '%s%s@%s'%(classname, nodeid, name)
1226         else:
1227             self._formname = name
1229         # If no value is already present for this property, see if one
1230         # is specified in the current form.
1231         form = self._client.form
1232         if not self._value and form.has_key(self._formname):
1233             if isinstance(prop, hyperdb.Multilink):
1234                 value = lookupIds(self._db, prop,
1235                                   handleListCGIValue(form[self._formname]),
1236                                   fail_ok=1)
1237             elif isinstance(prop, hyperdb.Link):
1238                 value = form.getfirst(self._formname).strip()
1239                 if value:
1240                     value = lookupIds(self._db, prop, [value],
1241                                       fail_ok=1)[0]
1242                 else:
1243                     value = None
1244             else:
1245                 value = form.getfirst(self._formname).strip() or None
1246             self._value = value
1248         HTMLInputMixin.__init__(self)
1250     def __repr__(self):
1251         return '<HTMLProperty(0x%x) %s %r %r>'%(id(self), self._formname,
1252             self._prop, self._value)
1253     def __str__(self):
1254         return self.plain()
1255     def __cmp__(self, other):
1256         if isinstance(other, HTMLProperty):
1257             return cmp(self._value, other._value)
1258         return cmp(self._value, other)
1260     def __nonzero__(self):
1261         return not not self._value
1263     def isset(self):
1264         """Is my _value not None?"""
1265         return self._value is not None
1267     def is_edit_ok(self):
1268         """Should the user be allowed to use an edit form field for this
1269         property. Check "Create" for new items, or "Edit" for existing
1270         ones.
1271         """
1272         if self._nodeid:
1273             return self._db.security.hasPermission('Edit', self._client.userid,
1274                 self._classname, self._name, self._nodeid)
1275         return self._db.security.hasPermission('Create', self._client.userid,
1276             self._classname, self._name) or \
1277             self._db.security.hasPermission('Register', self._client.userid,
1278                                             self._classname, self._name)
1280     def is_view_ok(self):
1281         """ Is the user allowed to View the current class?
1282         """
1283         if self._db.security.hasPermission('View', self._client.userid,
1284                 self._classname, self._name, self._nodeid):
1285             return 1
1286         return self.is_edit_ok()
1288 class StringHTMLProperty(HTMLProperty):
1289     hyper_re = re.compile(r'''(
1290         (?P<url>
1291          (
1292           (ht|f)tp(s?)://                   # protocol
1293           ([\w]+(:\w+)?@)?                  # username/password
1294           ([\w\-]+)                         # hostname
1295           ((\.[\w-]+)+)?                    # .domain.etc
1296          |                                  # ... or ...
1297           ([\w]+(:\w+)?@)?                  # username/password
1298           www\.                             # "www."
1299           ([\w\-]+\.)+                      # hostname
1300           [\w]{2,5}                         # TLD
1301          )
1302          (:[\d]{1,5})?                     # port
1303          (/[\w\-$.+!*(),;:@&=?/~\\#%]*)?   # path etc.
1304         )|
1305         (?P<email>[-+=%/\w\.]+@[\w\.\-]+)|
1306         (?P<item>(?P<class>[A-Za-z_]+)(\s*)(?P<id>\d+))
1307     )''', re.X | re.I)
1308     protocol_re = re.compile('^(ht|f)tp(s?)://', re.I)
1310     def _hyper_repl_item(self,match,replacement):
1311         item = match.group('item')
1312         cls = match.group('class').lower()
1313         id = match.group('id')
1314         try:
1315             # make sure cls is a valid tracker classname
1316             cl = self._db.getclass(cls)
1317             if not cl.hasnode(id):
1318                 return item
1319             return replacement % locals()
1320         except KeyError:
1321             return item
1323     def _hyper_repl(self, match):
1324         if match.group('url'):
1325             u = s = match.group('url')
1326             if not self.protocol_re.search(s):
1327                 u = 'http://' + s
1328             # catch an escaped ">" at the end of the URL
1329             if s.endswith('&gt;'):
1330                 u = s = s[:-4]
1331                 e = '&gt;'
1332             else:
1333                 e = ''
1334             return '<a href="%s">%s</a>%s'%(u, s, e)
1335         elif match.group('email'):
1336             s = match.group('email')
1337             return '<a href="mailto:%s">%s</a>'%(s, s)
1338         else:
1339             return self._hyper_repl_item(match,
1340                 '<a href="%(cls)s%(id)s">%(item)s</a>')
1342     def _hyper_repl_rst(self, match):
1343         if match.group('url'):
1344             s = match.group('url')
1345             return '`%s <%s>`_'%(s, s)
1346         elif match.group('email'):
1347             s = match.group('email')
1348             return '`%s <mailto:%s>`_'%(s, s)
1349         else:
1350             return self._hyper_repl_item(match,'`%(item)s <%(cls)s%(id)s>`_')
1352     def hyperlinked(self):
1353         """ Render a "hyperlinked" version of the text """
1354         return self.plain(hyperlink=1)
1356     def plain(self, escape=0, hyperlink=0):
1357         """Render a "plain" representation of the property
1359         - "escape" turns on/off HTML quoting
1360         - "hyperlink" turns on/off in-text hyperlinking of URLs, email
1361           addresses and designators
1362         """
1363         if not self.is_view_ok():
1364             return self._('[hidden]')
1366         if self._value is None:
1367             return ''
1368         if escape:
1369             s = cgi.escape(str(self._value))
1370         else:
1371             s = str(self._value)
1372         if hyperlink:
1373             # no, we *must* escape this text
1374             if not escape:
1375                 s = cgi.escape(s)
1376             s = self.hyper_re.sub(self._hyper_repl, s)
1377         return s
1379     def wrapped(self, escape=1, hyperlink=1):
1380         """Render a "wrapped" representation of the property.
1382         We wrap long lines at 80 columns on the nearest whitespace. Lines
1383         with no whitespace are not broken to force wrapping.
1385         Note that unlike plain() we default wrapped() to have the escaping
1386         and hyperlinking turned on since that's the most common usage.
1388         - "escape" turns on/off HTML quoting
1389         - "hyperlink" turns on/off in-text hyperlinking of URLs, email
1390           addresses and designators
1391         """
1392         if not self.is_view_ok():
1393             return self._('[hidden]')
1395         if self._value is None:
1396             return ''
1397         s = support.wrap(str(self._value), width=80)
1398         if escape:
1399             s = cgi.escape(s)
1400         if hyperlink:
1401             # no, we *must* escape this text
1402             if not escape:
1403                 s = cgi.escape(s)
1404             s = self.hyper_re.sub(self._hyper_repl, s)
1405         return s
1407     def stext(self, escape=0, hyperlink=1):
1408         """ Render the value of the property as StructuredText.
1410             This requires the StructureText module to be installed separately.
1411         """
1412         if not self.is_view_ok():
1413             return self._('[hidden]')
1415         s = self.plain(escape=escape, hyperlink=hyperlink)
1416         if not StructuredText:
1417             return s
1418         return StructuredText(s,level=1,header=0)
1420     def rst(self, hyperlink=1):
1421         """ Render the value of the property as ReStructuredText.
1423             This requires docutils to be installed separately.
1424         """
1425         if not self.is_view_ok():
1426             return self._('[hidden]')
1428         if not ReStructuredText:
1429             return self.plain(escape=0, hyperlink=hyperlink)
1430         s = self.plain(escape=0, hyperlink=0)
1431         if hyperlink:
1432             s = self.hyper_re.sub(self._hyper_repl_rst, s)
1433         return ReStructuredText(s, writer_name="html")["html_body"].encode("utf-8",
1434             "replace")
1436     def field(self, **kwargs):
1437         """ Render the property as a field in HTML.
1439             If not editable, just display the value via plain().
1440         """
1441         if not self.is_edit_ok():
1442             return self.plain(escape=1)
1444         value = self._value
1445         if value is None:
1446             value = ''
1448         kwargs.setdefault("size", 30)
1449         kwargs.update({"name": self._formname, "value": value})
1450         return self.input(**kwargs)
1452     def multiline(self, escape=0, rows=5, cols=40, **kwargs):
1453         """ Render a multiline form edit field for the property.
1455             If not editable, just display the plain() value in a <pre> tag.
1456         """
1457         if not self.is_edit_ok():
1458             return '<pre>%s</pre>'%self.plain()
1460         if self._value is None:
1461             value = ''
1462         else:
1463             value = cgi.escape(str(self._value))
1465             value = '&quot;'.join(value.split('"'))
1466         name = self._formname
1467         passthrough_args = ' '.join(['%s="%s"' % (k, cgi.escape(str(v), True))
1468             for k,v in kwargs.items()])
1469         return ('<textarea %(passthrough_args)s name="%(name)s" id="%(name)s"'
1470                 ' rows="%(rows)s" cols="%(cols)s">'
1471                  '%(value)s</textarea>') % locals()
1473     def email(self, escape=1):
1474         """ Render the value of the property as an obscured email address
1475         """
1476         if not self.is_view_ok():
1477             return self._('[hidden]')
1479         if self._value is None:
1480             value = ''
1481         else:
1482             value = str(self._value)
1483         split = value.split('@')
1484         if len(split) == 2:
1485             name, domain = split
1486             domain = ' '.join(domain.split('.')[:-1])
1487             name = name.replace('.', ' ')
1488             value = '%s at %s ...'%(name, domain)
1489         else:
1490             value = value.replace('.', ' ')
1491         if escape:
1492             value = cgi.escape(value)
1493         return value
1495 class PasswordHTMLProperty(HTMLProperty):
1496     def plain(self, escape=0):
1497         """ Render a "plain" representation of the property
1498         """
1499         if not self.is_view_ok():
1500             return self._('[hidden]')
1502         if self._value is None:
1503             return ''
1504         return self._('*encrypted*')
1506     def field(self, size=30):
1507         """ Render a form edit field for the property.
1509             If not editable, just display the value via plain().
1510         """
1511         if not self.is_edit_ok():
1512             return self.plain(escape=1)
1514         return self.input(type="password", name=self._formname, size=size)
1516     def confirm(self, size=30):
1517         """ Render a second form edit field for the property, used for
1518             confirmation that the user typed the password correctly. Generates
1519             a field with name "@confirm@name".
1521             If not editable, display nothing.
1522         """
1523         if not self.is_edit_ok():
1524             return ''
1526         return self.input(type="password",
1527             name="@confirm@%s"%self._formname,
1528             id="%s-confirm"%self._formname,
1529             size=size)
1531 class NumberHTMLProperty(HTMLProperty):
1532     def plain(self, escape=0):
1533         """ Render a "plain" representation of the property
1534         """
1535         if not self.is_view_ok():
1536             return self._('[hidden]')
1538         if self._value is None:
1539             return ''
1541         return str(self._value)
1543     def field(self, size=30):
1544         """ Render a form edit field for the property.
1546             If not editable, just display the value via plain().
1547         """
1548         if not self.is_edit_ok():
1549             return self.plain(escape=1)
1551         value = self._value
1552         if value is None:
1553             value = ''
1555         return self.input(name=self._formname, value=value, size=size)
1557     def __int__(self):
1558         """ Return an int of me
1559         """
1560         return int(self._value)
1562     def __float__(self):
1563         """ Return a float of me
1564         """
1565         return float(self._value)
1568 class BooleanHTMLProperty(HTMLProperty):
1569     def plain(self, escape=0):
1570         """ Render a "plain" representation of the property
1571         """
1572         if not self.is_view_ok():
1573             return self._('[hidden]')
1575         if self._value is None:
1576             return ''
1577         return self._value and self._("Yes") or self._("No")
1579     def field(self):
1580         """ Render a form edit field for the property
1582             If not editable, just display the value via plain().
1583         """
1584         if not self.is_edit_ok():
1585             return self.plain(escape=1)
1587         value = self._value
1588         if isinstance(value, str) or isinstance(value, unicode):
1589             value = value.strip().lower() in ('checked', 'yes', 'true',
1590                 'on', '1')
1592         checked = value and "checked" or ""
1593         if value:
1594             s = self.input(type="radio", name=self._formname, value="yes",
1595                 checked="checked")
1596             s += self._('Yes')
1597             s +=self.input(type="radio", name=self._formname, value="no")
1598             s += self._('No')
1599         else:
1600             s = self.input(type="radio", name=self._formname, value="yes")
1601             s += self._('Yes')
1602             s +=self.input(type="radio", name=self._formname, value="no",
1603                 checked="checked")
1604             s += self._('No')
1605         return s
1607 class DateHTMLProperty(HTMLProperty):
1609     _marker = []
1611     def __init__(self, client, classname, nodeid, prop, name, value,
1612             anonymous=0, offset=None):
1613         HTMLProperty.__init__(self, client, classname, nodeid, prop, name,
1614                 value, anonymous=anonymous)
1615         if self._value and not (isinstance(self._value, str) or
1616                 isinstance(self._value, unicode)):
1617             self._value.setTranslator(self._client.translator)
1618         self._offset = offset
1619         if self._offset is None :
1620             self._offset = self._prop.offset (self._db)
1622     def plain(self, escape=0):
1623         """ Render a "plain" representation of the property
1624         """
1625         if not self.is_view_ok():
1626             return self._('[hidden]')
1628         if self._value is None:
1629             return ''
1630         if self._offset is None:
1631             offset = self._db.getUserTimezone()
1632         else:
1633             offset = self._offset
1634         return str(self._value.local(offset))
1636     def now(self, str_interval=None):
1637         """ Return the current time.
1639             This is useful for defaulting a new value. Returns a
1640             DateHTMLProperty.
1641         """
1642         if not self.is_view_ok():
1643             return self._('[hidden]')
1645         ret = date.Date('.', translator=self._client)
1647         if isinstance(str_interval, basestring):
1648             sign = 1
1649             if str_interval[0] == '-':
1650                 sign = -1
1651                 str_interval = str_interval[1:]
1652             interval = date.Interval(str_interval, translator=self._client)
1653             if sign > 0:
1654                 ret = ret + interval
1655             else:
1656                 ret = ret - interval
1658         return DateHTMLProperty(self._client, self._classname, self._nodeid,
1659             self._prop, self._formname, ret)
1661     def field(self, size=30, default=None, format=_marker, popcal=True):
1662         """Render a form edit field for the property
1664         If not editable, just display the value via plain().
1666         If "popcal" then include the Javascript calendar editor.
1667         Default=yes.
1669         The format string is a standard python strftime format string.
1670         """
1671         if not self.is_edit_ok():
1672             if format is self._marker:
1673                 return self.plain(escape=1)
1674             else:
1675                 return self.pretty(format)
1677         value = self._value
1679         if value is None:
1680             if default is None:
1681                 raw_value = None
1682             else:
1683                 if isinstance(default, basestring):
1684                     raw_value = date.Date(default, translator=self._client)
1685                 elif isinstance(default, date.Date):
1686                     raw_value = default
1687                 elif isinstance(default, DateHTMLProperty):
1688                     raw_value = default._value
1689                 else:
1690                     raise ValueError, self._('default value for '
1691                         'DateHTMLProperty must be either DateHTMLProperty '
1692                         'or string date representation.')
1693         elif isinstance(value, str) or isinstance(value, unicode):
1694             # most likely erroneous input to be passed back to user
1695             if isinstance(value, unicode): value = value.encode('utf8')
1696             return self.input(name=self._formname, value=value, size=size)
1697         else:
1698             raw_value = value
1700         if raw_value is None:
1701             value = ''
1702         elif isinstance(raw_value, str) or isinstance(raw_value, unicode):
1703             if format is self._marker:
1704                 value = raw_value
1705             else:
1706                 value = date.Date(raw_value).pretty(format)
1707         else:
1708             if self._offset is None :
1709                 offset = self._db.getUserTimezone()
1710             else :
1711                 offset = self._offset
1712             value = raw_value.local(offset)
1713             if format is not self._marker:
1714                 value = value.pretty(format)
1716         s = self.input(name=self._formname, value=value, size=size)
1717         if popcal:
1718             s += self.popcal()
1719         return s
1721     def reldate(self, pretty=1):
1722         """ Render the interval between the date and now.
1724             If the "pretty" flag is true, then make the display pretty.
1725         """
1726         if not self.is_view_ok():
1727             return self._('[hidden]')
1729         if not self._value:
1730             return ''
1732         # figure the interval
1733         interval = self._value - date.Date('.', translator=self._client)
1734         if pretty:
1735             return interval.pretty()
1736         return str(interval)
1738     def pretty(self, format=_marker):
1739         """ Render the date in a pretty format (eg. month names, spaces).
1741             The format string is a standard python strftime format string.
1742             Note that if the day is zero, and appears at the start of the
1743             string, then it'll be stripped from the output. This is handy
1744             for the situation when a date only specifies a month and a year.
1745         """
1746         if not self.is_view_ok():
1747             return self._('[hidden]')
1749         if self._offset is None:
1750             offset = self._db.getUserTimezone()
1751         else:
1752             offset = self._offset
1754         if not self._value:
1755             return ''
1756         elif format is not self._marker:
1757             return self._value.local(offset).pretty(format)
1758         else:
1759             return self._value.local(offset).pretty()
1761     def local(self, offset):
1762         """ Return the date/time as a local (timezone offset) date/time.
1763         """
1764         if not self.is_view_ok():
1765             return self._('[hidden]')
1767         return DateHTMLProperty(self._client, self._classname, self._nodeid,
1768             self._prop, self._formname, self._value, offset=offset)
1770     def popcal(self, width=300, height=200, label="(cal)",
1771             form="itemSynopsis"):
1772         """Generate a link to a calendar pop-up window.
1774         item: HTMLProperty e.g.: context.deadline
1775         """
1776         if self.isset():
1777             date = "&date=%s"%self._value
1778         else :
1779             date = ""
1780         return ('<a class="classhelp" href="javascript:help_window('
1781             "'%s?@template=calendar&amp;property=%s&amp;form=%s%s', %d, %d)"
1782             '">%s</a>'%(self._classname, self._name, form, date, width,
1783             height, label))
1785 class IntervalHTMLProperty(HTMLProperty):
1786     def __init__(self, client, classname, nodeid, prop, name, value,
1787             anonymous=0):
1788         HTMLProperty.__init__(self, client, classname, nodeid, prop,
1789             name, value, anonymous)
1790         if self._value and not isinstance(self._value, (str, unicode)):
1791             self._value.setTranslator(self._client.translator)
1793     def plain(self, escape=0):
1794         """ Render a "plain" representation of the property
1795         """
1796         if not self.is_view_ok():
1797             return self._('[hidden]')
1799         if self._value is None:
1800             return ''
1801         return str(self._value)
1803     def pretty(self):
1804         """ Render the interval in a pretty format (eg. "yesterday")
1805         """
1806         if not self.is_view_ok():
1807             return self._('[hidden]')
1809         return self._value.pretty()
1811     def field(self, size=30):
1812         """ Render a form edit field for the property
1814             If not editable, just display the value via plain().
1815         """
1816         if not self.is_edit_ok():
1817             return self.plain(escape=1)
1819         value = self._value
1820         if value is None:
1821             value = ''
1823         return self.input(name=self._formname, value=value, size=size)
1825 class LinkHTMLProperty(HTMLProperty):
1826     """ Link HTMLProperty
1827         Include the above as well as being able to access the class
1828         information. Stringifying the object itself results in the value
1829         from the item being displayed. Accessing attributes of this object
1830         result in the appropriate entry from the class being queried for the
1831         property accessed (so item/assignedto/name would look up the user
1832         entry identified by the assignedto property on item, and then the
1833         name property of that user)
1834     """
1835     def __init__(self, *args, **kw):
1836         HTMLProperty.__init__(self, *args, **kw)
1837         # if we're representing a form value, then the -1 from the form really
1838         # should be a None
1839         if str(self._value) == '-1':
1840             self._value = None
1842     def __getattr__(self, attr):
1843         """ return a new HTMLItem """
1844         if not self._value:
1845             # handle a special page templates lookup
1846             if attr == '__render_with_namespace__':
1847                 def nothing(*args, **kw):
1848                     return ''
1849                 return nothing
1850             msg = self._('Attempt to look up %(attr)s on a missing value')
1851             return MissingValue(msg%locals())
1852         i = HTMLItem(self._client, self._prop.classname, self._value)
1853         return getattr(i, attr)
1855     def plain(self, escape=0):
1856         """ Render a "plain" representation of the property
1857         """
1858         if not self.is_view_ok():
1859             return self._('[hidden]')
1861         if self._value is None:
1862             return ''
1863         linkcl = self._db.classes[self._prop.classname]
1864         k = linkcl.labelprop(1)
1865         if num_re.match(self._value):
1866             try:
1867                 value = str(linkcl.get(self._value, k))
1868             except IndexError:
1869                 value = self._value
1870         else :
1871             value = self._value
1872         if escape:
1873             value = cgi.escape(value)
1874         return value
1876     def field(self, showid=0, size=None):
1877         """ Render a form edit field for the property
1879             If not editable, just display the value via plain().
1880         """
1881         if not self.is_edit_ok():
1882             return self.plain(escape=1)
1884         # edit field
1885         linkcl = self._db.getclass(self._prop.classname)
1886         if self._value is None:
1887             value = ''
1888         else:
1889             k = linkcl.getkey()
1890             if k and num_re.match(self._value):
1891                 value = linkcl.get(self._value, k)
1892             else:
1893                 value = self._value
1894         return self.input(name=self._formname, value=value, size=size)
1896     def menu(self, size=None, height=None, showid=0, additional=[], value=None,
1897             sort_on=None, **conditions):
1898         """ Render a form select list for this property
1900             "size" is used to limit the length of the list labels
1901             "height" is used to set the <select> tag's "size" attribute
1902             "showid" includes the item ids in the list labels
1903             "value" specifies which item is pre-selected
1904             "additional" lists properties which should be included in the
1905                 label
1906             "sort_on" indicates the property to sort the list on as
1907                 (direction, property) where direction is '+' or '-'. A
1908                 single string with the direction prepended may be used.
1909                 For example: ('-', 'order'), '+name'.
1911             The remaining keyword arguments are used as conditions for
1912             filtering the items in the list - they're passed as the
1913             "filterspec" argument to a Class.filter() call.
1915             If not editable, just display the value via plain().
1916         """
1917         if not self.is_edit_ok():
1918             return self.plain(escape=1)
1920         # Since None indicates the default, we need another way to
1921         # indicate "no selection".  We use -1 for this purpose, as
1922         # that is the value we use when submitting a form without the
1923         # value set.
1924         if value is None:
1925             value = self._value
1926         elif value == '-1':
1927             value = None
1929         linkcl = self._db.getclass(self._prop.classname)
1930         l = ['<select name="%s">'%self._formname]
1931         k = linkcl.labelprop(1)
1932         s = ''
1933         if value is None:
1934             s = 'selected="selected" '
1935         l.append(self._('<option %svalue="-1">- no selection -</option>')%s)
1937         if sort_on is not None:
1938             if not isinstance(sort_on, tuple):
1939                 if sort_on[0] in '+-':
1940                     sort_on = (sort_on[0], sort_on[1:])
1941                 else:
1942                     sort_on = ('+', sort_on)
1943         else:
1944             sort_on = ('+', linkcl.orderprop())
1946         options = [opt
1947             for opt in linkcl.filter(None, conditions, sort_on, (None, None))
1948             if self._db.security.hasPermission("View", self._client.userid,
1949                 linkcl.classname, itemid=opt)]
1951         # make sure we list the current value if it's retired
1952         if value and value not in options:
1953             options.insert(0, value)
1955         if additional:
1956             additional_fns = []
1957             props = linkcl.getprops()
1958             for propname in additional:
1959                 prop = props[propname]
1960                 if isinstance(prop, hyperdb.Link):
1961                     cl = self._db.getclass(prop.classname)
1962                     labelprop = cl.labelprop()
1963                     fn = lambda optionid: cl.get(linkcl.get(optionid,
1964                                                             propname),
1965                                                  labelprop)
1966                 else:
1967                     fn = lambda optionid: linkcl.get(optionid, propname)
1968             additional_fns.append(fn)
1970         for optionid in options:
1971             # get the option value, and if it's None use an empty string
1972             option = linkcl.get(optionid, k) or ''
1974             # figure if this option is selected
1975             s = ''
1976             if value in [optionid, option]:
1977                 s = 'selected="selected" '
1979             # figure the label
1980             if showid:
1981                 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1982             elif not option:
1983                 lab = '%s%s'%(self._prop.classname, optionid)
1984             else:
1985                 lab = option
1987             # truncate if it's too long
1988             if size is not None and len(lab) > size:
1989                 lab = lab[:size-3] + '...'
1990             if additional:
1991                 m = []
1992                 for fn in additional_fns:
1993                     m.append(str(fn(optionid)))
1994                 lab = lab + ' (%s)'%', '.join(m)
1996             # and generate
1997             lab = cgi.escape(self._(lab))
1998             l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
1999         l.append('</select>')
2000         return '\n'.join(l)
2001 #    def checklist(self, ...)
2005 class MultilinkHTMLProperty(HTMLProperty):
2006     """ Multilink HTMLProperty
2008         Also be iterable, returning a wrapper object like the Link case for
2009         each entry in the multilink.
2010     """
2011     def __init__(self, *args, **kwargs):
2012         HTMLProperty.__init__(self, *args, **kwargs)
2013         if self._value:
2014             display_value = lookupIds(self._db, self._prop, self._value,
2015                 fail_ok=1, do_lookup=False)
2016             sortfun = make_sort_function(self._db, self._prop.classname)
2017             # sorting fails if the value contains
2018             # items not yet stored in the database
2019             # ignore these errors to preserve user input
2020             try:
2021                 display_value.sort(sortfun)
2022             except:
2023                 pass
2024             self._value = display_value
2026     def __len__(self):
2027         """ length of the multilink """
2028         return len(self._value)
2030     def __getattr__(self, attr):
2031         """ no extended attribute accesses make sense here """
2032         raise AttributeError, attr
2034     def viewableGenerator(self, values):
2035         """Used to iterate over only the View'able items in a class."""
2036         check = self._db.security.hasPermission
2037         userid = self._client.userid
2038         classname = self._prop.classname
2039         for value in values:
2040             if check('View', userid, classname, itemid=value):
2041                 yield HTMLItem(self._client, classname, value)
2043     def __iter__(self):
2044         """ iterate and return a new HTMLItem
2045         """
2046         return self.viewableGenerator(self._value)
2048     def reverse(self):
2049         """ return the list in reverse order
2050         """
2051         l = self._value[:]
2052         l.reverse()
2053         return self.viewableGenerator(l)
2055     def sorted(self, property):
2056         """ Return this multilink sorted by the given property """
2057         value = list(self.__iter__())
2058         value.sort(lambda a,b:cmp(a[property], b[property]))
2059         return value
2061     def __contains__(self, value):
2062         """ Support the "in" operator. We have to make sure the passed-in
2063             value is a string first, not a HTMLProperty.
2064         """
2065         return str(value) in self._value
2067     def isset(self):
2068         """Is my _value not []?"""
2069         return self._value != []
2071     def plain(self, escape=0):
2072         """ Render a "plain" representation of the property
2073         """
2074         if not self.is_view_ok():
2075             return self._('[hidden]')
2077         linkcl = self._db.classes[self._prop.classname]
2078         k = linkcl.labelprop(1)
2079         labels = []
2080         for v in self._value:
2081             if num_re.match(v):
2082                 try:
2083                     label = linkcl.get(v, k)
2084                 except IndexError:
2085                     label = None
2086                 # fall back to designator if label is None
2087                 if label is None: label = '%s%s'%(self._prop.classname, k)
2088             else:
2089                 label = v
2090             labels.append(label)
2091         value = ', '.join(labels)
2092         if escape:
2093             value = cgi.escape(value)
2094         return value
2096     def field(self, size=30, showid=0):
2097         """ Render a form edit field for the property
2099             If not editable, just display the value via plain().
2100         """
2101         if not self.is_edit_ok():
2102             return self.plain(escape=1)
2104         linkcl = self._db.getclass(self._prop.classname)
2105         value = self._value[:]
2106         # map the id to the label property
2107         if not linkcl.getkey():
2108             showid=1
2109         if not showid:
2110             k = linkcl.labelprop(1)
2111             value = lookupKeys(linkcl, k, value)
2112         value = ','.join(value)
2113         return self.input(name=self._formname, size=size, value=value)
2115     def menu(self, size=None, height=None, showid=0, additional=[],
2116              value=None, sort_on=None, **conditions):
2117         """ Render a form <select> list for this property.
2119             "size" is used to limit the length of the list labels
2120             "height" is used to set the <select> tag's "size" attribute
2121             "showid" includes the item ids in the list labels
2122             "additional" lists properties which should be included in the
2123                 label
2124             "value" specifies which item is pre-selected
2125             "sort_on" indicates the property to sort the list on as
2126                 (direction, property) where direction is '+' or '-'. A
2127                 single string with the direction prepended may be used.
2128                 For example: ('-', 'order'), '+name'.
2130             The remaining keyword arguments are used as conditions for
2131             filtering the items in the list - they're passed as the
2132             "filterspec" argument to a Class.filter() call.
2134             If not editable, just display the value via plain().
2135         """
2136         if not self.is_edit_ok():
2137             return self.plain(escape=1)
2139         if value is None:
2140             value = self._value
2142         linkcl = self._db.getclass(self._prop.classname)
2144         if sort_on is not None:
2145             if not isinstance(sort_on, tuple):
2146                 if sort_on[0] in '+-':
2147                     sort_on = (sort_on[0], sort_on[1:])
2148                 else:
2149                     sort_on = ('+', sort_on)
2150         else:
2151             sort_on = ('+', linkcl.orderprop())
2153         options = [opt
2154             for opt in linkcl.filter(None, conditions, sort_on)
2155             if self._db.security.hasPermission("View", self._client.userid,
2156                 linkcl.classname, itemid=opt)]
2158         # make sure we list the current values if they're retired
2159         for val in value:
2160             if val not in options:
2161                 options.insert(0, val)
2163         if not height:
2164             height = len(options)
2165             if value:
2166                 # The "no selection" option.
2167                 height += 1
2168             height = min(height, 7)
2169         l = ['<select multiple name="%s" size="%s">'%(self._formname, height)]
2170         k = linkcl.labelprop(1)
2172         if value:
2173             l.append('<option value="%s">- no selection -</option>'
2174                      % ','.join(['-' + v for v in value]))
2176         if additional:
2177             additional_fns = []
2178             props = linkcl.getprops()
2179             for propname in additional:
2180                 prop = props[propname]
2181                 if isinstance(prop, hyperdb.Link):
2182                     cl = self._db.getclass(prop.classname)
2183                     labelprop = cl.labelprop()
2184                     fn = lambda optionid: cl.get(linkcl.get(optionid,
2185                                                             propname),
2186                                                  labelprop)
2187                 else:
2188                     fn = lambda optionid: linkcl.get(optionid, propname)
2189             additional_fns.append(fn)
2191         for optionid in options:
2192             # get the option value, and if it's None use an empty string
2193             option = linkcl.get(optionid, k) or ''
2195             # figure if this option is selected
2196             s = ''
2197             if optionid in value or option in value:
2198                 s = 'selected="selected" '
2200             # figure the label
2201             if showid:
2202                 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
2203             else:
2204                 lab = option
2205             # truncate if it's too long
2206             if size is not None and len(lab) > size:
2207                 lab = lab[:size-3] + '...'
2208             if additional:
2209                 m = []
2210                 for fn in additional_fns:
2211                     m.append(str(fn(optionid)))
2212                 lab = lab + ' (%s)'%', '.join(m)
2214             # and generate
2215             lab = cgi.escape(self._(lab))
2216             l.append('<option %svalue="%s">%s</option>'%(s, optionid,
2217                 lab))
2218         l.append('</select>')
2219         return '\n'.join(l)
2221 # set the propclasses for HTMLItem
2222 propclasses = (
2223     (hyperdb.String, StringHTMLProperty),
2224     (hyperdb.Number, NumberHTMLProperty),
2225     (hyperdb.Boolean, BooleanHTMLProperty),
2226     (hyperdb.Date, DateHTMLProperty),
2227     (hyperdb.Interval, IntervalHTMLProperty),
2228     (hyperdb.Password, PasswordHTMLProperty),
2229     (hyperdb.Link, LinkHTMLProperty),
2230     (hyperdb.Multilink, MultilinkHTMLProperty),
2233 def make_sort_function(db, classname, sort_on=None):
2234     """Make a sort function for a given class
2235     """
2236     linkcl = db.getclass(classname)
2237     if sort_on is None:
2238         sort_on = linkcl.orderprop()
2239     def sortfunc(a, b):
2240         return cmp(linkcl.get(a, sort_on), linkcl.get(b, sort_on))
2241     return sortfunc
2243 def handleListCGIValue(value):
2244     """ Value is either a single item or a list of items. Each item has a
2245         .value that we're actually interested in.
2246     """
2247     if isinstance(value, type([])):
2248         return [value.value for value in value]
2249     else:
2250         value = value.value.strip()
2251         if not value:
2252             return []
2253         return [v.strip() for v in value.split(',')]
2255 class HTMLRequest(HTMLInputMixin):
2256     """The *request*, holding the CGI form and environment.
2258     - "form" the CGI form as a cgi.FieldStorage
2259     - "env" the CGI environment variables
2260     - "base" the base URL for this instance
2261     - "user" a HTMLItem instance for this user
2262     - "language" as determined by the browser or config
2263     - "classname" the current classname (possibly None)
2264     - "template" the current template (suffix, also possibly None)
2266     Index args:
2268     - "columns" dictionary of the columns to display in an index page
2269     - "show" a convenience access to columns - request/show/colname will
2270       be true if the columns should be displayed, false otherwise
2271     - "sort" index sort column (direction, column name)
2272     - "group" index grouping property (direction, column name)
2273     - "filter" properties to filter the index on
2274     - "filterspec" values to filter the index on
2275     - "search_text" text to perform a full-text search on for an index
2276     """
2277     def __repr__(self):
2278         return '<HTMLRequest %r>'%self.__dict__
2280     def __init__(self, client):
2281         # _client is needed by HTMLInputMixin
2282         self._client = self.client = client
2284         # easier access vars
2285         self.form = client.form
2286         self.env = client.env
2287         self.base = client.base
2288         self.user = HTMLItem(client, 'user', client.userid)
2289         self.language = client.language
2291         # store the current class name and action
2292         self.classname = client.classname
2293         self.nodeid = client.nodeid
2294         self.template = client.template
2296         # the special char to use for special vars
2297         self.special_char = '@'
2299         HTMLInputMixin.__init__(self)
2301         self._post_init()
2303     def current_url(self):
2304         url = self.base
2305         if self.classname:
2306             url += self.classname
2307             if self.nodeid:
2308                 url += self.nodeid
2309         args = {}
2310         if self.template:
2311             args['@template'] = self.template
2312         return self.indexargs_url(url, args)
2314     def _parse_sort(self, var, name):
2315         """ Parse sort/group options. Append to var
2316         """
2317         fields = []
2318         dirs = []
2319         for special in '@:':
2320             idx = 0
2321             key = '%s%s%d'%(special, name, idx)
2322             while key in self.form:
2323                 self.special_char = special
2324                 fields.append(self.form.getfirst(key))
2325                 dirkey = '%s%sdir%d'%(special, name, idx)
2326                 if dirkey in self.form:
2327                     dirs.append(self.form.getfirst(dirkey))
2328                 else:
2329                     dirs.append(None)
2330                 idx += 1
2331                 key = '%s%s%d'%(special, name, idx)
2332             # backward compatible (and query) URL format
2333             key = special + name
2334             dirkey = key + 'dir'
2335             if key in self.form and not fields:
2336                 fields = handleListCGIValue(self.form[key])
2337                 if dirkey in self.form:
2338                     dirs.append(self.form.getfirst(dirkey))
2339             if fields: # only try other special char if nothing found
2340                 break
2341         for f, d in map(None, fields, dirs):
2342             if f.startswith('-'):
2343                 var.append(('-', f[1:]))
2344             elif d:
2345                 var.append(('-', f))
2346             else:
2347                 var.append(('+', f))
2349     def _post_init(self):
2350         """ Set attributes based on self.form
2351         """
2352         # extract the index display information from the form
2353         self.columns = []
2354         for name in ':columns @columns'.split():
2355             if self.form.has_key(name):
2356                 self.special_char = name[0]
2357                 self.columns = handleListCGIValue(self.form[name])
2358                 break
2359         self.show = support.TruthDict(self.columns)
2361         # sorting and grouping
2362         self.sort = []
2363         self.group = []
2364         self._parse_sort(self.sort, 'sort')
2365         self._parse_sort(self.group, 'group')
2367         # filtering
2368         self.filter = []
2369         for name in ':filter @filter'.split():
2370             if self.form.has_key(name):
2371                 self.special_char = name[0]
2372                 self.filter = handleListCGIValue(self.form[name])
2374         self.filterspec = {}
2375         db = self.client.db
2376         if self.classname is not None:
2377             cls = db.getclass (self.classname)
2378             for name in self.filter:
2379                 if not self.form.has_key(name):
2380                     continue
2381                 prop = cls.get_transitive_prop (name)
2382                 fv = self.form[name]
2383                 if (isinstance(prop, hyperdb.Link) or
2384                         isinstance(prop, hyperdb.Multilink)):
2385                     self.filterspec[name] = lookupIds(db, prop,
2386                         handleListCGIValue(fv))
2387                 else:
2388                     if isinstance(fv, type([])):
2389                         self.filterspec[name] = [v.value for v in fv]
2390                     elif name == 'id':
2391                         # special case "id" property
2392                         self.filterspec[name] = handleListCGIValue(fv)
2393                     else:
2394                         self.filterspec[name] = fv.value
2396         # full-text search argument
2397         self.search_text = None
2398         for name in ':search_text @search_text'.split():
2399             if self.form.has_key(name):
2400                 self.special_char = name[0]
2401                 self.search_text = self.form.getfirst(name)
2403         # pagination - size and start index
2404         # figure batch args
2405         self.pagesize = 50
2406         for name in ':pagesize @pagesize'.split():
2407             if self.form.has_key(name):
2408                 self.special_char = name[0]
2409                 try:
2410                     self.pagesize = int(self.form.getfirst(name))
2411                 except ValueError:
2412                     # not an integer - ignore
2413                     pass
2415         self.startwith = 0
2416         for name in ':startwith @startwith'.split():
2417             if self.form.has_key(name):
2418                 self.special_char = name[0]
2419                 try:
2420                     self.startwith = int(self.form.getfirst(name))
2421                 except ValueError:
2422                     # not an integer - ignore
2423                     pass
2425         # dispname
2426         if self.form.has_key('@dispname'):
2427             self.dispname = self.form.getfirst('@dispname')
2428         else:
2429             self.dispname = None
2431     def updateFromURL(self, url):
2432         """ Parse the URL for query args, and update my attributes using the
2433             values.
2434         """
2435         env = {'QUERY_STRING': url}
2436         self.form = cgi.FieldStorage(environ=env)
2438         self._post_init()
2440     def update(self, kwargs):
2441         """ Update my attributes using the keyword args
2442         """
2443         self.__dict__.update(kwargs)
2444         if kwargs.has_key('columns'):
2445             self.show = support.TruthDict(self.columns)
2447     def description(self):
2448         """ Return a description of the request - handle for the page title.
2449         """
2450         s = [self.client.db.config.TRACKER_NAME]
2451         if self.classname:
2452             if self.client.nodeid:
2453                 s.append('- %s%s'%(self.classname, self.client.nodeid))
2454             else:
2455                 if self.template == 'item':
2456                     s.append('- new %s'%self.classname)
2457                 elif self.template == 'index':
2458                     s.append('- %s index'%self.classname)
2459                 else:
2460                     s.append('- %s %s'%(self.classname, self.template))
2461         else:
2462             s.append('- home')
2463         return ' '.join(s)
2465     def __str__(self):
2466         d = {}
2467         d.update(self.__dict__)
2468         f = ''
2469         for k in self.form.keys():
2470             f += '\n      %r=%r'%(k,handleListCGIValue(self.form[k]))
2471         d['form'] = f
2472         e = ''
2473         for k,v in self.env.items():
2474             e += '\n     %r=%r'%(k, v)
2475         d['env'] = e
2476         return """
2477 form: %(form)s
2478 base: %(base)r
2479 classname: %(classname)r
2480 template: %(template)r
2481 columns: %(columns)r
2482 sort: %(sort)r
2483 group: %(group)r
2484 filter: %(filter)r
2485 search_text: %(search_text)r
2486 pagesize: %(pagesize)r
2487 startwith: %(startwith)r
2488 env: %(env)s
2489 """%d
2491     def indexargs_form(self, columns=1, sort=1, group=1, filter=1,
2492             filterspec=1, search_text=1):
2493         """ return the current index args as form elements """
2494         l = []
2495         sc = self.special_char
2496         def add(k, v):
2497             l.append(self.input(type="hidden", name=k, value=v))
2498         if columns and self.columns:
2499             add(sc+'columns', ','.join(self.columns))
2500         if sort:
2501             val = []
2502             for dir, attr in self.sort:
2503                 if dir == '-':
2504                     val.append('-'+attr)
2505                 else:
2506                     val.append(attr)
2507             add(sc+'sort', ','.join (val))
2508         if group:
2509             val = []
2510             for dir, attr in self.group:
2511                 if dir == '-':
2512                     val.append('-'+attr)
2513                 else:
2514                     val.append(attr)
2515             add(sc+'group', ','.join (val))
2516         if filter and self.filter:
2517             add(sc+'filter', ','.join(self.filter))
2518         if self.classname and filterspec:
2519             cls = self.client.db.getclass(self.classname)
2520             for k,v in self.filterspec.items():
2521                 if type(v) == type([]):
2522                     if isinstance(cls.get_transitive_prop(k), hyperdb.String):
2523                         add(k, ' '.join(v))
2524                     else:
2525                         add(k, ','.join(v))
2526                 else:
2527                     add(k, v)
2528         if search_text and self.search_text:
2529             add(sc+'search_text', self.search_text)
2530         add(sc+'pagesize', self.pagesize)
2531         add(sc+'startwith', self.startwith)
2532         return '\n'.join(l)
2534     def indexargs_url(self, url, args):
2535         """ Embed the current index args in a URL
2536         """
2537         q = urllib.quote
2538         sc = self.special_char
2539         l = ['%s=%s'%(k,v) for k,v in args.items()]
2541         # pull out the special values (prefixed by @ or :)
2542         specials = {}
2543         for key in args.keys():
2544             if key[0] in '@:':
2545                 specials[key[1:]] = args[key]
2547         # ok, now handle the specials we received in the request
2548         if self.columns and not specials.has_key('columns'):
2549             l.append(sc+'columns=%s'%(','.join(self.columns)))
2550         if self.sort and not specials.has_key('sort'):
2551             val = []
2552             for dir, attr in self.sort:
2553                 if dir == '-':
2554                     val.append('-'+attr)
2555                 else:
2556                     val.append(attr)
2557             l.append(sc+'sort=%s'%(','.join(val)))
2558         if self.group and not specials.has_key('group'):
2559             val = []
2560             for dir, attr in self.group:
2561                 if dir == '-':
2562                     val.append('-'+attr)
2563                 else:
2564                     val.append(attr)
2565             l.append(sc+'group=%s'%(','.join(val)))
2566         if self.filter and not specials.has_key('filter'):
2567             l.append(sc+'filter=%s'%(','.join(self.filter)))
2568         if self.search_text and not specials.has_key('search_text'):
2569             l.append(sc+'search_text=%s'%q(self.search_text))
2570         if not specials.has_key('pagesize'):
2571             l.append(sc+'pagesize=%s'%self.pagesize)
2572         if not specials.has_key('startwith'):
2573             l.append(sc+'startwith=%s'%self.startwith)
2575         # finally, the remainder of the filter args in the request
2576         if self.classname and self.filterspec:
2577             cls = self.client.db.getclass(self.classname)
2578             for k,v in self.filterspec.items():
2579                 if not args.has_key(k):
2580                     if type(v) == type([]):
2581                         prop = cls.get_transitive_prop(k)
2582                         if k != 'id' and isinstance(prop, hyperdb.String):
2583                             l.append('%s=%s'%(k, '%20'.join([q(i) for i in v])))
2584                         else:
2585                             l.append('%s=%s'%(k, ','.join([q(i) for i in v])))
2586                     else:
2587                         l.append('%s=%s'%(k, q(v)))
2588         return '%s?%s'%(url, '&'.join(l))
2589     indexargs_href = indexargs_url
2591     def base_javascript(self):
2592         return """
2593 <script type="text/javascript">
2594 submitted = false;
2595 function submit_once() {
2596     if (submitted) {
2597         alert("Your request is being processed.\\nPlease be patient.");
2598         event.returnValue = 0;    // work-around for IE
2599         return 0;
2600     }
2601     submitted = true;
2602     return 1;
2605 function help_window(helpurl, width, height) {
2606     HelpWin = window.open('%s' + helpurl, 'RoundupHelpWindow', 'scrollbars=yes,resizable=yes,toolbar=no,height='+height+',width='+width);
2608 </script>
2609 """%self.base
2611     def batch(self):
2612         """ Return a batch object for results from the "current search"
2613         """
2614         filterspec = self.filterspec
2615         sort = self.sort
2616         group = self.group
2618         # get the list of ids we're batching over
2619         klass = self.client.db.getclass(self.classname)
2620         if self.search_text:
2621             matches = self.client.db.indexer.search(
2622                 [w.upper().encode("utf-8", "replace") for w in re.findall(
2623                     r'(?u)\b\w{2,25}\b',
2624                     unicode(self.search_text, "utf-8", "replace")
2625                 )], klass)
2626         else:
2627             matches = None
2629         # filter for visibility
2630         check = self._client.db.security.hasPermission
2631         userid = self._client.userid
2632         l = [id for id in klass.filter(matches, filterspec, sort, group)
2633             if check('View', userid, self.classname, itemid=id)]
2635         # return the batch object, using IDs only
2636         return Batch(self.client, l, self.pagesize, self.startwith,
2637             classname=self.classname)
2639 # extend the standard ZTUtils Batch object to remove dependency on
2640 # Acquisition and add a couple of useful methods
2641 class Batch(ZTUtils.Batch):
2642     """ Use me to turn a list of items, or item ids of a given class, into a
2643         series of batches.
2645         ========= ========================================================
2646         Parameter  Usage
2647         ========= ========================================================
2648         sequence  a list of HTMLItems or item ids
2649         classname if sequence is a list of ids, this is the class of item
2650         size      how big to make the sequence.
2651         start     where to start (0-indexed) in the sequence.
2652         end       where to end (0-indexed) in the sequence.
2653         orphan    if the next batch would contain less items than this
2654                   value, then it is combined with this batch
2655         overlap   the number of items shared between adjacent batches
2656         ========= ========================================================
2658         Attributes: Note that the "start" attribute, unlike the
2659         argument, is a 1-based index (I know, lame).  "first" is the
2660         0-based index.  "length" is the actual number of elements in
2661         the batch.
2663         "sequence_length" is the length of the original, unbatched, sequence.
2664     """
2665     def __init__(self, client, sequence, size, start, end=0, orphan=0,
2666             overlap=0, classname=None):
2667         self.client = client
2668         self.last_index = self.last_item = None
2669         self.current_item = None
2670         self.classname = classname
2671         self.sequence_length = len(sequence)
2672         ZTUtils.Batch.__init__(self, sequence, size, start, end, orphan,
2673             overlap)
2675     # overwrite so we can late-instantiate the HTMLItem instance
2676     def __getitem__(self, index):
2677         if index < 0:
2678             if index + self.end < self.first: raise IndexError, index
2679             return self._sequence[index + self.end]
2681         if index >= self.length:
2682             raise IndexError, index
2684         # move the last_item along - but only if the fetched index changes
2685         # (for some reason, index 0 is fetched twice)
2686         if index != self.last_index:
2687             self.last_item = self.current_item
2688             self.last_index = index
2690         item = self._sequence[index + self.first]
2691         if self.classname:
2692             # map the item ids to instances
2693             item = HTMLItem(self.client, self.classname, item)
2694         self.current_item = item
2695         return item
2697     def propchanged(self, *properties):
2698         """ Detect if one of the properties marked as being a group
2699             property changed in the last iteration fetch
2700         """
2701         # we poke directly at the _value here since MissingValue can screw
2702         # us up and cause Nones to compare strangely
2703         if self.last_item is None:
2704             return 1
2705         for property in properties:
2706             if property == 'id' or isinstance (self.last_item[property], list):
2707                 if (str(self.last_item[property]) !=
2708                     str(self.current_item[property])):
2709                     return 1
2710             else:
2711                 if (self.last_item[property]._value !=
2712                     self.current_item[property]._value):
2713                     return 1
2714         return 0
2716     # override these 'cos we don't have access to acquisition
2717     def previous(self):
2718         if self.start == 1:
2719             return None
2720         return Batch(self.client, self._sequence, self._size,
2721             self.first - self._size + self.overlap, 0, self.orphan,
2722             self.overlap)
2724     def next(self):
2725         try:
2726             self._sequence[self.end]
2727         except IndexError:
2728             return None
2729         return Batch(self.client, self._sequence, self._size,
2730             self.end - self.overlap, 0, self.orphan, self.overlap)
2732 class TemplatingUtils:
2733     """ Utilities for templating
2734     """
2735     def __init__(self, client):
2736         self.client = client
2737     def Batch(self, sequence, size, start, end=0, orphan=0, overlap=0):
2738         return Batch(self.client, sequence, size, start, end, orphan,
2739             overlap)
2741     def url_quote(self, url):
2742         """URL-quote the supplied text."""
2743         return urllib.quote(url)
2745     def html_quote(self, html):
2746         """HTML-quote the supplied text."""
2747         return cgi.escape(html)
2749     def __getattr__(self, name):
2750         """Try the tracker's templating_utils."""
2751         if not hasattr(self.client.instance, 'templating_utils'):
2752             # backwards-compatibility
2753             raise AttributeError, name
2754         if not self.client.instance.templating_utils.has_key(name):
2755             raise AttributeError, name
2756         return self.client.instance.templating_utils[name]
2758     def html_calendar(self, request):
2759         """Generate a HTML calendar.
2761         `request`  the roundup.request object
2762                    - @template : name of the template
2763                    - form      : name of the form to store back the date
2764                    - property  : name of the property of the form to store
2765                                  back the date
2766                    - date      : current date
2767                    - display   : when browsing, specifies year and month
2769         html will simply be a table.
2770         """
2771         date_str  = request.form.getfirst("date", ".")
2772         display   = request.form.getfirst("display", date_str)
2773         template  = request.form.getfirst("@template", "calendar")
2774         form      = request.form.getfirst("form")
2775         property  = request.form.getfirst("property")
2776         curr_date = date.Date(date_str) # to highlight
2777         display   = date.Date(display)  # to show
2778         day       = display.day
2780         # for navigation
2781         date_prev_month = display + date.Interval("-1m")
2782         date_next_month = display + date.Interval("+1m")
2783         date_prev_year  = display + date.Interval("-1y")
2784         date_next_year  = display + date.Interval("+1y")
2786         res = []
2788         base_link = "%s?@template=%s&property=%s&form=%s&date=%s" % \
2789                     (request.classname, template, property, form, curr_date)
2791         # navigation
2792         # month
2793         res.append('<table class="calendar"><tr><td>')
2794         res.append(' <table width="100%" class="calendar_nav"><tr>')
2795         link = "&display=%s"%date_prev_month
2796         res.append('  <td><a href="%s&display=%s">&lt;</a></td>'%(base_link,
2797             date_prev_month))
2798         res.append('  <td>%s</td>'%calendar.month_name[display.month])
2799         res.append('  <td><a href="%s&display=%s">&gt;</a></td>'%(base_link,
2800             date_next_month))
2801         # spacer
2802         res.append('  <td width="100%"></td>')
2803         # year
2804         res.append('  <td><a href="%s&display=%s">&lt;</a></td>'%(base_link,
2805             date_prev_year))
2806         res.append('  <td>%s</td>'%display.year)
2807         res.append('  <td><a href="%s&display=%s">&gt;</a></td>'%(base_link,
2808             date_next_year))
2809         res.append(' </tr></table>')
2810         res.append(' </td></tr>')
2812         # the calendar
2813         res.append(' <tr><td><table class="calendar_display">')
2814         res.append('  <tr class="weekdays">')
2815         for day in calendar.weekheader(3).split():
2816             res.append('   <td>%s</td>'%day)
2817         res.append('  </tr>')
2818         for week in calendar.monthcalendar(display.year, display.month):
2819             res.append('  <tr>')
2820             for day in week:
2821                 link = "javascript:form[field].value = '%d-%02d-%02d'; " \
2822                       "window.close ();"%(display.year, display.month, day)
2823                 if (day == curr_date.day and display.month == curr_date.month
2824                         and display.year == curr_date.year):
2825                     # highlight
2826                     style = "today"
2827                 else :
2828                     style = ""
2829                 if day:
2830                     res.append('   <td class="%s"><a href="%s">%s</a></td>'%(
2831                         style, link, day))
2832                 else :
2833                     res.append('   <td></td>')
2834             res.append('  </tr>')
2835         res.append('</table></td></tr></table>')
2836         return "\n".join(res)
2838 class MissingValue:
2839     def __init__(self, description, **kwargs):
2840         self.__description = description
2841         for key, value in kwargs.items():
2842             self.__dict__[key] = value
2844     def __call__(self, *args, **kwargs): return MissingValue(self.__description)
2845     def __getattr__(self, name):
2846         # This allows assignments which assume all intermediate steps are Null
2847         # objects if they don't exist yet.
2848         #
2849         # For example (with just 'client' defined):
2850         #
2851         # client.db.config.TRACKER_WEB = 'BASE/'
2852         self.__dict__[name] = MissingValue(self.__description)
2853         return getattr(self, name)
2855     def __getitem__(self, key): return self
2856     def __nonzero__(self): return 0
2857     def __str__(self): return '[%s]'%self.__description
2858     def __repr__(self): return '<MissingValue 0x%x "%s">'%(id(self),
2859         self.__description)
2860     def gettext(self, str): return str
2861     _ = gettext
2863 # vim: set et sts=4 sw=4 :