6f0584f10d76a69698cb49225f3c108e4f4e3dc6
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 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 # Add it to the cache. We cannot do this until the template
193 # is fully initialized, as we could otherwise have a race
194 # condition when running with multiple threads:
195 #
196 # 1. Thread A notices the template is not in the cache,
197 # adds it, but has not yet set "mtime".
198 #
199 # 2. Thread B notices the template is in the cache, checks
200 # "mtime" (above) and crashes.
201 #
202 # Since Python dictionary access is atomic, as long as we
203 # insert "pt" only after it is fully initialized, we avoid
204 # this race condition. It's possible that two separate
205 # threads will both do the work of initializing the template,
206 # but the risk of wasted work is offset by avoiding a lock.
207 self.templates[src] = pt
208 return pt
210 def __getitem__(self, name):
211 name, extension = os.path.splitext(name)
212 if extension:
213 extension = extension[1:]
214 try:
215 return self.get(name, extension)
216 except NoTemplate, message:
217 raise KeyError, message
219 def context(client, template=None, classname=None, request=None):
220 """Return the rendering context dictionary
222 The dictionary includes following symbols:
224 *context*
225 this is one of three things:
227 1. None - we're viewing a "home" page
228 2. The current class of item being displayed. This is an HTMLClass
229 instance.
230 3. The current item from the database, if we're viewing a specific
231 item, as an HTMLItem instance.
233 *request*
234 Includes information about the current request, including:
236 - the url
237 - the current index information (``filterspec``, ``filter`` args,
238 ``properties``, etc) parsed out of the form.
239 - methods for easy filterspec link generation
240 - *user*, the current user node as an HTMLItem instance
241 - *form*, the current CGI form information as a FieldStorage
243 *config*
244 The current tracker config.
246 *db*
247 The current database, used to access arbitrary database items.
249 *utils*
250 This is a special class that has its base in the TemplatingUtils
251 class in this file. If the tracker interfaces module defines a
252 TemplatingUtils class then it is mixed in, overriding the methods
253 in the base class.
255 *templates*
256 Access to all the tracker templates by name.
257 Used mainly in *use-macro* commands.
259 *template*
260 Current rendering template.
262 *true*
263 Logical True value.
265 *false*
266 Logical False value.
268 *i18n*
269 Internationalization service, providing string translation
270 methods ``gettext`` and ``ngettext``.
272 """
273 # construct the TemplatingUtils class
274 utils = TemplatingUtils
275 if (hasattr(client.instance, 'interfaces') and
276 hasattr(client.instance.interfaces, 'TemplatingUtils')):
277 class utils(client.instance.interfaces.TemplatingUtils, utils):
278 pass
280 # if template, classname and/or request are not passed explicitely,
281 # compute form client
282 if template is None:
283 template = client.template
284 if classname is None:
285 classname = client.classname
286 if request is None:
287 request = HTMLRequest(client)
289 c = {
290 'context': None,
291 'options': {},
292 'nothing': None,
293 'request': request,
294 'db': HTMLDatabase(client),
295 'config': client.instance.config,
296 'tracker': client.instance,
297 'utils': utils(client),
298 'templates': client.instance.templates,
299 'template': template,
300 'true': 1,
301 'false': 0,
302 'i18n': client.translator
303 }
304 # add in the item if there is one
305 if client.nodeid:
306 c['context'] = HTMLItem(client, classname, client.nodeid,
307 anonymous=1)
308 elif client.db.classes.has_key(classname):
309 c['context'] = HTMLClass(client, classname, anonymous=1)
310 return c
312 class RoundupPageTemplate(PageTemplate.PageTemplate):
313 """A Roundup-specific PageTemplate.
315 Interrogate the client to set up Roundup-specific template variables
316 to be available. See 'context' function for the list of variables.
318 """
320 # 06-jun-2004 [als] i am not sure if this method is used yet
321 def getContext(self, client, classname, request):
322 return context(client, self, classname, request)
324 def render(self, client, classname, request, **options):
325 """Render this Page Template"""
327 if not self._v_cooked:
328 self._cook()
330 __traceback_supplement__ = (PageTemplate.PageTemplateTracebackSupplement, self)
332 if self._v_errors:
333 raise PageTemplate.PTRuntimeError, \
334 'Page Template %s has errors.'%self.id
336 # figure the context
337 c = context(client, self, classname, request)
338 c.update({'options': options})
340 # and go
341 output = StringIO.StringIO()
342 TALInterpreter.TALInterpreter(self._v_program, self.macros,
343 getEngine().getContext(c), output, tal=1, strictinsert=0)()
344 return output.getvalue()
346 def __repr__(self):
347 return '<Roundup PageTemplate %r>'%self.id
349 class HTMLDatabase:
350 """ Return HTMLClasses for valid class fetches
351 """
352 def __init__(self, client):
353 self._client = client
354 self._ = client._
355 self._db = client.db
357 # we want config to be exposed
358 self.config = client.db.config
360 def __getitem__(self, item, desre=re.compile(r'(?P<cl>[a-zA-Z_]+)(?P<id>[-\d]+)')):
361 # check to see if we're actually accessing an item
362 m = desre.match(item)
363 if m:
364 cl = m.group('cl')
365 self._client.db.getclass(cl)
366 return HTMLItem(self._client, cl, m.group('id'))
367 else:
368 self._client.db.getclass(item)
369 return HTMLClass(self._client, item)
371 def __getattr__(self, attr):
372 try:
373 return self[attr]
374 except KeyError:
375 raise AttributeError, attr
377 def classes(self):
378 l = self._client.db.classes.keys()
379 l.sort()
380 m = []
381 for item in l:
382 m.append(HTMLClass(self._client, item))
383 return m
385 num_re = re.compile('^-?\d+$')
387 def lookupIds(db, prop, ids, fail_ok=0, num_re=num_re, do_lookup=True):
388 """ "fail_ok" should be specified if we wish to pass through bad values
389 (most likely form values that we wish to represent back to the user)
390 "do_lookup" is there for preventing lookup by key-value (if we
391 know that the value passed *is* an id)
392 """
393 cl = db.getclass(prop.classname)
394 l = []
395 for entry in ids:
396 if do_lookup:
397 try:
398 item = cl.lookup(entry)
399 except (TypeError, KeyError):
400 pass
401 else:
402 l.append(item)
403 continue
404 # if fail_ok, ignore lookup error
405 # otherwise entry must be existing object id rather than key value
406 if fail_ok or num_re.match(entry):
407 l.append(entry)
408 return l
410 def lookupKeys(linkcl, key, ids, num_re=num_re):
411 """ Look up the "key" values for "ids" list - though some may already
412 be key values, not ids.
413 """
414 l = []
415 for entry in ids:
416 if num_re.match(entry):
417 label = linkcl.get(entry, key)
418 # fall back to designator if label is None
419 if label is None: label = '%s%s'%(linkcl.classname, entry)
420 l.append(label)
421 else:
422 l.append(entry)
423 return l
425 def _set_input_default_args(dic):
426 # 'text' is the default value anyway --
427 # but for CSS usage it should be present
428 dic.setdefault('type', 'text')
429 # useful e.g for HTML LABELs:
430 if not dic.has_key('id'):
431 try:
432 if dic['text'] in ('radio', 'checkbox'):
433 dic['id'] = '%(name)s-%(value)s' % dic
434 else:
435 dic['id'] = dic['name']
436 except KeyError:
437 pass
439 def cgi_escape_attrs(**attrs):
440 return ' '.join(['%s="%s"'%(k,cgi.escape(str(v), True))
441 for k,v in attrs.items()])
443 def input_html4(**attrs):
444 """Generate an 'input' (html4) element with given attributes"""
445 _set_input_default_args(attrs)
446 return '<input %s>'%cgi_escape_attrs(**attrs)
448 def input_xhtml(**attrs):
449 """Generate an 'input' (xhtml) element with given attributes"""
450 _set_input_default_args(attrs)
451 return '<input %s/>'%cgi_escape_attrs(**attrs)
453 class HTMLInputMixin:
454 """ requires a _client property """
455 def __init__(self):
456 html_version = 'html4'
457 if hasattr(self._client.instance.config, 'HTML_VERSION'):
458 html_version = self._client.instance.config.HTML_VERSION
459 if html_version == 'xhtml':
460 self.input = input_xhtml
461 else:
462 self.input = input_html4
463 # self._context is used for translations.
464 # will be initialized by the first call to .gettext()
465 self._context = None
467 def gettext(self, msgid):
468 """Return the localized translation of msgid"""
469 if self._context is None:
470 self._context = context(self._client)
471 return self._client.translator.translate(domain="roundup",
472 msgid=msgid, context=self._context)
474 _ = gettext
476 class HTMLPermissions:
478 def view_check(self):
479 """ Raise the Unauthorised exception if the user's not permitted to
480 view this class.
481 """
482 if not self.is_view_ok():
483 raise Unauthorised("view", self._classname,
484 translator=self._client.translator)
486 def edit_check(self):
487 """ Raise the Unauthorised exception if the user's not permitted to
488 edit items of this class.
489 """
490 if not self.is_edit_ok():
491 raise Unauthorised("edit", self._classname,
492 translator=self._client.translator)
494 def retire_check(self):
495 """ Raise the Unauthorised exception if the user's not permitted to
496 retire items of this class.
497 """
498 if not self.is_retire_ok():
499 raise Unauthorised("retire", self._classname,
500 translator=self._client.translator)
503 class HTMLClass(HTMLInputMixin, HTMLPermissions):
504 """ Accesses through a class (either through *class* or *db.<classname>*)
505 """
506 def __init__(self, client, classname, anonymous=0):
507 self._client = client
508 self._ = client._
509 self._db = client.db
510 self._anonymous = anonymous
512 # we want classname to be exposed, but _classname gives a
513 # consistent API for extending Class/Item
514 self._classname = self.classname = classname
515 self._klass = self._db.getclass(self.classname)
516 self._props = self._klass.getprops()
518 HTMLInputMixin.__init__(self)
520 def is_edit_ok(self):
521 """ Is the user allowed to Create the current class?
522 """
523 return self._db.security.hasPermission('Create', self._client.userid,
524 self._classname)
526 def is_retire_ok(self):
527 """ Is the user allowed to retire items of the current class?
528 """
529 return self._db.security.hasPermission('Retire', self._client.userid,
530 self._classname)
532 def is_view_ok(self):
533 """ Is the user allowed to View the current class?
534 """
535 return self._db.security.hasPermission('View', self._client.userid,
536 self._classname)
538 def is_only_view_ok(self):
539 """ Is the user only allowed to View (ie. not Create) the current class?
540 """
541 return self.is_view_ok() and not self.is_edit_ok()
543 def __repr__(self):
544 return '<HTMLClass(0x%x) %s>'%(id(self), self.classname)
546 def __getitem__(self, item):
547 """ return an HTMLProperty instance
548 """
550 # we don't exist
551 if item == 'id':
552 return None
554 # get the property
555 try:
556 prop = self._props[item]
557 except KeyError:
558 raise KeyError, 'No such property "%s" on %s'%(item, self.classname)
560 # look up the correct HTMLProperty class
561 form = self._client.form
562 for klass, htmlklass in propclasses:
563 if not isinstance(prop, klass):
564 continue
565 if isinstance(prop, hyperdb.Multilink):
566 value = []
567 else:
568 value = None
569 return htmlklass(self._client, self._classname, None, prop, item,
570 value, self._anonymous)
572 # no good
573 raise KeyError, item
575 def __getattr__(self, attr):
576 """ convenience access """
577 try:
578 return self[attr]
579 except KeyError:
580 raise AttributeError, attr
582 def designator(self):
583 """ Return this class' designator (classname) """
584 return self._classname
586 def getItem(self, itemid, num_re=num_re):
587 """ Get an item of this class by its item id.
588 """
589 # make sure we're looking at an itemid
590 if not isinstance(itemid, type(1)) and not num_re.match(itemid):
591 itemid = self._klass.lookup(itemid)
593 return HTMLItem(self._client, self.classname, itemid)
595 def properties(self, sort=1):
596 """ Return HTMLProperty for all of this class' properties.
597 """
598 l = []
599 for name, prop in self._props.items():
600 for klass, htmlklass in propclasses:
601 if isinstance(prop, hyperdb.Multilink):
602 value = []
603 else:
604 value = None
605 if isinstance(prop, klass):
606 l.append(htmlklass(self._client, self._classname, '',
607 prop, name, value, self._anonymous))
608 if sort:
609 l.sort(lambda a,b:cmp(a._name, b._name))
610 return l
612 def list(self, sort_on=None):
613 """ List all items in this class.
614 """
615 # get the list and sort it nicely
616 l = self._klass.list()
617 sortfunc = make_sort_function(self._db, self._classname, sort_on)
618 l.sort(sortfunc)
620 # check perms
621 check = self._client.db.security.hasPermission
622 userid = self._client.userid
624 l = [HTMLItem(self._client, self._classname, id) for id in l
625 if check('View', userid, self._classname, itemid=id)]
627 return l
629 def csv(self):
630 """ Return the items of this class as a chunk of CSV text.
631 """
632 props = self.propnames()
633 s = StringIO.StringIO()
634 writer = csv.writer(s)
635 writer.writerow(props)
636 check = self._client.db.security.hasPermission
637 for nodeid in self._klass.list():
638 l = []
639 for name in props:
640 # check permission to view this property on this item
641 if not check('View', self._client.userid, itemid=nodeid,
642 classname=self._klass.classname, property=name):
643 raise Unauthorised('view', self._klass.classname,
644 translator=self._client.translator)
645 value = self._klass.get(nodeid, name)
646 if value is None:
647 l.append('')
648 elif isinstance(value, type([])):
649 l.append(':'.join(map(str, value)))
650 else:
651 l.append(str(self._klass.get(nodeid, name)))
652 writer.writerow(l)
653 return s.getvalue()
655 def propnames(self):
656 """ Return the list of the names of the properties of this class.
657 """
658 idlessprops = self._klass.getprops(protected=0).keys()
659 idlessprops.sort()
660 return ['id'] + idlessprops
662 def filter(self, request=None, filterspec={}, sort=[], group=[]):
663 """ Return a list of items from this class, filtered and sorted
664 by the current requested filterspec/filter/sort/group args
666 "request" takes precedence over the other three arguments.
667 """
668 if request is not None:
669 filterspec = request.filterspec
670 sort = request.sort
671 group = request.group
673 check = self._db.security.hasPermission
674 userid = self._client.userid
676 l = [HTMLItem(self._client, self.classname, id)
677 for id in self._klass.filter(None, filterspec, sort, group)
678 if check('View', userid, self.classname, itemid=id)]
679 return l
681 def classhelp(self, properties=None, label=''"(list)", width='500',
682 height='400', property='', form='itemSynopsis',
683 pagesize=50, inputtype="checkbox", sort=None, filter=None):
684 """Pop up a javascript window with class help
686 This generates a link to a popup window which displays the
687 properties indicated by "properties" of the class named by
688 "classname". The "properties" should be a comma-separated list
689 (eg. 'id,name,description'). Properties defaults to all the
690 properties of a class (excluding id, creator, created and
691 activity).
693 You may optionally override the label displayed, the width,
694 the height, the number of items per page and the field on which
695 the list is sorted (defaults to username if in the displayed
696 properties).
698 With the "filter" arg it is possible to specify a filter for
699 which items are supposed to be displayed. It has to be of
700 the format "<field>=<values>;<field>=<values>;...".
702 The popup window will be resizable and scrollable.
704 If the "property" arg is given, it's passed through to the
705 javascript help_window function.
707 You can use inputtype="radio" to display a radio box instead
708 of the default checkbox (useful for entering Link-properties)
710 If the "form" arg is given, it's passed through to the
711 javascript help_window function. - it's the name of the form
712 the "property" belongs to.
713 """
714 if properties is None:
715 properties = self._klass.getprops(protected=0).keys()
716 properties.sort()
717 properties = ','.join(properties)
718 if sort is None:
719 if 'username' in properties.split( ',' ):
720 sort = 'username'
721 else:
722 sort = self._klass.orderprop()
723 sort = '&@sort=' + sort
724 if property:
725 property = '&property=%s'%property
726 if form:
727 form = '&form=%s'%form
728 if inputtype:
729 type= '&type=%s'%inputtype
730 if filter:
731 filterprops = filter.split(';')
732 filtervalues = []
733 names = []
734 for x in filterprops:
735 (name, values) = x.split('=')
736 names.append(name)
737 filtervalues.append('&%s=%s' % (name, urllib.quote(values)))
738 filter = '&@filter=%s%s' % (','.join(names), ''.join(filtervalues))
739 else:
740 filter = ''
741 help_url = "%s?@startwith=0&@template=help&"\
742 "properties=%s%s%s%s%s&@pagesize=%s%s" % \
743 (self.classname, properties, property, form, type,
744 sort, pagesize, filter)
745 onclick = "javascript:help_window('%s', '%s', '%s');return false;" % \
746 (help_url, width, height)
747 return '<a class="classhelp" href="%s" onclick="%s">%s</a>' % \
748 (help_url, onclick, self._(label))
750 def submit(self, label=''"Submit New Entry", action="new"):
751 """ Generate a submit button (and action hidden element)
753 Generate nothing if we're not editable.
754 """
755 if not self.is_edit_ok():
756 return ''
758 return self.input(type="hidden", name="@action", value=action) + \
759 '\n' + \
760 self.input(type="submit", name="submit_button", value=self._(label))
762 def history(self):
763 if not self.is_view_ok():
764 return self._('[hidden]')
765 return self._('New node - no history')
767 def renderWith(self, name, **kwargs):
768 """ Render this class with the given template.
769 """
770 # create a new request and override the specified args
771 req = HTMLRequest(self._client)
772 req.classname = self.classname
773 req.update(kwargs)
775 # new template, using the specified classname and request
776 pt = self._client.instance.templates.get(self.classname, name)
778 # use our fabricated request
779 args = {
780 'ok_message': self._client.ok_message,
781 'error_message': self._client.error_message
782 }
783 return pt.render(self._client, self.classname, req, **args)
785 class _HTMLItem(HTMLInputMixin, HTMLPermissions):
786 """ Accesses through an *item*
787 """
788 def __init__(self, client, classname, nodeid, anonymous=0):
789 self._client = client
790 self._db = client.db
791 self._classname = classname
792 self._nodeid = nodeid
793 self._klass = self._db.getclass(classname)
794 self._props = self._klass.getprops()
796 # do we prefix the form items with the item's identification?
797 self._anonymous = anonymous
799 HTMLInputMixin.__init__(self)
801 def is_edit_ok(self):
802 """ Is the user allowed to Edit this item?
803 """
804 return self._db.security.hasPermission('Edit', self._client.userid,
805 self._classname, itemid=self._nodeid)
807 def is_retire_ok(self):
808 """ Is the user allowed to Reture this item?
809 """
810 return self._db.security.hasPermission('Retire', self._client.userid,
811 self._classname, itemid=self._nodeid)
813 def is_view_ok(self):
814 """ Is the user allowed to View this item?
815 """
816 if self._db.security.hasPermission('View', self._client.userid,
817 self._classname, itemid=self._nodeid):
818 return 1
819 return self.is_edit_ok()
821 def is_only_view_ok(self):
822 """ Is the user only allowed to View (ie. not Edit) this item?
823 """
824 return self.is_view_ok() and not self.is_edit_ok()
826 def __repr__(self):
827 return '<HTMLItem(0x%x) %s %s>'%(id(self), self._classname,
828 self._nodeid)
830 def __getitem__(self, item):
831 """ return an HTMLProperty instance
832 this now can handle transitive lookups where item is of the
833 form x.y.z
834 """
835 if item == 'id':
836 return self._nodeid
838 items = item.split('.', 1)
839 has_rest = len(items) > 1
841 # get the property
842 prop = self._props[items[0]]
844 if has_rest and not isinstance(prop, (hyperdb.Link, hyperdb.Multilink)):
845 raise KeyError, item
847 # get the value, handling missing values
848 value = None
849 if int(self._nodeid) > 0:
850 value = self._klass.get(self._nodeid, items[0], None)
851 if value is None:
852 if isinstance(prop, hyperdb.Multilink):
853 value = []
855 # look up the correct HTMLProperty class
856 htmlprop = None
857 for klass, htmlklass in propclasses:
858 if isinstance(prop, klass):
859 htmlprop = htmlklass(self._client, self._classname,
860 self._nodeid, prop, items[0], value, self._anonymous)
861 if htmlprop is not None:
862 if has_rest:
863 if isinstance(htmlprop, MultilinkHTMLProperty):
864 return [h[items[1]] for h in htmlprop]
865 return htmlprop[items[1]]
866 return htmlprop
868 raise KeyError, item
870 def __getattr__(self, attr):
871 """ convenience access to properties """
872 try:
873 return self[attr]
874 except KeyError:
875 raise AttributeError, attr
877 def designator(self):
878 """Return this item's designator (classname + id)."""
879 return '%s%s'%(self._classname, self._nodeid)
881 def is_retired(self):
882 """Is this item retired?"""
883 return self._klass.is_retired(self._nodeid)
885 def submit(self, label=''"Submit Changes", action="edit"):
886 """Generate a submit button.
888 Also sneak in the lastactivity and action hidden elements.
889 """
890 return self.input(type="hidden", name="@lastactivity",
891 value=self.activity.local(0)) + '\n' + \
892 self.input(type="hidden", name="@action", value=action) + '\n' + \
893 self.input(type="submit", name="submit_button", value=self._(label))
895 def journal(self, direction='descending'):
896 """ Return a list of HTMLJournalEntry instances.
897 """
898 # XXX do this
899 return []
901 def history(self, direction='descending', dre=re.compile('^\d+$'),
902 limit=None):
903 if not self.is_view_ok():
904 return self._('[hidden]')
906 # pre-load the history with the current state
907 current = {}
908 for prop_n in self._props.keys():
909 prop = self[prop_n]
910 if not isinstance(prop, HTMLProperty):
911 continue
912 current[prop_n] = prop.plain(escape=1)
913 # make link if hrefable
914 if (self._props.has_key(prop_n) and
915 isinstance(self._props[prop_n], hyperdb.Link)):
916 classname = self._props[prop_n].classname
917 try:
918 template = find_template(self._db.config.TEMPLATES,
919 classname, 'item')
920 if template[1].startswith('_generic'):
921 raise NoTemplate, 'not really...'
922 except NoTemplate:
923 pass
924 else:
925 id = self._klass.get(self._nodeid, prop_n, None)
926 current[prop_n] = '<a href="%s%s">%s</a>'%(
927 classname, id, current[prop_n])
929 # get the journal, sort and reverse
930 history = self._klass.history(self._nodeid)
931 history.sort()
932 history.reverse()
934 # restrict the volume
935 if limit:
936 history = history[:limit]
938 timezone = self._db.getUserTimezone()
939 l = []
940 comments = {}
941 for id, evt_date, user, action, args in history:
942 date_s = str(evt_date.local(timezone)).replace("."," ")
943 arg_s = ''
944 if action == 'link' and type(args) == type(()):
945 if len(args) == 3:
946 linkcl, linkid, key = args
947 arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
948 linkcl, linkid, key)
949 else:
950 arg_s = str(args)
952 elif action == 'unlink' and type(args) == type(()):
953 if len(args) == 3:
954 linkcl, linkid, key = args
955 arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
956 linkcl, linkid, key)
957 else:
958 arg_s = str(args)
960 elif type(args) == type({}):
961 cell = []
962 for k in args.keys():
963 # try to get the relevant property and treat it
964 # specially
965 try:
966 prop = self._props[k]
967 except KeyError:
968 prop = None
969 if prop is None:
970 # property no longer exists
971 comments['no_exist'] = self._(
972 "<em>The indicated property no longer exists</em>")
973 cell.append(self._('<em>%s: %s</em>\n')
974 % (self._(k), str(args[k])))
975 continue
977 if args[k] and (isinstance(prop, hyperdb.Multilink) or
978 isinstance(prop, hyperdb.Link)):
979 # figure what the link class is
980 classname = prop.classname
981 try:
982 linkcl = self._db.getclass(classname)
983 except KeyError:
984 labelprop = None
985 comments[classname] = self._(
986 "The linked class %(classname)s no longer exists"
987 ) % locals()
988 labelprop = linkcl.labelprop(1)
989 try:
990 template = find_template(self._db.config.TEMPLATES,
991 classname, 'item')
992 if template[1].startswith('_generic'):
993 raise NoTemplate, 'not really...'
994 hrefable = 1
995 except NoTemplate:
996 hrefable = 0
998 if isinstance(prop, hyperdb.Multilink) and args[k]:
999 ml = []
1000 for linkid in args[k]:
1001 if isinstance(linkid, type(())):
1002 sublabel = linkid[0] + ' '
1003 linkids = linkid[1]
1004 else:
1005 sublabel = ''
1006 linkids = [linkid]
1007 subml = []
1008 for linkid in linkids:
1009 label = classname + linkid
1010 # if we have a label property, try to use it
1011 # TODO: test for node existence even when
1012 # there's no labelprop!
1013 try:
1014 if labelprop is not None and \
1015 labelprop != 'id':
1016 label = linkcl.get(linkid, labelprop)
1017 label = cgi.escape(label)
1018 except IndexError:
1019 comments['no_link'] = self._(
1020 "<strike>The linked node"
1021 " no longer exists</strike>")
1022 subml.append('<strike>%s</strike>'%label)
1023 else:
1024 if hrefable:
1025 subml.append('<a href="%s%s">%s</a>'%(
1026 classname, linkid, label))
1027 elif label is None:
1028 subml.append('%s%s'%(classname,
1029 linkid))
1030 else:
1031 subml.append(label)
1032 ml.append(sublabel + ', '.join(subml))
1033 cell.append('%s:\n %s'%(self._(k), ', '.join(ml)))
1034 elif isinstance(prop, hyperdb.Link) and args[k]:
1035 label = classname + args[k]
1036 # if we have a label property, try to use it
1037 # TODO: test for node existence even when
1038 # there's no labelprop!
1039 if labelprop is not None and labelprop != 'id':
1040 try:
1041 label = cgi.escape(linkcl.get(args[k],
1042 labelprop))
1043 except IndexError:
1044 comments['no_link'] = self._(
1045 "<strike>The linked node"
1046 " no longer exists</strike>")
1047 cell.append(' <strike>%s</strike>,\n'%label)
1048 # "flag" this is done .... euwww
1049 label = None
1050 if label is not None:
1051 if hrefable:
1052 old = '<a href="%s%s">%s</a>'%(classname,
1053 args[k], label)
1054 else:
1055 old = label;
1056 cell.append('%s: %s' % (self._(k), old))
1057 if current.has_key(k):
1058 cell[-1] += ' -> %s'%current[k]
1059 current[k] = old
1061 elif isinstance(prop, hyperdb.Date) and args[k]:
1062 if args[k] is None:
1063 d = ''
1064 else:
1065 d = date.Date(args[k],
1066 translator=self._client).local(timezone)
1067 cell.append('%s: %s'%(self._(k), str(d)))
1068 if current.has_key(k):
1069 cell[-1] += ' -> %s' % current[k]
1070 current[k] = str(d)
1072 elif isinstance(prop, hyperdb.Interval) and args[k]:
1073 val = str(date.Interval(args[k],
1074 translator=self._client))
1075 cell.append('%s: %s'%(self._(k), val))
1076 if current.has_key(k):
1077 cell[-1] += ' -> %s'%current[k]
1078 current[k] = val
1080 elif isinstance(prop, hyperdb.String) and args[k]:
1081 val = cgi.escape(args[k])
1082 cell.append('%s: %s'%(self._(k), val))
1083 if current.has_key(k):
1084 cell[-1] += ' -> %s'%current[k]
1085 current[k] = val
1087 elif isinstance(prop, hyperdb.Boolean) and args[k] is not None:
1088 val = args[k] and ''"Yes" or ''"No"
1089 cell.append('%s: %s'%(self._(k), val))
1090 if current.has_key(k):
1091 cell[-1] += ' -> %s'%current[k]
1092 current[k] = val
1094 elif not args[k]:
1095 if current.has_key(k):
1096 cell.append('%s: %s'%(self._(k), current[k]))
1097 current[k] = '(no value)'
1098 else:
1099 cell.append(self._('%s: (no value)')%self._(k))
1101 else:
1102 cell.append('%s: %s'%(self._(k), str(args[k])))
1103 if current.has_key(k):
1104 cell[-1] += ' -> %s'%current[k]
1105 current[k] = str(args[k])
1107 arg_s = '<br />'.join(cell)
1108 else:
1109 # unkown event!!
1110 comments['unknown'] = self._(
1111 "<strong><em>This event is not handled"
1112 " by the history display!</em></strong>")
1113 arg_s = '<strong><em>' + str(args) + '</em></strong>'
1114 date_s = date_s.replace(' ', ' ')
1115 # if the user's an itemid, figure the username (older journals
1116 # have the username)
1117 if dre.match(user):
1118 user = self._db.user.get(user, 'username')
1119 l.append('<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>'%(
1120 date_s, user, self._(action), arg_s))
1121 if comments:
1122 l.append(self._(
1123 '<tr><td colspan=4><strong>Note:</strong></td></tr>'))
1124 for entry in comments.values():
1125 l.append('<tr><td colspan=4>%s</td></tr>'%entry)
1127 if direction == 'ascending':
1128 l.reverse()
1130 l[0:0] = ['<table class="history">'
1131 '<tr><th colspan="4" class="header">',
1132 self._('History'),
1133 '</th></tr><tr>',
1134 self._('<th>Date</th>'),
1135 self._('<th>User</th>'),
1136 self._('<th>Action</th>'),
1137 self._('<th>Args</th>'),
1138 '</tr>']
1139 l.append('</table>')
1140 return '\n'.join(l)
1142 def renderQueryForm(self):
1143 """ Render this item, which is a query, as a search form.
1144 """
1145 # create a new request and override the specified args
1146 req = HTMLRequest(self._client)
1147 req.classname = self._klass.get(self._nodeid, 'klass')
1148 name = self._klass.get(self._nodeid, 'name')
1149 req.updateFromURL(self._klass.get(self._nodeid, 'url') +
1150 '&@queryname=%s'%urllib.quote(name))
1152 # new template, using the specified classname and request
1153 pt = self._client.instance.templates.get(req.classname, 'search')
1154 # The context for a search page should be the class, not any
1155 # node.
1156 self._client.nodeid = None
1158 # use our fabricated request
1159 return pt.render(self._client, req.classname, req)
1161 def download_url(self):
1162 """ Assume that this item is a FileClass and that it has a name
1163 and content. Construct a URL for the download of the content.
1164 """
1165 name = self._klass.get(self._nodeid, 'name')
1166 url = '%s%s/%s'%(self._classname, self._nodeid, name)
1167 return urllib.quote(url)
1169 def copy_url(self, exclude=("messages", "files")):
1170 """Construct a URL for creating a copy of this item
1172 "exclude" is an optional list of properties that should
1173 not be copied to the new object. By default, this list
1174 includes "messages" and "files" properties. Note that
1175 "id" property cannot be copied.
1177 """
1178 exclude = ("id", "activity", "actor", "creation", "creator") \
1179 + tuple(exclude)
1180 query = {
1181 "@template": "item",
1182 "@note": self._("Copy of %(class)s %(id)s") % {
1183 "class": self._(self._classname), "id": self._nodeid},
1184 }
1185 for name in self._props.keys():
1186 if name not in exclude:
1187 query[name] = self[name].plain()
1188 return self._classname + "?" + "&".join(
1189 ["%s=%s" % (key, urllib.quote(value))
1190 for key, value in query.items()])
1192 class _HTMLUser(_HTMLItem):
1193 """Add ability to check for permissions on users.
1194 """
1195 _marker = []
1196 def hasPermission(self, permission, classname=_marker,
1197 property=None, itemid=None):
1198 """Determine if the user has the Permission.
1200 The class being tested defaults to the template's class, but may
1201 be overidden for this test by suppling an alternate classname.
1202 """
1203 if classname is self._marker:
1204 classname = self._client.classname
1205 return self._db.security.hasPermission(permission,
1206 self._nodeid, classname, property, itemid)
1208 def hasRole(self, rolename):
1209 """Determine whether the user has the Role."""
1210 roles = self._db.user.get(self._nodeid, 'roles').split(',')
1211 for role in roles:
1212 if role.strip() == rolename: return True
1213 return False
1215 def HTMLItem(client, classname, nodeid, anonymous=0):
1216 if classname == 'user':
1217 return _HTMLUser(client, classname, nodeid, anonymous)
1218 else:
1219 return _HTMLItem(client, classname, nodeid, anonymous)
1221 class HTMLProperty(HTMLInputMixin, HTMLPermissions):
1222 """ String, Number, Date, Interval HTMLProperty
1224 Has useful attributes:
1226 _name the name of the property
1227 _value the value of the property if any
1229 A wrapper object which may be stringified for the plain() behaviour.
1230 """
1231 def __init__(self, client, classname, nodeid, prop, name, value,
1232 anonymous=0):
1233 self._client = client
1234 self._db = client.db
1235 self._ = client._
1236 self._classname = classname
1237 self._nodeid = nodeid
1238 self._prop = prop
1239 self._value = value
1240 self._anonymous = anonymous
1241 self._name = name
1242 if not anonymous:
1243 if nodeid:
1244 self._formname = '%s%s@%s'%(classname, nodeid, name)
1245 else:
1246 # This case occurs when creating a property for a
1247 # non-anonymous class.
1248 self._formname = '%s@%s'%(classname, name)
1249 else:
1250 self._formname = name
1252 # If no value is already present for this property, see if one
1253 # is specified in the current form.
1254 form = self._client.form
1255 if not self._value and form.has_key(self._formname):
1256 if isinstance(prop, hyperdb.Multilink):
1257 value = lookupIds(self._db, prop,
1258 handleListCGIValue(form[self._formname]),
1259 fail_ok=1)
1260 elif isinstance(prop, hyperdb.Link):
1261 value = form.getfirst(self._formname).strip()
1262 if value:
1263 value = lookupIds(self._db, prop, [value],
1264 fail_ok=1)[0]
1265 else:
1266 value = None
1267 else:
1268 value = form.getfirst(self._formname).strip() or None
1269 self._value = value
1271 HTMLInputMixin.__init__(self)
1273 def __repr__(self):
1274 return '<HTMLProperty(0x%x) %s %r %r>'%(id(self), self._formname,
1275 self._prop, self._value)
1276 def __str__(self):
1277 return self.plain()
1278 def __cmp__(self, other):
1279 if isinstance(other, HTMLProperty):
1280 return cmp(self._value, other._value)
1281 return cmp(self._value, other)
1283 def __nonzero__(self):
1284 return not not self._value
1286 def isset(self):
1287 """Is my _value not None?"""
1288 return self._value is not None
1290 def is_edit_ok(self):
1291 """Should the user be allowed to use an edit form field for this
1292 property. Check "Create" for new items, or "Edit" for existing
1293 ones.
1294 """
1295 if self._nodeid:
1296 return self._db.security.hasPermission('Edit', self._client.userid,
1297 self._classname, self._name, self._nodeid)
1298 return self._db.security.hasPermission('Create', self._client.userid,
1299 self._classname, self._name) or \
1300 self._db.security.hasPermission('Register', self._client.userid,
1301 self._classname, self._name)
1303 def is_view_ok(self):
1304 """ Is the user allowed to View the current class?
1305 """
1306 if self._db.security.hasPermission('View', self._client.userid,
1307 self._classname, self._name, self._nodeid):
1308 return 1
1309 return self.is_edit_ok()
1311 class StringHTMLProperty(HTMLProperty):
1312 hyper_re = re.compile(r'''(
1313 (?P<url>
1314 (
1315 (ht|f)tp(s?):// # protocol
1316 ([\w]+(:\w+)?@)? # username/password
1317 ([\w\-]+) # hostname
1318 ((\.[\w-]+)+)? # .domain.etc
1319 | # ... or ...
1320 ([\w]+(:\w+)?@)? # username/password
1321 www\. # "www."
1322 ([\w\-]+\.)+ # hostname
1323 [\w]{2,5} # TLD
1324 )
1325 (:[\d]{1,5})? # port
1326 (/[\w\-$.+!*(),;:@&=?/~\\#%]*)? # path etc.
1327 )|
1328 (?P<email>[-+=%/\w\.]+@[\w\.\-]+)|
1329 (?P<item>(?P<class>[A-Za-z_]+)(\s*)(?P<id>\d+))
1330 )''', re.X | re.I)
1331 protocol_re = re.compile('^(ht|f)tp(s?)://', re.I)
1333 def _hyper_repl_item(self,match,replacement):
1334 item = match.group('item')
1335 cls = match.group('class').lower()
1336 id = match.group('id')
1337 try:
1338 # make sure cls is a valid tracker classname
1339 cl = self._db.getclass(cls)
1340 if not cl.hasnode(id):
1341 return item
1342 return replacement % locals()
1343 except KeyError:
1344 return item
1346 def _hyper_repl(self, match):
1347 if match.group('url'):
1348 u = s = match.group('url')
1349 if not self.protocol_re.search(s):
1350 u = 'http://' + s
1351 # catch an escaped ">" at the end of the URL
1352 if s.endswith('>'):
1353 u = s = s[:-4]
1354 e = '>'
1355 else:
1356 e = ''
1357 return '<a href="%s">%s</a>%s'%(u, s, e)
1358 elif match.group('email'):
1359 s = match.group('email')
1360 return '<a href="mailto:%s">%s</a>'%(s, s)
1361 elif len(match.group('id')) < 10:
1362 return self._hyper_repl_item(match,
1363 '<a href="%(cls)s%(id)s">%(item)s</a>')
1364 else:
1365 # just return the matched text
1366 return match.group(0)
1368 def _hyper_repl_rst(self, match):
1369 if match.group('url'):
1370 s = match.group('url')
1371 return '`%s <%s>`_'%(s, s)
1372 elif match.group('email'):
1373 s = match.group('email')
1374 return '`%s <mailto:%s>`_'%(s, s)
1375 elif len(match.group('id')) < 10:
1376 return self._hyper_repl_item(match,'`%(item)s <%(cls)s%(id)s>`_')
1377 else:
1378 # just return the matched text
1379 return match.group(0)
1381 def hyperlinked(self):
1382 """ Render a "hyperlinked" version of the text """
1383 return self.plain(hyperlink=1)
1385 def plain(self, escape=0, hyperlink=0):
1386 """Render a "plain" representation of the property
1388 - "escape" turns on/off HTML quoting
1389 - "hyperlink" turns on/off in-text hyperlinking of URLs, email
1390 addresses and designators
1391 """
1392 if not self.is_view_ok():
1393 return self._('[hidden]')
1395 if self._value is None:
1396 return ''
1397 if escape:
1398 s = cgi.escape(str(self._value))
1399 else:
1400 s = str(self._value)
1401 if hyperlink:
1402 # no, we *must* escape this text
1403 if not escape:
1404 s = cgi.escape(s)
1405 s = self.hyper_re.sub(self._hyper_repl, s)
1406 return s
1408 def wrapped(self, escape=1, hyperlink=1):
1409 """Render a "wrapped" representation of the property.
1411 We wrap long lines at 80 columns on the nearest whitespace. Lines
1412 with no whitespace are not broken to force wrapping.
1414 Note that unlike plain() we default wrapped() to have the escaping
1415 and hyperlinking turned on since that's the most common usage.
1417 - "escape" turns on/off HTML quoting
1418 - "hyperlink" turns on/off in-text hyperlinking of URLs, email
1419 addresses and designators
1420 """
1421 if not self.is_view_ok():
1422 return self._('[hidden]')
1424 if self._value is None:
1425 return ''
1426 s = support.wrap(str(self._value), width=80)
1427 if escape:
1428 s = cgi.escape(s)
1429 if hyperlink:
1430 # no, we *must* escape this text
1431 if not escape:
1432 s = cgi.escape(s)
1433 s = self.hyper_re.sub(self._hyper_repl, s)
1434 return s
1436 def stext(self, escape=0, hyperlink=1):
1437 """ Render the value of the property as StructuredText.
1439 This requires the StructureText module to be installed separately.
1440 """
1441 if not self.is_view_ok():
1442 return self._('[hidden]')
1444 s = self.plain(escape=escape, hyperlink=hyperlink)
1445 if not StructuredText:
1446 return s
1447 return StructuredText(s,level=1,header=0)
1449 def rst(self, hyperlink=1):
1450 """ Render the value of the property as ReStructuredText.
1452 This requires docutils to be installed separately.
1453 """
1454 if not self.is_view_ok():
1455 return self._('[hidden]')
1457 if not ReStructuredText:
1458 return self.plain(escape=0, hyperlink=hyperlink)
1459 s = self.plain(escape=0, hyperlink=0)
1460 if hyperlink:
1461 s = self.hyper_re.sub(self._hyper_repl_rst, s)
1462 return ReStructuredText(s, writer_name="html")["html_body"].encode("utf-8",
1463 "replace")
1465 def field(self, **kwargs):
1466 """ Render the property as a field in HTML.
1468 If not editable, just display the value via plain().
1469 """
1470 if not self.is_edit_ok():
1471 return self.plain(escape=1)
1473 value = self._value
1474 if value is None:
1475 value = ''
1477 kwargs.setdefault("size", 30)
1478 kwargs.update({"name": self._formname, "value": value})
1479 return self.input(**kwargs)
1481 def multiline(self, escape=0, rows=5, cols=40, **kwargs):
1482 """ Render a multiline form edit field for the property.
1484 If not editable, just display the plain() value in a <pre> tag.
1485 """
1486 if not self.is_edit_ok():
1487 return '<pre>%s</pre>'%self.plain()
1489 if self._value is None:
1490 value = ''
1491 else:
1492 value = cgi.escape(str(self._value))
1494 value = '"'.join(value.split('"'))
1495 name = self._formname
1496 passthrough_args = cgi_escape_attrs(**kwargs)
1497 return ('<textarea %(passthrough_args)s name="%(name)s" id="%(name)s"'
1498 ' rows="%(rows)s" cols="%(cols)s">'
1499 '%(value)s</textarea>') % locals()
1501 def email(self, escape=1):
1502 """ Render the value of the property as an obscured email address
1503 """
1504 if not self.is_view_ok():
1505 return self._('[hidden]')
1507 if self._value is None:
1508 value = ''
1509 else:
1510 value = str(self._value)
1511 split = value.split('@')
1512 if len(split) == 2:
1513 name, domain = split
1514 domain = ' '.join(domain.split('.')[:-1])
1515 name = name.replace('.', ' ')
1516 value = '%s at %s ...'%(name, domain)
1517 else:
1518 value = value.replace('.', ' ')
1519 if escape:
1520 value = cgi.escape(value)
1521 return value
1523 class PasswordHTMLProperty(HTMLProperty):
1524 def plain(self, escape=0):
1525 """ Render a "plain" representation of the property
1526 """
1527 if not self.is_view_ok():
1528 return self._('[hidden]')
1530 if self._value is None:
1531 return ''
1532 return self._('*encrypted*')
1534 def field(self, size=30, **kwargs):
1535 """ Render a form edit field for the property.
1537 If not editable, just display the value via plain().
1538 """
1539 if not self.is_edit_ok():
1540 return self.plain(escape=1)
1542 return self.input(type="password", name=self._formname, size=size,
1543 **kwargs)
1545 def confirm(self, size=30):
1546 """ Render a second form edit field for the property, used for
1547 confirmation that the user typed the password correctly. Generates
1548 a field with name "@confirm@name".
1550 If not editable, display nothing.
1551 """
1552 if not self.is_edit_ok():
1553 return ''
1555 return self.input(type="password",
1556 name="@confirm@%s"%self._formname,
1557 id="%s-confirm"%self._formname,
1558 size=size)
1560 class NumberHTMLProperty(HTMLProperty):
1561 def plain(self, escape=0):
1562 """ Render a "plain" representation of the property
1563 """
1564 if not self.is_view_ok():
1565 return self._('[hidden]')
1567 if self._value is None:
1568 return ''
1570 return str(self._value)
1572 def field(self, size=30, **kwargs):
1573 """ Render a form edit field for the property.
1575 If not editable, just display the value via plain().
1576 """
1577 if not self.is_edit_ok():
1578 return self.plain(escape=1)
1580 value = self._value
1581 if value is None:
1582 value = ''
1584 return self.input(name=self._formname, value=value, size=size,
1585 **kwargs)
1587 def __int__(self):
1588 """ Return an int of me
1589 """
1590 return int(self._value)
1592 def __float__(self):
1593 """ Return a float of me
1594 """
1595 return float(self._value)
1598 class BooleanHTMLProperty(HTMLProperty):
1599 def plain(self, escape=0):
1600 """ Render a "plain" representation of the property
1601 """
1602 if not self.is_view_ok():
1603 return self._('[hidden]')
1605 if self._value is None:
1606 return ''
1607 return self._value and self._("Yes") or self._("No")
1609 def field(self, **kwargs):
1610 """ Render a form edit field for the property
1612 If not editable, just display the value via plain().
1613 """
1614 if not self.is_edit_ok():
1615 return self.plain(escape=1)
1617 value = self._value
1618 if isinstance(value, str) or isinstance(value, unicode):
1619 value = value.strip().lower() in ('checked', 'yes', 'true',
1620 'on', '1')
1622 checked = value and "checked" or ""
1623 if value:
1624 s = self.input(type="radio", name=self._formname, value="yes",
1625 checked="checked", **kwargs)
1626 s += self._('Yes')
1627 s +=self.input(type="radio", name=self._formname, value="no",
1628 **kwargs)
1629 s += self._('No')
1630 else:
1631 s = self.input(type="radio", name=self._formname, value="yes",
1632 **kwargs)
1633 s += self._('Yes')
1634 s +=self.input(type="radio", name=self._formname, value="no",
1635 checked="checked", **kwargs)
1636 s += self._('No')
1637 return s
1639 class DateHTMLProperty(HTMLProperty):
1641 _marker = []
1643 def __init__(self, client, classname, nodeid, prop, name, value,
1644 anonymous=0, offset=None):
1645 HTMLProperty.__init__(self, client, classname, nodeid, prop, name,
1646 value, anonymous=anonymous)
1647 if self._value and not (isinstance(self._value, str) or
1648 isinstance(self._value, unicode)):
1649 self._value.setTranslator(self._client.translator)
1650 self._offset = offset
1651 if self._offset is None :
1652 self._offset = self._prop.offset (self._db)
1654 def plain(self, escape=0):
1655 """ Render a "plain" representation of the property
1656 """
1657 if not self.is_view_ok():
1658 return self._('[hidden]')
1660 if self._value is None:
1661 return ''
1662 if self._offset is None:
1663 offset = self._db.getUserTimezone()
1664 else:
1665 offset = self._offset
1666 return str(self._value.local(offset))
1668 def now(self, str_interval=None):
1669 """ Return the current time.
1671 This is useful for defaulting a new value. Returns a
1672 DateHTMLProperty.
1673 """
1674 if not self.is_view_ok():
1675 return self._('[hidden]')
1677 ret = date.Date('.', translator=self._client)
1679 if isinstance(str_interval, basestring):
1680 sign = 1
1681 if str_interval[0] == '-':
1682 sign = -1
1683 str_interval = str_interval[1:]
1684 interval = date.Interval(str_interval, translator=self._client)
1685 if sign > 0:
1686 ret = ret + interval
1687 else:
1688 ret = ret - interval
1690 return DateHTMLProperty(self._client, self._classname, self._nodeid,
1691 self._prop, self._formname, ret)
1693 def field(self, size=30, default=None, format=_marker, popcal=True,
1694 **kwargs):
1695 """Render a form edit field for the property
1697 If not editable, just display the value via plain().
1699 If "popcal" then include the Javascript calendar editor.
1700 Default=yes.
1702 The format string is a standard python strftime format string.
1703 """
1704 if not self.is_edit_ok():
1705 if format is self._marker:
1706 return self.plain(escape=1)
1707 else:
1708 return self.pretty(format)
1710 value = self._value
1712 if value is None:
1713 if default is None:
1714 raw_value = None
1715 else:
1716 if isinstance(default, basestring):
1717 raw_value = date.Date(default, translator=self._client)
1718 elif isinstance(default, date.Date):
1719 raw_value = default
1720 elif isinstance(default, DateHTMLProperty):
1721 raw_value = default._value
1722 else:
1723 raise ValueError, self._('default value for '
1724 'DateHTMLProperty must be either DateHTMLProperty '
1725 'or string date representation.')
1726 elif isinstance(value, str) or isinstance(value, unicode):
1727 # most likely erroneous input to be passed back to user
1728 if isinstance(value, unicode): value = value.encode('utf8')
1729 return self.input(name=self._formname, value=value, size=size,
1730 **kwargs)
1731 else:
1732 raw_value = value
1734 if raw_value is None:
1735 value = ''
1736 elif isinstance(raw_value, str) or isinstance(raw_value, unicode):
1737 if format is self._marker:
1738 value = raw_value
1739 else:
1740 value = date.Date(raw_value).pretty(format)
1741 else:
1742 if self._offset is None :
1743 offset = self._db.getUserTimezone()
1744 else :
1745 offset = self._offset
1746 value = raw_value.local(offset)
1747 if format is not self._marker:
1748 value = value.pretty(format)
1750 s = self.input(name=self._formname, value=value, size=size,
1751 **kwargs)
1752 if popcal:
1753 s += self.popcal()
1754 return s
1756 def reldate(self, pretty=1):
1757 """ Render the interval between the date and now.
1759 If the "pretty" flag is true, then make the display pretty.
1760 """
1761 if not self.is_view_ok():
1762 return self._('[hidden]')
1764 if not self._value:
1765 return ''
1767 # figure the interval
1768 interval = self._value - date.Date('.', translator=self._client)
1769 if pretty:
1770 return interval.pretty()
1771 return str(interval)
1773 def pretty(self, format=_marker):
1774 """ Render the date in a pretty format (eg. month names, spaces).
1776 The format string is a standard python strftime format string.
1777 Note that if the day is zero, and appears at the start of the
1778 string, then it'll be stripped from the output. This is handy
1779 for the situation when a date only specifies a month and a year.
1780 """
1781 if not self.is_view_ok():
1782 return self._('[hidden]')
1784 if self._offset is None:
1785 offset = self._db.getUserTimezone()
1786 else:
1787 offset = self._offset
1789 if not self._value:
1790 return ''
1791 elif format is not self._marker:
1792 return self._value.local(offset).pretty(format)
1793 else:
1794 return self._value.local(offset).pretty()
1796 def local(self, offset):
1797 """ Return the date/time as a local (timezone offset) date/time.
1798 """
1799 if not self.is_view_ok():
1800 return self._('[hidden]')
1802 return DateHTMLProperty(self._client, self._classname, self._nodeid,
1803 self._prop, self._formname, self._value, offset=offset)
1805 def popcal(self, width=300, height=200, label="(cal)",
1806 form="itemSynopsis"):
1807 """Generate a link to a calendar pop-up window.
1809 item: HTMLProperty e.g.: context.deadline
1810 """
1811 if self.isset():
1812 date = "&date=%s"%self._value
1813 else :
1814 date = ""
1815 return ('<a class="classhelp" href="javascript:help_window('
1816 "'%s?@template=calendar&property=%s&form=%s%s', %d, %d)"
1817 '">%s</a>'%(self._classname, self._name, form, date, width,
1818 height, label))
1820 class IntervalHTMLProperty(HTMLProperty):
1821 def __init__(self, client, classname, nodeid, prop, name, value,
1822 anonymous=0):
1823 HTMLProperty.__init__(self, client, classname, nodeid, prop,
1824 name, value, anonymous)
1825 if self._value and not isinstance(self._value, (str, unicode)):
1826 self._value.setTranslator(self._client.translator)
1828 def plain(self, escape=0):
1829 """ Render a "plain" representation of the property
1830 """
1831 if not self.is_view_ok():
1832 return self._('[hidden]')
1834 if self._value is None:
1835 return ''
1836 return str(self._value)
1838 def pretty(self):
1839 """ Render the interval in a pretty format (eg. "yesterday")
1840 """
1841 if not self.is_view_ok():
1842 return self._('[hidden]')
1844 return self._value.pretty()
1846 def field(self, size=30, **kwargs):
1847 """ Render a form edit field for the property
1849 If not editable, just display the value via plain().
1850 """
1851 if not self.is_edit_ok():
1852 return self.plain(escape=1)
1854 value = self._value
1855 if value is None:
1856 value = ''
1858 return self.input(name=self._formname, value=value, size=size,
1859 **kwargs)
1861 class LinkHTMLProperty(HTMLProperty):
1862 """ Link HTMLProperty
1863 Include the above as well as being able to access the class
1864 information. Stringifying the object itself results in the value
1865 from the item being displayed. Accessing attributes of this object
1866 result in the appropriate entry from the class being queried for the
1867 property accessed (so item/assignedto/name would look up the user
1868 entry identified by the assignedto property on item, and then the
1869 name property of that user)
1870 """
1871 def __init__(self, *args, **kw):
1872 HTMLProperty.__init__(self, *args, **kw)
1873 # if we're representing a form value, then the -1 from the form really
1874 # should be a None
1875 if str(self._value) == '-1':
1876 self._value = None
1878 def __getattr__(self, attr):
1879 """ return a new HTMLItem """
1880 if not self._value:
1881 # handle a special page templates lookup
1882 if attr == '__render_with_namespace__':
1883 def nothing(*args, **kw):
1884 return ''
1885 return nothing
1886 msg = self._('Attempt to look up %(attr)s on a missing value')
1887 return MissingValue(msg%locals())
1888 i = HTMLItem(self._client, self._prop.classname, self._value)
1889 return getattr(i, attr)
1891 def plain(self, escape=0):
1892 """ Render a "plain" representation of the property
1893 """
1894 if not self.is_view_ok():
1895 return self._('[hidden]')
1897 if self._value is None:
1898 return ''
1899 linkcl = self._db.classes[self._prop.classname]
1900 k = linkcl.labelprop(1)
1901 if num_re.match(self._value):
1902 try:
1903 value = str(linkcl.get(self._value, k))
1904 except IndexError:
1905 value = self._value
1906 else :
1907 value = self._value
1908 if escape:
1909 value = cgi.escape(value)
1910 return value
1912 def field(self, showid=0, size=None, **kwargs):
1913 """ Render a form edit field for the property
1915 If not editable, just display the value via plain().
1916 """
1917 if not self.is_edit_ok():
1918 return self.plain(escape=1)
1920 # edit field
1921 linkcl = self._db.getclass(self._prop.classname)
1922 if self._value is None:
1923 value = ''
1924 else:
1925 k = linkcl.getkey()
1926 if k and num_re.match(self._value):
1927 value = linkcl.get(self._value, k)
1928 else:
1929 value = self._value
1930 return self.input(name=self._formname, value=value, size=size,
1931 **kwargs)
1933 def menu(self, size=None, height=None, showid=0, additional=[], value=None,
1934 sort_on=None, html_kwargs = {}, **conditions):
1935 """ Render a form select list for this property
1937 "size" is used to limit the length of the list labels
1938 "height" is used to set the <select> tag's "size" attribute
1939 "showid" includes the item ids in the list labels
1940 "value" specifies which item is pre-selected
1941 "additional" lists properties which should be included in the
1942 label
1943 "sort_on" indicates the property to sort the list on as
1944 (direction, property) where direction is '+' or '-'. A
1945 single string with the direction prepended may be used.
1946 For example: ('-', 'order'), '+name'.
1948 The remaining keyword arguments are used as conditions for
1949 filtering the items in the list - they're passed as the
1950 "filterspec" argument to a Class.filter() call.
1952 If not editable, just display the value via plain().
1953 """
1954 if not self.is_edit_ok():
1955 return self.plain(escape=1)
1957 # Since None indicates the default, we need another way to
1958 # indicate "no selection". We use -1 for this purpose, as
1959 # that is the value we use when submitting a form without the
1960 # value set.
1961 if value is None:
1962 value = self._value
1963 elif value == '-1':
1964 value = None
1966 linkcl = self._db.getclass(self._prop.classname)
1967 l = ['<select %s>'%cgi_escape_attrs(name = self._formname,
1968 **html_kwargs)]
1969 k = linkcl.labelprop(1)
1970 s = ''
1971 if value is None:
1972 s = 'selected="selected" '
1973 l.append(self._('<option %svalue="-1">- no selection -</option>')%s)
1975 if sort_on is not None:
1976 if not isinstance(sort_on, tuple):
1977 if sort_on[0] in '+-':
1978 sort_on = (sort_on[0], sort_on[1:])
1979 else:
1980 sort_on = ('+', sort_on)
1981 else:
1982 sort_on = ('+', linkcl.orderprop())
1984 options = [opt
1985 for opt in linkcl.filter(None, conditions, sort_on, (None, None))
1986 if self._db.security.hasPermission("View", self._client.userid,
1987 linkcl.classname, itemid=opt)]
1989 # make sure we list the current value if it's retired
1990 if value and value not in options:
1991 options.insert(0, value)
1993 if additional:
1994 additional_fns = []
1995 props = linkcl.getprops()
1996 for propname in additional:
1997 prop = props[propname]
1998 if isinstance(prop, hyperdb.Link):
1999 cl = self._db.getclass(prop.classname)
2000 labelprop = cl.labelprop()
2001 fn = lambda optionid: cl.get(linkcl.get(optionid,
2002 propname),
2003 labelprop)
2004 else:
2005 fn = lambda optionid: linkcl.get(optionid, propname)
2006 additional_fns.append(fn)
2008 for optionid in options:
2009 # get the option value, and if it's None use an empty string
2010 option = linkcl.get(optionid, k) or ''
2012 # figure if this option is selected
2013 s = ''
2014 if value in [optionid, option]:
2015 s = 'selected="selected" '
2017 # figure the label
2018 if showid:
2019 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
2020 elif not option:
2021 lab = '%s%s'%(self._prop.classname, optionid)
2022 else:
2023 lab = option
2025 # truncate if it's too long
2026 if size is not None and len(lab) > size:
2027 lab = lab[:size-3] + '...'
2028 if additional:
2029 m = []
2030 for fn in additional_fns:
2031 m.append(str(fn(optionid)))
2032 lab = lab + ' (%s)'%', '.join(m)
2034 # and generate
2035 lab = cgi.escape(self._(lab))
2036 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
2037 l.append('</select>')
2038 return '\n'.join(l)
2039 # def checklist(self, ...)
2043 class MultilinkHTMLProperty(HTMLProperty):
2044 """ Multilink HTMLProperty
2046 Also be iterable, returning a wrapper object like the Link case for
2047 each entry in the multilink.
2048 """
2049 def __init__(self, *args, **kwargs):
2050 HTMLProperty.__init__(self, *args, **kwargs)
2051 if self._value:
2052 display_value = lookupIds(self._db, self._prop, self._value,
2053 fail_ok=1, do_lookup=False)
2054 sortfun = make_sort_function(self._db, self._prop.classname)
2055 # sorting fails if the value contains
2056 # items not yet stored in the database
2057 # ignore these errors to preserve user input
2058 try:
2059 display_value.sort(sortfun)
2060 except:
2061 pass
2062 self._value = display_value
2064 def __len__(self):
2065 """ length of the multilink """
2066 return len(self._value)
2068 def __getattr__(self, attr):
2069 """ no extended attribute accesses make sense here """
2070 raise AttributeError, attr
2072 def viewableGenerator(self, values):
2073 """Used to iterate over only the View'able items in a class."""
2074 check = self._db.security.hasPermission
2075 userid = self._client.userid
2076 classname = self._prop.classname
2077 for value in values:
2078 if check('View', userid, classname, itemid=value):
2079 yield HTMLItem(self._client, classname, value)
2081 def __iter__(self):
2082 """ iterate and return a new HTMLItem
2083 """
2084 return self.viewableGenerator(self._value)
2086 def reverse(self):
2087 """ return the list in reverse order
2088 """
2089 l = self._value[:]
2090 l.reverse()
2091 return self.viewableGenerator(l)
2093 def sorted(self, property):
2094 """ Return this multilink sorted by the given property """
2095 value = list(self.__iter__())
2096 value.sort(lambda a,b:cmp(a[property], b[property]))
2097 return value
2099 def __contains__(self, value):
2100 """ Support the "in" operator. We have to make sure the passed-in
2101 value is a string first, not a HTMLProperty.
2102 """
2103 return str(value) in self._value
2105 def isset(self):
2106 """Is my _value not []?"""
2107 return self._value != []
2109 def plain(self, escape=0):
2110 """ Render a "plain" representation of the property
2111 """
2112 if not self.is_view_ok():
2113 return self._('[hidden]')
2115 linkcl = self._db.classes[self._prop.classname]
2116 k = linkcl.labelprop(1)
2117 labels = []
2118 for v in self._value:
2119 if num_re.match(v):
2120 try:
2121 label = linkcl.get(v, k)
2122 except IndexError:
2123 label = None
2124 # fall back to designator if label is None
2125 if label is None: label = '%s%s'%(self._prop.classname, k)
2126 else:
2127 label = v
2128 labels.append(label)
2129 value = ', '.join(labels)
2130 if escape:
2131 value = cgi.escape(value)
2132 return value
2134 def field(self, size=30, showid=0, **kwargs):
2135 """ Render a form edit field for the property
2137 If not editable, just display the value via plain().
2138 """
2139 if not self.is_edit_ok():
2140 return self.plain(escape=1)
2142 linkcl = self._db.getclass(self._prop.classname)
2143 value = self._value[:]
2144 # map the id to the label property
2145 if not linkcl.getkey():
2146 showid=1
2147 if not showid:
2148 k = linkcl.labelprop(1)
2149 value = lookupKeys(linkcl, k, value)
2150 value = ','.join(value)
2151 return self.input(name=self._formname, size=size, value=value,
2152 **kwargs)
2154 def menu(self, size=None, height=None, showid=0, additional=[],
2155 value=None, sort_on=None, html_kwargs = {}, **conditions):
2156 """ Render a form <select> list for this property.
2158 "size" is used to limit the length of the list labels
2159 "height" is used to set the <select> tag's "size" attribute
2160 "showid" includes the item ids in the list labels
2161 "additional" lists properties which should be included in the
2162 label
2163 "value" specifies which item is pre-selected
2164 "sort_on" indicates the property to sort the list on as
2165 (direction, property) where direction is '+' or '-'. A
2166 single string with the direction prepended may be used.
2167 For example: ('-', 'order'), '+name'.
2169 The remaining keyword arguments are used as conditions for
2170 filtering the items in the list - they're passed as the
2171 "filterspec" argument to a Class.filter() call.
2173 If not editable, just display the value via plain().
2174 """
2175 if not self.is_edit_ok():
2176 return self.plain(escape=1)
2178 if value is None:
2179 value = self._value
2181 linkcl = self._db.getclass(self._prop.classname)
2183 if sort_on is not None:
2184 if not isinstance(sort_on, tuple):
2185 if sort_on[0] in '+-':
2186 sort_on = (sort_on[0], sort_on[1:])
2187 else:
2188 sort_on = ('+', sort_on)
2189 else:
2190 sort_on = ('+', linkcl.orderprop())
2192 options = [opt
2193 for opt in linkcl.filter(None, conditions, sort_on)
2194 if self._db.security.hasPermission("View", self._client.userid,
2195 linkcl.classname, itemid=opt)]
2197 # make sure we list the current values if they're retired
2198 for val in value:
2199 if val not in options:
2200 options.insert(0, val)
2202 if not height:
2203 height = len(options)
2204 if value:
2205 # The "no selection" option.
2206 height += 1
2207 height = min(height, 7)
2208 l = ['<select multiple %s>'%cgi_escape_attrs(name = self._formname,
2209 size = height,
2210 **html_kwargs)]
2211 k = linkcl.labelprop(1)
2213 if value:
2214 l.append('<option value="%s">- no selection -</option>'
2215 % ','.join(['-' + v for v in value]))
2217 if additional:
2218 additional_fns = []
2219 props = linkcl.getprops()
2220 for propname in additional:
2221 prop = props[propname]
2222 if isinstance(prop, hyperdb.Link):
2223 cl = self._db.getclass(prop.classname)
2224 labelprop = cl.labelprop()
2225 fn = lambda optionid: cl.get(linkcl.get(optionid,
2226 propname),
2227 labelprop)
2228 else:
2229 fn = lambda optionid: linkcl.get(optionid, propname)
2230 additional_fns.append(fn)
2232 for optionid in options:
2233 # get the option value, and if it's None use an empty string
2234 option = linkcl.get(optionid, k) or ''
2236 # figure if this option is selected
2237 s = ''
2238 if optionid in value or option in value:
2239 s = 'selected="selected" '
2241 # figure the label
2242 if showid:
2243 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
2244 else:
2245 lab = option
2246 # truncate if it's too long
2247 if size is not None and len(lab) > size:
2248 lab = lab[:size-3] + '...'
2249 if additional:
2250 m = []
2251 for fn in additional_fns:
2252 m.append(str(fn(optionid)))
2253 lab = lab + ' (%s)'%', '.join(m)
2255 # and generate
2256 lab = cgi.escape(self._(lab))
2257 l.append('<option %svalue="%s">%s</option>'%(s, optionid,
2258 lab))
2259 l.append('</select>')
2260 return '\n'.join(l)
2262 # set the propclasses for HTMLItem
2263 propclasses = (
2264 (hyperdb.String, StringHTMLProperty),
2265 (hyperdb.Number, NumberHTMLProperty),
2266 (hyperdb.Boolean, BooleanHTMLProperty),
2267 (hyperdb.Date, DateHTMLProperty),
2268 (hyperdb.Interval, IntervalHTMLProperty),
2269 (hyperdb.Password, PasswordHTMLProperty),
2270 (hyperdb.Link, LinkHTMLProperty),
2271 (hyperdb.Multilink, MultilinkHTMLProperty),
2272 )
2274 def make_sort_function(db, classname, sort_on=None):
2275 """Make a sort function for a given class
2276 """
2277 linkcl = db.getclass(classname)
2278 if sort_on is None:
2279 sort_on = linkcl.orderprop()
2280 def sortfunc(a, b):
2281 return cmp(linkcl.get(a, sort_on), linkcl.get(b, sort_on))
2282 return sortfunc
2284 def handleListCGIValue(value):
2285 """ Value is either a single item or a list of items. Each item has a
2286 .value that we're actually interested in.
2287 """
2288 if isinstance(value, type([])):
2289 return [value.value for value in value]
2290 else:
2291 value = value.value.strip()
2292 if not value:
2293 return []
2294 return [v.strip() for v in value.split(',')]
2296 class HTMLRequest(HTMLInputMixin):
2297 """The *request*, holding the CGI form and environment.
2299 - "form" the CGI form as a cgi.FieldStorage
2300 - "env" the CGI environment variables
2301 - "base" the base URL for this instance
2302 - "user" a HTMLItem instance for this user
2303 - "language" as determined by the browser or config
2304 - "classname" the current classname (possibly None)
2305 - "template" the current template (suffix, also possibly None)
2307 Index args:
2309 - "columns" dictionary of the columns to display in an index page
2310 - "show" a convenience access to columns - request/show/colname will
2311 be true if the columns should be displayed, false otherwise
2312 - "sort" index sort column (direction, column name)
2313 - "group" index grouping property (direction, column name)
2314 - "filter" properties to filter the index on
2315 - "filterspec" values to filter the index on
2316 - "search_text" text to perform a full-text search on for an index
2317 """
2318 def __repr__(self):
2319 return '<HTMLRequest %r>'%self.__dict__
2321 def __init__(self, client):
2322 # _client is needed by HTMLInputMixin
2323 self._client = self.client = client
2325 # easier access vars
2326 self.form = client.form
2327 self.env = client.env
2328 self.base = client.base
2329 self.user = HTMLItem(client, 'user', client.userid)
2330 self.language = client.language
2332 # store the current class name and action
2333 self.classname = client.classname
2334 self.nodeid = client.nodeid
2335 self.template = client.template
2337 # the special char to use for special vars
2338 self.special_char = '@'
2340 HTMLInputMixin.__init__(self)
2342 self._post_init()
2344 def current_url(self):
2345 url = self.base
2346 if self.classname:
2347 url += self.classname
2348 if self.nodeid:
2349 url += self.nodeid
2350 args = {}
2351 if self.template:
2352 args['@template'] = self.template
2353 return self.indexargs_url(url, args)
2355 def _parse_sort(self, var, name):
2356 """ Parse sort/group options. Append to var
2357 """
2358 fields = []
2359 dirs = []
2360 for special in '@:':
2361 idx = 0
2362 key = '%s%s%d'%(special, name, idx)
2363 while key in self.form:
2364 self.special_char = special
2365 fields.append(self.form.getfirst(key))
2366 dirkey = '%s%sdir%d'%(special, name, idx)
2367 if dirkey in self.form:
2368 dirs.append(self.form.getfirst(dirkey))
2369 else:
2370 dirs.append(None)
2371 idx += 1
2372 key = '%s%s%d'%(special, name, idx)
2373 # backward compatible (and query) URL format
2374 key = special + name
2375 dirkey = key + 'dir'
2376 if key in self.form and not fields:
2377 fields = handleListCGIValue(self.form[key])
2378 if dirkey in self.form:
2379 dirs.append(self.form.getfirst(dirkey))
2380 if fields: # only try other special char if nothing found
2381 break
2382 for f, d in map(None, fields, dirs):
2383 if f.startswith('-'):
2384 var.append(('-', f[1:]))
2385 elif d:
2386 var.append(('-', f))
2387 else:
2388 var.append(('+', f))
2390 def _post_init(self):
2391 """ Set attributes based on self.form
2392 """
2393 # extract the index display information from the form
2394 self.columns = []
2395 for name in ':columns @columns'.split():
2396 if self.form.has_key(name):
2397 self.special_char = name[0]
2398 self.columns = handleListCGIValue(self.form[name])
2399 break
2400 self.show = support.TruthDict(self.columns)
2402 # sorting and grouping
2403 self.sort = []
2404 self.group = []
2405 self._parse_sort(self.sort, 'sort')
2406 self._parse_sort(self.group, 'group')
2408 # filtering
2409 self.filter = []
2410 for name in ':filter @filter'.split():
2411 if self.form.has_key(name):
2412 self.special_char = name[0]
2413 self.filter = handleListCGIValue(self.form[name])
2415 self.filterspec = {}
2416 db = self.client.db
2417 if self.classname is not None:
2418 cls = db.getclass (self.classname)
2419 for name in self.filter:
2420 if not self.form.has_key(name):
2421 continue
2422 prop = cls.get_transitive_prop (name)
2423 fv = self.form[name]
2424 if (isinstance(prop, hyperdb.Link) or
2425 isinstance(prop, hyperdb.Multilink)):
2426 self.filterspec[name] = lookupIds(db, prop,
2427 handleListCGIValue(fv))
2428 else:
2429 if isinstance(fv, type([])):
2430 self.filterspec[name] = [v.value for v in fv]
2431 elif name == 'id':
2432 # special case "id" property
2433 self.filterspec[name] = handleListCGIValue(fv)
2434 else:
2435 self.filterspec[name] = fv.value
2437 # full-text search argument
2438 self.search_text = None
2439 for name in ':search_text @search_text'.split():
2440 if self.form.has_key(name):
2441 self.special_char = name[0]
2442 self.search_text = self.form.getfirst(name)
2444 # pagination - size and start index
2445 # figure batch args
2446 self.pagesize = 50
2447 for name in ':pagesize @pagesize'.split():
2448 if self.form.has_key(name):
2449 self.special_char = name[0]
2450 try:
2451 self.pagesize = int(self.form.getfirst(name))
2452 except ValueError:
2453 # not an integer - ignore
2454 pass
2456 self.startwith = 0
2457 for name in ':startwith @startwith'.split():
2458 if self.form.has_key(name):
2459 self.special_char = name[0]
2460 try:
2461 self.startwith = int(self.form.getfirst(name))
2462 except ValueError:
2463 # not an integer - ignore
2464 pass
2466 # dispname
2467 if self.form.has_key('@dispname'):
2468 self.dispname = self.form.getfirst('@dispname')
2469 else:
2470 self.dispname = None
2472 def updateFromURL(self, url):
2473 """ Parse the URL for query args, and update my attributes using the
2474 values.
2475 """
2476 env = {'QUERY_STRING': url}
2477 self.form = cgi.FieldStorage(environ=env)
2479 self._post_init()
2481 def update(self, kwargs):
2482 """ Update my attributes using the keyword args
2483 """
2484 self.__dict__.update(kwargs)
2485 if kwargs.has_key('columns'):
2486 self.show = support.TruthDict(self.columns)
2488 def description(self):
2489 """ Return a description of the request - handle for the page title.
2490 """
2491 s = [self.client.db.config.TRACKER_NAME]
2492 if self.classname:
2493 if self.client.nodeid:
2494 s.append('- %s%s'%(self.classname, self.client.nodeid))
2495 else:
2496 if self.template == 'item':
2497 s.append('- new %s'%self.classname)
2498 elif self.template == 'index':
2499 s.append('- %s index'%self.classname)
2500 else:
2501 s.append('- %s %s'%(self.classname, self.template))
2502 else:
2503 s.append('- home')
2504 return ' '.join(s)
2506 def __str__(self):
2507 d = {}
2508 d.update(self.__dict__)
2509 f = ''
2510 for k in self.form.keys():
2511 f += '\n %r=%r'%(k,handleListCGIValue(self.form[k]))
2512 d['form'] = f
2513 e = ''
2514 for k,v in self.env.items():
2515 e += '\n %r=%r'%(k, v)
2516 d['env'] = e
2517 return """
2518 form: %(form)s
2519 base: %(base)r
2520 classname: %(classname)r
2521 template: %(template)r
2522 columns: %(columns)r
2523 sort: %(sort)r
2524 group: %(group)r
2525 filter: %(filter)r
2526 search_text: %(search_text)r
2527 pagesize: %(pagesize)r
2528 startwith: %(startwith)r
2529 env: %(env)s
2530 """%d
2532 def indexargs_form(self, columns=1, sort=1, group=1, filter=1,
2533 filterspec=1, search_text=1):
2534 """ return the current index args as form elements """
2535 l = []
2536 sc = self.special_char
2537 def add(k, v):
2538 l.append(self.input(type="hidden", name=k, value=v))
2539 if columns and self.columns:
2540 add(sc+'columns', ','.join(self.columns))
2541 if sort:
2542 val = []
2543 for dir, attr in self.sort:
2544 if dir == '-':
2545 val.append('-'+attr)
2546 else:
2547 val.append(attr)
2548 add(sc+'sort', ','.join (val))
2549 if group:
2550 val = []
2551 for dir, attr in self.group:
2552 if dir == '-':
2553 val.append('-'+attr)
2554 else:
2555 val.append(attr)
2556 add(sc+'group', ','.join (val))
2557 if filter and self.filter:
2558 add(sc+'filter', ','.join(self.filter))
2559 if self.classname and filterspec:
2560 cls = self.client.db.getclass(self.classname)
2561 for k,v in self.filterspec.items():
2562 if type(v) == type([]):
2563 if isinstance(cls.get_transitive_prop(k), hyperdb.String):
2564 add(k, ' '.join(v))
2565 else:
2566 add(k, ','.join(v))
2567 else:
2568 add(k, v)
2569 if search_text and self.search_text:
2570 add(sc+'search_text', self.search_text)
2571 add(sc+'pagesize', self.pagesize)
2572 add(sc+'startwith', self.startwith)
2573 return '\n'.join(l)
2575 def indexargs_url(self, url, args):
2576 """ Embed the current index args in a URL
2577 """
2578 q = urllib.quote
2579 sc = self.special_char
2580 l = ['%s=%s'%(k,v) for k,v in args.items()]
2582 # pull out the special values (prefixed by @ or :)
2583 specials = {}
2584 for key in args.keys():
2585 if key[0] in '@:':
2586 specials[key[1:]] = args[key]
2588 # ok, now handle the specials we received in the request
2589 if self.columns and not specials.has_key('columns'):
2590 l.append(sc+'columns=%s'%(','.join(self.columns)))
2591 if self.sort and not specials.has_key('sort'):
2592 val = []
2593 for dir, attr in self.sort:
2594 if dir == '-':
2595 val.append('-'+attr)
2596 else:
2597 val.append(attr)
2598 l.append(sc+'sort=%s'%(','.join(val)))
2599 if self.group and not specials.has_key('group'):
2600 val = []
2601 for dir, attr in self.group:
2602 if dir == '-':
2603 val.append('-'+attr)
2604 else:
2605 val.append(attr)
2606 l.append(sc+'group=%s'%(','.join(val)))
2607 if self.filter and not specials.has_key('filter'):
2608 l.append(sc+'filter=%s'%(','.join(self.filter)))
2609 if self.search_text and not specials.has_key('search_text'):
2610 l.append(sc+'search_text=%s'%q(self.search_text))
2611 if not specials.has_key('pagesize'):
2612 l.append(sc+'pagesize=%s'%self.pagesize)
2613 if not specials.has_key('startwith'):
2614 l.append(sc+'startwith=%s'%self.startwith)
2616 # finally, the remainder of the filter args in the request
2617 if self.classname and self.filterspec:
2618 cls = self.client.db.getclass(self.classname)
2619 for k,v in self.filterspec.items():
2620 if not args.has_key(k):
2621 if type(v) == type([]):
2622 prop = cls.get_transitive_prop(k)
2623 if k != 'id' and isinstance(prop, hyperdb.String):
2624 l.append('%s=%s'%(k, '%20'.join([q(i) for i in v])))
2625 else:
2626 l.append('%s=%s'%(k, ','.join([q(i) for i in v])))
2627 else:
2628 l.append('%s=%s'%(k, q(v)))
2629 return '%s?%s'%(url, '&'.join(l))
2630 indexargs_href = indexargs_url
2632 def base_javascript(self):
2633 return """
2634 <script type="text/javascript">
2635 submitted = false;
2636 function submit_once() {
2637 if (submitted) {
2638 alert("Your request is being processed.\\nPlease be patient.");
2639 event.returnValue = 0; // work-around for IE
2640 return 0;
2641 }
2642 submitted = true;
2643 return 1;
2644 }
2646 function help_window(helpurl, width, height) {
2647 HelpWin = window.open('%s' + helpurl, 'RoundupHelpWindow', 'scrollbars=yes,resizable=yes,toolbar=no,height='+height+',width='+width);
2648 }
2649 </script>
2650 """%self.base
2652 def batch(self):
2653 """ Return a batch object for results from the "current search"
2654 """
2655 filterspec = self.filterspec
2656 sort = self.sort
2657 group = self.group
2659 # get the list of ids we're batching over
2660 klass = self.client.db.getclass(self.classname)
2661 if self.search_text:
2662 matches = self.client.db.indexer.search(
2663 [w.upper().encode("utf-8", "replace") for w in re.findall(
2664 r'(?u)\b\w{2,25}\b',
2665 unicode(self.search_text, "utf-8", "replace")
2666 )], klass)
2667 else:
2668 matches = None
2670 # filter for visibility
2671 check = self._client.db.security.hasPermission
2672 userid = self._client.userid
2673 l = [id for id in klass.filter(matches, filterspec, sort, group)
2674 if check('View', userid, self.classname, itemid=id)]
2676 # return the batch object, using IDs only
2677 return Batch(self.client, l, self.pagesize, self.startwith,
2678 classname=self.classname)
2680 # extend the standard ZTUtils Batch object to remove dependency on
2681 # Acquisition and add a couple of useful methods
2682 class Batch(ZTUtils.Batch):
2683 """ Use me to turn a list of items, or item ids of a given class, into a
2684 series of batches.
2686 ========= ========================================================
2687 Parameter Usage
2688 ========= ========================================================
2689 sequence a list of HTMLItems or item ids
2690 classname if sequence is a list of ids, this is the class of item
2691 size how big to make the sequence.
2692 start where to start (0-indexed) in the sequence.
2693 end where to end (0-indexed) in the sequence.
2694 orphan if the next batch would contain less items than this
2695 value, then it is combined with this batch
2696 overlap the number of items shared between adjacent batches
2697 ========= ========================================================
2699 Attributes: Note that the "start" attribute, unlike the
2700 argument, is a 1-based index (I know, lame). "first" is the
2701 0-based index. "length" is the actual number of elements in
2702 the batch.
2704 "sequence_length" is the length of the original, unbatched, sequence.
2705 """
2706 def __init__(self, client, sequence, size, start, end=0, orphan=0,
2707 overlap=0, classname=None):
2708 self.client = client
2709 self.last_index = self.last_item = None
2710 self.current_item = None
2711 self.classname = classname
2712 self.sequence_length = len(sequence)
2713 ZTUtils.Batch.__init__(self, sequence, size, start, end, orphan,
2714 overlap)
2716 # overwrite so we can late-instantiate the HTMLItem instance
2717 def __getitem__(self, index):
2718 if index < 0:
2719 if index + self.end < self.first: raise IndexError, index
2720 return self._sequence[index + self.end]
2722 if index >= self.length:
2723 raise IndexError, index
2725 # move the last_item along - but only if the fetched index changes
2726 # (for some reason, index 0 is fetched twice)
2727 if index != self.last_index:
2728 self.last_item = self.current_item
2729 self.last_index = index
2731 item = self._sequence[index + self.first]
2732 if self.classname:
2733 # map the item ids to instances
2734 item = HTMLItem(self.client, self.classname, item)
2735 self.current_item = item
2736 return item
2738 def propchanged(self, *properties):
2739 """ Detect if one of the properties marked as being a group
2740 property changed in the last iteration fetch
2741 """
2742 # we poke directly at the _value here since MissingValue can screw
2743 # us up and cause Nones to compare strangely
2744 if self.last_item is None:
2745 return 1
2746 for property in properties:
2747 if property == 'id' or isinstance (self.last_item[property], list):
2748 if (str(self.last_item[property]) !=
2749 str(self.current_item[property])):
2750 return 1
2751 else:
2752 if (self.last_item[property]._value !=
2753 self.current_item[property]._value):
2754 return 1
2755 return 0
2757 # override these 'cos we don't have access to acquisition
2758 def previous(self):
2759 if self.start == 1:
2760 return None
2761 return Batch(self.client, self._sequence, self._size,
2762 self.first - self._size + self.overlap, 0, self.orphan,
2763 self.overlap)
2765 def next(self):
2766 try:
2767 self._sequence[self.end]
2768 except IndexError:
2769 return None
2770 return Batch(self.client, self._sequence, self._size,
2771 self.end - self.overlap, 0, self.orphan, self.overlap)
2773 class TemplatingUtils:
2774 """ Utilities for templating
2775 """
2776 def __init__(self, client):
2777 self.client = client
2778 def Batch(self, sequence, size, start, end=0, orphan=0, overlap=0):
2779 return Batch(self.client, sequence, size, start, end, orphan,
2780 overlap)
2782 def url_quote(self, url):
2783 """URL-quote the supplied text."""
2784 return urllib.quote(url)
2786 def html_quote(self, html):
2787 """HTML-quote the supplied text."""
2788 return cgi.escape(html)
2790 def __getattr__(self, name):
2791 """Try the tracker's templating_utils."""
2792 if not hasattr(self.client.instance, 'templating_utils'):
2793 # backwards-compatibility
2794 raise AttributeError, name
2795 if not self.client.instance.templating_utils.has_key(name):
2796 raise AttributeError, name
2797 return self.client.instance.templating_utils[name]
2799 def html_calendar(self, request):
2800 """Generate a HTML calendar.
2802 `request` the roundup.request object
2803 - @template : name of the template
2804 - form : name of the form to store back the date
2805 - property : name of the property of the form to store
2806 back the date
2807 - date : current date
2808 - display : when browsing, specifies year and month
2810 html will simply be a table.
2811 """
2812 date_str = request.form.getfirst("date", ".")
2813 display = request.form.getfirst("display", date_str)
2814 template = request.form.getfirst("@template", "calendar")
2815 form = request.form.getfirst("form")
2816 property = request.form.getfirst("property")
2817 curr_date = date.Date(date_str) # to highlight
2818 display = date.Date(display) # to show
2819 day = display.day
2821 # for navigation
2822 date_prev_month = display + date.Interval("-1m")
2823 date_next_month = display + date.Interval("+1m")
2824 date_prev_year = display + date.Interval("-1y")
2825 date_next_year = display + date.Interval("+1y")
2827 res = []
2829 base_link = "%s?@template=%s&property=%s&form=%s&date=%s" % \
2830 (request.classname, template, property, form, curr_date)
2832 # navigation
2833 # month
2834 res.append('<table class="calendar"><tr><td>')
2835 res.append(' <table width="100%" class="calendar_nav"><tr>')
2836 link = "&display=%s"%date_prev_month
2837 res.append(' <td><a href="%s&display=%s"><</a></td>'%(base_link,
2838 date_prev_month))
2839 res.append(' <td>%s</td>'%calendar.month_name[display.month])
2840 res.append(' <td><a href="%s&display=%s">></a></td>'%(base_link,
2841 date_next_month))
2842 # spacer
2843 res.append(' <td width="100%"></td>')
2844 # year
2845 res.append(' <td><a href="%s&display=%s"><</a></td>'%(base_link,
2846 date_prev_year))
2847 res.append(' <td>%s</td>'%display.year)
2848 res.append(' <td><a href="%s&display=%s">></a></td>'%(base_link,
2849 date_next_year))
2850 res.append(' </tr></table>')
2851 res.append(' </td></tr>')
2853 # the calendar
2854 res.append(' <tr><td><table class="calendar_display">')
2855 res.append(' <tr class="weekdays">')
2856 for day in calendar.weekheader(3).split():
2857 res.append(' <td>%s</td>'%day)
2858 res.append(' </tr>')
2859 for week in calendar.monthcalendar(display.year, display.month):
2860 res.append(' <tr>')
2861 for day in week:
2862 link = "javascript:form[field].value = '%d-%02d-%02d'; " \
2863 "window.close ();"%(display.year, display.month, day)
2864 if (day == curr_date.day and display.month == curr_date.month
2865 and display.year == curr_date.year):
2866 # highlight
2867 style = "today"
2868 else :
2869 style = ""
2870 if day:
2871 res.append(' <td class="%s"><a href="%s">%s</a></td>'%(
2872 style, link, day))
2873 else :
2874 res.append(' <td></td>')
2875 res.append(' </tr>')
2876 res.append('</table></td></tr></table>')
2877 return "\n".join(res)
2879 class MissingValue:
2880 def __init__(self, description, **kwargs):
2881 self.__description = description
2882 for key, value in kwargs.items():
2883 self.__dict__[key] = value
2885 def __call__(self, *args, **kwargs): return MissingValue(self.__description)
2886 def __getattr__(self, name):
2887 # This allows assignments which assume all intermediate steps are Null
2888 # objects if they don't exist yet.
2889 #
2890 # For example (with just 'client' defined):
2891 #
2892 # client.db.config.TRACKER_WEB = 'BASE/'
2893 self.__dict__[name] = MissingValue(self.__description)
2894 return getattr(self, name)
2896 def __getitem__(self, key): return self
2897 def __nonzero__(self): return 0
2898 def __str__(self): return '[%s]'%self.__description
2899 def __repr__(self): return '<MissingValue 0x%x "%s">'%(id(self),
2900 self.__description)
2901 def gettext(self, str): return str
2902 _ = gettext
2904 # vim: set et sts=4 sw=4 :