Code

Robustify (Multi)linkHTMLProperty.plain.
[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')
1105         # The context for a search page should be the class, not any
1106         # node.
1107         self._client.nodeid = None
1108         
1109         # use our fabricated request
1110         return pt.render(self._client, req.classname, req)
1112     def download_url(self):
1113         """ Assume that this item is a FileClass and that it has a name
1114         and content. Construct a URL for the download of the content.
1115         """
1116         name = self._klass.get(self._nodeid, 'name')
1117         url = '%s%s/%s'%(self._classname, self._nodeid, name)
1118         return urllib.quote(url)
1120     def copy_url(self, exclude=("messages", "files")):
1121         """Construct a URL for creating a copy of this item
1123         "exclude" is an optional list of properties that should
1124         not be copied to the new object.  By default, this list
1125         includes "messages" and "files" properties.  Note that
1126         "id" property cannot be copied.
1128         """
1129         exclude = ("id", "activity", "actor", "creation", "creator") \
1130             + tuple(exclude)
1131         query = {
1132             "@template": "item",
1133             "@note": self._("Copy of %(class)s %(id)s") % {
1134                 "class": self._(self._classname), "id": self._nodeid},
1135         }
1136         for name in self._props.keys():
1137             if name not in exclude:
1138                 query[name] = self[name].plain()
1139         return self._classname + "?" + "&".join(
1140             ["%s=%s" % (key, urllib.quote(value))
1141                 for key, value in query.items()])
1143 class _HTMLUser(_HTMLItem):
1144     """Add ability to check for permissions on users.
1145     """
1146     _marker = []
1147     def hasPermission(self, permission, classname=_marker,
1148             property=None, itemid=None):
1149         """Determine if the user has the Permission.
1151         The class being tested defaults to the template's class, but may
1152         be overidden for this test by suppling an alternate classname.
1153         """
1154         if classname is self._marker:
1155             classname = self._client.classname
1156         return self._db.security.hasPermission(permission,
1157             self._nodeid, classname, property, itemid)
1159     def hasRole(self, rolename):
1160         """Determine whether the user has the Role."""
1161         roles = self._db.user.get(self._nodeid, 'roles').split(',')
1162         for role in roles:
1163             if role.strip() == rolename: return True
1164         return False
1166 def HTMLItem(client, classname, nodeid, anonymous=0):
1167     if classname == 'user':
1168         return _HTMLUser(client, classname, nodeid, anonymous)
1169     else:
1170         return _HTMLItem(client, classname, nodeid, anonymous)
1172 class HTMLProperty(HTMLInputMixin, HTMLPermissions):
1173     """ String, Number, Date, Interval HTMLProperty
1175         Has useful attributes:
1177          _name  the name of the property
1178          _value the value of the property if any
1180         A wrapper object which may be stringified for the plain() behaviour.
1181     """
1182     def __init__(self, client, classname, nodeid, prop, name, value,
1183             anonymous=0):
1184         self._client = client
1185         self._db = client.db
1186         self._ = client._
1187         self._classname = classname
1188         self._nodeid = nodeid
1189         self._prop = prop
1190         self._value = value
1191         self._anonymous = anonymous
1192         self._name = name
1193         if not anonymous:
1194             self._formname = '%s%s@%s'%(classname, nodeid, name)
1195         else:
1196             self._formname = name
1198         # If no value is already present for this property, see if one
1199         # is specified in the current form.
1200         form = self._client.form
1201         if not self._value and form.has_key(self._formname):
1202             if isinstance(prop, hyperdb.Multilink):
1203                 value = lookupIds(self._db, prop,
1204                                   handleListCGIValue(form[self._formname]),
1205                                   fail_ok=1)
1206             elif isinstance(prop, hyperdb.Link):
1207                 value = form.getfirst(self._formname).strip()
1208                 if value:
1209                     value = lookupIds(self._db, prop, [value],
1210                                       fail_ok=1)[0]
1211                 else:
1212                     value = None
1213             else:
1214                 value = form.getfirst(self._formname).strip() or None
1215             self._value = value
1217         HTMLInputMixin.__init__(self)
1219     def __repr__(self):
1220         return '<HTMLProperty(0x%x) %s %r %r>'%(id(self), self._formname,
1221             self._prop, self._value)
1222     def __str__(self):
1223         return self.plain()
1224     def __cmp__(self, other):
1225         if isinstance(other, HTMLProperty):
1226             return cmp(self._value, other._value)
1227         return cmp(self._value, other)
1229     def __nonzero__(self):
1230         return not not self._value
1232     def isset(self):
1233         """Is my _value not None?"""
1234         return self._value is not None
1236     def is_edit_ok(self):
1237         """Should the user be allowed to use an edit form field for this
1238         property. Check "Create" for new items, or "Edit" for existing
1239         ones.
1240         """
1241         if self._nodeid:
1242             return self._db.security.hasPermission('Edit', self._client.userid,
1243                 self._classname, self._name, self._nodeid)
1244         return self._db.security.hasPermission('Create', self._client.userid,
1245             self._classname, self._name)
1247     def is_view_ok(self):
1248         """ Is the user allowed to View the current class?
1249         """
1250         if self._db.security.hasPermission('View', self._client.userid,
1251                 self._classname, self._name, self._nodeid):
1252             return 1
1253         return self.is_edit_ok()
1255 class StringHTMLProperty(HTMLProperty):
1256     hyper_re = re.compile(r'''(
1257         (?P<url>
1258          (
1259           (ht|f)tp(s?)://                   # protocol
1260           ([\w]+(:\w+)?@)?                  # username/password
1261           ([\w\-]+)                         # hostname
1262           ((\.[\w-]+)+)?                    # .domain.etc
1263          |                                  # ... or ...
1264           ([\w]+(:\w+)?@)?                  # username/password
1265           www\.                             # "www."
1266           ([\w\-]+\.)+                      # hostname
1267           [\w]{2,5}                         # TLD
1268          )
1269          (:[\d]{1,5})?                     # port
1270          (/[\w\-$.+!*(),;:@&=?/~\\#%]*)?   # path etc.
1271         )|
1272         (?P<email>[-+=%/\w\.]+@[\w\.\-]+)|
1273         (?P<item>(?P<class>[A-Za-z_]+)(\s*)(?P<id>\d+))
1274     )''', re.X | re.I)
1275     protocol_re = re.compile('^(ht|f)tp(s?)://', re.I)
1277     def _hyper_repl_item(self,match,replacement):
1278         item = match.group('item')
1279         cls = match.group('class').lower()
1280         id = match.group('id')
1281         try:
1282             # make sure cls is a valid tracker classname
1283             cl = self._db.getclass(cls)
1284             if not cl.hasnode(id):
1285                 return item
1286             return replacement % locals()
1287         except KeyError:
1288             return item
1290     def _hyper_repl(self, match):
1291         if match.group('url'):
1292             u = s = match.group('url')
1293             if not self.protocol_re.search(s):
1294                 u = 'http://' + s
1295             # catch an escaped ">" at the end of the URL
1296             if s.endswith('&gt;'):
1297                 u = s = s[:-4]
1298                 e = '&gt;'
1299             else:
1300                 e = ''
1301             return '<a href="%s">%s</a>%s'%(u, s, e)
1302         elif match.group('email'):
1303             s = match.group('email')
1304             return '<a href="mailto:%s">%s</a>'%(s, s)
1305         else:
1306             return self._hyper_repl_item(match,
1307                 '<a href="%(cls)s%(id)s">%(item)s</a>')
1309     def _hyper_repl_rst(self, match):
1310         if match.group('url'):
1311             s = match.group('url')
1312             return '`%s <%s>`_'%(s, s)
1313         elif match.group('email'):
1314             s = match.group('email')
1315             return '`%s <mailto:%s>`_'%(s, s)
1316         else:
1317             return self._hyper_repl_item(match,'`%(item)s <%(cls)s%(id)s>`_')
1319     def hyperlinked(self):
1320         """ Render a "hyperlinked" version of the text """
1321         return self.plain(hyperlink=1)
1323     def plain(self, escape=0, hyperlink=0):
1324         """Render a "plain" representation of the property
1326         - "escape" turns on/off HTML quoting
1327         - "hyperlink" turns on/off in-text hyperlinking of URLs, email
1328           addresses and designators
1329         """
1330         if not self.is_view_ok():
1331             return self._('[hidden]')
1333         if self._value is None:
1334             return ''
1335         if escape:
1336             s = cgi.escape(str(self._value))
1337         else:
1338             s = str(self._value)
1339         if hyperlink:
1340             # no, we *must* escape this text
1341             if not escape:
1342                 s = cgi.escape(s)
1343             s = self.hyper_re.sub(self._hyper_repl, s)
1344         return s
1346     def wrapped(self, escape=1, hyperlink=1):
1347         """Render a "wrapped" representation of the property.
1349         We wrap long lines at 80 columns on the nearest whitespace. Lines
1350         with no whitespace are not broken to force wrapping.
1352         Note that unlike plain() we default wrapped() to have the escaping
1353         and hyperlinking turned on since that's the most common usage.
1355         - "escape" turns on/off HTML quoting
1356         - "hyperlink" turns on/off in-text hyperlinking of URLs, email
1357           addresses and designators
1358         """
1359         if not self.is_view_ok():
1360             return self._('[hidden]')
1362         if self._value is None:
1363             return ''
1364         s = support.wrap(str(self._value), width=80)
1365         if escape:
1366             s = cgi.escape(s)
1367         if hyperlink:
1368             # no, we *must* escape this text
1369             if not escape:
1370                 s = cgi.escape(s)
1371             s = self.hyper_re.sub(self._hyper_repl, s)
1372         return s
1374     def stext(self, escape=0, hyperlink=1):
1375         """ Render the value of the property as StructuredText.
1377             This requires the StructureText module to be installed separately.
1378         """
1379         if not self.is_view_ok():
1380             return self._('[hidden]')
1382         s = self.plain(escape=escape, hyperlink=hyperlink)
1383         if not StructuredText:
1384             return s
1385         return StructuredText(s,level=1,header=0)
1387     def rst(self, hyperlink=1):
1388         """ Render the value of the property as ReStructuredText.
1390             This requires docutils to be installed separately.
1391         """
1392         if not self.is_view_ok():
1393             return self._('[hidden]')
1395         if not ReStructuredText:
1396             return self.plain(escape=0, hyperlink=hyperlink)
1397         s = self.plain(escape=0, hyperlink=0)
1398         if hyperlink:
1399             s = self.hyper_re.sub(self._hyper_repl_rst, s)
1400         return ReStructuredText(s, writer_name="html")["body"].encode("utf-8",
1401             "replace")
1403     def field(self, **kwargs):
1404         """ Render the property as a field in HTML.
1406             If not editable, just display the value via plain().
1407         """
1408         if not self.is_edit_ok():
1409             return self.plain(escape=1)
1411         value = self._value
1412         if value is None:
1413             value = ''
1415         kwargs.setdefault("size", 30)
1416         kwargs.update({"name": self._formname, "value": value})
1417         return self.input(**kwargs)
1419     def multiline(self, escape=0, rows=5, cols=40, **kwargs):
1420         """ Render a multiline form edit field for the property.
1422             If not editable, just display the plain() value in a <pre> tag.
1423         """
1424         if not self.is_edit_ok():
1425             return '<pre>%s</pre>'%self.plain()
1427         if self._value is None:
1428             value = ''
1429         else:
1430             value = cgi.escape(str(self._value))
1432             value = '&quot;'.join(value.split('"'))
1433         name = self._formname
1434         passthrough_args = ' '.join(['%s="%s"' % (k, cgi.escape(str(v), True))
1435             for k,v in kwargs.items()])
1436         return ('<textarea %(passthrough_args)s name="%(name)s" id="%(name)s"'
1437                 ' rows="%(rows)s" cols="%(cols)s">'
1438                  '%(value)s</textarea>') % locals()
1440     def email(self, escape=1):
1441         """ Render the value of the property as an obscured email address
1442         """
1443         if not self.is_view_ok():
1444             return self._('[hidden]')
1446         if self._value is None:
1447             value = ''
1448         else:
1449             value = str(self._value)
1450         split = value.split('@')
1451         if len(split) == 2:
1452             name, domain = split
1453             domain = ' '.join(domain.split('.')[:-1])
1454             name = name.replace('.', ' ')
1455             value = '%s at %s ...'%(name, domain)
1456         else:
1457             value = value.replace('.', ' ')
1458         if escape:
1459             value = cgi.escape(value)
1460         return value
1462 class PasswordHTMLProperty(HTMLProperty):
1463     def plain(self, escape=0):
1464         """ Render a "plain" representation of the property
1465         """
1466         if not self.is_view_ok():
1467             return self._('[hidden]')
1469         if self._value is None:
1470             return ''
1471         return self._('*encrypted*')
1473     def field(self, size=30):
1474         """ Render a form edit field for the property.
1476             If not editable, just display the value via plain().
1477         """
1478         if not self.is_edit_ok():
1479             return self.plain(escape=1)
1481         return self.input(type="password", name=self._formname, size=size)
1483     def confirm(self, size=30):
1484         """ Render a second form edit field for the property, used for
1485             confirmation that the user typed the password correctly. Generates
1486             a field with name "@confirm@name".
1488             If not editable, display nothing.
1489         """
1490         if not self.is_edit_ok():
1491             return ''
1493         return self.input(type="password",
1494             name="@confirm@%s"%self._formname,
1495             id="%s-confirm"%self._formname,
1496             size=size)
1498 class NumberHTMLProperty(HTMLProperty):
1499     def plain(self, escape=0):
1500         """ Render a "plain" representation of the property
1501         """
1502         if not self.is_view_ok():
1503             return self._('[hidden]')
1505         if self._value is None:
1506             return ''
1508         return str(self._value)
1510     def field(self, size=30):
1511         """ Render a form edit field for the property.
1513             If not editable, just display the value via plain().
1514         """
1515         if not self.is_edit_ok():
1516             return self.plain(escape=1)
1518         value = self._value
1519         if value is None:
1520             value = ''
1522         return self.input(name=self._formname, value=value, size=size)
1524     def __int__(self):
1525         """ Return an int of me
1526         """
1527         return int(self._value)
1529     def __float__(self):
1530         """ Return a float of me
1531         """
1532         return float(self._value)
1535 class BooleanHTMLProperty(HTMLProperty):
1536     def plain(self, escape=0):
1537         """ Render a "plain" representation of the property
1538         """
1539         if not self.is_view_ok():
1540             return self._('[hidden]')
1542         if self._value is None:
1543             return ''
1544         return self._value and self._("Yes") or self._("No")
1546     def field(self):
1547         """ Render a form edit field for the property
1549             If not editable, just display the value via plain().
1550         """
1551         if not self.is_edit_ok():
1552             return self.plain(escape=1)
1554         value = self._value
1555         if isinstance(value, str) or isinstance(value, unicode):
1556             value = value.strip().lower() in ('checked', 'yes', 'true',
1557                 'on', '1')
1559         checked = value and "checked" or ""
1560         if value:
1561             s = self.input(type="radio", name=self._formname, value="yes",
1562                 checked="checked")
1563             s += self._('Yes')
1564             s +=self.input(type="radio", name=self._formname, value="no")
1565             s += self._('No')
1566         else:
1567             s = self.input(type="radio", name=self._formname, value="yes")
1568             s += self._('Yes')
1569             s +=self.input(type="radio", name=self._formname, value="no",
1570                 checked="checked")
1571             s += self._('No')
1572         return s
1574 class DateHTMLProperty(HTMLProperty):
1576     _marker = []
1578     def __init__(self, client, classname, nodeid, prop, name, value,
1579             anonymous=0, offset=None):
1580         HTMLProperty.__init__(self, client, classname, nodeid, prop, name,
1581                 value, anonymous=anonymous)
1582         if self._value and not (isinstance(self._value, str) or
1583                 isinstance(self._value, unicode)):
1584             self._value.setTranslator(self._client.translator)
1585         self._offset = offset
1586         if self._offset is None :
1587             self._offset = self._prop.offset (self._db)
1589     def plain(self, escape=0):
1590         """ Render a "plain" representation of the property
1591         """
1592         if not self.is_view_ok():
1593             return self._('[hidden]')
1595         if self._value is None:
1596             return ''
1597         if self._offset is None:
1598             offset = self._db.getUserTimezone()
1599         else:
1600             offset = self._offset
1601         return str(self._value.local(offset))
1603     def now(self, str_interval=None):
1604         """ Return the current time.
1606             This is useful for defaulting a new value. Returns a
1607             DateHTMLProperty.
1608         """
1609         if not self.is_view_ok():
1610             return self._('[hidden]')
1612         ret = date.Date('.', translator=self._client)
1614         if isinstance(str_interval, basestring):
1615             sign = 1
1616             if str_interval[0] == '-':
1617                 sign = -1
1618                 str_interval = str_interval[1:]
1619             interval = date.Interval(str_interval, translator=self._client)
1620             if sign > 0:
1621                 ret = ret + interval
1622             else:
1623                 ret = ret - interval
1625         return DateHTMLProperty(self._client, self._classname, self._nodeid,
1626             self._prop, self._formname, ret)
1628     def field(self, size=30, default=None, format=_marker, popcal=True):
1629         """Render a form edit field for the property
1631         If not editable, just display the value via plain().
1633         If "popcal" then include the Javascript calendar editor.
1634         Default=yes.
1636         The format string is a standard python strftime format string.
1637         """
1638         if not self.is_edit_ok():
1639             if format is self._marker:
1640                 return self.plain(escape=1)
1641             else:
1642                 return self.pretty(format)
1644         value = self._value
1646         if value is None:
1647             if default is None:
1648                 raw_value = None
1649             else:
1650                 if isinstance(default, basestring):
1651                     raw_value = date.Date(default, translator=self._client)
1652                 elif isinstance(default, date.Date):
1653                     raw_value = default
1654                 elif isinstance(default, DateHTMLProperty):
1655                     raw_value = default._value
1656                 else:
1657                     raise ValueError, self._('default value for '
1658                         'DateHTMLProperty must be either DateHTMLProperty '
1659                         'or string date representation.')
1660         elif isinstance(value, str) or isinstance(value, unicode):
1661             # most likely erroneous input to be passed back to user
1662             if isinstance(value, unicode): value = value.encode('utf8')
1663             return self.input(name=self._formname, value=value, size=size)
1664         else:
1665             raw_value = value
1667         if raw_value is None:
1668             value = ''
1669         elif isinstance(raw_value, str) or isinstance(raw_value, unicode):
1670             if format is self._marker:
1671                 value = raw_value
1672             else:
1673                 value = date.Date(raw_value).pretty(format)
1674         else:
1675             if self._offset is None :
1676                 offset = self._db.getUserTimezone()
1677             else :
1678                 offset = self._offset
1679             value = raw_value.local(offset)
1680             if format is not self._marker:
1681                 value = value.pretty(format)
1683         s = self.input(name=self._formname, value=value, size=size)
1684         if popcal:
1685             s += self.popcal()
1686         return s
1688     def reldate(self, pretty=1):
1689         """ Render the interval between the date and now.
1691             If the "pretty" flag is true, then make the display pretty.
1692         """
1693         if not self.is_view_ok():
1694             return self._('[hidden]')
1696         if not self._value:
1697             return ''
1699         # figure the interval
1700         interval = self._value - date.Date('.', translator=self._client)
1701         if pretty:
1702             return interval.pretty()
1703         return str(interval)
1705     def pretty(self, format=_marker):
1706         """ Render the date in a pretty format (eg. month names, spaces).
1708             The format string is a standard python strftime format string.
1709             Note that if the day is zero, and appears at the start of the
1710             string, then it'll be stripped from the output. This is handy
1711             for the situation when a date only specifies a month and a year.
1712         """
1713         if not self.is_view_ok():
1714             return self._('[hidden]')
1716         if self._offset is None:
1717             offset = self._db.getUserTimezone()
1718         else:
1719             offset = self._offset
1721         if not self._value:
1722             return ''
1723         elif format is not self._marker:
1724             return self._value.local(offset).pretty(format)
1725         else:
1726             return self._value.local(offset).pretty()
1728     def local(self, offset):
1729         """ Return the date/time as a local (timezone offset) date/time.
1730         """
1731         if not self.is_view_ok():
1732             return self._('[hidden]')
1734         return DateHTMLProperty(self._client, self._classname, self._nodeid,
1735             self._prop, self._formname, self._value, offset=offset)
1737     def popcal(self, width=300, height=200, label="(cal)",
1738             form="itemSynopsis"):
1739         """Generate a link to a calendar pop-up window.
1741         item: HTMLProperty e.g.: context.deadline
1742         """
1743         if self.isset():
1744             date = "&date=%s"%self._value
1745         else :
1746             date = ""
1747         return ('<a class="classhelp" href="javascript:help_window('
1748             "'%s?@template=calendar&amp;property=%s&amp;form=%s%s', %d, %d)"
1749             '">%s</a>'%(self._classname, self._name, form, date, width,
1750             height, label))
1752 class IntervalHTMLProperty(HTMLProperty):
1753     def __init__(self, client, classname, nodeid, prop, name, value,
1754             anonymous=0):
1755         HTMLProperty.__init__(self, client, classname, nodeid, prop,
1756             name, value, anonymous)
1757         if self._value and not isinstance(self._value, (str, unicode)):
1758             self._value.setTranslator(self._client.translator)
1760     def plain(self, escape=0):
1761         """ Render a "plain" representation of the property
1762         """
1763         if not self.is_view_ok():
1764             return self._('[hidden]')
1766         if self._value is None:
1767             return ''
1768         return str(self._value)
1770     def pretty(self):
1771         """ Render the interval in a pretty format (eg. "yesterday")
1772         """
1773         if not self.is_view_ok():
1774             return self._('[hidden]')
1776         return self._value.pretty()
1778     def field(self, size=30):
1779         """ Render a form edit field for the property
1781             If not editable, just display the value via plain().
1782         """
1783         if not self.is_edit_ok():
1784             return self.plain(escape=1)
1786         value = self._value
1787         if value is None:
1788             value = ''
1790         return self.input(name=self._formname, value=value, size=size)
1792 class LinkHTMLProperty(HTMLProperty):
1793     """ Link HTMLProperty
1794         Include the above as well as being able to access the class
1795         information. Stringifying the object itself results in the value
1796         from the item being displayed. Accessing attributes of this object
1797         result in the appropriate entry from the class being queried for the
1798         property accessed (so item/assignedto/name would look up the user
1799         entry identified by the assignedto property on item, and then the
1800         name property of that user)
1801     """
1802     def __init__(self, *args, **kw):
1803         HTMLProperty.__init__(self, *args, **kw)
1804         # if we're representing a form value, then the -1 from the form really
1805         # should be a None
1806         if str(self._value) == '-1':
1807             self._value = None
1809     def __getattr__(self, attr):
1810         """ return a new HTMLItem """
1811         if not self._value:
1812             # handle a special page templates lookup
1813             if attr == '__render_with_namespace__':
1814                 def nothing(*args, **kw):
1815                     return ''
1816                 return nothing
1817             msg = self._('Attempt to look up %(attr)s on a missing value')
1818             return MissingValue(msg%locals())
1819         i = HTMLItem(self._client, self._prop.classname, self._value)
1820         return getattr(i, attr)
1822     def plain(self, escape=0):
1823         """ Render a "plain" representation of the property
1824         """
1825         if not self.is_view_ok():
1826             return self._('[hidden]')
1828         if self._value is None:
1829             return ''
1830         linkcl = self._db.classes[self._prop.classname]
1831         k = linkcl.labelprop(1)
1832         if num_re.match(self._value):
1833             try:
1834                 value = str(linkcl.get(self._value, k))
1835             except IndexError:
1836                 value = self._value
1837         else :
1838             value = self._value
1839         if escape:
1840             value = cgi.escape(value)
1841         return value
1843     def field(self, showid=0, size=None):
1844         """ Render a form edit field for the property
1846             If not editable, just display the value via plain().
1847         """
1848         if not self.is_edit_ok():
1849             return self.plain(escape=1)
1851         # edit field
1852         linkcl = self._db.getclass(self._prop.classname)
1853         if self._value is None:
1854             value = ''
1855         else:
1856             k = linkcl.getkey()
1857             if k and num_re.match(self._value):
1858                 value = linkcl.get(self._value, k)
1859             else:
1860                 value = self._value
1861         return self.input(name=self._formname, value=value, size=size)
1863     def menu(self, size=None, height=None, showid=0, additional=[], value=None,
1864             sort_on=None, **conditions):
1865         """ Render a form select list for this property
1867             "size" is used to limit the length of the list labels
1868             "height" is used to set the <select> tag's "size" attribute
1869             "showid" includes the item ids in the list labels
1870             "value" specifies which item is pre-selected
1871             "additional" lists properties which should be included in the
1872                 label
1873             "sort_on" indicates the property to sort the list on as
1874                 (direction, property) where direction is '+' or '-'. A
1875                 single string with the direction prepended may be used.
1876                 For example: ('-', 'order'), '+name'.
1878             The remaining keyword arguments are used as conditions for
1879             filtering the items in the list - they're passed as the
1880             "filterspec" argument to a Class.filter() call.
1882             If not editable, just display the value via plain().
1883         """
1884         if not self.is_edit_ok():
1885             return self.plain(escape=1)
1887         # Since None indicates the default, we need another way to
1888         # indicate "no selection".  We use -1 for this purpose, as
1889         # that is the value we use when submitting a form without the
1890         # value set.
1891         if value is None:
1892             value = self._value
1893         elif value == '-1':
1894             value = None
1896         linkcl = self._db.getclass(self._prop.classname)
1897         l = ['<select name="%s">'%self._formname]
1898         k = linkcl.labelprop(1)
1899         s = ''
1900         if value is None:
1901             s = 'selected="selected" '
1902         l.append(self._('<option %svalue="-1">- no selection -</option>')%s)
1904         if sort_on is not None:
1905             if not isinstance(sort_on, tuple):
1906                 if sort_on[0] in '+-':
1907                     sort_on = (sort_on[0], sort_on[1:])
1908                 else:
1909                     sort_on = ('+', sort_on)
1910         else:
1911             sort_on = ('+', linkcl.orderprop())
1913         options = [opt
1914             for opt in linkcl.filter(None, conditions, sort_on, (None, None))
1915             if self._db.security.hasPermission("View", self._client.userid,
1916                 linkcl.classname, itemid=opt)]
1918         # make sure we list the current value if it's retired
1919         if value and value not in options:
1920             options.insert(0, value)
1922         if additional:
1923             additional_fns = []
1924             props = linkcl.getprops()
1925             for propname in additional:
1926                 prop = props[propname]
1927                 if isinstance(prop, hyperdb.Link):
1928                     cl = self._db.getclass(prop.classname)
1929                     labelprop = cl.labelprop()
1930                     fn = lambda optionid: cl.get(linkcl.get(optionid,
1931                                                             propname),
1932                                                  labelprop)
1933                 else:
1934                     fn = lambda optionid: linkcl.get(optionid, propname)
1935             additional_fns.append(fn)
1936             
1937         for optionid in options:
1938             # get the option value, and if it's None use an empty string
1939             option = linkcl.get(optionid, k) or ''
1941             # figure if this option is selected
1942             s = ''
1943             if value in [optionid, option]:
1944                 s = 'selected="selected" '
1946             # figure the label
1947             if showid:
1948                 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1949             elif not option:
1950                 lab = '%s%s'%(self._prop.classname, optionid)
1951             else:
1952                 lab = option
1954             # truncate if it's too long
1955             if size is not None and len(lab) > size:
1956                 lab = lab[:size-3] + '...'
1957             if additional:
1958                 m = []
1959                 for fn in additional_fns:
1960                     m.append(str(fn(optionid)))
1961                 lab = lab + ' (%s)'%', '.join(m)
1963             # and generate
1964             lab = cgi.escape(self._(lab))
1965             l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
1966         l.append('</select>')
1967         return '\n'.join(l)
1968 #    def checklist(self, ...)
1972 class MultilinkHTMLProperty(HTMLProperty):
1973     """ Multilink HTMLProperty
1975         Also be iterable, returning a wrapper object like the Link case for
1976         each entry in the multilink.
1977     """
1978     def __init__(self, *args, **kwargs):
1979         HTMLProperty.__init__(self, *args, **kwargs)
1980         if self._value:
1981             display_value = lookupIds(self._db, self._prop, self._value,
1982                 fail_ok=1, do_lookup=False)
1983             sortfun = make_sort_function(self._db, self._prop.classname)
1984             # sorting fails if the value contains
1985             # items not yet stored in the database
1986             # ignore these errors to preserve user input
1987             try:
1988                 display_value.sort(sortfun)
1989             except:
1990                 pass
1991             self._value = display_value
1993     def __len__(self):
1994         """ length of the multilink """
1995         return len(self._value)
1997     def __getattr__(self, attr):
1998         """ no extended attribute accesses make sense here """
1999         raise AttributeError, attr
2001     def viewableGenerator(self, values):
2002         """Used to iterate over only the View'able items in a class."""
2003         check = self._db.security.hasPermission
2004         userid = self._client.userid
2005         classname = self._prop.classname
2006         for value in values:
2007             if check('View', userid, classname, itemid=value):
2008                 yield HTMLItem(self._client, classname, value)
2010     def __iter__(self):
2011         """ iterate and return a new HTMLItem
2012         """
2013         return self.viewableGenerator(self._value)
2015     def reverse(self):
2016         """ return the list in reverse order
2017         """
2018         l = self._value[:]
2019         l.reverse()
2020         return self.viewableGenerator(l)
2022     def sorted(self, property):
2023         """ Return this multilink sorted by the given property """
2024         value = list(self.__iter__())
2025         value.sort(lambda a,b:cmp(a[property], b[property]))
2026         return value
2028     def __contains__(self, value):
2029         """ Support the "in" operator. We have to make sure the passed-in
2030             value is a string first, not a HTMLProperty.
2031         """
2032         return str(value) in self._value
2034     def isset(self):
2035         """Is my _value not []?"""
2036         return self._value != []
2038     def plain(self, escape=0):
2039         """ Render a "plain" representation of the property
2040         """
2041         if not self.is_view_ok():
2042             return self._('[hidden]')
2044         linkcl = self._db.classes[self._prop.classname]
2045         k = linkcl.labelprop(1)
2046         labels = []
2047         for v in self._value:
2048             if num_re.match(v):
2049                 try:
2050                     label = linkcl.get(v, k)
2051                 except IndexError:
2052                     label = None
2053                 # fall back to designator if label is None
2054                 if label is None: label = '%s%s'%(self._prop.classname, k)
2055             else:
2056                 label = v
2057             labels.append(label)
2058         value = ', '.join(labels)
2059         if escape:
2060             value = cgi.escape(value)
2061         return value
2063     def field(self, size=30, showid=0):
2064         """ Render a form edit field for the property
2066             If not editable, just display the value via plain().
2067         """
2068         if not self.is_edit_ok():
2069             return self.plain(escape=1)
2071         linkcl = self._db.getclass(self._prop.classname)
2072         value = self._value[:]
2073         # map the id to the label property
2074         if not linkcl.getkey():
2075             showid=1
2076         if not showid:
2077             k = linkcl.labelprop(1)
2078             value = lookupKeys(linkcl, k, value)
2079         value = ','.join(value)
2080         return self.input(name=self._formname, size=size, value=value)
2082     def menu(self, size=None, height=None, showid=0, additional=[],
2083              value=None, sort_on=None, **conditions):
2084         """ Render a form <select> list for this property.
2086             "size" is used to limit the length of the list labels
2087             "height" is used to set the <select> tag's "size" attribute
2088             "showid" includes the item ids in the list labels
2089             "additional" lists properties which should be included in the
2090                 label
2091             "value" specifies which item is pre-selected
2092             "sort_on" indicates the property to sort the list on as
2093                 (direction, property) where direction is '+' or '-'. A
2094                 single string with the direction prepended may be used.
2095                 For example: ('-', 'order'), '+name'.
2097             The remaining keyword arguments are used as conditions for
2098             filtering the items in the list - they're passed as the
2099             "filterspec" argument to a Class.filter() call.
2101             If not editable, just display the value via plain().
2102         """
2103         if not self.is_edit_ok():
2104             return self.plain(escape=1)
2106         if value is None:
2107             value = self._value
2109         linkcl = self._db.getclass(self._prop.classname)
2111         if sort_on is not None:
2112             if not isinstance(sort_on, tuple):
2113                 if sort_on[0] in '+-':
2114                     sort_on = (sort_on[0], sort_on[1:])
2115                 else:
2116                     sort_on = ('+', sort_on)
2117         else:
2118             sort_on = ('+', linkcl.orderprop())
2120         options = [opt
2121             for opt in linkcl.filter(None, conditions, sort_on)
2122             if self._db.security.hasPermission("View", self._client.userid,
2123                 linkcl.classname, itemid=opt)]
2124         
2125         # make sure we list the current values if they're retired
2126         for val in value:
2127             if val not in options:
2128                 options.insert(0, val)
2130         if not height:
2131             height = len(options)
2132             if value:
2133                 # The "no selection" option.
2134                 height += 1
2135             height = min(height, 7)
2136         l = ['<select multiple name="%s" size="%s">'%(self._formname, height)]
2137         k = linkcl.labelprop(1)
2139         if value:
2140             l.append('<option value="%s">- no selection -</option>'
2141                      % ','.join(['-' + v for v in value]))
2143         if additional:
2144             additional_fns = []
2145             props = linkcl.getprops()
2146             for propname in additional:
2147                 prop = props[propname]
2148                 if isinstance(prop, hyperdb.Link):
2149                     cl = self._db.getclass(prop.classname)
2150                     labelprop = cl.labelprop()
2151                     fn = lambda optionid: cl.get(linkcl.get(optionid,
2152                                                             propname),
2153                                                  labelprop)
2154                 else:
2155                     fn = lambda optionid: linkcl.get(optionid, propname)
2156             additional_fns.append(fn)
2157             
2158         for optionid in options:
2159             # get the option value, and if it's None use an empty string
2160             option = linkcl.get(optionid, k) or ''
2162             # figure if this option is selected
2163             s = ''
2164             if optionid in value or option in value:
2165                 s = 'selected="selected" '
2167             # figure the label
2168             if showid:
2169                 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
2170             else:
2171                 lab = option
2172             # truncate if it's too long
2173             if size is not None and len(lab) > size:
2174                 lab = lab[:size-3] + '...'
2175             if additional:
2176                 m = []
2177                 for fn in additional_fns:
2178                     m.append(str(fn(optionid)))
2179                 lab = lab + ' (%s)'%', '.join(m)
2181             # and generate
2182             lab = cgi.escape(self._(lab))
2183             l.append('<option %svalue="%s">%s</option>'%(s, optionid,
2184                 lab))
2185         l.append('</select>')
2186         return '\n'.join(l)
2188 # set the propclasses for HTMLItem
2189 propclasses = (
2190     (hyperdb.String, StringHTMLProperty),
2191     (hyperdb.Number, NumberHTMLProperty),
2192     (hyperdb.Boolean, BooleanHTMLProperty),
2193     (hyperdb.Date, DateHTMLProperty),
2194     (hyperdb.Interval, IntervalHTMLProperty),
2195     (hyperdb.Password, PasswordHTMLProperty),
2196     (hyperdb.Link, LinkHTMLProperty),
2197     (hyperdb.Multilink, MultilinkHTMLProperty),
2200 def make_sort_function(db, classname, sort_on=None):
2201     """Make a sort function for a given class
2202     """
2203     linkcl = db.getclass(classname)
2204     if sort_on is None:
2205         sort_on = linkcl.orderprop()
2206     def sortfunc(a, b):
2207         return cmp(linkcl.get(a, sort_on), linkcl.get(b, sort_on))
2208     return sortfunc
2210 def handleListCGIValue(value):
2211     """ Value is either a single item or a list of items. Each item has a
2212         .value that we're actually interested in.
2213     """
2214     if isinstance(value, type([])):
2215         return [value.value for value in value]
2216     else:
2217         value = value.value.strip()
2218         if not value:
2219             return []
2220         return [v.strip() for v in value.split(',')]
2222 class HTMLRequest(HTMLInputMixin):
2223     """The *request*, holding the CGI form and environment.
2225     - "form" the CGI form as a cgi.FieldStorage
2226     - "env" the CGI environment variables
2227     - "base" the base URL for this instance
2228     - "user" a HTMLItem instance for this user
2229     - "language" as determined by the browser or config
2230     - "classname" the current classname (possibly None)
2231     - "template" the current template (suffix, also possibly None)
2233     Index args:
2235     - "columns" dictionary of the columns to display in an index page
2236     - "show" a convenience access to columns - request/show/colname will
2237       be true if the columns should be displayed, false otherwise
2238     - "sort" index sort column (direction, column name)
2239     - "group" index grouping property (direction, column name)
2240     - "filter" properties to filter the index on
2241     - "filterspec" values to filter the index on
2242     - "search_text" text to perform a full-text search on for an index
2243     """
2244     def __repr__(self):
2245         return '<HTMLRequest %r>'%self.__dict__
2247     def __init__(self, client):
2248         # _client is needed by HTMLInputMixin
2249         self._client = self.client = client
2251         # easier access vars
2252         self.form = client.form
2253         self.env = client.env
2254         self.base = client.base
2255         self.user = HTMLItem(client, 'user', client.userid)
2256         self.language = client.language
2258         # store the current class name and action
2259         self.classname = client.classname
2260         self.nodeid = client.nodeid
2261         self.template = client.template
2263         # the special char to use for special vars
2264         self.special_char = '@'
2266         HTMLInputMixin.__init__(self)
2268         self._post_init()
2270     def current_url(self):
2271         url = self.base
2272         if self.classname:
2273             url += self.classname
2274             if self.nodeid:
2275                 url += self.nodeid
2276         args = {}
2277         if self.template:
2278             args['@template'] = self.template
2279         return self.indexargs_url(url, args)
2281     def _parse_sort(self, var, name):
2282         """ Parse sort/group options. Append to var
2283         """
2284         fields = []
2285         dirs = []
2286         for special in '@:':
2287             idx = 0
2288             key = '%s%s%d'%(special, name, idx)
2289             while key in self.form:
2290                 self.special_char = special
2291                 fields.append(self.form.getfirst(key))
2292                 dirkey = '%s%sdir%d'%(special, name, idx)
2293                 if dirkey in self.form:
2294                     dirs.append(self.form.getfirst(dirkey))
2295                 else:
2296                     dirs.append(None)
2297                 idx += 1
2298                 key = '%s%s%d'%(special, name, idx)
2299             # backward compatible (and query) URL format
2300             key = special + name
2301             dirkey = key + 'dir'
2302             if key in self.form and not fields:
2303                 fields = handleListCGIValue(self.form[key])
2304                 if dirkey in self.form:
2305                     dirs.append(self.form.getfirst(dirkey))
2306             if fields: # only try other special char if nothing found
2307                 break
2308         for f, d in map(None, fields, dirs):
2309             if f.startswith('-'):
2310                 var.append(('-', f[1:]))
2311             elif d:
2312                 var.append(('-', f))
2313             else:
2314                 var.append(('+', f))
2316     def _post_init(self):
2317         """ Set attributes based on self.form
2318         """
2319         # extract the index display information from the form
2320         self.columns = []
2321         for name in ':columns @columns'.split():
2322             if self.form.has_key(name):
2323                 self.special_char = name[0]
2324                 self.columns = handleListCGIValue(self.form[name])
2325                 break
2326         self.show = support.TruthDict(self.columns)
2328         # sorting and grouping
2329         self.sort = []
2330         self.group = []
2331         self._parse_sort(self.sort, 'sort')
2332         self._parse_sort(self.group, 'group')
2334         # filtering
2335         self.filter = []
2336         for name in ':filter @filter'.split():
2337             if self.form.has_key(name):
2338                 self.special_char = name[0]
2339                 self.filter = handleListCGIValue(self.form[name])
2341         self.filterspec = {}
2342         db = self.client.db
2343         if self.classname is not None:
2344             cls = db.getclass (self.classname)
2345             for name in self.filter:
2346                 if not self.form.has_key(name):
2347                     continue
2348                 prop = cls.get_transitive_prop (name)
2349                 fv = self.form[name]
2350                 if (isinstance(prop, hyperdb.Link) or
2351                         isinstance(prop, hyperdb.Multilink)):
2352                     self.filterspec[name] = lookupIds(db, prop,
2353                         handleListCGIValue(fv))
2354                 else:
2355                     if isinstance(fv, type([])):
2356                         self.filterspec[name] = [v.value for v in fv]
2357                     elif name == 'id':
2358                         # special case "id" property
2359                         self.filterspec[name] = handleListCGIValue(fv)
2360                     else:
2361                         self.filterspec[name] = fv.value
2363         # full-text search argument
2364         self.search_text = None
2365         for name in ':search_text @search_text'.split():
2366             if self.form.has_key(name):
2367                 self.special_char = name[0]
2368                 self.search_text = self.form.getfirst(name)
2370         # pagination - size and start index
2371         # figure batch args
2372         self.pagesize = 50
2373         for name in ':pagesize @pagesize'.split():
2374             if self.form.has_key(name):
2375                 self.special_char = name[0]
2376                 self.pagesize = int(self.form.getfirst(name))
2378         self.startwith = 0
2379         for name in ':startwith @startwith'.split():
2380             if self.form.has_key(name):
2381                 self.special_char = name[0]
2382                 self.startwith = int(self.form.getfirst(name))
2384         # dispname
2385         if self.form.has_key('@dispname'):
2386             self.dispname = self.form.getfirst('@dispname')
2387         else:
2388             self.dispname = None
2390     def updateFromURL(self, url):
2391         """ Parse the URL for query args, and update my attributes using the
2392             values.
2393         """
2394         env = {'QUERY_STRING': url}
2395         self.form = cgi.FieldStorage(environ=env)
2397         self._post_init()
2399     def update(self, kwargs):
2400         """ Update my attributes using the keyword args
2401         """
2402         self.__dict__.update(kwargs)
2403         if kwargs.has_key('columns'):
2404             self.show = support.TruthDict(self.columns)
2406     def description(self):
2407         """ Return a description of the request - handle for the page title.
2408         """
2409         s = [self.client.db.config.TRACKER_NAME]
2410         if self.classname:
2411             if self.client.nodeid:
2412                 s.append('- %s%s'%(self.classname, self.client.nodeid))
2413             else:
2414                 if self.template == 'item':
2415                     s.append('- new %s'%self.classname)
2416                 elif self.template == 'index':
2417                     s.append('- %s index'%self.classname)
2418                 else:
2419                     s.append('- %s %s'%(self.classname, self.template))
2420         else:
2421             s.append('- home')
2422         return ' '.join(s)
2424     def __str__(self):
2425         d = {}
2426         d.update(self.__dict__)
2427         f = ''
2428         for k in self.form.keys():
2429             f += '\n      %r=%r'%(k,handleListCGIValue(self.form[k]))
2430         d['form'] = f
2431         e = ''
2432         for k,v in self.env.items():
2433             e += '\n     %r=%r'%(k, v)
2434         d['env'] = e
2435         return """
2436 form: %(form)s
2437 base: %(base)r
2438 classname: %(classname)r
2439 template: %(template)r
2440 columns: %(columns)r
2441 sort: %(sort)r
2442 group: %(group)r
2443 filter: %(filter)r
2444 search_text: %(search_text)r
2445 pagesize: %(pagesize)r
2446 startwith: %(startwith)r
2447 env: %(env)s
2448 """%d
2450     def indexargs_form(self, columns=1, sort=1, group=1, filter=1,
2451             filterspec=1, search_text=1):
2452         """ return the current index args as form elements """
2453         l = []
2454         sc = self.special_char
2455         def add(k, v):
2456             l.append(self.input(type="hidden", name=k, value=v))
2457         if columns and self.columns:
2458             add(sc+'columns', ','.join(self.columns))
2459         if sort:
2460             val = []
2461             for dir, attr in self.sort:
2462                 if dir == '-':
2463                     val.append('-'+attr)
2464                 else:
2465                     val.append(attr)
2466             add(sc+'sort', ','.join (val))
2467         if group:
2468             val = []
2469             for dir, attr in self.group:
2470                 if dir == '-':
2471                     val.append('-'+attr)
2472                 else:
2473                     val.append(attr)
2474             add(sc+'group', ','.join (val))
2475         if filter and self.filter:
2476             add(sc+'filter', ','.join(self.filter))
2477         if self.classname and filterspec:
2478             cls = self.client.db.getclass(self.classname)
2479             for k,v in self.filterspec.items():
2480                 if type(v) == type([]):
2481                     if isinstance(cls.get_transitive_prop(k), hyperdb.String):
2482                         add(k, ' '.join(v))
2483                     else:
2484                         add(k, ','.join(v))
2485                 else:
2486                     add(k, v)
2487         if search_text and self.search_text:
2488             add(sc+'search_text', self.search_text)
2489         add(sc+'pagesize', self.pagesize)
2490         add(sc+'startwith', self.startwith)
2491         return '\n'.join(l)
2493     def indexargs_url(self, url, args):
2494         """ Embed the current index args in a URL
2495         """
2496         q = urllib.quote
2497         sc = self.special_char
2498         l = ['%s=%s'%(k,v) for k,v in args.items()]
2500         # pull out the special values (prefixed by @ or :)
2501         specials = {}
2502         for key in args.keys():
2503             if key[0] in '@:':
2504                 specials[key[1:]] = args[key]
2506         # ok, now handle the specials we received in the request
2507         if self.columns and not specials.has_key('columns'):
2508             l.append(sc+'columns=%s'%(','.join(self.columns)))
2509         if self.sort and not specials.has_key('sort'):
2510             val = []
2511             for dir, attr in self.sort:
2512                 if dir == '-':
2513                     val.append('-'+attr)
2514                 else:
2515                     val.append(attr)
2516             l.append(sc+'sort=%s'%(','.join(val)))
2517         if self.group and not specials.has_key('group'):
2518             val = []
2519             for dir, attr in self.group:
2520                 if dir == '-':
2521                     val.append('-'+attr)
2522                 else:
2523                     val.append(attr)
2524             l.append(sc+'group=%s'%(','.join(val)))
2525         if self.filter and not specials.has_key('filter'):
2526             l.append(sc+'filter=%s'%(','.join(self.filter)))
2527         if self.search_text and not specials.has_key('search_text'):
2528             l.append(sc+'search_text=%s'%q(self.search_text))
2529         if not specials.has_key('pagesize'):
2530             l.append(sc+'pagesize=%s'%self.pagesize)
2531         if not specials.has_key('startwith'):
2532             l.append(sc+'startwith=%s'%self.startwith)
2534         # finally, the remainder of the filter args in the request
2535         if self.classname and self.filterspec:
2536             cls = self.client.db.getclass(self.classname)
2537             for k,v in self.filterspec.items():
2538                 if not args.has_key(k):
2539                     if type(v) == type([]):
2540                         prop = cls.get_transitive_prop(k)
2541                         if k != 'id' and isinstance(prop, hyperdb.String):
2542                             l.append('%s=%s'%(k, '%20'.join([q(i) for i in v])))
2543                         else:
2544                             l.append('%s=%s'%(k, ','.join([q(i) for i in v])))
2545                     else:
2546                         l.append('%s=%s'%(k, q(v)))
2547         return '%s?%s'%(url, '&'.join(l))
2548     indexargs_href = indexargs_url
2550     def base_javascript(self):
2551         return """
2552 <script type="text/javascript">
2553 submitted = false;
2554 function submit_once() {
2555     if (submitted) {
2556         alert("Your request is being processed.\\nPlease be patient.");
2557         event.returnValue = 0;    // work-around for IE
2558         return 0;
2559     }
2560     submitted = true;
2561     return 1;
2564 function help_window(helpurl, width, height) {
2565     HelpWin = window.open('%s' + helpurl, 'RoundupHelpWindow', 'scrollbars=yes,resizable=yes,toolbar=no,height='+height+',width='+width);
2567 </script>
2568 """%self.base
2570     def batch(self):
2571         """ Return a batch object for results from the "current search"
2572         """
2573         filterspec = self.filterspec
2574         sort = self.sort
2575         group = self.group
2577         # get the list of ids we're batching over
2578         klass = self.client.db.getclass(self.classname)
2579         if self.search_text:
2580             matches = self.client.db.indexer.search(
2581                 [w.upper().encode("utf-8", "replace") for w in re.findall(
2582                     r'(?u)\b\w{2,25}\b',
2583                     unicode(self.search_text, "utf-8", "replace")
2584                 )], klass)
2585         else:
2586             matches = None
2588         # filter for visibility
2589         check = self._client.db.security.hasPermission
2590         userid = self._client.userid
2591         l = [id for id in klass.filter(matches, filterspec, sort, group)
2592             if check('View', userid, self.classname, itemid=id)]
2594         # return the batch object, using IDs only
2595         return Batch(self.client, l, self.pagesize, self.startwith,
2596             classname=self.classname)
2598 # extend the standard ZTUtils Batch object to remove dependency on
2599 # Acquisition and add a couple of useful methods
2600 class Batch(ZTUtils.Batch):
2601     """ Use me to turn a list of items, or item ids of a given class, into a
2602         series of batches.
2604         ========= ========================================================
2605         Parameter  Usage
2606         ========= ========================================================
2607         sequence  a list of HTMLItems or item ids
2608         classname if sequence is a list of ids, this is the class of item
2609         size      how big to make the sequence.
2610         start     where to start (0-indexed) in the sequence.
2611         end       where to end (0-indexed) in the sequence.
2612         orphan    if the next batch would contain less items than this
2613                   value, then it is combined with this batch
2614         overlap   the number of items shared between adjacent batches
2615         ========= ========================================================
2617         Attributes: Note that the "start" attribute, unlike the
2618         argument, is a 1-based index (I know, lame).  "first" is the
2619         0-based index.  "length" is the actual number of elements in
2620         the batch.
2622         "sequence_length" is the length of the original, unbatched, sequence.
2623     """
2624     def __init__(self, client, sequence, size, start, end=0, orphan=0,
2625             overlap=0, classname=None):
2626         self.client = client
2627         self.last_index = self.last_item = None
2628         self.current_item = None
2629         self.classname = classname
2630         self.sequence_length = len(sequence)
2631         ZTUtils.Batch.__init__(self, sequence, size, start, end, orphan,
2632             overlap)
2634     # overwrite so we can late-instantiate the HTMLItem instance
2635     def __getitem__(self, index):
2636         if index < 0:
2637             if index + self.end < self.first: raise IndexError, index
2638             return self._sequence[index + self.end]
2640         if index >= self.length:
2641             raise IndexError, index
2643         # move the last_item along - but only if the fetched index changes
2644         # (for some reason, index 0 is fetched twice)
2645         if index != self.last_index:
2646             self.last_item = self.current_item
2647             self.last_index = index
2649         item = self._sequence[index + self.first]
2650         if self.classname:
2651             # map the item ids to instances
2652             item = HTMLItem(self.client, self.classname, item)
2653         self.current_item = item
2654         return item
2656     def propchanged(self, *properties):
2657         """ Detect if one of the properties marked as being a group
2658             property changed in the last iteration fetch
2659         """
2660         # we poke directly at the _value here since MissingValue can screw
2661         # us up and cause Nones to compare strangely
2662         if self.last_item is None:
2663             return 1
2664         for property in properties:
2665             if property == 'id' or isinstance (self.last_item[property], list):
2666                 if (str(self.last_item[property]) !=
2667                     str(self.current_item[property])):
2668                     return 1
2669             else:
2670                 if (self.last_item[property]._value !=
2671                     self.current_item[property]._value):
2672                     return 1
2673         return 0
2675     # override these 'cos we don't have access to acquisition
2676     def previous(self):
2677         if self.start == 1:
2678             return None
2679         return Batch(self.client, self._sequence, self._size,
2680             self.first - self._size + self.overlap, 0, self.orphan,
2681             self.overlap)
2683     def next(self):
2684         try:
2685             self._sequence[self.end]
2686         except IndexError:
2687             return None
2688         return Batch(self.client, self._sequence, self._size,
2689             self.end - self.overlap, 0, self.orphan, self.overlap)
2691 class TemplatingUtils:
2692     """ Utilities for templating
2693     """
2694     def __init__(self, client):
2695         self.client = client
2696     def Batch(self, sequence, size, start, end=0, orphan=0, overlap=0):
2697         return Batch(self.client, sequence, size, start, end, orphan,
2698             overlap)
2700     def url_quote(self, url):
2701         """URL-quote the supplied text."""
2702         return urllib.quote(url)
2704     def html_quote(self, html):
2705         """HTML-quote the supplied text."""
2706         return cgi.escape(html)
2708     def __getattr__(self, name):
2709         """Try the tracker's templating_utils."""
2710         if not hasattr(self.client.instance, 'templating_utils'):
2711             # backwards-compatibility
2712             raise AttributeError, name
2713         if not self.client.instance.templating_utils.has_key(name):
2714             raise AttributeError, name
2715         return self.client.instance.templating_utils[name]
2717     def html_calendar(self, request):
2718         """Generate a HTML calendar.
2720         `request`  the roundup.request object
2721                    - @template : name of the template
2722                    - form      : name of the form to store back the date
2723                    - property  : name of the property of the form to store
2724                                  back the date
2725                    - date      : current date
2726                    - display   : when browsing, specifies year and month
2728         html will simply be a table.
2729         """
2730         date_str  = request.form.getfirst("date", ".")
2731         display   = request.form.getfirst("display", date_str)
2732         template  = request.form.getfirst("@template", "calendar")
2733         form      = request.form.getfirst("form")
2734         property  = request.form.getfirst("property")
2735         curr_date = date.Date(date_str) # to highlight
2736         display   = date.Date(display)  # to show
2737         day       = display.day
2739         # for navigation
2740         date_prev_month = display + date.Interval("-1m")
2741         date_next_month = display + date.Interval("+1m")
2742         date_prev_year  = display + date.Interval("-1y")
2743         date_next_year  = display + date.Interval("+1y")
2745         res = []
2747         base_link = "%s?@template=%s&property=%s&form=%s&date=%s" % \
2748                     (request.classname, template, property, form, curr_date)
2750         # navigation
2751         # month
2752         res.append('<table class="calendar"><tr><td>')
2753         res.append(' <table width="100%" class="calendar_nav"><tr>')
2754         link = "&display=%s"%date_prev_month
2755         res.append('  <td><a href="%s&display=%s">&lt;</a></td>'%(base_link,
2756             date_prev_month))
2757         res.append('  <td>%s</td>'%calendar.month_name[display.month])
2758         res.append('  <td><a href="%s&display=%s">&gt;</a></td>'%(base_link,
2759             date_next_month))
2760         # spacer
2761         res.append('  <td width="100%"></td>')
2762         # year
2763         res.append('  <td><a href="%s&display=%s">&lt;</a></td>'%(base_link,
2764             date_prev_year))
2765         res.append('  <td>%s</td>'%display.year)
2766         res.append('  <td><a href="%s&display=%s">&gt;</a></td>'%(base_link,
2767             date_next_year))
2768         res.append(' </tr></table>')
2769         res.append(' </td></tr>')
2771         # the calendar
2772         res.append(' <tr><td><table class="calendar_display">')
2773         res.append('  <tr class="weekdays">')
2774         for day in calendar.weekheader(3).split():
2775             res.append('   <td>%s</td>'%day)
2776         res.append('  </tr>')
2777         for week in calendar.monthcalendar(display.year, display.month):
2778             res.append('  <tr>')
2779             for day in week:
2780                 link = "javascript:form[field].value = '%d-%02d-%02d'; " \
2781                       "window.close ();"%(display.year, display.month, day)
2782                 if (day == curr_date.day and display.month == curr_date.month
2783                         and display.year == curr_date.year):
2784                     # highlight
2785                     style = "today"
2786                 else :
2787                     style = ""
2788                 if day:
2789                     res.append('   <td class="%s"><a href="%s">%s</a></td>'%(
2790                         style, link, day))
2791                 else :
2792                     res.append('   <td></td>')
2793             res.append('  </tr>')
2794         res.append('</table></td></tr></table>')
2795         return "\n".join(res)
2797 class MissingValue:
2798     def __init__(self, description, **kwargs):
2799         self.__description = description
2800         for key, value in kwargs.items():
2801             self.__dict__[key] = value
2803     def __call__(self, *args, **kwargs): return MissingValue(self.__description)
2804     def __getattr__(self, name):
2805         # This allows assignments which assume all intermediate steps are Null
2806         # objects if they don't exist yet.
2807         #
2808         # For example (with just 'client' defined):
2809         #
2810         # client.db.config.TRACKER_WEB = 'BASE/'
2811         self.__dict__[name] = MissingValue(self.__description)
2812         return getattr(self, name)
2814     def __getitem__(self, key): return self
2815     def __nonzero__(self): return 0
2816     def __str__(self): return '[%s]'%self.__description
2817     def __repr__(self): return '<MissingValue 0x%x "%s">'%(id(self),
2818         self.__description)
2819     def gettext(self, str): return str
2820     _ = gettext
2822 # vim: set et sts=4 sw=4 :