Code

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