1 from __future__ import nested_scopes
3 """Implements the API used in the HTML templating for the web interface.
4 """
6 todo = """
7 - Most methods should have a "default" arg to supply a value
8 when none appears in the hyperdb or request.
9 - Multilink property additions: change_note and new_upload
10 - Add class.find() too
11 - NumberHTMLProperty should support numeric operations
12 - LinkHTMLProperty should handle comparisons to strings (cf. linked name)
13 - HTMLRequest.default(self, sort, group, filter, columns, **filterspec):
14 '''Set the request's view arguments to the given values when no
15 values are found in the CGI environment.
16 '''
17 - have menu() methods accept filtering arguments
18 """
20 __docformat__ = 'restructuredtext'
23 import sys, cgi, urllib, os, re, os.path, time, errno, mimetypes, csv
24 import calendar, textwrap
26 from roundup import hyperdb, date, support
27 from roundup import i18n
28 from roundup.i18n import _
30 try:
31 import cPickle as pickle
32 except ImportError:
33 import pickle
34 try:
35 import cStringIO as StringIO
36 except ImportError:
37 import StringIO
38 try:
39 from StructuredText.StructuredText import HTML as StructuredText
40 except ImportError:
41 try: # older version
42 import StructuredText
43 except ImportError:
44 StructuredText = None
45 try:
46 from docutils.core import publish_parts as ReStructuredText
47 except ImportError:
48 ReStructuredText = None
50 # bring in the templating support
51 from roundup.cgi.PageTemplates import PageTemplate, GlobalTranslationService
52 from roundup.cgi.PageTemplates.Expressions import getEngine
53 from roundup.cgi.TAL import TALInterpreter
54 from roundup.cgi import TranslationService, ZTUtils
56 ### i18n services
57 # this global translation service is not thread-safe.
58 # it is left here for backward compatibility
59 # until all Web UI translations are done via client.translator object
60 translationService = TranslationService.get_translation()
61 GlobalTranslationService.setGlobalTranslationService(translationService)
63 ### templating
65 class NoTemplate(Exception):
66 pass
68 class Unauthorised(Exception):
69 def __init__(self, action, klass, translator=None):
70 self.action = action
71 self.klass = klass
72 if translator:
73 self._ = translator.gettext
74 else:
75 self._ = TranslationService.get_translation().gettext
76 def __str__(self):
77 return self._('You are not allowed to %(action)s '
78 'items of class %(class)s') % {
79 'action': self.action, 'class': self.klass}
81 def find_template(dir, name, view):
82 """ Find a template in the nominated dir
83 """
84 # find the source
85 if view:
86 filename = '%s.%s'%(name, view)
87 else:
88 filename = name
90 # try old-style
91 src = os.path.join(dir, filename)
92 if os.path.exists(src):
93 return (src, filename)
95 # try with a .html or .xml extension (new-style)
96 for extension in '.html', '.xml':
97 f = filename + extension
98 src = os.path.join(dir, f)
99 if os.path.exists(src):
100 return (src, f)
102 # no view == no generic template is possible
103 if not view:
104 raise NoTemplate, 'Template file "%s" doesn\'t exist'%name
106 # try for a _generic template
107 generic = '_generic.%s'%view
108 src = os.path.join(dir, generic)
109 if os.path.exists(src):
110 return (src, generic)
112 # finally, try _generic.html
113 generic = generic + '.html'
114 src = os.path.join(dir, generic)
115 if os.path.exists(src):
116 return (src, generic)
118 raise NoTemplate, 'No template file exists for templating "%s" '\
119 'with template "%s" (neither "%s" nor "%s")'%(name, view,
120 filename, generic)
122 class Templates:
123 templates = {}
125 def __init__(self, dir):
126 self.dir = dir
128 def precompileTemplates(self):
129 """ Go through a directory and precompile all the templates therein
130 """
131 for filename in os.listdir(self.dir):
132 # skip subdirs
133 if os.path.isdir(filename):
134 continue
136 # skip files without ".html" or ".xml" extension - .css, .js etc.
137 for extension in '.html', '.xml':
138 if filename.endswith(extension):
139 break
140 else:
141 continue
143 # remove extension
144 filename = filename[:-len(extension)]
146 # load the template
147 if '.' in filename:
148 name, extension = filename.split('.', 1)
149 self.get(name, extension)
150 else:
151 self.get(filename, None)
153 def get(self, name, extension=None):
154 """ Interface to get a template, possibly loading a compiled template.
156 "name" and "extension" indicate the template we're after, which in
157 most cases will be "name.extension". If "extension" is None, then
158 we look for a template just called "name" with no extension.
160 If the file "name.extension" doesn't exist, we look for
161 "_generic.extension" as a fallback.
162 """
163 # default the name to "home"
164 if name is None:
165 name = 'home'
166 elif extension is None and '.' in name:
167 # split name
168 name, extension = name.split('.')
170 # find the source
171 src, filename = find_template(self.dir, name, extension)
173 # has it changed?
174 try:
175 stime = os.stat(src)[os.path.stat.ST_MTIME]
176 except os.error, error:
177 if error.errno != errno.ENOENT:
178 raise
180 if self.templates.has_key(src) and \
181 stime <= self.templates[src].mtime:
182 # compiled template is up to date
183 return self.templates[src]
185 # compile the template
186 self.templates[src] = pt = RoundupPageTemplate()
187 # use pt_edit so we can pass the content_type guess too
188 content_type = mimetypes.guess_type(filename)[0] or 'text/html'
189 pt.pt_edit(open(src).read(), content_type)
190 pt.id = filename
191 pt.mtime = stime
192 return pt
194 def __getitem__(self, name):
195 name, extension = os.path.splitext(name)
196 if extension:
197 extension = extension[1:]
198 try:
199 return self.get(name, extension)
200 except NoTemplate, message:
201 raise KeyError, message
203 def context(client, template=None, classname=None, request=None):
204 """Return the rendering context dictionary
206 The dictionary includes following symbols:
208 *context*
209 this is one of three things:
211 1. None - we're viewing a "home" page
212 2. The current class of item being displayed. This is an HTMLClass
213 instance.
214 3. The current item from the database, if we're viewing a specific
215 item, as an HTMLItem instance.
217 *request*
218 Includes information about the current request, including:
220 - the url
221 - the current index information (``filterspec``, ``filter`` args,
222 ``properties``, etc) parsed out of the form.
223 - methods for easy filterspec link generation
224 - *user*, the current user node as an HTMLItem instance
225 - *form*, the current CGI form information as a FieldStorage
227 *config*
228 The current tracker config.
230 *db*
231 The current database, used to access arbitrary database items.
233 *utils*
234 This is a special class that has its base in the TemplatingUtils
235 class in this file. If the tracker interfaces module defines a
236 TemplatingUtils class then it is mixed in, overriding the methods
237 in the base class.
239 *templates*
240 Access to all the tracker templates by name.
241 Used mainly in *use-macro* commands.
243 *template*
244 Current rendering template.
246 *true*
247 Logical True value.
249 *false*
250 Logical False value.
252 *i18n*
253 Internationalization service, providing string translation
254 methods ``gettext`` and ``ngettext``.
256 """
257 # construct the TemplatingUtils class
258 utils = TemplatingUtils
259 if (hasattr(client.instance, 'interfaces') and
260 hasattr(client.instance.interfaces, 'TemplatingUtils')):
261 class utils(client.instance.interfaces.TemplatingUtils, utils):
262 pass
264 # if template, classname and/or request are not passed explicitely,
265 # compute form client
266 if template is None:
267 template = client.template
268 if classname is None:
269 classname = client.classname
270 if request is None:
271 request = HTMLRequest(client)
273 c = {
274 'context': None,
275 'options': {},
276 'nothing': None,
277 'request': request,
278 'db': HTMLDatabase(client),
279 'config': client.instance.config,
280 'tracker': client.instance,
281 'utils': utils(client),
282 'templates': client.instance.templates,
283 'template': template,
284 'true': 1,
285 'false': 0,
286 'i18n': client.translator
287 }
288 # add in the item if there is one
289 if client.nodeid:
290 c['context'] = HTMLItem(client, classname, client.nodeid,
291 anonymous=1)
292 elif client.db.classes.has_key(classname):
293 c['context'] = HTMLClass(client, classname, anonymous=1)
294 return c
296 class RoundupPageTemplate(PageTemplate.PageTemplate):
297 """A Roundup-specific PageTemplate.
299 Interrogate the client to set up Roundup-specific template variables
300 to be available. See 'context' function for the list of variables.
302 """
304 # 06-jun-2004 [als] i am not sure if this method is used yet
305 def getContext(self, client, classname, request):
306 return context(client, self, classname, request)
308 def render(self, client, classname, request, **options):
309 """Render this Page Template"""
311 if not self._v_cooked:
312 self._cook()
314 __traceback_supplement__ = (PageTemplate.PageTemplateTracebackSupplement, self)
316 if self._v_errors:
317 raise PageTemplate.PTRuntimeError, \
318 'Page Template %s has errors.'%self.id
320 # figure the context
321 c = context(client, self, classname, request)
322 c.update({'options': options})
324 # and go
325 output = StringIO.StringIO()
326 TALInterpreter.TALInterpreter(self._v_program, self.macros,
327 getEngine().getContext(c), output, tal=1, strictinsert=0)()
328 return output.getvalue()
330 def __repr__(self):
331 return '<Roundup PageTemplate %r>'%self.id
333 class HTMLDatabase:
334 """ Return HTMLClasses for valid class fetches
335 """
336 def __init__(self, client):
337 self._client = client
338 self._ = client._
339 self._db = client.db
341 # we want config to be exposed
342 self.config = client.db.config
344 def __getitem__(self, item, desre=re.compile(r'(?P<cl>[a-zA-Z_]+)(?P<id>[-\d]+)')):
345 # check to see if we're actually accessing an item
346 m = desre.match(item)
347 if m:
348 cl = m.group('cl')
349 self._client.db.getclass(cl)
350 return HTMLItem(self._client, cl, m.group('id'))
351 else:
352 self._client.db.getclass(item)
353 return HTMLClass(self._client, item)
355 def __getattr__(self, attr):
356 try:
357 return self[attr]
358 except KeyError:
359 raise AttributeError, attr
361 def classes(self):
362 l = self._client.db.classes.keys()
363 l.sort()
364 m = []
365 for item in l:
366 m.append(HTMLClass(self._client, item))
367 return m
369 num_re = re.compile('^-?\d+$')
371 def lookupIds(db, prop, ids, fail_ok=0, num_re=num_re, do_lookup=True):
372 """ "fail_ok" should be specified if we wish to pass through bad values
373 (most likely form values that we wish to represent back to the user)
374 "do_lookup" is there for preventing lookup by key-value (if we
375 know that the value passed *is* an id)
376 """
377 cl = db.getclass(prop.classname)
378 l = []
379 for entry in ids:
380 if do_lookup:
381 try:
382 item = cl.lookup(entry)
383 except (TypeError, KeyError):
384 pass
385 else:
386 l.append(item)
387 continue
388 # if fail_ok, ignore lookup error
389 # otherwise entry must be existing object id rather than key value
390 if fail_ok or num_re.match(entry):
391 l.append(entry)
392 return l
394 def lookupKeys(linkcl, key, ids, num_re=num_re):
395 """ Look up the "key" values for "ids" list - though some may already
396 be key values, not ids.
397 """
398 l = []
399 for entry in ids:
400 if num_re.match(entry):
401 label = linkcl.get(entry, key)
402 # fall back to designator if label is None
403 if label is None: label = '%s%s'%(linkcl.classname, entry)
404 l.append(label)
405 else:
406 l.append(entry)
407 return l
409 def _set_input_default_args(dic):
410 # 'text' is the default value anyway --
411 # but for CSS usage it should be present
412 dic.setdefault('type', 'text')
413 # useful e.g for HTML LABELs:
414 if not dic.has_key('id'):
415 try:
416 if dic['text'] in ('radio', 'checkbox'):
417 dic['id'] = '%(name)s-%(value)s' % dic
418 else:
419 dic['id'] = dic['name']
420 except KeyError:
421 pass
423 def input_html4(**attrs):
424 """Generate an 'input' (html4) element with given attributes"""
425 _set_input_default_args(attrs)
426 return '<input %s>'%' '.join(['%s="%s"'%(k,cgi.escape(str(v), True))
427 for k,v in attrs.items()])
429 def input_xhtml(**attrs):
430 """Generate an 'input' (xhtml) element with given attributes"""
431 _set_input_default_args(attrs)
432 return '<input %s/>'%' '.join(['%s="%s"'%(k,cgi.escape(str(v), True))
433 for k,v in attrs.items()])
435 class HTMLInputMixin:
436 """ requires a _client property """
437 def __init__(self):
438 html_version = 'html4'
439 if hasattr(self._client.instance.config, 'HTML_VERSION'):
440 html_version = self._client.instance.config.HTML_VERSION
441 if html_version == 'xhtml':
442 self.input = input_xhtml
443 else:
444 self.input = input_html4
445 # self._context is used for translations.
446 # will be initialized by the first call to .gettext()
447 self._context = None
449 def gettext(self, msgid):
450 """Return the localized translation of msgid"""
451 if self._context is None:
452 self._context = context(self._client)
453 return self._client.translator.translate(domain="roundup",
454 msgid=msgid, context=self._context)
456 _ = gettext
458 class HTMLPermissions:
460 def view_check(self):
461 """ Raise the Unauthorised exception if the user's not permitted to
462 view this class.
463 """
464 if not self.is_view_ok():
465 raise Unauthorised("view", self._classname,
466 translator=self._client.translator)
468 def edit_check(self):
469 """ Raise the Unauthorised exception if the user's not permitted to
470 edit items of this class.
471 """
472 if not self.is_edit_ok():
473 raise Unauthorised("edit", self._classname,
474 translator=self._client.translator)
476 def retire_check(self):
477 """ Raise the Unauthorised exception if the user's not permitted to
478 retire items of this class.
479 """
480 if not self.is_retire_ok():
481 raise Unauthorised("retire", self._classname,
482 translator=self._client.translator)
485 class HTMLClass(HTMLInputMixin, HTMLPermissions):
486 """ Accesses through a class (either through *class* or *db.<classname>*)
487 """
488 def __init__(self, client, classname, anonymous=0):
489 self._client = client
490 self._ = client._
491 self._db = client.db
492 self._anonymous = anonymous
494 # we want classname to be exposed, but _classname gives a
495 # consistent API for extending Class/Item
496 self._classname = self.classname = classname
497 self._klass = self._db.getclass(self.classname)
498 self._props = self._klass.getprops()
500 HTMLInputMixin.__init__(self)
502 def is_edit_ok(self):
503 """ Is the user allowed to Create the current class?
504 """
505 return self._db.security.hasPermission('Create', self._client.userid,
506 self._classname)
508 def is_retire_ok(self):
509 """ Is the user allowed to retire items of the current class?
510 """
511 return self._db.security.hasPermission('Retire', self._client.userid,
512 self._classname)
514 def is_view_ok(self):
515 """ Is the user allowed to View the current class?
516 """
517 return self._db.security.hasPermission('View', self._client.userid,
518 self._classname)
520 def is_only_view_ok(self):
521 """ Is the user only allowed to View (ie. not Create) the current class?
522 """
523 return self.is_view_ok() and not self.is_edit_ok()
525 def __repr__(self):
526 return '<HTMLClass(0x%x) %s>'%(id(self), self.classname)
528 def __getitem__(self, item):
529 """ return an HTMLProperty instance
530 """
532 # we don't exist
533 if item == 'id':
534 return None
536 # get the property
537 try:
538 prop = self._props[item]
539 except KeyError:
540 raise KeyError, 'No such property "%s" on %s'%(item, self.classname)
542 # look up the correct HTMLProperty class
543 form = self._client.form
544 for klass, htmlklass in propclasses:
545 if not isinstance(prop, klass):
546 continue
547 if isinstance(prop, hyperdb.Multilink):
548 value = []
549 else:
550 value = None
551 return htmlklass(self._client, self._classname, None, prop, item,
552 value, self._anonymous)
554 # no good
555 raise KeyError, item
557 def __getattr__(self, attr):
558 """ convenience access """
559 try:
560 return self[attr]
561 except KeyError:
562 raise AttributeError, attr
564 def designator(self):
565 """ Return this class' designator (classname) """
566 return self._classname
568 def getItem(self, itemid, num_re=num_re):
569 """ Get an item of this class by its item id.
570 """
571 # make sure we're looking at an itemid
572 if not isinstance(itemid, type(1)) and not num_re.match(itemid):
573 itemid = self._klass.lookup(itemid)
575 return HTMLItem(self._client, self.classname, itemid)
577 def properties(self, sort=1):
578 """ Return HTMLProperty for all of this class' properties.
579 """
580 l = []
581 for name, prop in self._props.items():
582 for klass, htmlklass in propclasses:
583 if isinstance(prop, hyperdb.Multilink):
584 value = []
585 else:
586 value = None
587 if isinstance(prop, klass):
588 l.append(htmlklass(self._client, self._classname, '',
589 prop, name, value, self._anonymous))
590 if sort:
591 l.sort(lambda a,b:cmp(a._name, b._name))
592 return l
594 def list(self, sort_on=None):
595 """ List all items in this class.
596 """
597 # get the list and sort it nicely
598 l = self._klass.list()
599 sortfunc = make_sort_function(self._db, self._classname, sort_on)
600 l.sort(sortfunc)
602 # check perms
603 check = self._client.db.security.hasPermission
604 userid = self._client.userid
606 l = [HTMLItem(self._client, self._classname, id) for id in l
607 if check('View', userid, self._classname, itemid=id)]
609 return l
611 def csv(self):
612 """ Return the items of this class as a chunk of CSV text.
613 """
614 props = self.propnames()
615 s = StringIO.StringIO()
616 writer = csv.writer(s)
617 writer.writerow(props)
618 check = self._client.db.security.hasPermission
619 for nodeid in self._klass.list():
620 l = []
621 for name in props:
622 # check permission to view this property on this item
623 if not check('View', self._client.userid, itemid=nodeid,
624 classname=self._klass.classname, property=name):
625 raise Unauthorised('view', self._klass.classname,
626 translator=self._client.translator)
627 row.append(str(klass.get(itemid, name)))
628 value = self._klass.get(nodeid, name)
629 if value is None:
630 l.append('')
631 elif isinstance(value, type([])):
632 l.append(':'.join(map(str, value)))
633 else:
634 l.append(str(self._klass.get(nodeid, name)))
635 writer.writerow(l)
636 return s.getvalue()
638 def propnames(self):
639 """ Return the list of the names of the properties of this class.
640 """
641 idlessprops = self._klass.getprops(protected=0).keys()
642 idlessprops.sort()
643 return ['id'] + idlessprops
645 def filter(self, request=None, filterspec={}, sort=[], group=[]):
646 """ Return a list of items from this class, filtered and sorted
647 by the current requested filterspec/filter/sort/group args
649 "request" takes precedence over the other three arguments.
650 """
651 if request is not None:
652 filterspec = request.filterspec
653 sort = request.sort
654 group = request.group
656 check = self._db.security.hasPermission
657 userid = self._client.userid
659 l = [HTMLItem(self._client, self.classname, id)
660 for id in self._klass.filter(None, filterspec, sort, group)
661 if check('View', userid, self.classname, itemid=id)]
662 return l
664 def classhelp(self, properties=None, label=''"(list)", width='500',
665 height='400', property='', form='itemSynopsis',
666 pagesize=50, inputtype="checkbox", sort=None, filter=None):
667 """Pop up a javascript window with class help
669 This generates a link to a popup window which displays the
670 properties indicated by "properties" of the class named by
671 "classname". The "properties" should be a comma-separated list
672 (eg. 'id,name,description'). Properties defaults to all the
673 properties of a class (excluding id, creator, created and
674 activity).
676 You may optionally override the label displayed, the width,
677 the height, the number of items per page and the field on which
678 the list is sorted (defaults to username if in the displayed
679 properties).
681 With the "filter" arg it is possible to specify a filter for
682 which items are supposed to be displayed. It has to be of
683 the format "<field>=<values>;<field>=<values>;...".
685 The popup window will be resizable and scrollable.
687 If the "property" arg is given, it's passed through to the
688 javascript help_window function.
690 You can use inputtype="radio" to display a radio box instead
691 of the default checkbox (useful for entering Link-properties)
693 If the "form" arg is given, it's passed through to the
694 javascript help_window function. - it's the name of the form
695 the "property" belongs to.
696 """
697 if properties is None:
698 properties = self._klass.getprops(protected=0).keys()
699 properties.sort()
700 properties = ','.join(properties)
701 if sort is None:
702 if 'username' in properties.split( ',' ):
703 sort = 'username'
704 else:
705 sort = self._klass.orderprop()
706 sort = '&@sort=' + sort
707 if property:
708 property = '&property=%s'%property
709 if form:
710 form = '&form=%s'%form
711 if inputtype:
712 type= '&type=%s'%inputtype
713 if filter:
714 filterprops = filter.split(';')
715 filtervalues = []
716 names = []
717 for x in filterprops:
718 (name, values) = x.split('=')
719 names.append(name)
720 filtervalues.append('&%s=%s' % (name, urllib.quote(values)))
721 filter = '&@filter=%s%s' % (','.join(names), ''.join(filtervalues))
722 else:
723 filter = ''
724 help_url = "%s?@startwith=0&@template=help&"\
725 "properties=%s%s%s%s%s&@pagesize=%s%s" % \
726 (self.classname, properties, property, form, type,
727 sort, pagesize, filter)
728 onclick = "javascript:help_window('%s', '%s', '%s');return false;" % \
729 (help_url, width, height)
730 return '<a class="classhelp" href="%s" onclick="%s">%s</a>' % \
731 (help_url, onclick, self._(label))
733 def submit(self, label=''"Submit New Entry", action="new"):
734 """ Generate a submit button (and action hidden element)
736 Generate nothing if we're not editable.
737 """
738 if not self.is_edit_ok():
739 return ''
741 return self.input(type="hidden", name="@action", value=action) + \
742 '\n' + \
743 self.input(type="submit", name="submit_button", value=self._(label))
745 def history(self):
746 if not self.is_view_ok():
747 return self._('[hidden]')
748 return self._('New node - no history')
750 def renderWith(self, name, **kwargs):
751 """ Render this class with the given template.
752 """
753 # create a new request and override the specified args
754 req = HTMLRequest(self._client)
755 req.classname = self.classname
756 req.update(kwargs)
758 # new template, using the specified classname and request
759 pt = self._client.instance.templates.get(self.classname, name)
761 # use our fabricated request
762 args = {
763 'ok_message': self._client.ok_message,
764 'error_message': self._client.error_message
765 }
766 return pt.render(self._client, self.classname, req, **args)
768 class _HTMLItem(HTMLInputMixin, HTMLPermissions):
769 """ Accesses through an *item*
770 """
771 def __init__(self, client, classname, nodeid, anonymous=0):
772 self._client = client
773 self._db = client.db
774 self._classname = classname
775 self._nodeid = nodeid
776 self._klass = self._db.getclass(classname)
777 self._props = self._klass.getprops()
779 # do we prefix the form items with the item's identification?
780 self._anonymous = anonymous
782 HTMLInputMixin.__init__(self)
784 def is_edit_ok(self):
785 """ Is the user allowed to Edit this item?
786 """
787 return self._db.security.hasPermission('Edit', self._client.userid,
788 self._classname, itemid=self._nodeid)
790 def is_retire_ok(self):
791 """ Is the user allowed to Reture this item?
792 """
793 return self._db.security.hasPermission('Retire', self._client.userid,
794 self._classname, itemid=self._nodeid)
796 def is_view_ok(self):
797 """ Is the user allowed to View this item?
798 """
799 if self._db.security.hasPermission('View', self._client.userid,
800 self._classname, itemid=self._nodeid):
801 return 1
802 return self.is_edit_ok()
804 def is_only_view_ok(self):
805 """ Is the user only allowed to View (ie. not Edit) this item?
806 """
807 return self.is_view_ok() and not self.is_edit_ok()
809 def __repr__(self):
810 return '<HTMLItem(0x%x) %s %s>'%(id(self), self._classname,
811 self._nodeid)
813 def __getitem__(self, item):
814 """ return an HTMLProperty instance
815 this now can handle transitive lookups where item is of the
816 form x.y.z
817 """
818 if item == 'id':
819 return self._nodeid
821 items = item.split('.', 1)
822 has_rest = len(items) > 1
824 # get the property
825 prop = self._props[items[0]]
827 if has_rest and not isinstance(prop, (hyperdb.Link, hyperdb.Multilink)):
828 raise KeyError, item
830 # get the value, handling missing values
831 value = None
832 if int(self._nodeid) > 0:
833 value = self._klass.get(self._nodeid, items[0], None)
834 if value is None:
835 if isinstance(prop, hyperdb.Multilink):
836 value = []
838 # look up the correct HTMLProperty class
839 htmlprop = None
840 for klass, htmlklass in propclasses:
841 if isinstance(prop, klass):
842 htmlprop = htmlklass(self._client, self._classname,
843 self._nodeid, prop, items[0], value, self._anonymous)
844 if htmlprop is not None:
845 if has_rest:
846 if isinstance(htmlprop, MultilinkHTMLProperty):
847 return [h[items[1]] for h in htmlprop]
848 return htmlprop[items[1]]
849 return htmlprop
851 raise KeyError, item
853 def __getattr__(self, attr):
854 """ convenience access to properties """
855 try:
856 return self[attr]
857 except KeyError:
858 raise AttributeError, attr
860 def designator(self):
861 """Return this item's designator (classname + id)."""
862 return '%s%s'%(self._classname, self._nodeid)
864 def is_retired(self):
865 """Is this item retired?"""
866 return self._klass.is_retired(self._nodeid)
868 def submit(self, label=''"Submit Changes", action="edit"):
869 """Generate a submit button.
871 Also sneak in the lastactivity and action hidden elements.
872 """
873 return self.input(type="hidden", name="@lastactivity",
874 value=self.activity.local(0)) + '\n' + \
875 self.input(type="hidden", name="@action", value=action) + '\n' + \
876 self.input(type="submit", name="submit_button", value=self._(label))
878 def journal(self, direction='descending'):
879 """ Return a list of HTMLJournalEntry instances.
880 """
881 # XXX do this
882 return []
884 def history(self, direction='descending', dre=re.compile('^\d+$')):
885 if not self.is_view_ok():
886 return self._('[hidden]')
888 # pre-load the history with the current state
889 current = {}
890 for prop_n in self._props.keys():
891 prop = self[prop_n]
892 if not isinstance(prop, HTMLProperty):
893 continue
894 current[prop_n] = prop.plain(escape=1)
895 # make link if hrefable
896 if (self._props.has_key(prop_n) and
897 isinstance(self._props[prop_n], hyperdb.Link)):
898 classname = self._props[prop_n].classname
899 try:
900 template = find_template(self._db.config.TEMPLATES,
901 classname, 'item')
902 if template[1].startswith('_generic'):
903 raise NoTemplate, 'not really...'
904 except NoTemplate:
905 pass
906 else:
907 id = self._klass.get(self._nodeid, prop_n, None)
908 current[prop_n] = '<a href="%s%s">%s</a>'%(
909 classname, id, current[prop_n])
911 # get the journal, sort and reverse
912 history = self._klass.history(self._nodeid)
913 history.sort()
914 history.reverse()
916 timezone = self._db.getUserTimezone()
917 l = []
918 comments = {}
919 for id, evt_date, user, action, args in history:
920 date_s = str(evt_date.local(timezone)).replace("."," ")
921 arg_s = ''
922 if action == 'link' and type(args) == type(()):
923 if len(args) == 3:
924 linkcl, linkid, key = args
925 arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
926 linkcl, linkid, key)
927 else:
928 arg_s = str(args)
930 elif action == 'unlink' and type(args) == type(()):
931 if len(args) == 3:
932 linkcl, linkid, key = args
933 arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
934 linkcl, linkid, key)
935 else:
936 arg_s = str(args)
938 elif type(args) == type({}):
939 cell = []
940 for k in args.keys():
941 # try to get the relevant property and treat it
942 # specially
943 try:
944 prop = self._props[k]
945 except KeyError:
946 prop = None
947 if prop is None:
948 # property no longer exists
949 comments['no_exist'] = self._(
950 "<em>The indicated property no longer exists</em>")
951 cell.append(self._('<em>%s: %s</em>\n')
952 % (self._(k), str(args[k])))
953 continue
955 if args[k] and (isinstance(prop, hyperdb.Multilink) or
956 isinstance(prop, hyperdb.Link)):
957 # figure what the link class is
958 classname = prop.classname
959 try:
960 linkcl = self._db.getclass(classname)
961 except KeyError:
962 labelprop = None
963 comments[classname] = self._(
964 "The linked class %(classname)s no longer exists"
965 ) % locals()
966 labelprop = linkcl.labelprop(1)
967 try:
968 template = find_template(self._db.config.TEMPLATES,
969 classname, 'item')
970 if template[1].startswith('_generic'):
971 raise NoTemplate, 'not really...'
972 hrefable = 1
973 except NoTemplate:
974 hrefable = 0
976 if isinstance(prop, hyperdb.Multilink) and args[k]:
977 ml = []
978 for linkid in args[k]:
979 if isinstance(linkid, type(())):
980 sublabel = linkid[0] + ' '
981 linkids = linkid[1]
982 else:
983 sublabel = ''
984 linkids = [linkid]
985 subml = []
986 for linkid in linkids:
987 label = classname + linkid
988 # if we have a label property, try to use it
989 # TODO: test for node existence even when
990 # there's no labelprop!
991 try:
992 if labelprop is not None and \
993 labelprop != 'id':
994 label = linkcl.get(linkid, labelprop)
995 label = cgi.escape(label)
996 except IndexError:
997 comments['no_link'] = self._(
998 "<strike>The linked node"
999 " no longer exists</strike>")
1000 subml.append('<strike>%s</strike>'%label)
1001 else:
1002 if hrefable:
1003 subml.append('<a href="%s%s">%s</a>'%(
1004 classname, linkid, label))
1005 elif label is None:
1006 subml.append('%s%s'%(classname,
1007 linkid))
1008 else:
1009 subml.append(label)
1010 ml.append(sublabel + ', '.join(subml))
1011 cell.append('%s:\n %s'%(self._(k), ', '.join(ml)))
1012 elif isinstance(prop, hyperdb.Link) and args[k]:
1013 label = classname + args[k]
1014 # if we have a label property, try to use it
1015 # TODO: test for node existence even when
1016 # there's no labelprop!
1017 if labelprop is not None and labelprop != 'id':
1018 try:
1019 label = cgi.escape(linkcl.get(args[k],
1020 labelprop))
1021 except IndexError:
1022 comments['no_link'] = self._(
1023 "<strike>The linked node"
1024 " no longer exists</strike>")
1025 cell.append(' <strike>%s</strike>,\n'%label)
1026 # "flag" this is done .... euwww
1027 label = None
1028 if label is not None:
1029 if hrefable:
1030 old = '<a href="%s%s">%s</a>'%(classname,
1031 args[k], label)
1032 else:
1033 old = label;
1034 cell.append('%s: %s' % (self._(k), old))
1035 if current.has_key(k):
1036 cell[-1] += ' -> %s'%current[k]
1037 current[k] = old
1039 elif isinstance(prop, hyperdb.Date) and args[k]:
1040 if args[k] is None:
1041 d = ''
1042 else:
1043 d = date.Date(args[k],
1044 translator=self._client).local(timezone)
1045 cell.append('%s: %s'%(self._(k), str(d)))
1046 if current.has_key(k):
1047 cell[-1] += ' -> %s' % current[k]
1048 current[k] = str(d)
1050 elif isinstance(prop, hyperdb.Interval) and args[k]:
1051 val = str(date.Interval(args[k],
1052 translator=self._client))
1053 cell.append('%s: %s'%(self._(k), val))
1054 if current.has_key(k):
1055 cell[-1] += ' -> %s'%current[k]
1056 current[k] = val
1058 elif isinstance(prop, hyperdb.String) and args[k]:
1059 val = cgi.escape(args[k])
1060 cell.append('%s: %s'%(self._(k), val))
1061 if current.has_key(k):
1062 cell[-1] += ' -> %s'%current[k]
1063 current[k] = val
1065 elif isinstance(prop, hyperdb.Boolean) and args[k] is not None:
1066 val = args[k] and ''"Yes" or ''"No"
1067 cell.append('%s: %s'%(self._(k), val))
1068 if current.has_key(k):
1069 cell[-1] += ' -> %s'%current[k]
1070 current[k] = val
1072 elif not args[k]:
1073 if current.has_key(k):
1074 cell.append('%s: %s'%(self._(k), current[k]))
1075 current[k] = '(no value)'
1076 else:
1077 cell.append(self._('%s: (no value)')%self._(k))
1079 else:
1080 cell.append('%s: %s'%(self._(k), str(args[k])))
1081 if current.has_key(k):
1082 cell[-1] += ' -> %s'%current[k]
1083 current[k] = str(args[k])
1085 arg_s = '<br />'.join(cell)
1086 else:
1087 # unkown event!!
1088 comments['unknown'] = self._(
1089 "<strong><em>This event is not handled"
1090 " by the history display!</em></strong>")
1091 arg_s = '<strong><em>' + str(args) + '</em></strong>'
1092 date_s = date_s.replace(' ', ' ')
1093 # if the user's an itemid, figure the username (older journals
1094 # have the username)
1095 if dre.match(user):
1096 user = self._db.user.get(user, 'username')
1097 l.append('<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>'%(
1098 date_s, user, self._(action), arg_s))
1099 if comments:
1100 l.append(self._(
1101 '<tr><td colspan=4><strong>Note:</strong></td></tr>'))
1102 for entry in comments.values():
1103 l.append('<tr><td colspan=4>%s</td></tr>'%entry)
1105 if direction == 'ascending':
1106 l.reverse()
1108 l[0:0] = ['<table class="history">'
1109 '<tr><th colspan="4" class="header">',
1110 self._('History'),
1111 '</th></tr><tr>',
1112 self._('<th>Date</th>'),
1113 self._('<th>User</th>'),
1114 self._('<th>Action</th>'),
1115 self._('<th>Args</th>'),
1116 '</tr>']
1117 l.append('</table>')
1118 return '\n'.join(l)
1120 def renderQueryForm(self):
1121 """ Render this item, which is a query, as a search form.
1122 """
1123 # create a new request and override the specified args
1124 req = HTMLRequest(self._client)
1125 req.classname = self._klass.get(self._nodeid, 'klass')
1126 name = self._klass.get(self._nodeid, 'name')
1127 req.updateFromURL(self._klass.get(self._nodeid, 'url') +
1128 '&@queryname=%s'%urllib.quote(name))
1130 # new template, using the specified classname and request
1131 pt = self._client.instance.templates.get(req.classname, 'search')
1132 # The context for a search page should be the class, not any
1133 # node.
1134 self._client.nodeid = None
1136 # use our fabricated request
1137 return pt.render(self._client, req.classname, req)
1139 def download_url(self):
1140 """ Assume that this item is a FileClass and that it has a name
1141 and content. Construct a URL for the download of the content.
1142 """
1143 name = self._klass.get(self._nodeid, 'name')
1144 url = '%s%s/%s'%(self._classname, self._nodeid, name)
1145 return urllib.quote(url)
1147 def copy_url(self, exclude=("messages", "files")):
1148 """Construct a URL for creating a copy of this item
1150 "exclude" is an optional list of properties that should
1151 not be copied to the new object. By default, this list
1152 includes "messages" and "files" properties. Note that
1153 "id" property cannot be copied.
1155 """
1156 exclude = ("id", "activity", "actor", "creation", "creator") \
1157 + tuple(exclude)
1158 query = {
1159 "@template": "item",
1160 "@note": self._("Copy of %(class)s %(id)s") % {
1161 "class": self._(self._classname), "id": self._nodeid},
1162 }
1163 for name in self._props.keys():
1164 if name not in exclude:
1165 query[name] = self[name].plain()
1166 return self._classname + "?" + "&".join(
1167 ["%s=%s" % (key, urllib.quote(value))
1168 for key, value in query.items()])
1170 class _HTMLUser(_HTMLItem):
1171 """Add ability to check for permissions on users.
1172 """
1173 _marker = []
1174 def hasPermission(self, permission, classname=_marker,
1175 property=None, itemid=None):
1176 """Determine if the user has the Permission.
1178 The class being tested defaults to the template's class, but may
1179 be overidden for this test by suppling an alternate classname.
1180 """
1181 if classname is self._marker:
1182 classname = self._client.classname
1183 return self._db.security.hasPermission(permission,
1184 self._nodeid, classname, property, itemid)
1186 def hasRole(self, rolename):
1187 """Determine whether the user has the Role."""
1188 roles = self._db.user.get(self._nodeid, 'roles').split(',')
1189 for role in roles:
1190 if role.strip() == rolename: return True
1191 return False
1193 def HTMLItem(client, classname, nodeid, anonymous=0):
1194 if classname == 'user':
1195 return _HTMLUser(client, classname, nodeid, anonymous)
1196 else:
1197 return _HTMLItem(client, classname, nodeid, anonymous)
1199 class HTMLProperty(HTMLInputMixin, HTMLPermissions):
1200 """ String, Number, Date, Interval HTMLProperty
1202 Has useful attributes:
1204 _name the name of the property
1205 _value the value of the property if any
1207 A wrapper object which may be stringified for the plain() behaviour.
1208 """
1209 def __init__(self, client, classname, nodeid, prop, name, value,
1210 anonymous=0):
1211 self._client = client
1212 self._db = client.db
1213 self._ = client._
1214 self._classname = classname
1215 self._nodeid = nodeid
1216 self._prop = prop
1217 self._value = value
1218 self._anonymous = anonymous
1219 self._name = name
1220 if not anonymous:
1221 self._formname = '%s%s@%s'%(classname, nodeid, name)
1222 else:
1223 self._formname = name
1225 # If no value is already present for this property, see if one
1226 # is specified in the current form.
1227 form = self._client.form
1228 if not self._value and form.has_key(self._formname):
1229 if isinstance(prop, hyperdb.Multilink):
1230 value = lookupIds(self._db, prop,
1231 handleListCGIValue(form[self._formname]),
1232 fail_ok=1)
1233 elif isinstance(prop, hyperdb.Link):
1234 value = form.getfirst(self._formname).strip()
1235 if value:
1236 value = lookupIds(self._db, prop, [value],
1237 fail_ok=1)[0]
1238 else:
1239 value = None
1240 else:
1241 value = form.getfirst(self._formname).strip() or None
1242 self._value = value
1244 HTMLInputMixin.__init__(self)
1246 def __repr__(self):
1247 return '<HTMLProperty(0x%x) %s %r %r>'%(id(self), self._formname,
1248 self._prop, self._value)
1249 def __str__(self):
1250 return self.plain()
1251 def __cmp__(self, other):
1252 if isinstance(other, HTMLProperty):
1253 return cmp(self._value, other._value)
1254 return cmp(self._value, other)
1256 def __nonzero__(self):
1257 return not not self._value
1259 def isset(self):
1260 """Is my _value not None?"""
1261 return self._value is not None
1263 def is_edit_ok(self):
1264 """Should the user be allowed to use an edit form field for this
1265 property. Check "Create" for new items, or "Edit" for existing
1266 ones.
1267 """
1268 if self._nodeid:
1269 return self._db.security.hasPermission('Edit', self._client.userid,
1270 self._classname, self._name, self._nodeid)
1271 return self._db.security.hasPermission('Create', self._client.userid,
1272 self._classname, self._name)
1274 def is_view_ok(self):
1275 """ Is the user allowed to View the current class?
1276 """
1277 if self._db.security.hasPermission('View', self._client.userid,
1278 self._classname, self._name, self._nodeid):
1279 return 1
1280 return self.is_edit_ok()
1282 class StringHTMLProperty(HTMLProperty):
1283 hyper_re = re.compile(r'''(
1284 (?P<url>
1285 (
1286 (ht|f)tp(s?):// # protocol
1287 ([\w]+(:\w+)?@)? # username/password
1288 ([\w\-]+) # hostname
1289 ((\.[\w-]+)+)? # .domain.etc
1290 | # ... or ...
1291 ([\w]+(:\w+)?@)? # username/password
1292 www\. # "www."
1293 ([\w\-]+\.)+ # hostname
1294 [\w]{2,5} # TLD
1295 )
1296 (:[\d]{1,5})? # port
1297 (/[\w\-$.+!*(),;:@&=?/~\\#%]*)? # path etc.
1298 )|
1299 (?P<email>[-+=%/\w\.]+@[\w\.\-]+)|
1300 (?P<item>(?P<class>[A-Za-z_]+)(\s*)(?P<id>\d+))
1301 )''', re.X | re.I)
1302 protocol_re = re.compile('^(ht|f)tp(s?)://', re.I)
1304 def _hyper_repl_item(self,match,replacement):
1305 item = match.group('item')
1306 cls = match.group('class').lower()
1307 id = match.group('id')
1308 try:
1309 # make sure cls is a valid tracker classname
1310 cl = self._db.getclass(cls)
1311 if not cl.hasnode(id):
1312 return item
1313 return replacement % locals()
1314 except KeyError:
1315 return item
1317 def _hyper_repl(self, match):
1318 if match.group('url'):
1319 u = s = match.group('url')
1320 if not self.protocol_re.search(s):
1321 u = 'http://' + s
1322 # catch an escaped ">" at the end of the URL
1323 if s.endswith('>'):
1324 u = s = s[:-4]
1325 e = '>'
1326 else:
1327 e = ''
1328 return '<a href="%s">%s</a>%s'%(u, s, e)
1329 elif match.group('email'):
1330 s = match.group('email')
1331 return '<a href="mailto:%s">%s</a>'%(s, s)
1332 else:
1333 return self._hyper_repl_item(match,
1334 '<a href="%(cls)s%(id)s">%(item)s</a>')
1336 def _hyper_repl_rst(self, match):
1337 if match.group('url'):
1338 s = match.group('url')
1339 return '`%s <%s>`_'%(s, s)
1340 elif match.group('email'):
1341 s = match.group('email')
1342 return '`%s <mailto:%s>`_'%(s, s)
1343 else:
1344 return self._hyper_repl_item(match,'`%(item)s <%(cls)s%(id)s>`_')
1346 def hyperlinked(self):
1347 """ Render a "hyperlinked" version of the text """
1348 return self.plain(hyperlink=1)
1350 def plain(self, escape=0, hyperlink=0):
1351 """Render a "plain" representation of the property
1353 - "escape" turns on/off HTML quoting
1354 - "hyperlink" turns on/off in-text hyperlinking of URLs, email
1355 addresses and designators
1356 """
1357 if not self.is_view_ok():
1358 return self._('[hidden]')
1360 if self._value is None:
1361 return ''
1362 if escape:
1363 s = cgi.escape(str(self._value))
1364 else:
1365 s = str(self._value)
1366 if hyperlink:
1367 # no, we *must* escape this text
1368 if not escape:
1369 s = cgi.escape(s)
1370 s = self.hyper_re.sub(self._hyper_repl, s)
1371 return s
1373 def wrapped(self, escape=1, hyperlink=1):
1374 """Render a "wrapped" representation of the property.
1376 We wrap long lines at 80 columns on the nearest whitespace. Lines
1377 with no whitespace are not broken to force wrapping.
1379 Note that unlike plain() we default wrapped() to have the escaping
1380 and hyperlinking turned on since that's the most common usage.
1382 - "escape" turns on/off HTML quoting
1383 - "hyperlink" turns on/off in-text hyperlinking of URLs, email
1384 addresses and designators
1385 """
1386 if not self.is_view_ok():
1387 return self._('[hidden]')
1389 if self._value is None:
1390 return ''
1391 s = support.wrap(str(self._value), width=80)
1392 if escape:
1393 s = cgi.escape(s)
1394 if hyperlink:
1395 # no, we *must* escape this text
1396 if not escape:
1397 s = cgi.escape(s)
1398 s = self.hyper_re.sub(self._hyper_repl, s)
1399 return s
1401 def stext(self, escape=0, hyperlink=1):
1402 """ Render the value of the property as StructuredText.
1404 This requires the StructureText module to be installed separately.
1405 """
1406 if not self.is_view_ok():
1407 return self._('[hidden]')
1409 s = self.plain(escape=escape, hyperlink=hyperlink)
1410 if not StructuredText:
1411 return s
1412 return StructuredText(s,level=1,header=0)
1414 def rst(self, hyperlink=1):
1415 """ Render the value of the property as ReStructuredText.
1417 This requires docutils to be installed separately.
1418 """
1419 if not self.is_view_ok():
1420 return self._('[hidden]')
1422 if not ReStructuredText:
1423 return self.plain(escape=0, hyperlink=hyperlink)
1424 s = self.plain(escape=0, hyperlink=0)
1425 if hyperlink:
1426 s = self.hyper_re.sub(self._hyper_repl_rst, s)
1427 return ReStructuredText(s, writer_name="html")["body"].encode("utf-8",
1428 "replace")
1430 def field(self, **kwargs):
1431 """ Render the property as a field in HTML.
1433 If not editable, just display the value via plain().
1434 """
1435 if not self.is_edit_ok():
1436 return self.plain(escape=1)
1438 value = self._value
1439 if value is None:
1440 value = ''
1442 kwargs.setdefault("size", 30)
1443 kwargs.update({"name": self._formname, "value": value})
1444 return self.input(**kwargs)
1446 def multiline(self, escape=0, rows=5, cols=40, **kwargs):
1447 """ Render a multiline form edit field for the property.
1449 If not editable, just display the plain() value in a <pre> tag.
1450 """
1451 if not self.is_edit_ok():
1452 return '<pre>%s</pre>'%self.plain()
1454 if self._value is None:
1455 value = ''
1456 else:
1457 value = cgi.escape(str(self._value))
1459 value = '"'.join(value.split('"'))
1460 name = self._formname
1461 passthrough_args = ' '.join(['%s="%s"' % (k, cgi.escape(str(v), True))
1462 for k,v in kwargs.items()])
1463 return ('<textarea %(passthrough_args)s name="%(name)s" id="%(name)s"'
1464 ' rows="%(rows)s" cols="%(cols)s">'
1465 '%(value)s</textarea>') % locals()
1467 def email(self, escape=1):
1468 """ Render the value of the property as an obscured email address
1469 """
1470 if not self.is_view_ok():
1471 return self._('[hidden]')
1473 if self._value is None:
1474 value = ''
1475 else:
1476 value = str(self._value)
1477 split = value.split('@')
1478 if len(split) == 2:
1479 name, domain = split
1480 domain = ' '.join(domain.split('.')[:-1])
1481 name = name.replace('.', ' ')
1482 value = '%s at %s ...'%(name, domain)
1483 else:
1484 value = value.replace('.', ' ')
1485 if escape:
1486 value = cgi.escape(value)
1487 return value
1489 class PasswordHTMLProperty(HTMLProperty):
1490 def plain(self, escape=0):
1491 """ Render a "plain" representation of the property
1492 """
1493 if not self.is_view_ok():
1494 return self._('[hidden]')
1496 if self._value is None:
1497 return ''
1498 return self._('*encrypted*')
1500 def field(self, size=30):
1501 """ Render a form edit field for the property.
1503 If not editable, just display the value via plain().
1504 """
1505 if not self.is_edit_ok():
1506 return self.plain(escape=1)
1508 return self.input(type="password", name=self._formname, size=size)
1510 def confirm(self, size=30):
1511 """ Render a second form edit field for the property, used for
1512 confirmation that the user typed the password correctly. Generates
1513 a field with name "@confirm@name".
1515 If not editable, display nothing.
1516 """
1517 if not self.is_edit_ok():
1518 return ''
1520 return self.input(type="password",
1521 name="@confirm@%s"%self._formname,
1522 id="%s-confirm"%self._formname,
1523 size=size)
1525 class NumberHTMLProperty(HTMLProperty):
1526 def plain(self, escape=0):
1527 """ Render a "plain" representation of the property
1528 """
1529 if not self.is_view_ok():
1530 return self._('[hidden]')
1532 if self._value is None:
1533 return ''
1535 return str(self._value)
1537 def field(self, size=30):
1538 """ Render a form edit field for the property.
1540 If not editable, just display the value via plain().
1541 """
1542 if not self.is_edit_ok():
1543 return self.plain(escape=1)
1545 value = self._value
1546 if value is None:
1547 value = ''
1549 return self.input(name=self._formname, value=value, size=size)
1551 def __int__(self):
1552 """ Return an int of me
1553 """
1554 return int(self._value)
1556 def __float__(self):
1557 """ Return a float of me
1558 """
1559 return float(self._value)
1562 class BooleanHTMLProperty(HTMLProperty):
1563 def plain(self, escape=0):
1564 """ Render a "plain" representation of the property
1565 """
1566 if not self.is_view_ok():
1567 return self._('[hidden]')
1569 if self._value is None:
1570 return ''
1571 return self._value and self._("Yes") or self._("No")
1573 def field(self):
1574 """ Render a form edit field for the property
1576 If not editable, just display the value via plain().
1577 """
1578 if not self.is_edit_ok():
1579 return self.plain(escape=1)
1581 value = self._value
1582 if isinstance(value, str) or isinstance(value, unicode):
1583 value = value.strip().lower() in ('checked', 'yes', 'true',
1584 'on', '1')
1586 checked = value and "checked" or ""
1587 if value:
1588 s = self.input(type="radio", name=self._formname, value="yes",
1589 checked="checked")
1590 s += self._('Yes')
1591 s +=self.input(type="radio", name=self._formname, value="no")
1592 s += self._('No')
1593 else:
1594 s = self.input(type="radio", name=self._formname, value="yes")
1595 s += self._('Yes')
1596 s +=self.input(type="radio", name=self._formname, value="no",
1597 checked="checked")
1598 s += self._('No')
1599 return s
1601 class DateHTMLProperty(HTMLProperty):
1603 _marker = []
1605 def __init__(self, client, classname, nodeid, prop, name, value,
1606 anonymous=0, offset=None):
1607 HTMLProperty.__init__(self, client, classname, nodeid, prop, name,
1608 value, anonymous=anonymous)
1609 if self._value and not (isinstance(self._value, str) or
1610 isinstance(self._value, unicode)):
1611 self._value.setTranslator(self._client.translator)
1612 self._offset = offset
1613 if self._offset is None :
1614 self._offset = self._prop.offset (self._db)
1616 def plain(self, escape=0):
1617 """ Render a "plain" representation of the property
1618 """
1619 if not self.is_view_ok():
1620 return self._('[hidden]')
1622 if self._value is None:
1623 return ''
1624 if self._offset is None:
1625 offset = self._db.getUserTimezone()
1626 else:
1627 offset = self._offset
1628 return str(self._value.local(offset))
1630 def now(self, str_interval=None):
1631 """ Return the current time.
1633 This is useful for defaulting a new value. Returns a
1634 DateHTMLProperty.
1635 """
1636 if not self.is_view_ok():
1637 return self._('[hidden]')
1639 ret = date.Date('.', translator=self._client)
1641 if isinstance(str_interval, basestring):
1642 sign = 1
1643 if str_interval[0] == '-':
1644 sign = -1
1645 str_interval = str_interval[1:]
1646 interval = date.Interval(str_interval, translator=self._client)
1647 if sign > 0:
1648 ret = ret + interval
1649 else:
1650 ret = ret - interval
1652 return DateHTMLProperty(self._client, self._classname, self._nodeid,
1653 self._prop, self._formname, ret)
1655 def field(self, size=30, default=None, format=_marker, popcal=True):
1656 """Render a form edit field for the property
1658 If not editable, just display the value via plain().
1660 If "popcal" then include the Javascript calendar editor.
1661 Default=yes.
1663 The format string is a standard python strftime format string.
1664 """
1665 if not self.is_edit_ok():
1666 if format is self._marker:
1667 return self.plain(escape=1)
1668 else:
1669 return self.pretty(format)
1671 value = self._value
1673 if value is None:
1674 if default is None:
1675 raw_value = None
1676 else:
1677 if isinstance(default, basestring):
1678 raw_value = date.Date(default, translator=self._client)
1679 elif isinstance(default, date.Date):
1680 raw_value = default
1681 elif isinstance(default, DateHTMLProperty):
1682 raw_value = default._value
1683 else:
1684 raise ValueError, self._('default value for '
1685 'DateHTMLProperty must be either DateHTMLProperty '
1686 'or string date representation.')
1687 elif isinstance(value, str) or isinstance(value, unicode):
1688 # most likely erroneous input to be passed back to user
1689 if isinstance(value, unicode): value = value.encode('utf8')
1690 return self.input(name=self._formname, value=value, size=size)
1691 else:
1692 raw_value = value
1694 if raw_value is None:
1695 value = ''
1696 elif isinstance(raw_value, str) or isinstance(raw_value, unicode):
1697 if format is self._marker:
1698 value = raw_value
1699 else:
1700 value = date.Date(raw_value).pretty(format)
1701 else:
1702 if self._offset is None :
1703 offset = self._db.getUserTimezone()
1704 else :
1705 offset = self._offset
1706 value = raw_value.local(offset)
1707 if format is not self._marker:
1708 value = value.pretty(format)
1710 s = self.input(name=self._formname, value=value, size=size)
1711 if popcal:
1712 s += self.popcal()
1713 return s
1715 def reldate(self, pretty=1):
1716 """ Render the interval between the date and now.
1718 If the "pretty" flag is true, then make the display pretty.
1719 """
1720 if not self.is_view_ok():
1721 return self._('[hidden]')
1723 if not self._value:
1724 return ''
1726 # figure the interval
1727 interval = self._value - date.Date('.', translator=self._client)
1728 if pretty:
1729 return interval.pretty()
1730 return str(interval)
1732 def pretty(self, format=_marker):
1733 """ Render the date in a pretty format (eg. month names, spaces).
1735 The format string is a standard python strftime format string.
1736 Note that if the day is zero, and appears at the start of the
1737 string, then it'll be stripped from the output. This is handy
1738 for the situation when a date only specifies a month and a year.
1739 """
1740 if not self.is_view_ok():
1741 return self._('[hidden]')
1743 if self._offset is None:
1744 offset = self._db.getUserTimezone()
1745 else:
1746 offset = self._offset
1748 if not self._value:
1749 return ''
1750 elif format is not self._marker:
1751 return self._value.local(offset).pretty(format)
1752 else:
1753 return self._value.local(offset).pretty()
1755 def local(self, offset):
1756 """ Return the date/time as a local (timezone offset) date/time.
1757 """
1758 if not self.is_view_ok():
1759 return self._('[hidden]')
1761 return DateHTMLProperty(self._client, self._classname, self._nodeid,
1762 self._prop, self._formname, self._value, offset=offset)
1764 def popcal(self, width=300, height=200, label="(cal)",
1765 form="itemSynopsis"):
1766 """Generate a link to a calendar pop-up window.
1768 item: HTMLProperty e.g.: context.deadline
1769 """
1770 if self.isset():
1771 date = "&date=%s"%self._value
1772 else :
1773 date = ""
1774 return ('<a class="classhelp" href="javascript:help_window('
1775 "'%s?@template=calendar&property=%s&form=%s%s', %d, %d)"
1776 '">%s</a>'%(self._classname, self._name, form, date, width,
1777 height, label))
1779 class IntervalHTMLProperty(HTMLProperty):
1780 def __init__(self, client, classname, nodeid, prop, name, value,
1781 anonymous=0):
1782 HTMLProperty.__init__(self, client, classname, nodeid, prop,
1783 name, value, anonymous)
1784 if self._value and not isinstance(self._value, (str, unicode)):
1785 self._value.setTranslator(self._client.translator)
1787 def plain(self, escape=0):
1788 """ Render a "plain" representation of the property
1789 """
1790 if not self.is_view_ok():
1791 return self._('[hidden]')
1793 if self._value is None:
1794 return ''
1795 return str(self._value)
1797 def pretty(self):
1798 """ Render the interval in a pretty format (eg. "yesterday")
1799 """
1800 if not self.is_view_ok():
1801 return self._('[hidden]')
1803 return self._value.pretty()
1805 def field(self, size=30):
1806 """ Render a form edit field for the property
1808 If not editable, just display the value via plain().
1809 """
1810 if not self.is_edit_ok():
1811 return self.plain(escape=1)
1813 value = self._value
1814 if value is None:
1815 value = ''
1817 return self.input(name=self._formname, value=value, size=size)
1819 class LinkHTMLProperty(HTMLProperty):
1820 """ Link HTMLProperty
1821 Include the above as well as being able to access the class
1822 information. Stringifying the object itself results in the value
1823 from the item being displayed. Accessing attributes of this object
1824 result in the appropriate entry from the class being queried for the
1825 property accessed (so item/assignedto/name would look up the user
1826 entry identified by the assignedto property on item, and then the
1827 name property of that user)
1828 """
1829 def __init__(self, *args, **kw):
1830 HTMLProperty.__init__(self, *args, **kw)
1831 # if we're representing a form value, then the -1 from the form really
1832 # should be a None
1833 if str(self._value) == '-1':
1834 self._value = None
1836 def __getattr__(self, attr):
1837 """ return a new HTMLItem """
1838 if not self._value:
1839 # handle a special page templates lookup
1840 if attr == '__render_with_namespace__':
1841 def nothing(*args, **kw):
1842 return ''
1843 return nothing
1844 msg = self._('Attempt to look up %(attr)s on a missing value')
1845 return MissingValue(msg%locals())
1846 i = HTMLItem(self._client, self._prop.classname, self._value)
1847 return getattr(i, attr)
1849 def plain(self, escape=0):
1850 """ Render a "plain" representation of the property
1851 """
1852 if not self.is_view_ok():
1853 return self._('[hidden]')
1855 if self._value is None:
1856 return ''
1857 linkcl = self._db.classes[self._prop.classname]
1858 k = linkcl.labelprop(1)
1859 if num_re.match(self._value):
1860 try:
1861 value = str(linkcl.get(self._value, k))
1862 except IndexError:
1863 value = self._value
1864 else :
1865 value = self._value
1866 if escape:
1867 value = cgi.escape(value)
1868 return value
1870 def field(self, showid=0, size=None):
1871 """ Render a form edit field for the property
1873 If not editable, just display the value via plain().
1874 """
1875 if not self.is_edit_ok():
1876 return self.plain(escape=1)
1878 # edit field
1879 linkcl = self._db.getclass(self._prop.classname)
1880 if self._value is None:
1881 value = ''
1882 else:
1883 k = linkcl.getkey()
1884 if k and num_re.match(self._value):
1885 value = linkcl.get(self._value, k)
1886 else:
1887 value = self._value
1888 return self.input(name=self._formname, value=value, size=size)
1890 def menu(self, size=None, height=None, showid=0, additional=[], value=None,
1891 sort_on=None, **conditions):
1892 """ Render a form select list for this property
1894 "size" is used to limit the length of the list labels
1895 "height" is used to set the <select> tag's "size" attribute
1896 "showid" includes the item ids in the list labels
1897 "value" specifies which item is pre-selected
1898 "additional" lists properties which should be included in the
1899 label
1900 "sort_on" indicates the property to sort the list on as
1901 (direction, property) where direction is '+' or '-'. A
1902 single string with the direction prepended may be used.
1903 For example: ('-', 'order'), '+name'.
1905 The remaining keyword arguments are used as conditions for
1906 filtering the items in the list - they're passed as the
1907 "filterspec" argument to a Class.filter() call.
1909 If not editable, just display the value via plain().
1910 """
1911 if not self.is_edit_ok():
1912 return self.plain(escape=1)
1914 # Since None indicates the default, we need another way to
1915 # indicate "no selection". We use -1 for this purpose, as
1916 # that is the value we use when submitting a form without the
1917 # value set.
1918 if value is None:
1919 value = self._value
1920 elif value == '-1':
1921 value = None
1923 linkcl = self._db.getclass(self._prop.classname)
1924 l = ['<select name="%s">'%self._formname]
1925 k = linkcl.labelprop(1)
1926 s = ''
1927 if value is None:
1928 s = 'selected="selected" '
1929 l.append(self._('<option %svalue="-1">- no selection -</option>')%s)
1931 if sort_on is not None:
1932 if not isinstance(sort_on, tuple):
1933 if sort_on[0] in '+-':
1934 sort_on = (sort_on[0], sort_on[1:])
1935 else:
1936 sort_on = ('+', sort_on)
1937 else:
1938 sort_on = ('+', linkcl.orderprop())
1940 options = [opt
1941 for opt in linkcl.filter(None, conditions, sort_on, (None, None))
1942 if self._db.security.hasPermission("View", self._client.userid,
1943 linkcl.classname, itemid=opt)]
1945 # make sure we list the current value if it's retired
1946 if value and value not in options:
1947 options.insert(0, value)
1949 if additional:
1950 additional_fns = []
1951 props = linkcl.getprops()
1952 for propname in additional:
1953 prop = props[propname]
1954 if isinstance(prop, hyperdb.Link):
1955 cl = self._db.getclass(prop.classname)
1956 labelprop = cl.labelprop()
1957 fn = lambda optionid: cl.get(linkcl.get(optionid,
1958 propname),
1959 labelprop)
1960 else:
1961 fn = lambda optionid: linkcl.get(optionid, propname)
1962 additional_fns.append(fn)
1964 for optionid in options:
1965 # get the option value, and if it's None use an empty string
1966 option = linkcl.get(optionid, k) or ''
1968 # figure if this option is selected
1969 s = ''
1970 if value in [optionid, option]:
1971 s = 'selected="selected" '
1973 # figure the label
1974 if showid:
1975 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1976 elif not option:
1977 lab = '%s%s'%(self._prop.classname, optionid)
1978 else:
1979 lab = option
1981 # truncate if it's too long
1982 if size is not None and len(lab) > size:
1983 lab = lab[:size-3] + '...'
1984 if additional:
1985 m = []
1986 for fn in additional_fns:
1987 m.append(str(fn(optionid)))
1988 lab = lab + ' (%s)'%', '.join(m)
1990 # and generate
1991 lab = cgi.escape(self._(lab))
1992 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
1993 l.append('</select>')
1994 return '\n'.join(l)
1995 # def checklist(self, ...)
1999 class MultilinkHTMLProperty(HTMLProperty):
2000 """ Multilink HTMLProperty
2002 Also be iterable, returning a wrapper object like the Link case for
2003 each entry in the multilink.
2004 """
2005 def __init__(self, *args, **kwargs):
2006 HTMLProperty.__init__(self, *args, **kwargs)
2007 if self._value:
2008 display_value = lookupIds(self._db, self._prop, self._value,
2009 fail_ok=1, do_lookup=False)
2010 sortfun = make_sort_function(self._db, self._prop.classname)
2011 # sorting fails if the value contains
2012 # items not yet stored in the database
2013 # ignore these errors to preserve user input
2014 try:
2015 display_value.sort(sortfun)
2016 except:
2017 pass
2018 self._value = display_value
2020 def __len__(self):
2021 """ length of the multilink """
2022 return len(self._value)
2024 def __getattr__(self, attr):
2025 """ no extended attribute accesses make sense here """
2026 raise AttributeError, attr
2028 def viewableGenerator(self, values):
2029 """Used to iterate over only the View'able items in a class."""
2030 check = self._db.security.hasPermission
2031 userid = self._client.userid
2032 classname = self._prop.classname
2033 for value in values:
2034 if check('View', userid, classname, itemid=value):
2035 yield HTMLItem(self._client, classname, value)
2037 def __iter__(self):
2038 """ iterate and return a new HTMLItem
2039 """
2040 return self.viewableGenerator(self._value)
2042 def reverse(self):
2043 """ return the list in reverse order
2044 """
2045 l = self._value[:]
2046 l.reverse()
2047 return self.viewableGenerator(l)
2049 def sorted(self, property):
2050 """ Return this multilink sorted by the given property """
2051 value = list(self.__iter__())
2052 value.sort(lambda a,b:cmp(a[property], b[property]))
2053 return value
2055 def __contains__(self, value):
2056 """ Support the "in" operator. We have to make sure the passed-in
2057 value is a string first, not a HTMLProperty.
2058 """
2059 return str(value) in self._value
2061 def isset(self):
2062 """Is my _value not []?"""
2063 return self._value != []
2065 def plain(self, escape=0):
2066 """ Render a "plain" representation of the property
2067 """
2068 if not self.is_view_ok():
2069 return self._('[hidden]')
2071 linkcl = self._db.classes[self._prop.classname]
2072 k = linkcl.labelprop(1)
2073 labels = []
2074 for v in self._value:
2075 if num_re.match(v):
2076 try:
2077 label = linkcl.get(v, k)
2078 except IndexError:
2079 label = None
2080 # fall back to designator if label is None
2081 if label is None: label = '%s%s'%(self._prop.classname, k)
2082 else:
2083 label = v
2084 labels.append(label)
2085 value = ', '.join(labels)
2086 if escape:
2087 value = cgi.escape(value)
2088 return value
2090 def field(self, size=30, showid=0):
2091 """ Render a form edit field for the property
2093 If not editable, just display the value via plain().
2094 """
2095 if not self.is_edit_ok():
2096 return self.plain(escape=1)
2098 linkcl = self._db.getclass(self._prop.classname)
2099 value = self._value[:]
2100 # map the id to the label property
2101 if not linkcl.getkey():
2102 showid=1
2103 if not showid:
2104 k = linkcl.labelprop(1)
2105 value = lookupKeys(linkcl, k, value)
2106 value = ','.join(value)
2107 return self.input(name=self._formname, size=size, value=value)
2109 def menu(self, size=None, height=None, showid=0, additional=[],
2110 value=None, sort_on=None, **conditions):
2111 """ Render a form <select> list for this property.
2113 "size" is used to limit the length of the list labels
2114 "height" is used to set the <select> tag's "size" attribute
2115 "showid" includes the item ids in the list labels
2116 "additional" lists properties which should be included in the
2117 label
2118 "value" specifies which item is pre-selected
2119 "sort_on" indicates the property to sort the list on as
2120 (direction, property) where direction is '+' or '-'. A
2121 single string with the direction prepended may be used.
2122 For example: ('-', 'order'), '+name'.
2124 The remaining keyword arguments are used as conditions for
2125 filtering the items in the list - they're passed as the
2126 "filterspec" argument to a Class.filter() call.
2128 If not editable, just display the value via plain().
2129 """
2130 if not self.is_edit_ok():
2131 return self.plain(escape=1)
2133 if value is None:
2134 value = self._value
2136 linkcl = self._db.getclass(self._prop.classname)
2138 if sort_on is not None:
2139 if not isinstance(sort_on, tuple):
2140 if sort_on[0] in '+-':
2141 sort_on = (sort_on[0], sort_on[1:])
2142 else:
2143 sort_on = ('+', sort_on)
2144 else:
2145 sort_on = ('+', linkcl.orderprop())
2147 options = [opt
2148 for opt in linkcl.filter(None, conditions, sort_on)
2149 if self._db.security.hasPermission("View", self._client.userid,
2150 linkcl.classname, itemid=opt)]
2152 # make sure we list the current values if they're retired
2153 for val in value:
2154 if val not in options:
2155 options.insert(0, val)
2157 if not height:
2158 height = len(options)
2159 if value:
2160 # The "no selection" option.
2161 height += 1
2162 height = min(height, 7)
2163 l = ['<select multiple name="%s" size="%s">'%(self._formname, height)]
2164 k = linkcl.labelprop(1)
2166 if value:
2167 l.append('<option value="%s">- no selection -</option>'
2168 % ','.join(['-' + v for v in value]))
2170 if additional:
2171 additional_fns = []
2172 props = linkcl.getprops()
2173 for propname in additional:
2174 prop = props[propname]
2175 if isinstance(prop, hyperdb.Link):
2176 cl = self._db.getclass(prop.classname)
2177 labelprop = cl.labelprop()
2178 fn = lambda optionid: cl.get(linkcl.get(optionid,
2179 propname),
2180 labelprop)
2181 else:
2182 fn = lambda optionid: linkcl.get(optionid, propname)
2183 additional_fns.append(fn)
2185 for optionid in options:
2186 # get the option value, and if it's None use an empty string
2187 option = linkcl.get(optionid, k) or ''
2189 # figure if this option is selected
2190 s = ''
2191 if optionid in value or option in value:
2192 s = 'selected="selected" '
2194 # figure the label
2195 if showid:
2196 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
2197 else:
2198 lab = option
2199 # truncate if it's too long
2200 if size is not None and len(lab) > size:
2201 lab = lab[:size-3] + '...'
2202 if additional:
2203 m = []
2204 for fn in additional_fns:
2205 m.append(str(fn(optionid)))
2206 lab = lab + ' (%s)'%', '.join(m)
2208 # and generate
2209 lab = cgi.escape(self._(lab))
2210 l.append('<option %svalue="%s">%s</option>'%(s, optionid,
2211 lab))
2212 l.append('</select>')
2213 return '\n'.join(l)
2215 # set the propclasses for HTMLItem
2216 propclasses = (
2217 (hyperdb.String, StringHTMLProperty),
2218 (hyperdb.Number, NumberHTMLProperty),
2219 (hyperdb.Boolean, BooleanHTMLProperty),
2220 (hyperdb.Date, DateHTMLProperty),
2221 (hyperdb.Interval, IntervalHTMLProperty),
2222 (hyperdb.Password, PasswordHTMLProperty),
2223 (hyperdb.Link, LinkHTMLProperty),
2224 (hyperdb.Multilink, MultilinkHTMLProperty),
2225 )
2227 def make_sort_function(db, classname, sort_on=None):
2228 """Make a sort function for a given class
2229 """
2230 linkcl = db.getclass(classname)
2231 if sort_on is None:
2232 sort_on = linkcl.orderprop()
2233 def sortfunc(a, b):
2234 return cmp(linkcl.get(a, sort_on), linkcl.get(b, sort_on))
2235 return sortfunc
2237 def handleListCGIValue(value):
2238 """ Value is either a single item or a list of items. Each item has a
2239 .value that we're actually interested in.
2240 """
2241 if isinstance(value, type([])):
2242 return [value.value for value in value]
2243 else:
2244 value = value.value.strip()
2245 if not value:
2246 return []
2247 return [v.strip() for v in value.split(',')]
2249 class HTMLRequest(HTMLInputMixin):
2250 """The *request*, holding the CGI form and environment.
2252 - "form" the CGI form as a cgi.FieldStorage
2253 - "env" the CGI environment variables
2254 - "base" the base URL for this instance
2255 - "user" a HTMLItem instance for this user
2256 - "language" as determined by the browser or config
2257 - "classname" the current classname (possibly None)
2258 - "template" the current template (suffix, also possibly None)
2260 Index args:
2262 - "columns" dictionary of the columns to display in an index page
2263 - "show" a convenience access to columns - request/show/colname will
2264 be true if the columns should be displayed, false otherwise
2265 - "sort" index sort column (direction, column name)
2266 - "group" index grouping property (direction, column name)
2267 - "filter" properties to filter the index on
2268 - "filterspec" values to filter the index on
2269 - "search_text" text to perform a full-text search on for an index
2270 """
2271 def __repr__(self):
2272 return '<HTMLRequest %r>'%self.__dict__
2274 def __init__(self, client):
2275 # _client is needed by HTMLInputMixin
2276 self._client = self.client = client
2278 # easier access vars
2279 self.form = client.form
2280 self.env = client.env
2281 self.base = client.base
2282 self.user = HTMLItem(client, 'user', client.userid)
2283 self.language = client.language
2285 # store the current class name and action
2286 self.classname = client.classname
2287 self.nodeid = client.nodeid
2288 self.template = client.template
2290 # the special char to use for special vars
2291 self.special_char = '@'
2293 HTMLInputMixin.__init__(self)
2295 self._post_init()
2297 def current_url(self):
2298 url = self.base
2299 if self.classname:
2300 url += self.classname
2301 if self.nodeid:
2302 url += self.nodeid
2303 args = {}
2304 if self.template:
2305 args['@template'] = self.template
2306 return self.indexargs_url(url, args)
2308 def _parse_sort(self, var, name):
2309 """ Parse sort/group options. Append to var
2310 """
2311 fields = []
2312 dirs = []
2313 for special in '@:':
2314 idx = 0
2315 key = '%s%s%d'%(special, name, idx)
2316 while key in self.form:
2317 self.special_char = special
2318 fields.append(self.form.getfirst(key))
2319 dirkey = '%s%sdir%d'%(special, name, idx)
2320 if dirkey in self.form:
2321 dirs.append(self.form.getfirst(dirkey))
2322 else:
2323 dirs.append(None)
2324 idx += 1
2325 key = '%s%s%d'%(special, name, idx)
2326 # backward compatible (and query) URL format
2327 key = special + name
2328 dirkey = key + 'dir'
2329 if key in self.form and not fields:
2330 fields = handleListCGIValue(self.form[key])
2331 if dirkey in self.form:
2332 dirs.append(self.form.getfirst(dirkey))
2333 if fields: # only try other special char if nothing found
2334 break
2335 for f, d in map(None, fields, dirs):
2336 if f.startswith('-'):
2337 var.append(('-', f[1:]))
2338 elif d:
2339 var.append(('-', f))
2340 else:
2341 var.append(('+', f))
2343 def _post_init(self):
2344 """ Set attributes based on self.form
2345 """
2346 # extract the index display information from the form
2347 self.columns = []
2348 for name in ':columns @columns'.split():
2349 if self.form.has_key(name):
2350 self.special_char = name[0]
2351 self.columns = handleListCGIValue(self.form[name])
2352 break
2353 self.show = support.TruthDict(self.columns)
2355 # sorting and grouping
2356 self.sort = []
2357 self.group = []
2358 self._parse_sort(self.sort, 'sort')
2359 self._parse_sort(self.group, 'group')
2361 # filtering
2362 self.filter = []
2363 for name in ':filter @filter'.split():
2364 if self.form.has_key(name):
2365 self.special_char = name[0]
2366 self.filter = handleListCGIValue(self.form[name])
2368 self.filterspec = {}
2369 db = self.client.db
2370 if self.classname is not None:
2371 cls = db.getclass (self.classname)
2372 for name in self.filter:
2373 if not self.form.has_key(name):
2374 continue
2375 prop = cls.get_transitive_prop (name)
2376 fv = self.form[name]
2377 if (isinstance(prop, hyperdb.Link) or
2378 isinstance(prop, hyperdb.Multilink)):
2379 self.filterspec[name] = lookupIds(db, prop,
2380 handleListCGIValue(fv))
2381 else:
2382 if isinstance(fv, type([])):
2383 self.filterspec[name] = [v.value for v in fv]
2384 elif name == 'id':
2385 # special case "id" property
2386 self.filterspec[name] = handleListCGIValue(fv)
2387 else:
2388 self.filterspec[name] = fv.value
2390 # full-text search argument
2391 self.search_text = None
2392 for name in ':search_text @search_text'.split():
2393 if self.form.has_key(name):
2394 self.special_char = name[0]
2395 self.search_text = self.form.getfirst(name)
2397 # pagination - size and start index
2398 # figure batch args
2399 self.pagesize = 50
2400 for name in ':pagesize @pagesize'.split():
2401 if self.form.has_key(name):
2402 self.special_char = name[0]
2403 self.pagesize = int(self.form.getfirst(name))
2405 self.startwith = 0
2406 for name in ':startwith @startwith'.split():
2407 if self.form.has_key(name):
2408 self.special_char = name[0]
2409 self.startwith = int(self.form.getfirst(name))
2411 # dispname
2412 if self.form.has_key('@dispname'):
2413 self.dispname = self.form.getfirst('@dispname')
2414 else:
2415 self.dispname = None
2417 def updateFromURL(self, url):
2418 """ Parse the URL for query args, and update my attributes using the
2419 values.
2420 """
2421 env = {'QUERY_STRING': url}
2422 self.form = cgi.FieldStorage(environ=env)
2424 self._post_init()
2426 def update(self, kwargs):
2427 """ Update my attributes using the keyword args
2428 """
2429 self.__dict__.update(kwargs)
2430 if kwargs.has_key('columns'):
2431 self.show = support.TruthDict(self.columns)
2433 def description(self):
2434 """ Return a description of the request - handle for the page title.
2435 """
2436 s = [self.client.db.config.TRACKER_NAME]
2437 if self.classname:
2438 if self.client.nodeid:
2439 s.append('- %s%s'%(self.classname, self.client.nodeid))
2440 else:
2441 if self.template == 'item':
2442 s.append('- new %s'%self.classname)
2443 elif self.template == 'index':
2444 s.append('- %s index'%self.classname)
2445 else:
2446 s.append('- %s %s'%(self.classname, self.template))
2447 else:
2448 s.append('- home')
2449 return ' '.join(s)
2451 def __str__(self):
2452 d = {}
2453 d.update(self.__dict__)
2454 f = ''
2455 for k in self.form.keys():
2456 f += '\n %r=%r'%(k,handleListCGIValue(self.form[k]))
2457 d['form'] = f
2458 e = ''
2459 for k,v in self.env.items():
2460 e += '\n %r=%r'%(k, v)
2461 d['env'] = e
2462 return """
2463 form: %(form)s
2464 base: %(base)r
2465 classname: %(classname)r
2466 template: %(template)r
2467 columns: %(columns)r
2468 sort: %(sort)r
2469 group: %(group)r
2470 filter: %(filter)r
2471 search_text: %(search_text)r
2472 pagesize: %(pagesize)r
2473 startwith: %(startwith)r
2474 env: %(env)s
2475 """%d
2477 def indexargs_form(self, columns=1, sort=1, group=1, filter=1,
2478 filterspec=1, search_text=1):
2479 """ return the current index args as form elements """
2480 l = []
2481 sc = self.special_char
2482 def add(k, v):
2483 l.append(self.input(type="hidden", name=k, value=v))
2484 if columns and self.columns:
2485 add(sc+'columns', ','.join(self.columns))
2486 if sort:
2487 val = []
2488 for dir, attr in self.sort:
2489 if dir == '-':
2490 val.append('-'+attr)
2491 else:
2492 val.append(attr)
2493 add(sc+'sort', ','.join (val))
2494 if group:
2495 val = []
2496 for dir, attr in self.group:
2497 if dir == '-':
2498 val.append('-'+attr)
2499 else:
2500 val.append(attr)
2501 add(sc+'group', ','.join (val))
2502 if filter and self.filter:
2503 add(sc+'filter', ','.join(self.filter))
2504 if self.classname and filterspec:
2505 cls = self.client.db.getclass(self.classname)
2506 for k,v in self.filterspec.items():
2507 if type(v) == type([]):
2508 if isinstance(cls.get_transitive_prop(k), hyperdb.String):
2509 add(k, ' '.join(v))
2510 else:
2511 add(k, ','.join(v))
2512 else:
2513 add(k, v)
2514 if search_text and self.search_text:
2515 add(sc+'search_text', self.search_text)
2516 add(sc+'pagesize', self.pagesize)
2517 add(sc+'startwith', self.startwith)
2518 return '\n'.join(l)
2520 def indexargs_url(self, url, args):
2521 """ Embed the current index args in a URL
2522 """
2523 q = urllib.quote
2524 sc = self.special_char
2525 l = ['%s=%s'%(k,v) for k,v in args.items()]
2527 # pull out the special values (prefixed by @ or :)
2528 specials = {}
2529 for key in args.keys():
2530 if key[0] in '@:':
2531 specials[key[1:]] = args[key]
2533 # ok, now handle the specials we received in the request
2534 if self.columns and not specials.has_key('columns'):
2535 l.append(sc+'columns=%s'%(','.join(self.columns)))
2536 if self.sort and not specials.has_key('sort'):
2537 val = []
2538 for dir, attr in self.sort:
2539 if dir == '-':
2540 val.append('-'+attr)
2541 else:
2542 val.append(attr)
2543 l.append(sc+'sort=%s'%(','.join(val)))
2544 if self.group and not specials.has_key('group'):
2545 val = []
2546 for dir, attr in self.group:
2547 if dir == '-':
2548 val.append('-'+attr)
2549 else:
2550 val.append(attr)
2551 l.append(sc+'group=%s'%(','.join(val)))
2552 if self.filter and not specials.has_key('filter'):
2553 l.append(sc+'filter=%s'%(','.join(self.filter)))
2554 if self.search_text and not specials.has_key('search_text'):
2555 l.append(sc+'search_text=%s'%q(self.search_text))
2556 if not specials.has_key('pagesize'):
2557 l.append(sc+'pagesize=%s'%self.pagesize)
2558 if not specials.has_key('startwith'):
2559 l.append(sc+'startwith=%s'%self.startwith)
2561 # finally, the remainder of the filter args in the request
2562 if self.classname and self.filterspec:
2563 cls = self.client.db.getclass(self.classname)
2564 for k,v in self.filterspec.items():
2565 if not args.has_key(k):
2566 if type(v) == type([]):
2567 prop = cls.get_transitive_prop(k)
2568 if k != 'id' and isinstance(prop, hyperdb.String):
2569 l.append('%s=%s'%(k, '%20'.join([q(i) for i in v])))
2570 else:
2571 l.append('%s=%s'%(k, ','.join([q(i) for i in v])))
2572 else:
2573 l.append('%s=%s'%(k, q(v)))
2574 return '%s?%s'%(url, '&'.join(l))
2575 indexargs_href = indexargs_url
2577 def base_javascript(self):
2578 return """
2579 <script type="text/javascript">
2580 submitted = false;
2581 function submit_once() {
2582 if (submitted) {
2583 alert("Your request is being processed.\\nPlease be patient.");
2584 event.returnValue = 0; // work-around for IE
2585 return 0;
2586 }
2587 submitted = true;
2588 return 1;
2589 }
2591 function help_window(helpurl, width, height) {
2592 HelpWin = window.open('%s' + helpurl, 'RoundupHelpWindow', 'scrollbars=yes,resizable=yes,toolbar=no,height='+height+',width='+width);
2593 }
2594 </script>
2595 """%self.base
2597 def batch(self):
2598 """ Return a batch object for results from the "current search"
2599 """
2600 filterspec = self.filterspec
2601 sort = self.sort
2602 group = self.group
2604 # get the list of ids we're batching over
2605 klass = self.client.db.getclass(self.classname)
2606 if self.search_text:
2607 matches = self.client.db.indexer.search(
2608 [w.upper().encode("utf-8", "replace") for w in re.findall(
2609 r'(?u)\b\w{2,25}\b',
2610 unicode(self.search_text, "utf-8", "replace")
2611 )], klass)
2612 else:
2613 matches = None
2615 # filter for visibility
2616 check = self._client.db.security.hasPermission
2617 userid = self._client.userid
2618 l = [id for id in klass.filter(matches, filterspec, sort, group)
2619 if check('View', userid, self.classname, itemid=id)]
2621 # return the batch object, using IDs only
2622 return Batch(self.client, l, self.pagesize, self.startwith,
2623 classname=self.classname)
2625 # extend the standard ZTUtils Batch object to remove dependency on
2626 # Acquisition and add a couple of useful methods
2627 class Batch(ZTUtils.Batch):
2628 """ Use me to turn a list of items, or item ids of a given class, into a
2629 series of batches.
2631 ========= ========================================================
2632 Parameter Usage
2633 ========= ========================================================
2634 sequence a list of HTMLItems or item ids
2635 classname if sequence is a list of ids, this is the class of item
2636 size how big to make the sequence.
2637 start where to start (0-indexed) in the sequence.
2638 end where to end (0-indexed) in the sequence.
2639 orphan if the next batch would contain less items than this
2640 value, then it is combined with this batch
2641 overlap the number of items shared between adjacent batches
2642 ========= ========================================================
2644 Attributes: Note that the "start" attribute, unlike the
2645 argument, is a 1-based index (I know, lame). "first" is the
2646 0-based index. "length" is the actual number of elements in
2647 the batch.
2649 "sequence_length" is the length of the original, unbatched, sequence.
2650 """
2651 def __init__(self, client, sequence, size, start, end=0, orphan=0,
2652 overlap=0, classname=None):
2653 self.client = client
2654 self.last_index = self.last_item = None
2655 self.current_item = None
2656 self.classname = classname
2657 self.sequence_length = len(sequence)
2658 ZTUtils.Batch.__init__(self, sequence, size, start, end, orphan,
2659 overlap)
2661 # overwrite so we can late-instantiate the HTMLItem instance
2662 def __getitem__(self, index):
2663 if index < 0:
2664 if index + self.end < self.first: raise IndexError, index
2665 return self._sequence[index + self.end]
2667 if index >= self.length:
2668 raise IndexError, index
2670 # move the last_item along - but only if the fetched index changes
2671 # (for some reason, index 0 is fetched twice)
2672 if index != self.last_index:
2673 self.last_item = self.current_item
2674 self.last_index = index
2676 item = self._sequence[index + self.first]
2677 if self.classname:
2678 # map the item ids to instances
2679 item = HTMLItem(self.client, self.classname, item)
2680 self.current_item = item
2681 return item
2683 def propchanged(self, *properties):
2684 """ Detect if one of the properties marked as being a group
2685 property changed in the last iteration fetch
2686 """
2687 # we poke directly at the _value here since MissingValue can screw
2688 # us up and cause Nones to compare strangely
2689 if self.last_item is None:
2690 return 1
2691 for property in properties:
2692 if property == 'id' or isinstance (self.last_item[property], list):
2693 if (str(self.last_item[property]) !=
2694 str(self.current_item[property])):
2695 return 1
2696 else:
2697 if (self.last_item[property]._value !=
2698 self.current_item[property]._value):
2699 return 1
2700 return 0
2702 # override these 'cos we don't have access to acquisition
2703 def previous(self):
2704 if self.start == 1:
2705 return None
2706 return Batch(self.client, self._sequence, self._size,
2707 self.first - self._size + self.overlap, 0, self.orphan,
2708 self.overlap)
2710 def next(self):
2711 try:
2712 self._sequence[self.end]
2713 except IndexError:
2714 return None
2715 return Batch(self.client, self._sequence, self._size,
2716 self.end - self.overlap, 0, self.orphan, self.overlap)
2718 class TemplatingUtils:
2719 """ Utilities for templating
2720 """
2721 def __init__(self, client):
2722 self.client = client
2723 def Batch(self, sequence, size, start, end=0, orphan=0, overlap=0):
2724 return Batch(self.client, sequence, size, start, end, orphan,
2725 overlap)
2727 def url_quote(self, url):
2728 """URL-quote the supplied text."""
2729 return urllib.quote(url)
2731 def html_quote(self, html):
2732 """HTML-quote the supplied text."""
2733 return cgi.escape(html)
2735 def __getattr__(self, name):
2736 """Try the tracker's templating_utils."""
2737 if not hasattr(self.client.instance, 'templating_utils'):
2738 # backwards-compatibility
2739 raise AttributeError, name
2740 if not self.client.instance.templating_utils.has_key(name):
2741 raise AttributeError, name
2742 return self.client.instance.templating_utils[name]
2744 def html_calendar(self, request):
2745 """Generate a HTML calendar.
2747 `request` the roundup.request object
2748 - @template : name of the template
2749 - form : name of the form to store back the date
2750 - property : name of the property of the form to store
2751 back the date
2752 - date : current date
2753 - display : when browsing, specifies year and month
2755 html will simply be a table.
2756 """
2757 date_str = request.form.getfirst("date", ".")
2758 display = request.form.getfirst("display", date_str)
2759 template = request.form.getfirst("@template", "calendar")
2760 form = request.form.getfirst("form")
2761 property = request.form.getfirst("property")
2762 curr_date = date.Date(date_str) # to highlight
2763 display = date.Date(display) # to show
2764 day = display.day
2766 # for navigation
2767 date_prev_month = display + date.Interval("-1m")
2768 date_next_month = display + date.Interval("+1m")
2769 date_prev_year = display + date.Interval("-1y")
2770 date_next_year = display + date.Interval("+1y")
2772 res = []
2774 base_link = "%s?@template=%s&property=%s&form=%s&date=%s" % \
2775 (request.classname, template, property, form, curr_date)
2777 # navigation
2778 # month
2779 res.append('<table class="calendar"><tr><td>')
2780 res.append(' <table width="100%" class="calendar_nav"><tr>')
2781 link = "&display=%s"%date_prev_month
2782 res.append(' <td><a href="%s&display=%s"><</a></td>'%(base_link,
2783 date_prev_month))
2784 res.append(' <td>%s</td>'%calendar.month_name[display.month])
2785 res.append(' <td><a href="%s&display=%s">></a></td>'%(base_link,
2786 date_next_month))
2787 # spacer
2788 res.append(' <td width="100%"></td>')
2789 # year
2790 res.append(' <td><a href="%s&display=%s"><</a></td>'%(base_link,
2791 date_prev_year))
2792 res.append(' <td>%s</td>'%display.year)
2793 res.append(' <td><a href="%s&display=%s">></a></td>'%(base_link,
2794 date_next_year))
2795 res.append(' </tr></table>')
2796 res.append(' </td></tr>')
2798 # the calendar
2799 res.append(' <tr><td><table class="calendar_display">')
2800 res.append(' <tr class="weekdays">')
2801 for day in calendar.weekheader(3).split():
2802 res.append(' <td>%s</td>'%day)
2803 res.append(' </tr>')
2804 for week in calendar.monthcalendar(display.year, display.month):
2805 res.append(' <tr>')
2806 for day in week:
2807 link = "javascript:form[field].value = '%d-%02d-%02d'; " \
2808 "window.close ();"%(display.year, display.month, day)
2809 if (day == curr_date.day and display.month == curr_date.month
2810 and display.year == curr_date.year):
2811 # highlight
2812 style = "today"
2813 else :
2814 style = ""
2815 if day:
2816 res.append(' <td class="%s"><a href="%s">%s</a></td>'%(
2817 style, link, day))
2818 else :
2819 res.append(' <td></td>')
2820 res.append(' </tr>')
2821 res.append('</table></td></tr></table>')
2822 return "\n".join(res)
2824 class MissingValue:
2825 def __init__(self, description, **kwargs):
2826 self.__description = description
2827 for key, value in kwargs.items():
2828 self.__dict__[key] = value
2830 def __call__(self, *args, **kwargs): return MissingValue(self.__description)
2831 def __getattr__(self, name):
2832 # This allows assignments which assume all intermediate steps are Null
2833 # objects if they don't exist yet.
2834 #
2835 # For example (with just 'client' defined):
2836 #
2837 # client.db.config.TRACKER_WEB = 'BASE/'
2838 self.__dict__[name] = MissingValue(self.__description)
2839 return getattr(self, name)
2841 def __getitem__(self, key): return self
2842 def __nonzero__(self): return 0
2843 def __str__(self): return '[%s]'%self.__description
2844 def __repr__(self): return '<MissingValue 0x%x "%s">'%(id(self),
2845 self.__description)
2846 def gettext(self, str): return str
2847 _ = gettext
2849 # vim: set et sts=4 sw=4 :