Code

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