Code

Fix designator regular expression in HTMLDatabase.__getitem__.
[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)
477 class HTMLClass(HTMLInputMixin, HTMLPermissions):
478     """ Accesses through a class (either through *class* or *db.<classname>*)
479     """
480     def __init__(self, client, classname, anonymous=0):
481         self._client = client
482         self._ = client._
483         self._db = client.db
484         self._anonymous = anonymous
486         # we want classname to be exposed, but _classname gives a
487         # consistent API for extending Class/Item
488         self._classname = self.classname = classname
489         self._klass = self._db.getclass(self.classname)
490         self._props = self._klass.getprops()
492         HTMLInputMixin.__init__(self)
494     def is_edit_ok(self):
495         """ Is the user allowed to Create the current class?
496         """
497         return self._db.security.hasPermission('Create', self._client.userid,
498             self._classname)
500     def is_view_ok(self):
501         """ Is the user allowed to View the current class?
502         """
503         return self._db.security.hasPermission('View', self._client.userid,
504             self._classname)
506     def is_only_view_ok(self):
507         """ Is the user only allowed to View (ie. not Create) the current class?
508         """
509         return self.is_view_ok() and not self.is_edit_ok()
511     def __repr__(self):
512         return '<HTMLClass(0x%x) %s>'%(id(self), self.classname)
514     def __getitem__(self, item):
515         """ return an HTMLProperty instance
516         """
518         # we don't exist
519         if item == 'id':
520             return None
522         # get the property
523         try:
524             prop = self._props[item]
525         except KeyError:
526             raise KeyError, 'No such property "%s" on %s'%(item, self.classname)
528         # look up the correct HTMLProperty class
529         form = self._client.form
530         for klass, htmlklass in propclasses:
531             if not isinstance(prop, klass):
532                 continue
533             if form.has_key(item):
534                 if isinstance(prop, hyperdb.Multilink):
535                     value = lookupIds(self._db, prop,
536                         handleListCGIValue(form[item]), fail_ok=1)
537                 elif isinstance(prop, hyperdb.Link):
538                     value = form.getfirst(item).strip()
539                     if value:
540                         value = lookupIds(self._db, prop, [value],
541                             fail_ok=1)[0]
542                     else:
543                         value = None
544                 else:
545                     value = form.getfirst(item).strip() or None
546             else:
547                 if isinstance(prop, hyperdb.Multilink):
548                     value = []
549                 else:
550                     value = None
551             return htmlklass(self._client, self._classname, None, prop, item,
552                 value, self._anonymous)
554         # no good
555         raise KeyError, item
557     def __getattr__(self, attr):
558         """ convenience access """
559         try:
560             return self[attr]
561         except KeyError:
562             raise AttributeError, attr
564     def designator(self):
565         """ Return this class' designator (classname) """
566         return self._classname
568     def getItem(self, itemid, num_re=num_re):
569         """ Get an item of this class by its item id.
570         """
571         # make sure we're looking at an itemid
572         if not isinstance(itemid, type(1)) and not num_re.match(itemid):
573             itemid = self._klass.lookup(itemid)
575         return HTMLItem(self._client, self.classname, itemid)
577     def properties(self, sort=1):
578         """ Return HTMLProperty for all of this class' properties.
579         """
580         l = []
581         for name, prop in self._props.items():
582             for klass, htmlklass in propclasses:
583                 if isinstance(prop, hyperdb.Multilink):
584                     value = []
585                 else:
586                     value = None
587                 if isinstance(prop, klass):
588                     l.append(htmlklass(self._client, self._classname, '',
589                         prop, name, value, self._anonymous))
590         if sort:
591             l.sort(lambda a,b:cmp(a._name, b._name))
592         return l
594     def list(self, sort_on=None):
595         """ List all items in this class.
596         """
597         # get the list and sort it nicely
598         l = self._klass.list()
599         sortfunc = make_sort_function(self._db, self._classname, sort_on)
600         l.sort(sortfunc)
602         # check perms
603         check = self._client.db.security.hasPermission
604         userid = self._client.userid
606         l = [HTMLItem(self._client, self._classname, id) for id in l
607             if check('View', userid, self._classname, itemid=id)]
609         return l
611     def csv(self):
612         """ Return the items of this class as a chunk of CSV text.
613         """
614         props = self.propnames()
615         s = StringIO.StringIO()
616         writer = csv.writer(s)
617         writer.writerow(props)
618         for nodeid in self._klass.list():
619             l = []
620             for name in props:
621                 value = self._klass.get(nodeid, name)
622                 if value is None:
623                     l.append('')
624                 elif isinstance(value, type([])):
625                     l.append(':'.join(map(str, value)))
626                 else:
627                     l.append(str(self._klass.get(nodeid, name)))
628             writer.writerow(l)
629         return s.getvalue()
631     def propnames(self):
632         """ Return the list of the names of the properties of this class.
633         """
634         idlessprops = self._klass.getprops(protected=0).keys()
635         idlessprops.sort()
636         return ['id'] + idlessprops
638     def filter(self, request=None, filterspec={}, sort=[], group=[]):
639         """ Return a list of items from this class, filtered and sorted
640             by the current requested filterspec/filter/sort/group args
642             "request" takes precedence over the other three arguments.
643         """
644         if request is not None:
645             filterspec = request.filterspec
646             sort = request.sort
647             group = request.group
649         check = self._db.security.hasPermission
650         userid = self._client.userid
652         l = [HTMLItem(self._client, self.classname, id)
653              for id in self._klass.filter(None, filterspec, sort, group)
654              if check('View', userid, self.classname, itemid=id)]
655         return l
657     def classhelp(self, properties=None, label=''"(list)", width='500',
658             height='400', property='', form='itemSynopsis',
659             pagesize=50, inputtype="checkbox", sort=None, filter=None):
660         """Pop up a javascript window with class help
662         This generates a link to a popup window which displays the
663         properties indicated by "properties" of the class named by
664         "classname". The "properties" should be a comma-separated list
665         (eg. 'id,name,description'). Properties defaults to all the
666         properties of a class (excluding id, creator, created and
667         activity).
669         You may optionally override the label displayed, the width,
670         the height, the number of items per page and the field on which
671         the list is sorted (defaults to username if in the displayed
672         properties).
674         With the "filter" arg it is possible to specify a filter for
675         which items are supposed to be displayed. It has to be of
676         the format "<field>=<values>;<field>=<values>;...".
678         The popup window will be resizable and scrollable.
680         If the "property" arg is given, it's passed through to the
681         javascript help_window function.
683         You can use inputtype="radio" to display a radio box instead
684         of the default checkbox (useful for entering Link-properties)
686         If the "form" arg is given, it's passed through to the
687         javascript help_window function. - it's the name of the form
688         the "property" belongs to.
689         """
690         if properties is None:
691             properties = self._klass.getprops(protected=0).keys()
692             properties.sort()
693             properties = ','.join(properties)
694         if sort is None:
695             if 'username' in properties.split( ',' ):
696                 sort = 'username'
697             else:
698                 sort = self._klass.orderprop()
699         sort = '&amp;@sort=' + sort
700         if property:
701             property = '&amp;property=%s'%property
702         if form:
703             form = '&amp;form=%s'%form
704         if inputtype:
705             type= '&amp;type=%s'%inputtype
706         if filter:
707             filterprops = filter.split(';')
708             filtervalues = []
709             names = []
710             for x in filterprops:
711                 (name, values) = x.split('=')
712                 names.append(name)
713                 filtervalues.append('&amp;%s=%s' % (name, urllib.quote(values)))
714             filter = '&amp;@filter=%s%s' % (','.join(names), ''.join(filtervalues))
715         else:
716            filter = ''
717         help_url = "%s?@startwith=0&amp;@template=help&amp;"\
718                    "properties=%s%s%s%s%s&amp;@pagesize=%s%s" % \
719                    (self.classname, properties, property, form, type,
720                    sort, pagesize, filter)
721         onclick = "javascript:help_window('%s', '%s', '%s');return false;" % \
722                   (help_url, width, height)
723         return '<a class="classhelp" href="%s" onclick="%s">%s</a>' % \
724                (help_url, onclick, self._(label))
726     def submit(self, label=''"Submit New Entry", action="new"):
727         """ Generate a submit button (and action hidden element)
729         Generate nothing if we're not editable.
730         """
731         if not self.is_edit_ok():
732             return ''
734         return self.input(type="hidden", name="@action", value=action) + \
735             '\n' + \
736             self.input(type="submit", name="submit_button", value=self._(label))
738     def history(self):
739         if not self.is_view_ok():
740             return self._('[hidden]')
741         return self._('New node - no history')
743     def renderWith(self, name, **kwargs):
744         """ Render this class with the given template.
745         """
746         # create a new request and override the specified args
747         req = HTMLRequest(self._client)
748         req.classname = self.classname
749         req.update(kwargs)
751         # new template, using the specified classname and request
752         pt = self._client.instance.templates.get(self.classname, name)
754         # use our fabricated request
755         args = {
756             'ok_message': self._client.ok_message,
757             'error_message': self._client.error_message
758         }
759         return pt.render(self._client, self.classname, req, **args)
761 class _HTMLItem(HTMLInputMixin, HTMLPermissions):
762     """ Accesses through an *item*
763     """
764     def __init__(self, client, classname, nodeid, anonymous=0):
765         self._client = client
766         self._db = client.db
767         self._classname = classname
768         self._nodeid = nodeid
769         self._klass = self._db.getclass(classname)
770         self._props = self._klass.getprops()
772         # do we prefix the form items with the item's identification?
773         self._anonymous = anonymous
775         HTMLInputMixin.__init__(self)
777     def is_edit_ok(self):
778         """ Is the user allowed to Edit the current class?
779         """
780         return self._db.security.hasPermission('Edit', self._client.userid,
781             self._classname, itemid=self._nodeid)
783     def is_view_ok(self):
784         """ Is the user allowed to View the current class?
785         """
786         if self._db.security.hasPermission('View', self._client.userid,
787                 self._classname, itemid=self._nodeid):
788             return 1
789         return self.is_edit_ok()
791     def is_only_view_ok(self):
792         """ Is the user only allowed to View (ie. not Edit) the current class?
793         """
794         return self.is_view_ok() and not self.is_edit_ok()
796     def __repr__(self):
797         return '<HTMLItem(0x%x) %s %s>'%(id(self), self._classname,
798             self._nodeid)
800     def __getitem__(self, item):
801         """ return an HTMLProperty instance
802             this now can handle transitive lookups where item is of the
803             form x.y.z
804         """
805         if item == 'id':
806             return self._nodeid
808         items = item.split('.', 1)
809         has_rest = len(items) > 1
811         # get the property
812         prop = self._props[items[0]]
814         if has_rest and not isinstance(prop, (hyperdb.Link, hyperdb.Multilink)):
815             raise KeyError, item
817         # get the value, handling missing values
818         value = None
819         if int(self._nodeid) > 0:
820             value = self._klass.get(self._nodeid, items[0], None)
821         if value is None:
822             if isinstance(prop, hyperdb.Multilink):
823                 value = []
825         # look up the correct HTMLProperty class
826         htmlprop = None
827         for klass, htmlklass in propclasses:
828             if isinstance(prop, klass):
829                 htmlprop = htmlklass(self._client, self._classname,
830                     self._nodeid, prop, items[0], value, self._anonymous)
831         if htmlprop is not None:
832             if has_rest:
833                 if isinstance(htmlprop, MultilinkHTMLProperty):
834                     return [h[items[1]] for h in htmlprop]
835                 return htmlprop[items[1]]
836             return htmlprop
838         raise KeyError, item
840     def __getattr__(self, attr):
841         """ convenience access to properties """
842         try:
843             return self[attr]
844         except KeyError:
845             raise AttributeError, attr
847     def designator(self):
848         """Return this item's designator (classname + id)."""
849         return '%s%s'%(self._classname, self._nodeid)
851     def is_retired(self):
852         """Is this item retired?"""
853         return self._klass.is_retired(self._nodeid)
855     def submit(self, label=''"Submit Changes", action="edit"):
856         """Generate a submit button.
858         Also sneak in the lastactivity and action hidden elements.
859         """
860         return self.input(type="hidden", name="@lastactivity",
861             value=self.activity.local(0)) + '\n' + \
862             self.input(type="hidden", name="@action", value=action) + '\n' + \
863             self.input(type="submit", name="submit_button", value=self._(label))
865     def journal(self, direction='descending'):
866         """ Return a list of HTMLJournalEntry instances.
867         """
868         # XXX do this
869         return []
871     def history(self, direction='descending', dre=re.compile('^\d+$')):
872         if not self.is_view_ok():
873             return self._('[hidden]')
875         # pre-load the history with the current state
876         current = {}
877         for prop_n in self._props.keys():
878             prop = self[prop_n]
879             if not isinstance(prop, HTMLProperty):
880                 continue
881             current[prop_n] = prop.plain(escape=1)
882             # make link if hrefable
883             if (self._props.has_key(prop_n) and
884                     isinstance(self._props[prop_n], hyperdb.Link)):
885                 classname = self._props[prop_n].classname
886                 try:
887                     template = find_template(self._db.config.TEMPLATES,
888                         classname, 'item')
889                     if template[1].startswith('_generic'):
890                         raise NoTemplate, 'not really...'
891                 except NoTemplate:
892                     pass
893                 else:
894                     id = self._klass.get(self._nodeid, prop_n, None)
895                     current[prop_n] = '<a href="%s%s">%s</a>'%(
896                         classname, id, current[prop_n])
898         # get the journal, sort and reverse
899         history = self._klass.history(self._nodeid)
900         history.sort()
901         history.reverse()
903         timezone = self._db.getUserTimezone()
904         l = []
905         comments = {}
906         for id, evt_date, user, action, args in history:
907             date_s = str(evt_date.local(timezone)).replace("."," ")
908             arg_s = ''
909             if action == 'link' and type(args) == type(()):
910                 if len(args) == 3:
911                     linkcl, linkid, key = args
912                     arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
913                         linkcl, linkid, key)
914                 else:
915                     arg_s = str(args)
917             elif action == 'unlink' and type(args) == type(()):
918                 if len(args) == 3:
919                     linkcl, linkid, key = args
920                     arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
921                         linkcl, linkid, key)
922                 else:
923                     arg_s = str(args)
925             elif type(args) == type({}):
926                 cell = []
927                 for k in args.keys():
928                     # try to get the relevant property and treat it
929                     # specially
930                     try:
931                         prop = self._props[k]
932                     except KeyError:
933                         prop = None
934                     if prop is None:
935                         # property no longer exists
936                         comments['no_exist'] = self._(
937                             "<em>The indicated property no longer exists</em>")
938                         cell.append(self._('<em>%s: %s</em>\n')
939                             % (self._(k), str(args[k])))
940                         continue
942                     if args[k] and (isinstance(prop, hyperdb.Multilink) or
943                             isinstance(prop, hyperdb.Link)):
944                         # figure what the link class is
945                         classname = prop.classname
946                         try:
947                             linkcl = self._db.getclass(classname)
948                         except KeyError:
949                             labelprop = None
950                             comments[classname] = self._(
951                                 "The linked class %(classname)s no longer exists"
952                             ) % locals()
953                         labelprop = linkcl.labelprop(1)
954                         try:
955                             template = find_template(self._db.config.TEMPLATES,
956                                 classname, 'item')
957                             if template[1].startswith('_generic'):
958                                 raise NoTemplate, 'not really...'
959                             hrefable = 1
960                         except NoTemplate:
961                             hrefable = 0
963                     if isinstance(prop, hyperdb.Multilink) and args[k]:
964                         ml = []
965                         for linkid in args[k]:
966                             if isinstance(linkid, type(())):
967                                 sublabel = linkid[0] + ' '
968                                 linkids = linkid[1]
969                             else:
970                                 sublabel = ''
971                                 linkids = [linkid]
972                             subml = []
973                             for linkid in linkids:
974                                 label = classname + linkid
975                                 # if we have a label property, try to use it
976                                 # TODO: test for node existence even when
977                                 # there's no labelprop!
978                                 try:
979                                     if labelprop is not None and \
980                                             labelprop != 'id':
981                                         label = linkcl.get(linkid, labelprop)
982                                         label = cgi.escape(label)
983                                 except IndexError:
984                                     comments['no_link'] = self._(
985                                         "<strike>The linked node"
986                                         " no longer exists</strike>")
987                                     subml.append('<strike>%s</strike>'%label)
988                                 else:
989                                     if hrefable:
990                                         subml.append('<a href="%s%s">%s</a>'%(
991                                             classname, linkid, label))
992                                     elif label is None:
993                                         subml.append('%s%s'%(classname,
994                                             linkid))
995                                     else:
996                                         subml.append(label)
997                             ml.append(sublabel + ', '.join(subml))
998                         cell.append('%s:\n  %s'%(self._(k), ', '.join(ml)))
999                     elif isinstance(prop, hyperdb.Link) and args[k]:
1000                         label = classname + args[k]
1001                         # if we have a label property, try to use it
1002                         # TODO: test for node existence even when
1003                         # there's no labelprop!
1004                         if labelprop is not None and labelprop != 'id':
1005                             try:
1006                                 label = cgi.escape(linkcl.get(args[k],
1007                                     labelprop))
1008                             except IndexError:
1009                                 comments['no_link'] = self._(
1010                                     "<strike>The linked node"
1011                                     " no longer exists</strike>")
1012                                 cell.append(' <strike>%s</strike>,\n'%label)
1013                                 # "flag" this is done .... euwww
1014                                 label = None
1015                         if label is not None:
1016                             if hrefable:
1017                                 old = '<a href="%s%s">%s</a>'%(classname,
1018                                     args[k], label)
1019                             else:
1020                                 old = label;
1021                             cell.append('%s: %s' % (self._(k), old))
1022                             if current.has_key(k):
1023                                 cell[-1] += ' -> %s'%current[k]
1024                                 current[k] = old
1026                     elif isinstance(prop, hyperdb.Date) and args[k]:
1027                         if args[k] is None:
1028                             d = ''
1029                         else:
1030                             d = date.Date(args[k],
1031                                 translator=self._client).local(timezone)
1032                         cell.append('%s: %s'%(self._(k), str(d)))
1033                         if current.has_key(k):
1034                             cell[-1] += ' -> %s' % current[k]
1035                             current[k] = str(d)
1037                     elif isinstance(prop, hyperdb.Interval) and args[k]:
1038                         val = str(date.Interval(args[k],
1039                             translator=self._client))
1040                         cell.append('%s: %s'%(self._(k), val))
1041                         if current.has_key(k):
1042                             cell[-1] += ' -> %s'%current[k]
1043                             current[k] = val
1045                     elif isinstance(prop, hyperdb.String) and args[k]:
1046                         val = cgi.escape(args[k])
1047                         cell.append('%s: %s'%(self._(k), val))
1048                         if current.has_key(k):
1049                             cell[-1] += ' -> %s'%current[k]
1050                             current[k] = val
1052                     elif isinstance(prop, hyperdb.Boolean) and args[k] is not None:
1053                         val = args[k] and ''"Yes" or ''"No"
1054                         cell.append('%s: %s'%(self._(k), val))
1055                         if current.has_key(k):
1056                             cell[-1] += ' -> %s'%current[k]
1057                             current[k] = val
1059                     elif not args[k]:
1060                         if current.has_key(k):
1061                             cell.append('%s: %s'%(self._(k), current[k]))
1062                             current[k] = '(no value)'
1063                         else:
1064                             cell.append(self._('%s: (no value)')%self._(k))
1066                     else:
1067                         cell.append('%s: %s'%(self._(k), str(args[k])))
1068                         if current.has_key(k):
1069                             cell[-1] += ' -> %s'%current[k]
1070                             current[k] = str(args[k])
1072                 arg_s = '<br />'.join(cell)
1073             else:
1074                 # unkown event!!
1075                 comments['unknown'] = self._(
1076                     "<strong><em>This event is not handled"
1077                     " by the history display!</em></strong>")
1078                 arg_s = '<strong><em>' + str(args) + '</em></strong>'
1079             date_s = date_s.replace(' ', '&nbsp;')
1080             # if the user's an itemid, figure the username (older journals
1081             # have the username)
1082             if dre.match(user):
1083                 user = self._db.user.get(user, 'username')
1084             l.append('<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>'%(
1085                 date_s, user, self._(action), arg_s))
1086         if comments:
1087             l.append(self._(
1088                 '<tr><td colspan=4><strong>Note:</strong></td></tr>'))
1089         for entry in comments.values():
1090             l.append('<tr><td colspan=4>%s</td></tr>'%entry)
1092         if direction == 'ascending':
1093             l.reverse()
1095         l[0:0] = ['<table class="history">'
1096              '<tr><th colspan="4" class="header">',
1097              self._('History'),
1098              '</th></tr><tr>',
1099              self._('<th>Date</th>'),
1100              self._('<th>User</th>'),
1101              self._('<th>Action</th>'),
1102              self._('<th>Args</th>'),
1103             '</tr>']
1104         l.append('</table>')
1105         return '\n'.join(l)
1107     def renderQueryForm(self):
1108         """ Render this item, which is a query, as a search form.
1109         """
1110         # create a new request and override the specified args
1111         req = HTMLRequest(self._client)
1112         req.classname = self._klass.get(self._nodeid, 'klass')
1113         name = self._klass.get(self._nodeid, 'name')
1114         req.updateFromURL(self._klass.get(self._nodeid, 'url') +
1115             '&@queryname=%s'%urllib.quote(name))
1117         # new template, using the specified classname and request
1118         pt = self._client.instance.templates.get(req.classname, 'search')
1120         # use our fabricated request
1121         return pt.render(self._client, req.classname, req)
1123     def download_url(self):
1124         """ Assume that this item is a FileClass and that it has a name
1125         and content. Construct a URL for the download of the content.
1126         """
1127         name = self._klass.get(self._nodeid, 'name')
1128         url = '%s%s/%s'%(self._classname, self._nodeid, name)
1129         return urllib.quote(url)
1131     def copy_url(self, exclude=("messages", "files")):
1132         """Construct a URL for creating a copy of this item
1134         "exclude" is an optional list of properties that should
1135         not be copied to the new object.  By default, this list
1136         includes "messages" and "files" properties.  Note that
1137         "id" property cannot be copied.
1139         """
1140         exclude = ("id", "activity", "actor", "creation", "creator") \
1141             + tuple(exclude)
1142         query = {
1143             "@template": "item",
1144             "@note": self._("Copy of %(class)s %(id)s") % {
1145                 "class": self._(self._classname), "id": self._nodeid},
1146         }
1147         for name in self._props.keys():
1148             if name not in exclude:
1149                 query[name] = self[name].plain()
1150         return self._classname + "?" + "&".join(
1151             ["%s=%s" % (key, urllib.quote(value))
1152                 for key, value in query.items()])
1154 class _HTMLUser(_HTMLItem):
1155     """Add ability to check for permissions on users.
1156     """
1157     _marker = []
1158     def hasPermission(self, permission, classname=_marker,
1159             property=None, itemid=None):
1160         """Determine if the user has the Permission.
1162         The class being tested defaults to the template's class, but may
1163         be overidden for this test by suppling an alternate classname.
1164         """
1165         if classname is self._marker:
1166             classname = self._client.classname
1167         return self._db.security.hasPermission(permission,
1168             self._nodeid, classname, property, itemid)
1170     def hasRole(self, rolename):
1171         """Determine whether the user has the Role."""
1172         roles = self._db.user.get(self._nodeid, 'roles').split(',')
1173         for role in roles:
1174             if role.strip() == rolename: return True
1175         return False
1177 def HTMLItem(client, classname, nodeid, anonymous=0):
1178     if classname == 'user':
1179         return _HTMLUser(client, classname, nodeid, anonymous)
1180     else:
1181         return _HTMLItem(client, classname, nodeid, anonymous)
1183 class HTMLProperty(HTMLInputMixin, HTMLPermissions):
1184     """ String, Number, Date, Interval HTMLProperty
1186         Has useful attributes:
1188          _name  the name of the property
1189          _value the value of the property if any
1191         A wrapper object which may be stringified for the plain() behaviour.
1192     """
1193     def __init__(self, client, classname, nodeid, prop, name, value,
1194             anonymous=0):
1195         self._client = client
1196         self._db = client.db
1197         self._ = client._
1198         self._classname = classname
1199         self._nodeid = nodeid
1200         self._prop = prop
1201         self._value = value
1202         self._anonymous = anonymous
1203         self._name = name
1204         if not anonymous:
1205             self._formname = '%s%s@%s'%(classname, nodeid, name)
1206         else:
1207             self._formname = name
1209         HTMLInputMixin.__init__(self)
1211     def __repr__(self):
1212         return '<HTMLProperty(0x%x) %s %r %r>'%(id(self), self._formname,
1213             self._prop, self._value)
1214     def __str__(self):
1215         return self.plain()
1216     def __cmp__(self, other):
1217         if isinstance(other, HTMLProperty):
1218             return cmp(self._value, other._value)
1219         return cmp(self._value, other)
1221     def __nonzero__(self):
1222         return not not self._value
1224     def isset(self):
1225         """Is my _value not None?"""
1226         return self._value is not None
1228     def is_edit_ok(self):
1229         """Should the user be allowed to use an edit form field for this
1230         property. Check "Create" for new items, or "Edit" for existing
1231         ones.
1232         """
1233         if self._nodeid:
1234             return self._db.security.hasPermission('Edit', self._client.userid,
1235                 self._classname, self._name, self._nodeid)
1236         return self._db.security.hasPermission('Create', self._client.userid,
1237             self._classname, self._name)
1239     def is_view_ok(self):
1240         """ Is the user allowed to View the current class?
1241         """
1242         if self._db.security.hasPermission('View', self._client.userid,
1243                 self._classname, self._name, self._nodeid):
1244             return 1
1245         return self.is_edit_ok()
1247 class StringHTMLProperty(HTMLProperty):
1248     hyper_re = re.compile(r'''(
1249         (?P<url>
1250          (
1251           (ht|f)tp(s?)://                   # protocol
1252           ([\w]+(:\w+)?@)?                  # username/password
1253           ([\w\-]+)                         # hostname
1254           ((\.[\w-]+)+)?                    # .domain.etc
1255          |                                  # ... or ...
1256           ([\w]+(:\w+)?@)?                  # username/password
1257           www\.                             # "www."
1258           ([\w\-]+\.)+                      # hostname
1259           [\w]{2,5}                         # TLD
1260          )
1261          (:[\d]{1,5})?                     # port
1262          (/[\w\-$.+!*(),;:@&=?/~\\#%]*)?   # path etc.
1263         )|
1264         (?P<email>[-+=%/\w\.]+@[\w\.\-]+)|
1265         (?P<item>(?P<class>[A-Za-z_]+)(\s*)(?P<id>\d+))
1266     )''', re.X | re.I)
1267     protocol_re = re.compile('^(ht|f)tp(s?)://', re.I)
1269     def _hyper_repl_item(self,match,replacement):
1270         item = match.group('item')
1271         cls = match.group('class').lower()
1272         id = match.group('id')
1273         try:
1274             # make sure cls is a valid tracker classname
1275             cl = self._db.getclass(cls)
1276             if not cl.hasnode(id):
1277                 return item
1278             return replacement % locals()
1279         except KeyError:
1280             return item
1282     def _hyper_repl(self, match):
1283         if match.group('url'):
1284             u = s = match.group('url')
1285             if not self.protocol_re.search(s):
1286                 u = 'http://' + s
1287             # catch an escaped ">" at the end of the URL
1288             if s.endswith('&gt;'):
1289                 u = s = s[:-4]
1290                 e = '&gt;'
1291             else:
1292                 e = ''
1293             return '<a href="%s">%s</a>%s'%(u, s, e)
1294         elif match.group('email'):
1295             s = match.group('email')
1296             return '<a href="mailto:%s">%s</a>'%(s, s)
1297         else:
1298             return self._hyper_repl_item(match,
1299                 '<a href="%(cls)s%(id)s">%(item)s</a>')
1301     def _hyper_repl_rst(self, match):
1302         if match.group('url'):
1303             s = match.group('url')
1304             return '`%s <%s>`_'%(s, s)
1305         elif match.group('email'):
1306             s = match.group('email')
1307             return '`%s <mailto:%s>`_'%(s, s)
1308         else:
1309             return self._hyper_repl_item(match,'`%(item)s <%(cls)s%(id)s>`_')
1311     def hyperlinked(self):
1312         """ Render a "hyperlinked" version of the text """
1313         return self.plain(hyperlink=1)
1315     def plain(self, escape=0, hyperlink=0):
1316         """Render a "plain" representation of the property
1318         - "escape" turns on/off HTML quoting
1319         - "hyperlink" turns on/off in-text hyperlinking of URLs, email
1320           addresses and designators
1321         """
1322         if not self.is_view_ok():
1323             return self._('[hidden]')
1325         if self._value is None:
1326             return ''
1327         if escape:
1328             s = cgi.escape(str(self._value))
1329         else:
1330             s = str(self._value)
1331         if hyperlink:
1332             # no, we *must* escape this text
1333             if not escape:
1334                 s = cgi.escape(s)
1335             s = self.hyper_re.sub(self._hyper_repl, s)
1336         return s
1338     def wrapped(self, escape=1, hyperlink=1):
1339         """Render a "wrapped" representation of the property.
1341         We wrap long lines at 80 columns on the nearest whitespace. Lines
1342         with no whitespace are not broken to force wrapping.
1344         Note that unlike plain() we default wrapped() to have the escaping
1345         and hyperlinking turned on since that's the most common usage.
1347         - "escape" turns on/off HTML quoting
1348         - "hyperlink" turns on/off in-text hyperlinking of URLs, email
1349           addresses and designators
1350         """
1351         if not self.is_view_ok():
1352             return self._('[hidden]')
1354         if self._value is None:
1355             return ''
1356         s = support.wrap(str(self._value), width=80)
1357         if escape:
1358             s = cgi.escape(s)
1359         if hyperlink:
1360             # no, we *must* escape this text
1361             if not escape:
1362                 s = cgi.escape(s)
1363             s = self.hyper_re.sub(self._hyper_repl, s)
1364         return s
1366     def stext(self, escape=0, hyperlink=1):
1367         """ Render the value of the property as StructuredText.
1369             This requires the StructureText module to be installed separately.
1370         """
1371         if not self.is_view_ok():
1372             return self._('[hidden]')
1374         s = self.plain(escape=escape, hyperlink=hyperlink)
1375         if not StructuredText:
1376             return s
1377         return StructuredText(s,level=1,header=0)
1379     def rst(self, hyperlink=1):
1380         """ Render the value of the property as ReStructuredText.
1382             This requires docutils to be installed separately.
1383         """
1384         if not self.is_view_ok():
1385             return self._('[hidden]')
1387         if not ReStructuredText:
1388             return self.plain(escape=0, hyperlink=hyperlink)
1389         s = self.plain(escape=0, hyperlink=0)
1390         if hyperlink:
1391             s = self.hyper_re.sub(self._hyper_repl_rst, s)
1392         return ReStructuredText(s, writer_name="html")["body"].encode("utf-8",
1393             "replace")
1395     def field(self, **kwargs):
1396         """ Render the property as a field in HTML.
1398             If not editable, just display the value via plain().
1399         """
1400         if not self.is_edit_ok():
1401             return self.plain(escape=1)
1403         value = self._value
1404         if value is None:
1405             value = ''
1407         kwargs.setdefault("size", 30)
1408         kwargs.update({"name": self._formname, "value": value})
1409         return self.input(**kwargs)
1411     def multiline(self, escape=0, rows=5, cols=40, **kwargs):
1412         """ Render a multiline form edit field for the property.
1414             If not editable, just display the plain() value in a <pre> tag.
1415         """
1416         if not self.is_edit_ok():
1417             return '<pre>%s</pre>'%self.plain()
1419         if self._value is None:
1420             value = ''
1421         else:
1422             value = cgi.escape(str(self._value))
1424             value = '&quot;'.join(value.split('"'))
1425         name = self._formname
1426         passthrough_args = ' '.join(['%s="%s"' % (k, cgi.escape(str(v), True))
1427             for k,v in kwargs.items()])
1428         return ('<textarea %(passthrough_args)s name="%(name)s" id="%(name)s"'
1429                 ' rows="%(rows)s" cols="%(cols)s">'
1430                  '%(value)s</textarea>') % locals()
1432     def email(self, escape=1):
1433         """ Render the value of the property as an obscured email address
1434         """
1435         if not self.is_view_ok():
1436             return self._('[hidden]')
1438         if self._value is None:
1439             value = ''
1440         else:
1441             value = str(self._value)
1442         split = value.split('@')
1443         if len(split) == 2:
1444             name, domain = split
1445             domain = ' '.join(domain.split('.')[:-1])
1446             name = name.replace('.', ' ')
1447             value = '%s at %s ...'%(name, domain)
1448         else:
1449             value = value.replace('.', ' ')
1450         if escape:
1451             value = cgi.escape(value)
1452         return value
1454 class PasswordHTMLProperty(HTMLProperty):
1455     def plain(self, escape=0):
1456         """ Render a "plain" representation of the property
1457         """
1458         if not self.is_view_ok():
1459             return self._('[hidden]')
1461         if self._value is None:
1462             return ''
1463         return self._('*encrypted*')
1465     def field(self, size=30):
1466         """ Render a form edit field for the property.
1468             If not editable, just display the value via plain().
1469         """
1470         if not self.is_edit_ok():
1471             return self.plain(escape=1)
1473         return self.input(type="password", name=self._formname, size=size)
1475     def confirm(self, size=30):
1476         """ Render a second form edit field for the property, used for
1477             confirmation that the user typed the password correctly. Generates
1478             a field with name "@confirm@name".
1480             If not editable, display nothing.
1481         """
1482         if not self.is_edit_ok():
1483             return ''
1485         return self.input(type="password",
1486             name="@confirm@%s"%self._formname,
1487             id="%s-confirm"%self._formname,
1488             size=size)
1490 class NumberHTMLProperty(HTMLProperty):
1491     def plain(self, escape=0):
1492         """ Render a "plain" representation of the property
1493         """
1494         if not self.is_view_ok():
1495             return self._('[hidden]')
1497         if self._value is None:
1498             return ''
1500         return str(self._value)
1502     def field(self, size=30):
1503         """ Render a form edit field for the property.
1505             If not editable, just display the value via plain().
1506         """
1507         if not self.is_edit_ok():
1508             return self.plain(escape=1)
1510         value = self._value
1511         if value is None:
1512             value = ''
1514         return self.input(name=self._formname, value=value, size=size)
1516     def __int__(self):
1517         """ Return an int of me
1518         """
1519         return int(self._value)
1521     def __float__(self):
1522         """ Return a float of me
1523         """
1524         return float(self._value)
1527 class BooleanHTMLProperty(HTMLProperty):
1528     def plain(self, escape=0):
1529         """ Render a "plain" representation of the property
1530         """
1531         if not self.is_view_ok():
1532             return self._('[hidden]')
1534         if self._value is None:
1535             return ''
1536         return self._value and self._("Yes") or self._("No")
1538     def field(self):
1539         """ Render a form edit field for the property
1541             If not editable, just display the value via plain().
1542         """
1543         if not self.is_edit_ok():
1544             return self.plain(escape=1)
1546         value = self._value
1547         if isinstance(value, str) or isinstance(value, unicode):
1548             value = value.strip().lower() in ('checked', 'yes', 'true',
1549                 'on', '1')
1551         checked = value and "checked" or ""
1552         if value:
1553             s = self.input(type="radio", name=self._formname, value="yes",
1554                 checked="checked")
1555             s += self._('Yes')
1556             s +=self.input(type="radio", name=self._formname, value="no")
1557             s += self._('No')
1558         else:
1559             s = self.input(type="radio", name=self._formname, value="yes")
1560             s += self._('Yes')
1561             s +=self.input(type="radio", name=self._formname, value="no",
1562                 checked="checked")
1563             s += self._('No')
1564         return s
1566 class DateHTMLProperty(HTMLProperty):
1568     _marker = []
1570     def __init__(self, client, classname, nodeid, prop, name, value,
1571             anonymous=0, offset=None):
1572         HTMLProperty.__init__(self, client, classname, nodeid, prop, name,
1573                 value, anonymous=anonymous)
1574         if self._value and not (isinstance(self._value, str) or
1575                 isinstance(self._value, unicode)):
1576             self._value.setTranslator(self._client.translator)
1577         self._offset = offset
1578         if self._offset is None :
1579             self._offset = self._prop.offset (self._db)
1581     def plain(self, escape=0):
1582         """ Render a "plain" representation of the property
1583         """
1584         if not self.is_view_ok():
1585             return self._('[hidden]')
1587         if self._value is None:
1588             return ''
1589         if self._offset is None:
1590             offset = self._db.getUserTimezone()
1591         else:
1592             offset = self._offset
1593         return str(self._value.local(offset))
1595     def now(self, str_interval=None):
1596         """ Return the current time.
1598             This is useful for defaulting a new value. Returns a
1599             DateHTMLProperty.
1600         """
1601         if not self.is_view_ok():
1602             return self._('[hidden]')
1604         ret = date.Date('.', translator=self._client)
1606         if isinstance(str_interval, basestring):
1607             sign = 1
1608             if str_interval[0] == '-':
1609                 sign = -1
1610                 str_interval = str_interval[1:]
1611             interval = date.Interval(str_interval, translator=self._client)
1612             if sign > 0:
1613                 ret = ret + interval
1614             else:
1615                 ret = ret - interval
1617         return DateHTMLProperty(self._client, self._classname, self._nodeid,
1618             self._prop, self._formname, ret)
1620     def field(self, size=30, default=None, format=_marker, popcal=True):
1621         """Render a form edit field for the property
1623         If not editable, just display the value via plain().
1625         If "popcal" then include the Javascript calendar editor.
1626         Default=yes.
1628         The format string is a standard python strftime format string.
1629         """
1630         if not self.is_edit_ok():
1631             if format is self._marker:
1632                 return self.plain(escape=1)
1633             else:
1634                 return self.pretty(format)
1636         value = self._value
1638         if value is None:
1639             if default is None:
1640                 raw_value = None
1641             else:
1642                 if isinstance(default, basestring):
1643                     raw_value = date.Date(default, translator=self._client)
1644                 elif isinstance(default, date.Date):
1645                     raw_value = default
1646                 elif isinstance(default, DateHTMLProperty):
1647                     raw_value = default._value
1648                 else:
1649                     raise ValueError, self._('default value for '
1650                         'DateHTMLProperty must be either DateHTMLProperty '
1651                         'or string date representation.')
1652         elif isinstance(value, str) or isinstance(value, unicode):
1653             # most likely erroneous input to be passed back to user
1654             if isinstance(value, unicode): value = value.encode('utf8')
1655             return self.input(name=self._formname, value=value, size=size)
1656         else:
1657             raw_value = value
1659         if raw_value is None:
1660             value = ''
1661         elif isinstance(raw_value, str) or isinstance(raw_value, unicode):
1662             if format is self._marker:
1663                 value = raw_value
1664             else:
1665                 value = date.Date(raw_value).pretty(format)
1666         else:
1667             if self._offset is None :
1668                 offset = self._db.getUserTimezone()
1669             else :
1670                 offset = self._offset
1671             value = raw_value.local(offset)
1672             if format is not self._marker:
1673                 value = value.pretty(format)
1675         s = self.input(name=self._formname, value=value, size=size)
1676         if popcal:
1677             s += self.popcal()
1678         return s
1680     def reldate(self, pretty=1):
1681         """ Render the interval between the date and now.
1683             If the "pretty" flag is true, then make the display pretty.
1684         """
1685         if not self.is_view_ok():
1686             return self._('[hidden]')
1688         if not self._value:
1689             return ''
1691         # figure the interval
1692         interval = self._value - date.Date('.', translator=self._client)
1693         if pretty:
1694             return interval.pretty()
1695         return str(interval)
1697     def pretty(self, format=_marker):
1698         """ Render the date in a pretty format (eg. month names, spaces).
1700             The format string is a standard python strftime format string.
1701             Note that if the day is zero, and appears at the start of the
1702             string, then it'll be stripped from the output. This is handy
1703             for the situation when a date only specifies a month and a year.
1704         """
1705         if not self.is_view_ok():
1706             return self._('[hidden]')
1708         if self._offset is None:
1709             offset = self._db.getUserTimezone()
1710         else:
1711             offset = self._offset
1713         if not self._value:
1714             return ''
1715         elif format is not self._marker:
1716             return self._value.local(offset).pretty(format)
1717         else:
1718             return self._value.local(offset).pretty()
1720     def local(self, offset):
1721         """ Return the date/time as a local (timezone offset) date/time.
1722         """
1723         if not self.is_view_ok():
1724             return self._('[hidden]')
1726         return DateHTMLProperty(self._client, self._classname, self._nodeid,
1727             self._prop, self._formname, self._value, offset=offset)
1729     def popcal(self, width=300, height=200, label="(cal)",
1730             form="itemSynopsis"):
1731         """Generate a link to a calendar pop-up window.
1733         item: HTMLProperty e.g.: context.deadline
1734         """
1735         if self.isset():
1736             date = "&date=%s"%self._value
1737         else :
1738             date = ""
1739         return ('<a class="classhelp" href="javascript:help_window('
1740             "'%s?@template=calendar&amp;property=%s&amp;form=%s%s', %d, %d)"
1741             '">%s</a>'%(self._classname, self._name, form, date, width,
1742             height, label))
1744 class IntervalHTMLProperty(HTMLProperty):
1745     def __init__(self, client, classname, nodeid, prop, name, value,
1746             anonymous=0):
1747         HTMLProperty.__init__(self, client, classname, nodeid, prop,
1748             name, value, anonymous)
1749         if self._value and not isinstance(self._value, (str, unicode)):
1750             self._value.setTranslator(self._client.translator)
1752     def plain(self, escape=0):
1753         """ Render a "plain" representation of the property
1754         """
1755         if not self.is_view_ok():
1756             return self._('[hidden]')
1758         if self._value is None:
1759             return ''
1760         return str(self._value)
1762     def pretty(self):
1763         """ Render the interval in a pretty format (eg. "yesterday")
1764         """
1765         if not self.is_view_ok():
1766             return self._('[hidden]')
1768         return self._value.pretty()
1770     def field(self, size=30):
1771         """ Render a form edit field for the property
1773             If not editable, just display the value via plain().
1774         """
1775         if not self.is_edit_ok():
1776             return self.plain(escape=1)
1778         value = self._value
1779         if value is None:
1780             value = ''
1782         return self.input(name=self._formname, value=value, size=size)
1784 class LinkHTMLProperty(HTMLProperty):
1785     """ Link HTMLProperty
1786         Include the above as well as being able to access the class
1787         information. Stringifying the object itself results in the value
1788         from the item being displayed. Accessing attributes of this object
1789         result in the appropriate entry from the class being queried for the
1790         property accessed (so item/assignedto/name would look up the user
1791         entry identified by the assignedto property on item, and then the
1792         name property of that user)
1793     """
1794     def __init__(self, *args, **kw):
1795         HTMLProperty.__init__(self, *args, **kw)
1796         # if we're representing a form value, then the -1 from the form really
1797         # should be a None
1798         if str(self._value) == '-1':
1799             self._value = None
1801     def __getattr__(self, attr):
1802         """ return a new HTMLItem """
1803         if not self._value:
1804             # handle a special page templates lookup
1805             if attr == '__render_with_namespace__':
1806                 def nothing(*args, **kw):
1807                     return ''
1808                 return nothing
1809             msg = self._('Attempt to look up %(attr)s on a missing value')
1810             return MissingValue(msg%locals())
1811         i = HTMLItem(self._client, self._prop.classname, self._value)
1812         return getattr(i, attr)
1814     def plain(self, escape=0):
1815         """ Render a "plain" representation of the property
1816         """
1817         if not self.is_view_ok():
1818             return self._('[hidden]')
1820         if self._value is None:
1821             return ''
1822         linkcl = self._db.classes[self._prop.classname]
1823         k = linkcl.labelprop(1)
1824         if num_re.match(self._value):
1825             value = str(linkcl.get(self._value, k))
1826         else :
1827             value = self._value
1828         if escape:
1829             value = cgi.escape(value)
1830         return value
1832     def field(self, showid=0, size=None):
1833         """ Render a form edit field for the property
1835             If not editable, just display the value via plain().
1836         """
1837         if not self.is_edit_ok():
1838             return self.plain(escape=1)
1840         # edit field
1841         linkcl = self._db.getclass(self._prop.classname)
1842         if self._value is None:
1843             value = ''
1844         else:
1845             k = linkcl.getkey()
1846             if k and num_re.match(self._value):
1847                 value = linkcl.get(self._value, k)
1848             else:
1849                 value = self._value
1850         return self.input(name=self._formname, value=value, size=size)
1852     def menu(self, size=None, height=None, showid=0, additional=[], value=None,
1853             sort_on=None, **conditions):
1854         """ Render a form select list for this property
1856             "size" is used to limit the length of the list labels
1857             "height" is used to set the <select> tag's "size" attribute
1858             "showid" includes the item ids in the list labels
1859             "value" specifies which item is pre-selected
1860             "additional" lists properties which should be included in the
1861                 label
1862             "sort_on" indicates the property to sort the list on as
1863                 (direction, property) where direction is '+' or '-'. A
1864                 single string with the direction prepended may be used.
1865                 For example: ('-', 'order'), '+name'.
1867             The remaining keyword arguments are used as conditions for
1868             filtering the items in the list - they're passed as the
1869             "filterspec" argument to a Class.filter() call.
1871             If not editable, just display the value via plain().
1872         """
1873         if not self.is_edit_ok():
1874             return self.plain(escape=1)
1876         # Since None indicates the default, we need another way to
1877         # indicate "no selection".  We use -1 for this purpose, as
1878         # that is the value we use when submitting a form without the
1879         # value set.
1880         if value is None:
1881             value = self._value
1882         elif value == '-1':
1883             value = None
1885         linkcl = self._db.getclass(self._prop.classname)
1886         l = ['<select name="%s">'%self._formname]
1887         k = linkcl.labelprop(1)
1888         s = ''
1889         if value is None:
1890             s = 'selected="selected" '
1891         l.append(self._('<option %svalue="-1">- no selection -</option>')%s)
1893         if sort_on is not None:
1894             if not isinstance(sort_on, tuple):
1895                 if sort_on[0] in '+-':
1896                     sort_on = (sort_on[0], sort_on[1:])
1897                 else:
1898                     sort_on = ('+', sort_on)
1899         else:
1900             sort_on = ('+', linkcl.orderprop())
1902         options = [opt
1903             for opt in linkcl.filter(None, conditions, sort_on, (None, None))
1904             if self._db.security.hasPermission("View", self._client.userid,
1905                 linkcl.classname, itemid=opt)]
1907         # make sure we list the current value if it's retired
1908         if value and value not in options:
1909             options.insert(0, value)
1911         if additional:
1912             additional_fns = []
1913             props = linkcl.getprops()
1914             for propname in additional:
1915                 prop = props[propname]
1916                 if isinstance(prop, hyperdb.Link):
1917                     cl = self._db.getclass(prop.classname)
1918                     labelprop = cl.labelprop()
1919                     fn = lambda optionid: cl.get(linkcl.get(optionid,
1920                                                             propname),
1921                                                  labelprop)
1922                 else:
1923                     fn = lambda optionid: linkcl.get(optionid, propname)
1924             additional_fns.append(fn)
1925             
1926         for optionid in options:
1927             # get the option value, and if it's None use an empty string
1928             option = linkcl.get(optionid, k) or ''
1930             # figure if this option is selected
1931             s = ''
1932             if value in [optionid, option]:
1933                 s = 'selected="selected" '
1935             # figure the label
1936             if showid:
1937                 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1938             elif not option:
1939                 lab = '%s%s'%(self._prop.classname, optionid)
1940             else:
1941                 lab = option
1943             # truncate if it's too long
1944             if size is not None and len(lab) > size:
1945                 lab = lab[:size-3] + '...'
1946             if additional:
1947                 m = []
1948                 for fn in additional_fns:
1949                     m.append(str(fn(optionid)))
1950                 lab = lab + ' (%s)'%', '.join(m)
1952             # and generate
1953             lab = cgi.escape(self._(lab))
1954             l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
1955         l.append('</select>')
1956         return '\n'.join(l)
1957 #    def checklist(self, ...)
1961 class MultilinkHTMLProperty(HTMLProperty):
1962     """ Multilink HTMLProperty
1964         Also be iterable, returning a wrapper object like the Link case for
1965         each entry in the multilink.
1966     """
1967     def __init__(self, *args, **kwargs):
1968         HTMLProperty.__init__(self, *args, **kwargs)
1969         if self._value:
1970             display_value = lookupIds(self._db, self._prop, self._value,
1971                 fail_ok=1, do_lookup=False)
1972             sortfun = make_sort_function(self._db, self._prop.classname)
1973             # sorting fails if the value contains
1974             # items not yet stored in the database
1975             # ignore these errors to preserve user input
1976             try:
1977                 display_value.sort(sortfun)
1978             except:
1979                 pass
1980             self._value = display_value
1982     def __len__(self):
1983         """ length of the multilink """
1984         return len(self._value)
1986     def __getattr__(self, attr):
1987         """ no extended attribute accesses make sense here """
1988         raise AttributeError, attr
1990     def viewableGenerator(self, values):
1991         """Used to iterate over only the View'able items in a class."""
1992         check = self._db.security.hasPermission
1993         userid = self._client.userid
1994         classname = self._prop.classname
1995         for value in values:
1996             if check('View', userid, classname, itemid=value):
1997                 yield HTMLItem(self._client, classname, value)
1999     def __iter__(self):
2000         """ iterate and return a new HTMLItem
2001         """
2002         return self.viewableGenerator(self._value)
2004     def reverse(self):
2005         """ return the list in reverse order
2006         """
2007         l = self._value[:]
2008         l.reverse()
2009         return self.viewableGenerator(l)
2011     def sorted(self, property):
2012         """ Return this multilink sorted by the given property """
2013         value = list(self.__iter__())
2014         value.sort(lambda a,b:cmp(a[property], b[property]))
2015         return value
2017     def __contains__(self, value):
2018         """ Support the "in" operator. We have to make sure the passed-in
2019             value is a string first, not a HTMLProperty.
2020         """
2021         return str(value) in self._value
2023     def isset(self):
2024         """Is my _value not []?"""
2025         return self._value != []
2027     def plain(self, escape=0):
2028         """ Render a "plain" representation of the property
2029         """
2030         if not self.is_view_ok():
2031             return self._('[hidden]')
2033         linkcl = self._db.classes[self._prop.classname]
2034         k = linkcl.labelprop(1)
2035         labels = []
2036         for v in self._value:
2037             label = linkcl.get(v, k)
2038             # fall back to designator if label is None
2039             if label is None: label = '%s%s'%(self._prop.classname, k)
2040             labels.append(label)
2041         value = ', '.join(labels)
2042         if escape:
2043             value = cgi.escape(value)
2044         return value
2046     def field(self, size=30, showid=0):
2047         """ Render a form edit field for the property
2049             If not editable, just display the value via plain().
2050         """
2051         if not self.is_edit_ok():
2052             return self.plain(escape=1)
2054         linkcl = self._db.getclass(self._prop.classname)
2055         value = self._value[:]
2056         # map the id to the label property
2057         if not linkcl.getkey():
2058             showid=1
2059         if not showid:
2060             k = linkcl.labelprop(1)
2061             value = lookupKeys(linkcl, k, value)
2062         value = ','.join(value)
2063         return self.input(name=self._formname, size=size, value=value)
2065     def menu(self, size=None, height=None, showid=0, additional=[],
2066              value=None, sort_on=None, **conditions):
2067         """ Render a form <select> list for this property.
2069             "size" is used to limit the length of the list labels
2070             "height" is used to set the <select> tag's "size" attribute
2071             "showid" includes the item ids in the list labels
2072             "additional" lists properties which should be included in the
2073                 label
2074             "value" specifies which item is pre-selected
2075             "sort_on" indicates the property to sort the list on as
2076                 (direction, property) where direction is '+' or '-'. A
2077                 single string with the direction prepended may be used.
2078                 For example: ('-', 'order'), '+name'.
2080             The remaining keyword arguments are used as conditions for
2081             filtering the items in the list - they're passed as the
2082             "filterspec" argument to a Class.filter() call.
2084             If not editable, just display the value via plain().
2085         """
2086         if not self.is_edit_ok():
2087             return self.plain(escape=1)
2089         if value is None:
2090             value = self._value
2092         linkcl = self._db.getclass(self._prop.classname)
2094         if sort_on is not None:
2095             if not isinstance(sort_on, tuple):
2096                 if sort_on[0] in '+-':
2097                     sort_on = (sort_on[0], sort_on[1:])
2098                 else:
2099                     sort_on = ('+', sort_on)
2100         else:
2101             sort_on = ('+', linkcl.orderprop())
2103         options = [opt
2104             for opt in linkcl.filter(None, conditions, sort_on)
2105             if self._db.security.hasPermission("View", self._client.userid,
2106                 linkcl.classname, itemid=opt)]
2107         
2108         # make sure we list the current values if they're retired
2109         for val in value:
2110             if val not in options:
2111                 options.insert(0, val)
2113         if not height:
2114             height = len(options)
2115             if value:
2116                 # The "no selection" option.
2117                 height += 1
2118             height = min(height, 7)
2119         l = ['<select multiple name="%s" size="%s">'%(self._formname, height)]
2120         k = linkcl.labelprop(1)
2122         if value:
2123             l.append('<option value="%s">- no selection -</option>'
2124                      % ','.join(['-' + v for v in value]))
2126         if additional:
2127             additional_fns = []
2128             props = linkcl.getprops()
2129             for propname in additional:
2130                 prop = props[propname]
2131                 if isinstance(prop, hyperdb.Link):
2132                     cl = self._db.getclass(prop.classname)
2133                     labelprop = cl.labelprop()
2134                     fn = lambda optionid: cl.get(linkcl.get(optionid,
2135                                                             propname),
2136                                                  labelprop)
2137                 else:
2138                     fn = lambda optionid: linkcl.get(optionid, propname)
2139             additional_fns.append(fn)
2140             
2141         for optionid in options:
2142             # get the option value, and if it's None use an empty string
2143             option = linkcl.get(optionid, k) or ''
2145             # figure if this option is selected
2146             s = ''
2147             if optionid in value or option in value:
2148                 s = 'selected="selected" '
2150             # figure the label
2151             if showid:
2152                 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
2153             else:
2154                 lab = option
2155             # truncate if it's too long
2156             if size is not None and len(lab) > size:
2157                 lab = lab[:size-3] + '...'
2158             if additional:
2159                 m = []
2160                 for fn in additional_fns:
2161                     m.append(str(fn(optionid)))
2162                 lab = lab + ' (%s)'%', '.join(m)
2164             # and generate
2165             lab = cgi.escape(self._(lab))
2166             l.append('<option %svalue="%s">%s</option>'%(s, optionid,
2167                 lab))
2168         l.append('</select>')
2169         return '\n'.join(l)
2171 # set the propclasses for HTMLItem
2172 propclasses = (
2173     (hyperdb.String, StringHTMLProperty),
2174     (hyperdb.Number, NumberHTMLProperty),
2175     (hyperdb.Boolean, BooleanHTMLProperty),
2176     (hyperdb.Date, DateHTMLProperty),
2177     (hyperdb.Interval, IntervalHTMLProperty),
2178     (hyperdb.Password, PasswordHTMLProperty),
2179     (hyperdb.Link, LinkHTMLProperty),
2180     (hyperdb.Multilink, MultilinkHTMLProperty),
2183 def make_sort_function(db, classname, sort_on=None):
2184     """Make a sort function for a given class
2185     """
2186     linkcl = db.getclass(classname)
2187     if sort_on is None:
2188         sort_on = linkcl.orderprop()
2189     def sortfunc(a, b):
2190         return cmp(linkcl.get(a, sort_on), linkcl.get(b, sort_on))
2191     return sortfunc
2193 def handleListCGIValue(value):
2194     """ Value is either a single item or a list of items. Each item has a
2195         .value that we're actually interested in.
2196     """
2197     if isinstance(value, type([])):
2198         return [value.value for value in value]
2199     else:
2200         value = value.value.strip()
2201         if not value:
2202             return []
2203         return [v.strip() for v in value.split(',')]
2205 class HTMLRequest(HTMLInputMixin):
2206     """The *request*, holding the CGI form and environment.
2208     - "form" the CGI form as a cgi.FieldStorage
2209     - "env" the CGI environment variables
2210     - "base" the base URL for this instance
2211     - "user" a HTMLItem instance for this user
2212     - "language" as determined by the browser or config
2213     - "classname" the current classname (possibly None)
2214     - "template" the current template (suffix, also possibly None)
2216     Index args:
2218     - "columns" dictionary of the columns to display in an index page
2219     - "show" a convenience access to columns - request/show/colname will
2220       be true if the columns should be displayed, false otherwise
2221     - "sort" index sort column (direction, column name)
2222     - "group" index grouping property (direction, column name)
2223     - "filter" properties to filter the index on
2224     - "filterspec" values to filter the index on
2225     - "search_text" text to perform a full-text search on for an index
2226     """
2227     def __repr__(self):
2228         return '<HTMLRequest %r>'%self.__dict__
2230     def __init__(self, client):
2231         # _client is needed by HTMLInputMixin
2232         self._client = self.client = client
2234         # easier access vars
2235         self.form = client.form
2236         self.env = client.env
2237         self.base = client.base
2238         self.user = HTMLItem(client, 'user', client.userid)
2239         self.language = client.language
2241         # store the current class name and action
2242         self.classname = client.classname
2243         self.nodeid = client.nodeid
2244         self.template = client.template
2246         # the special char to use for special vars
2247         self.special_char = '@'
2249         HTMLInputMixin.__init__(self)
2251         self._post_init()
2253     def current_url(self):
2254         url = self.base
2255         if self.classname:
2256             url += self.classname
2257             if self.nodeid:
2258                 url += self.nodeid
2259         args = {}
2260         if self.template:
2261             args['@template'] = self.template
2262         return self.indexargs_url(url, args)
2264     def _parse_sort(self, var, name):
2265         """ Parse sort/group options. Append to var
2266         """
2267         fields = []
2268         dirs = []
2269         for special in '@:':
2270             idx = 0
2271             key = '%s%s%d'%(special, name, idx)
2272             while key in self.form:
2273                 self.special_char = special
2274                 fields.append(self.form.getfirst(key))
2275                 dirkey = '%s%sdir%d'%(special, name, idx)
2276                 if dirkey in self.form:
2277                     dirs.append(self.form.getfirst(dirkey))
2278                 else:
2279                     dirs.append(None)
2280                 idx += 1
2281                 key = '%s%s%d'%(special, name, idx)
2282             # backward compatible (and query) URL format
2283             key = special + name
2284             dirkey = key + 'dir'
2285             if key in self.form and not fields:
2286                 fields = handleListCGIValue(self.form[key])
2287                 if dirkey in self.form:
2288                     dirs.append(self.form.getfirst(dirkey))
2289             if fields: # only try other special char if nothing found
2290                 break
2291         for f, d in map(None, fields, dirs):
2292             if f.startswith('-'):
2293                 var.append(('-', f[1:]))
2294             elif d:
2295                 var.append(('-', f))
2296             else:
2297                 var.append(('+', f))
2299     def _post_init(self):
2300         """ Set attributes based on self.form
2301         """
2302         # extract the index display information from the form
2303         self.columns = []
2304         for name in ':columns @columns'.split():
2305             if self.form.has_key(name):
2306                 self.special_char = name[0]
2307                 self.columns = handleListCGIValue(self.form[name])
2308                 break
2309         self.show = support.TruthDict(self.columns)
2311         # sorting and grouping
2312         self.sort = []
2313         self.group = []
2314         self._parse_sort(self.sort, 'sort')
2315         self._parse_sort(self.group, 'group')
2317         # filtering
2318         self.filter = []
2319         for name in ':filter @filter'.split():
2320             if self.form.has_key(name):
2321                 self.special_char = name[0]
2322                 self.filter = handleListCGIValue(self.form[name])
2324         self.filterspec = {}
2325         db = self.client.db
2326         if self.classname is not None:
2327             cls = db.getclass (self.classname)
2328             for name in self.filter:
2329                 if not self.form.has_key(name):
2330                     continue
2331                 prop = cls.get_transitive_prop (name)
2332                 fv = self.form[name]
2333                 if (isinstance(prop, hyperdb.Link) or
2334                         isinstance(prop, hyperdb.Multilink)):
2335                     self.filterspec[name] = lookupIds(db, prop,
2336                         handleListCGIValue(fv))
2337                 else:
2338                     if isinstance(fv, type([])):
2339                         self.filterspec[name] = [v.value for v in fv]
2340                     elif name == 'id':
2341                         # special case "id" property
2342                         self.filterspec[name] = handleListCGIValue(fv)
2343                     else:
2344                         self.filterspec[name] = fv.value
2346         # full-text search argument
2347         self.search_text = None
2348         for name in ':search_text @search_text'.split():
2349             if self.form.has_key(name):
2350                 self.special_char = name[0]
2351                 self.search_text = self.form.getfirst(name)
2353         # pagination - size and start index
2354         # figure batch args
2355         self.pagesize = 50
2356         for name in ':pagesize @pagesize'.split():
2357             if self.form.has_key(name):
2358                 self.special_char = name[0]
2359                 self.pagesize = int(self.form.getfirst(name))
2361         self.startwith = 0
2362         for name in ':startwith @startwith'.split():
2363             if self.form.has_key(name):
2364                 self.special_char = name[0]
2365                 self.startwith = int(self.form.getfirst(name))
2367         # dispname
2368         if self.form.has_key('@dispname'):
2369             self.dispname = self.form.getfirst('@dispname')
2370         else:
2371             self.dispname = None
2373     def updateFromURL(self, url):
2374         """ Parse the URL for query args, and update my attributes using the
2375             values.
2376         """
2377         env = {'QUERY_STRING': url}
2378         self.form = cgi.FieldStorage(environ=env)
2380         self._post_init()
2382     def update(self, kwargs):
2383         """ Update my attributes using the keyword args
2384         """
2385         self.__dict__.update(kwargs)
2386         if kwargs.has_key('columns'):
2387             self.show = support.TruthDict(self.columns)
2389     def description(self):
2390         """ Return a description of the request - handle for the page title.
2391         """
2392         s = [self.client.db.config.TRACKER_NAME]
2393         if self.classname:
2394             if self.client.nodeid:
2395                 s.append('- %s%s'%(self.classname, self.client.nodeid))
2396             else:
2397                 if self.template == 'item':
2398                     s.append('- new %s'%self.classname)
2399                 elif self.template == 'index':
2400                     s.append('- %s index'%self.classname)
2401                 else:
2402                     s.append('- %s %s'%(self.classname, self.template))
2403         else:
2404             s.append('- home')
2405         return ' '.join(s)
2407     def __str__(self):
2408         d = {}
2409         d.update(self.__dict__)
2410         f = ''
2411         for k in self.form.keys():
2412             f += '\n      %r=%r'%(k,handleListCGIValue(self.form[k]))
2413         d['form'] = f
2414         e = ''
2415         for k,v in self.env.items():
2416             e += '\n     %r=%r'%(k, v)
2417         d['env'] = e
2418         return """
2419 form: %(form)s
2420 base: %(base)r
2421 classname: %(classname)r
2422 template: %(template)r
2423 columns: %(columns)r
2424 sort: %(sort)r
2425 group: %(group)r
2426 filter: %(filter)r
2427 search_text: %(search_text)r
2428 pagesize: %(pagesize)r
2429 startwith: %(startwith)r
2430 env: %(env)s
2431 """%d
2433     def indexargs_form(self, columns=1, sort=1, group=1, filter=1,
2434             filterspec=1, search_text=1):
2435         """ return the current index args as form elements """
2436         l = []
2437         sc = self.special_char
2438         def add(k, v):
2439             l.append(self.input(type="hidden", name=k, value=v))
2440         if columns and self.columns:
2441             add(sc+'columns', ','.join(self.columns))
2442         if sort:
2443             val = []
2444             for dir, attr in self.sort:
2445                 if dir == '-':
2446                     val.append('-'+attr)
2447                 else:
2448                     val.append(attr)
2449             add(sc+'sort', ','.join (val))
2450         if group:
2451             val = []
2452             for dir, attr in self.group:
2453                 if dir == '-':
2454                     val.append('-'+attr)
2455                 else:
2456                     val.append(attr)
2457             add(sc+'group', ','.join (val))
2458         if filter and self.filter:
2459             add(sc+'filter', ','.join(self.filter))
2460         if self.classname and filterspec:
2461             cls = self.client.db.getclass(self.classname)
2462             for k,v in self.filterspec.items():
2463                 if type(v) == type([]):
2464                     if isinstance(cls.get_transitive_prop(k), hyperdb.String):
2465                         add(k, ' '.join(v))
2466                     else:
2467                         add(k, ','.join(v))
2468                 else:
2469                     add(k, v)
2470         if search_text and self.search_text:
2471             add(sc+'search_text', self.search_text)
2472         add(sc+'pagesize', self.pagesize)
2473         add(sc+'startwith', self.startwith)
2474         return '\n'.join(l)
2476     def indexargs_url(self, url, args):
2477         """ Embed the current index args in a URL
2478         """
2479         q = urllib.quote
2480         sc = self.special_char
2481         l = ['%s=%s'%(k,v) for k,v in args.items()]
2483         # pull out the special values (prefixed by @ or :)
2484         specials = {}
2485         for key in args.keys():
2486             if key[0] in '@:':
2487                 specials[key[1:]] = args[key]
2489         # ok, now handle the specials we received in the request
2490         if self.columns and not specials.has_key('columns'):
2491             l.append(sc+'columns=%s'%(','.join(self.columns)))
2492         if self.sort and not specials.has_key('sort'):
2493             val = []
2494             for dir, attr in self.sort:
2495                 if dir == '-':
2496                     val.append('-'+attr)
2497                 else:
2498                     val.append(attr)
2499             l.append(sc+'sort=%s'%(','.join(val)))
2500         if self.group and not specials.has_key('group'):
2501             val = []
2502             for dir, attr in self.group:
2503                 if dir == '-':
2504                     val.append('-'+attr)
2505                 else:
2506                     val.append(attr)
2507             l.append(sc+'group=%s'%(','.join(val)))
2508         if self.filter and not specials.has_key('filter'):
2509             l.append(sc+'filter=%s'%(','.join(self.filter)))
2510         if self.search_text and not specials.has_key('search_text'):
2511             l.append(sc+'search_text=%s'%q(self.search_text))
2512         if not specials.has_key('pagesize'):
2513             l.append(sc+'pagesize=%s'%self.pagesize)
2514         if not specials.has_key('startwith'):
2515             l.append(sc+'startwith=%s'%self.startwith)
2517         # finally, the remainder of the filter args in the request
2518         if self.classname and self.filterspec:
2519             cls = self.client.db.getclass(self.classname)
2520             for k,v in self.filterspec.items():
2521                 if not args.has_key(k):
2522                     if type(v) == type([]):
2523                         prop = cls.get_transitive_prop(k)
2524                         if k != 'id' and isinstance(prop, hyperdb.String):
2525                             l.append('%s=%s'%(k, '%20'.join([q(i) for i in v])))
2526                         else:
2527                             l.append('%s=%s'%(k, ','.join([q(i) for i in v])))
2528                     else:
2529                         l.append('%s=%s'%(k, q(v)))
2530         return '%s?%s'%(url, '&'.join(l))
2531     indexargs_href = indexargs_url
2533     def base_javascript(self):
2534         return """
2535 <script type="text/javascript">
2536 submitted = false;
2537 function submit_once() {
2538     if (submitted) {
2539         alert("Your request is being processed.\\nPlease be patient.");
2540         event.returnValue = 0;    // work-around for IE
2541         return 0;
2542     }
2543     submitted = true;
2544     return 1;
2547 function help_window(helpurl, width, height) {
2548     HelpWin = window.open('%s' + helpurl, 'RoundupHelpWindow', 'scrollbars=yes,resizable=yes,toolbar=no,height='+height+',width='+width);
2550 </script>
2551 """%self.base
2553     def batch(self):
2554         """ Return a batch object for results from the "current search"
2555         """
2556         filterspec = self.filterspec
2557         sort = self.sort
2558         group = self.group
2560         # get the list of ids we're batching over
2561         klass = self.client.db.getclass(self.classname)
2562         if self.search_text:
2563             matches = self.client.db.indexer.search(
2564                 [w.upper().encode("utf-8", "replace") for w in re.findall(
2565                     r'(?u)\b\w{2,25}\b',
2566                     unicode(self.search_text, "utf-8", "replace")
2567                 )], klass)
2568         else:
2569             matches = None
2571         # filter for visibility
2572         check = self._client.db.security.hasPermission
2573         userid = self._client.userid
2574         l = [id for id in klass.filter(matches, filterspec, sort, group)
2575             if check('View', userid, self.classname, itemid=id)]
2577         # return the batch object, using IDs only
2578         return Batch(self.client, l, self.pagesize, self.startwith,
2579             classname=self.classname)
2581 # extend the standard ZTUtils Batch object to remove dependency on
2582 # Acquisition and add a couple of useful methods
2583 class Batch(ZTUtils.Batch):
2584     """ Use me to turn a list of items, or item ids of a given class, into a
2585         series of batches.
2587         ========= ========================================================
2588         Parameter  Usage
2589         ========= ========================================================
2590         sequence  a list of HTMLItems or item ids
2591         classname if sequence is a list of ids, this is the class of item
2592         size      how big to make the sequence.
2593         start     where to start (0-indexed) in the sequence.
2594         end       where to end (0-indexed) in the sequence.
2595         orphan    if the next batch would contain less items than this
2596                   value, then it is combined with this batch
2597         overlap   the number of items shared between adjacent batches
2598         ========= ========================================================
2600         Attributes: Note that the "start" attribute, unlike the
2601         argument, is a 1-based index (I know, lame).  "first" is the
2602         0-based index.  "length" is the actual number of elements in
2603         the batch.
2605         "sequence_length" is the length of the original, unbatched, sequence.
2606     """
2607     def __init__(self, client, sequence, size, start, end=0, orphan=0,
2608             overlap=0, classname=None):
2609         self.client = client
2610         self.last_index = self.last_item = None
2611         self.current_item = None
2612         self.classname = classname
2613         self.sequence_length = len(sequence)
2614         ZTUtils.Batch.__init__(self, sequence, size, start, end, orphan,
2615             overlap)
2617     # overwrite so we can late-instantiate the HTMLItem instance
2618     def __getitem__(self, index):
2619         if index < 0:
2620             if index + self.end < self.first: raise IndexError, index
2621             return self._sequence[index + self.end]
2623         if index >= self.length:
2624             raise IndexError, index
2626         # move the last_item along - but only if the fetched index changes
2627         # (for some reason, index 0 is fetched twice)
2628         if index != self.last_index:
2629             self.last_item = self.current_item
2630             self.last_index = index
2632         item = self._sequence[index + self.first]
2633         if self.classname:
2634             # map the item ids to instances
2635             item = HTMLItem(self.client, self.classname, item)
2636         self.current_item = item
2637         return item
2639     def propchanged(self, *properties):
2640         """ Detect if one of the properties marked as being a group
2641             property changed in the last iteration fetch
2642         """
2643         # we poke directly at the _value here since MissingValue can screw
2644         # us up and cause Nones to compare strangely
2645         if self.last_item is None:
2646             return 1
2647         for property in properties:
2648             if property == 'id' or isinstance (self.last_item[property], list):
2649                 if (str(self.last_item[property]) !=
2650                     str(self.current_item[property])):
2651                     return 1
2652             else:
2653                 if (self.last_item[property]._value !=
2654                     self.current_item[property]._value):
2655                     return 1
2656         return 0
2658     # override these 'cos we don't have access to acquisition
2659     def previous(self):
2660         if self.start == 1:
2661             return None
2662         return Batch(self.client, self._sequence, self._size,
2663             self.first - self._size + self.overlap, 0, self.orphan,
2664             self.overlap)
2666     def next(self):
2667         try:
2668             self._sequence[self.end]
2669         except IndexError:
2670             return None
2671         return Batch(self.client, self._sequence, self._size,
2672             self.end - self.overlap, 0, self.orphan, self.overlap)
2674 class TemplatingUtils:
2675     """ Utilities for templating
2676     """
2677     def __init__(self, client):
2678         self.client = client
2679     def Batch(self, sequence, size, start, end=0, orphan=0, overlap=0):
2680         return Batch(self.client, sequence, size, start, end, orphan,
2681             overlap)
2683     def url_quote(self, url):
2684         """URL-quote the supplied text."""
2685         return urllib.quote(url)
2687     def html_quote(self, html):
2688         """HTML-quote the supplied text."""
2689         return cgi.escape(html)
2691     def __getattr__(self, name):
2692         """Try the tracker's templating_utils."""
2693         if not hasattr(self.client.instance, 'templating_utils'):
2694             # backwards-compatibility
2695             raise AttributeError, name
2696         if not self.client.instance.templating_utils.has_key(name):
2697             raise AttributeError, name
2698         return self.client.instance.templating_utils[name]
2700     def html_calendar(self, request):
2701         """Generate a HTML calendar.
2703         `request`  the roundup.request object
2704                    - @template : name of the template
2705                    - form      : name of the form to store back the date
2706                    - property  : name of the property of the form to store
2707                                  back the date
2708                    - date      : current date
2709                    - display   : when browsing, specifies year and month
2711         html will simply be a table.
2712         """
2713         date_str  = request.form.getfirst("date", ".")
2714         display   = request.form.getfirst("display", date_str)
2715         template  = request.form.getfirst("@template", "calendar")
2716         form      = request.form.getfirst("form")
2717         property  = request.form.getfirst("property")
2718         curr_date = date.Date(date_str) # to highlight
2719         display   = date.Date(display)  # to show
2720         day       = display.day
2722         # for navigation
2723         date_prev_month = display + date.Interval("-1m")
2724         date_next_month = display + date.Interval("+1m")
2725         date_prev_year  = display + date.Interval("-1y")
2726         date_next_year  = display + date.Interval("+1y")
2728         res = []
2730         base_link = "%s?@template=%s&property=%s&form=%s&date=%s" % \
2731                     (request.classname, template, property, form, curr_date)
2733         # navigation
2734         # month
2735         res.append('<table class="calendar"><tr><td>')
2736         res.append(' <table width="100%" class="calendar_nav"><tr>')
2737         link = "&display=%s"%date_prev_month
2738         res.append('  <td><a href="%s&display=%s">&lt;</a></td>'%(base_link,
2739             date_prev_month))
2740         res.append('  <td>%s</td>'%calendar.month_name[display.month])
2741         res.append('  <td><a href="%s&display=%s">&gt;</a></td>'%(base_link,
2742             date_next_month))
2743         # spacer
2744         res.append('  <td width="100%"></td>')
2745         # year
2746         res.append('  <td><a href="%s&display=%s">&lt;</a></td>'%(base_link,
2747             date_prev_year))
2748         res.append('  <td>%s</td>'%display.year)
2749         res.append('  <td><a href="%s&display=%s">&gt;</a></td>'%(base_link,
2750             date_next_year))
2751         res.append(' </tr></table>')
2752         res.append(' </td></tr>')
2754         # the calendar
2755         res.append(' <tr><td><table class="calendar_display">')
2756         res.append('  <tr class="weekdays">')
2757         for day in calendar.weekheader(3).split():
2758             res.append('   <td>%s</td>'%day)
2759         res.append('  </tr>')
2760         for week in calendar.monthcalendar(display.year, display.month):
2761             res.append('  <tr>')
2762             for day in week:
2763                 link = "javascript:form[field].value = '%d-%02d-%02d'; " \
2764                       "window.close ();"%(display.year, display.month, day)
2765                 if (day == curr_date.day and display.month == curr_date.month
2766                         and display.year == curr_date.year):
2767                     # highlight
2768                     style = "today"
2769                 else :
2770                     style = ""
2771                 if day:
2772                     res.append('   <td class="%s"><a href="%s">%s</a></td>'%(
2773                         style, link, day))
2774                 else :
2775                     res.append('   <td></td>')
2776             res.append('  </tr>')
2777         res.append('</table></td></tr></table>')
2778         return "\n".join(res)
2780 class MissingValue:
2781     def __init__(self, description, **kwargs):
2782         self.__description = description
2783         for key, value in kwargs.items():
2784             self.__dict__[key] = value
2786     def __call__(self, *args, **kwargs): return MissingValue(self.__description)
2787     def __getattr__(self, name):
2788         # This allows assignments which assume all intermediate steps are Null
2789         # objects if they don't exist yet.
2790         #
2791         # For example (with just 'client' defined):
2792         #
2793         # client.db.config.TRACKER_WEB = 'BASE/'
2794         self.__dict__[name] = MissingValue(self.__description)
2795         return getattr(self, name)
2797     def __getitem__(self, key): return self
2798     def __nonzero__(self): return 0
2799     def __str__(self): return '[%s]'%self.__description
2800     def __repr__(self): return '<MissingValue 0x%x "%s">'%(id(self),
2801         self.__description)
2802     def gettext(self, str): return str
2803     _ = gettext
2805 # vim: set et sts=4 sw=4 :