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 self._formname = '%s%s@%s'%(classname, nodeid, name)
1244 else:
1245 self._formname = name
1247 # If no value is already present for this property, see if one
1248 # is specified in the current form.
1249 form = self._client.form
1250 if not self._value and form.has_key(self._formname):
1251 if isinstance(prop, hyperdb.Multilink):
1252 value = lookupIds(self._db, prop,
1253 handleListCGIValue(form[self._formname]),
1254 fail_ok=1)
1255 elif isinstance(prop, hyperdb.Link):
1256 value = form.getfirst(self._formname).strip()
1257 if value:
1258 value = lookupIds(self._db, prop, [value],
1259 fail_ok=1)[0]
1260 else:
1261 value = None
1262 else:
1263 value = form.getfirst(self._formname).strip() or None
1264 self._value = value
1266 HTMLInputMixin.__init__(self)
1268 def __repr__(self):
1269 return '<HTMLProperty(0x%x) %s %r %r>'%(id(self), self._formname,
1270 self._prop, self._value)
1271 def __str__(self):
1272 return self.plain()
1273 def __cmp__(self, other):
1274 if isinstance(other, HTMLProperty):
1275 return cmp(self._value, other._value)
1276 return cmp(self._value, other)
1278 def __nonzero__(self):
1279 return not not self._value
1281 def isset(self):
1282 """Is my _value not None?"""
1283 return self._value is not None
1285 def is_edit_ok(self):
1286 """Should the user be allowed to use an edit form field for this
1287 property. Check "Create" for new items, or "Edit" for existing
1288 ones.
1289 """
1290 if self._nodeid:
1291 return self._db.security.hasPermission('Edit', self._client.userid,
1292 self._classname, self._name, self._nodeid)
1293 return self._db.security.hasPermission('Create', self._client.userid,
1294 self._classname, self._name) or \
1295 self._db.security.hasPermission('Register', self._client.userid,
1296 self._classname, self._name)
1298 def is_view_ok(self):
1299 """ Is the user allowed to View the current class?
1300 """
1301 if self._db.security.hasPermission('View', self._client.userid,
1302 self._classname, self._name, self._nodeid):
1303 return 1
1304 return self.is_edit_ok()
1306 class StringHTMLProperty(HTMLProperty):
1307 hyper_re = re.compile(r'''(
1308 (?P<url>
1309 (
1310 (ht|f)tp(s?):// # protocol
1311 ([\w]+(:\w+)?@)? # username/password
1312 ([\w\-]+) # hostname
1313 ((\.[\w-]+)+)? # .domain.etc
1314 | # ... or ...
1315 ([\w]+(:\w+)?@)? # username/password
1316 www\. # "www."
1317 ([\w\-]+\.)+ # hostname
1318 [\w]{2,5} # TLD
1319 )
1320 (:[\d]{1,5})? # port
1321 (/[\w\-$.+!*(),;:@&=?/~\\#%]*)? # path etc.
1322 )|
1323 (?P<email>[-+=%/\w\.]+@[\w\.\-]+)|
1324 (?P<item>(?P<class>[A-Za-z_]+)(\s*)(?P<id>\d+))
1325 )''', re.X | re.I)
1326 protocol_re = re.compile('^(ht|f)tp(s?)://', re.I)
1328 def _hyper_repl_item(self,match,replacement):
1329 item = match.group('item')
1330 cls = match.group('class').lower()
1331 id = match.group('id')
1332 try:
1333 # make sure cls is a valid tracker classname
1334 cl = self._db.getclass(cls)
1335 if not cl.hasnode(id):
1336 return item
1337 return replacement % locals()
1338 except KeyError:
1339 return item
1341 def _hyper_repl(self, match):
1342 if match.group('url'):
1343 u = s = match.group('url')
1344 if not self.protocol_re.search(s):
1345 u = 'http://' + s
1346 # catch an escaped ">" at the end of the URL
1347 if s.endswith('>'):
1348 u = s = s[:-4]
1349 e = '>'
1350 else:
1351 e = ''
1352 return '<a href="%s">%s</a>%s'%(u, s, e)
1353 elif match.group('email'):
1354 s = match.group('email')
1355 return '<a href="mailto:%s">%s</a>'%(s, s)
1356 else:
1357 return self._hyper_repl_item(match,
1358 '<a href="%(cls)s%(id)s">%(item)s</a>')
1360 def _hyper_repl_rst(self, match):
1361 if match.group('url'):
1362 s = match.group('url')
1363 return '`%s <%s>`_'%(s, s)
1364 elif match.group('email'):
1365 s = match.group('email')
1366 return '`%s <mailto:%s>`_'%(s, s)
1367 else:
1368 return self._hyper_repl_item(match,'`%(item)s <%(cls)s%(id)s>`_')
1370 def hyperlinked(self):
1371 """ Render a "hyperlinked" version of the text """
1372 return self.plain(hyperlink=1)
1374 def plain(self, escape=0, hyperlink=0):
1375 """Render a "plain" representation of the property
1377 - "escape" turns on/off HTML quoting
1378 - "hyperlink" turns on/off in-text hyperlinking of URLs, email
1379 addresses and designators
1380 """
1381 if not self.is_view_ok():
1382 return self._('[hidden]')
1384 if self._value is None:
1385 return ''
1386 if escape:
1387 s = cgi.escape(str(self._value))
1388 else:
1389 s = str(self._value)
1390 if hyperlink:
1391 # no, we *must* escape this text
1392 if not escape:
1393 s = cgi.escape(s)
1394 s = self.hyper_re.sub(self._hyper_repl, s)
1395 return s
1397 def wrapped(self, escape=1, hyperlink=1):
1398 """Render a "wrapped" representation of the property.
1400 We wrap long lines at 80 columns on the nearest whitespace. Lines
1401 with no whitespace are not broken to force wrapping.
1403 Note that unlike plain() we default wrapped() to have the escaping
1404 and hyperlinking turned on since that's the most common usage.
1406 - "escape" turns on/off HTML quoting
1407 - "hyperlink" turns on/off in-text hyperlinking of URLs, email
1408 addresses and designators
1409 """
1410 if not self.is_view_ok():
1411 return self._('[hidden]')
1413 if self._value is None:
1414 return ''
1415 s = support.wrap(str(self._value), width=80)
1416 if escape:
1417 s = cgi.escape(s)
1418 if hyperlink:
1419 # no, we *must* escape this text
1420 if not escape:
1421 s = cgi.escape(s)
1422 s = self.hyper_re.sub(self._hyper_repl, s)
1423 return s
1425 def stext(self, escape=0, hyperlink=1):
1426 """ Render the value of the property as StructuredText.
1428 This requires the StructureText module to be installed separately.
1429 """
1430 if not self.is_view_ok():
1431 return self._('[hidden]')
1433 s = self.plain(escape=escape, hyperlink=hyperlink)
1434 if not StructuredText:
1435 return s
1436 return StructuredText(s,level=1,header=0)
1438 def rst(self, hyperlink=1):
1439 """ Render the value of the property as ReStructuredText.
1441 This requires docutils to be installed separately.
1442 """
1443 if not self.is_view_ok():
1444 return self._('[hidden]')
1446 if not ReStructuredText:
1447 return self.plain(escape=0, hyperlink=hyperlink)
1448 s = self.plain(escape=0, hyperlink=0)
1449 if hyperlink:
1450 s = self.hyper_re.sub(self._hyper_repl_rst, s)
1451 return ReStructuredText(s, writer_name="html")["html_body"].encode("utf-8",
1452 "replace")
1454 def field(self, **kwargs):
1455 """ Render the property as a field in HTML.
1457 If not editable, just display the value via plain().
1458 """
1459 if not self.is_edit_ok():
1460 return self.plain(escape=1)
1462 value = self._value
1463 if value is None:
1464 value = ''
1466 kwargs.setdefault("size", 30)
1467 kwargs.update({"name": self._formname, "value": value})
1468 return self.input(**kwargs)
1470 def multiline(self, escape=0, rows=5, cols=40, **kwargs):
1471 """ Render a multiline form edit field for the property.
1473 If not editable, just display the plain() value in a <pre> tag.
1474 """
1475 if not self.is_edit_ok():
1476 return '<pre>%s</pre>'%self.plain()
1478 if self._value is None:
1479 value = ''
1480 else:
1481 value = cgi.escape(str(self._value))
1483 value = '"'.join(value.split('"'))
1484 name = self._formname
1485 passthrough_args = cgi_escape_attrs(**kwargs)
1486 return ('<textarea %(passthrough_args)s name="%(name)s" id="%(name)s"'
1487 ' rows="%(rows)s" cols="%(cols)s">'
1488 '%(value)s</textarea>') % locals()
1490 def email(self, escape=1):
1491 """ Render the value of the property as an obscured email address
1492 """
1493 if not self.is_view_ok():
1494 return self._('[hidden]')
1496 if self._value is None:
1497 value = ''
1498 else:
1499 value = str(self._value)
1500 split = value.split('@')
1501 if len(split) == 2:
1502 name, domain = split
1503 domain = ' '.join(domain.split('.')[:-1])
1504 name = name.replace('.', ' ')
1505 value = '%s at %s ...'%(name, domain)
1506 else:
1507 value = value.replace('.', ' ')
1508 if escape:
1509 value = cgi.escape(value)
1510 return value
1512 class PasswordHTMLProperty(HTMLProperty):
1513 def plain(self, escape=0):
1514 """ Render a "plain" representation of the property
1515 """
1516 if not self.is_view_ok():
1517 return self._('[hidden]')
1519 if self._value is None:
1520 return ''
1521 return self._('*encrypted*')
1523 def field(self, size=30, **kwargs):
1524 """ Render a form edit field for the property.
1526 If not editable, just display the value via plain().
1527 """
1528 if not self.is_edit_ok():
1529 return self.plain(escape=1)
1531 return self.input(type="password", name=self._formname, size=size,
1532 **kwargs)
1534 def confirm(self, size=30):
1535 """ Render a second form edit field for the property, used for
1536 confirmation that the user typed the password correctly. Generates
1537 a field with name "@confirm@name".
1539 If not editable, display nothing.
1540 """
1541 if not self.is_edit_ok():
1542 return ''
1544 return self.input(type="password",
1545 name="@confirm@%s"%self._formname,
1546 id="%s-confirm"%self._formname,
1547 size=size)
1549 class NumberHTMLProperty(HTMLProperty):
1550 def plain(self, escape=0):
1551 """ Render a "plain" representation of the property
1552 """
1553 if not self.is_view_ok():
1554 return self._('[hidden]')
1556 if self._value is None:
1557 return ''
1559 return str(self._value)
1561 def field(self, size=30, **kwargs):
1562 """ Render a form edit field for the property.
1564 If not editable, just display the value via plain().
1565 """
1566 if not self.is_edit_ok():
1567 return self.plain(escape=1)
1569 value = self._value
1570 if value is None:
1571 value = ''
1573 return self.input(name=self._formname, value=value, size=size,
1574 **kwargs)
1576 def __int__(self):
1577 """ Return an int of me
1578 """
1579 return int(self._value)
1581 def __float__(self):
1582 """ Return a float of me
1583 """
1584 return float(self._value)
1587 class BooleanHTMLProperty(HTMLProperty):
1588 def plain(self, escape=0):
1589 """ Render a "plain" representation of the property
1590 """
1591 if not self.is_view_ok():
1592 return self._('[hidden]')
1594 if self._value is None:
1595 return ''
1596 return self._value and self._("Yes") or self._("No")
1598 def field(self, **kwargs):
1599 """ Render a form edit field for the property
1601 If not editable, just display the value via plain().
1602 """
1603 if not self.is_edit_ok():
1604 return self.plain(escape=1)
1606 value = self._value
1607 if isinstance(value, str) or isinstance(value, unicode):
1608 value = value.strip().lower() in ('checked', 'yes', 'true',
1609 'on', '1')
1611 checked = value and "checked" or ""
1612 if value:
1613 s = self.input(type="radio", name=self._formname, value="yes",
1614 checked="checked", **kwargs)
1615 s += self._('Yes')
1616 s +=self.input(type="radio", name=self._formname, value="no",
1617 **kwargs)
1618 s += self._('No')
1619 else:
1620 s = self.input(type="radio", name=self._formname, value="yes",
1621 **kwargs)
1622 s += self._('Yes')
1623 s +=self.input(type="radio", name=self._formname, value="no",
1624 checked="checked", **kwargs)
1625 s += self._('No')
1626 return s
1628 class DateHTMLProperty(HTMLProperty):
1630 _marker = []
1632 def __init__(self, client, classname, nodeid, prop, name, value,
1633 anonymous=0, offset=None):
1634 HTMLProperty.__init__(self, client, classname, nodeid, prop, name,
1635 value, anonymous=anonymous)
1636 if self._value and not (isinstance(self._value, str) or
1637 isinstance(self._value, unicode)):
1638 self._value.setTranslator(self._client.translator)
1639 self._offset = offset
1640 if self._offset is None :
1641 self._offset = self._prop.offset (self._db)
1643 def plain(self, escape=0):
1644 """ Render a "plain" representation of the property
1645 """
1646 if not self.is_view_ok():
1647 return self._('[hidden]')
1649 if self._value is None:
1650 return ''
1651 if self._offset is None:
1652 offset = self._db.getUserTimezone()
1653 else:
1654 offset = self._offset
1655 return str(self._value.local(offset))
1657 def now(self, str_interval=None):
1658 """ Return the current time.
1660 This is useful for defaulting a new value. Returns a
1661 DateHTMLProperty.
1662 """
1663 if not self.is_view_ok():
1664 return self._('[hidden]')
1666 ret = date.Date('.', translator=self._client)
1668 if isinstance(str_interval, basestring):
1669 sign = 1
1670 if str_interval[0] == '-':
1671 sign = -1
1672 str_interval = str_interval[1:]
1673 interval = date.Interval(str_interval, translator=self._client)
1674 if sign > 0:
1675 ret = ret + interval
1676 else:
1677 ret = ret - interval
1679 return DateHTMLProperty(self._client, self._classname, self._nodeid,
1680 self._prop, self._formname, ret)
1682 def field(self, size=30, default=None, format=_marker, popcal=True,
1683 **kwargs):
1684 """Render a form edit field for the property
1686 If not editable, just display the value via plain().
1688 If "popcal" then include the Javascript calendar editor.
1689 Default=yes.
1691 The format string is a standard python strftime format string.
1692 """
1693 if not self.is_edit_ok():
1694 if format is self._marker:
1695 return self.plain(escape=1)
1696 else:
1697 return self.pretty(format)
1699 value = self._value
1701 if value is None:
1702 if default is None:
1703 raw_value = None
1704 else:
1705 if isinstance(default, basestring):
1706 raw_value = date.Date(default, translator=self._client)
1707 elif isinstance(default, date.Date):
1708 raw_value = default
1709 elif isinstance(default, DateHTMLProperty):
1710 raw_value = default._value
1711 else:
1712 raise ValueError, self._('default value for '
1713 'DateHTMLProperty must be either DateHTMLProperty '
1714 'or string date representation.')
1715 elif isinstance(value, str) or isinstance(value, unicode):
1716 # most likely erroneous input to be passed back to user
1717 if isinstance(value, unicode): value = value.encode('utf8')
1718 return self.input(name=self._formname, value=value, size=size,
1719 **kwargs)
1720 else:
1721 raw_value = value
1723 if raw_value is None:
1724 value = ''
1725 elif isinstance(raw_value, str) or isinstance(raw_value, unicode):
1726 if format is self._marker:
1727 value = raw_value
1728 else:
1729 value = date.Date(raw_value).pretty(format)
1730 else:
1731 if self._offset is None :
1732 offset = self._db.getUserTimezone()
1733 else :
1734 offset = self._offset
1735 value = raw_value.local(offset)
1736 if format is not self._marker:
1737 value = value.pretty(format)
1739 s = self.input(name=self._formname, value=value, size=size,
1740 **kwargs)
1741 if popcal:
1742 s += self.popcal()
1743 return s
1745 def reldate(self, pretty=1):
1746 """ Render the interval between the date and now.
1748 If the "pretty" flag is true, then make the display pretty.
1749 """
1750 if not self.is_view_ok():
1751 return self._('[hidden]')
1753 if not self._value:
1754 return ''
1756 # figure the interval
1757 interval = self._value - date.Date('.', translator=self._client)
1758 if pretty:
1759 return interval.pretty()
1760 return str(interval)
1762 def pretty(self, format=_marker):
1763 """ Render the date in a pretty format (eg. month names, spaces).
1765 The format string is a standard python strftime format string.
1766 Note that if the day is zero, and appears at the start of the
1767 string, then it'll be stripped from the output. This is handy
1768 for the situation when a date only specifies a month and a year.
1769 """
1770 if not self.is_view_ok():
1771 return self._('[hidden]')
1773 if self._offset is None:
1774 offset = self._db.getUserTimezone()
1775 else:
1776 offset = self._offset
1778 if not self._value:
1779 return ''
1780 elif format is not self._marker:
1781 return self._value.local(offset).pretty(format)
1782 else:
1783 return self._value.local(offset).pretty()
1785 def local(self, offset):
1786 """ Return the date/time as a local (timezone offset) date/time.
1787 """
1788 if not self.is_view_ok():
1789 return self._('[hidden]')
1791 return DateHTMLProperty(self._client, self._classname, self._nodeid,
1792 self._prop, self._formname, self._value, offset=offset)
1794 def popcal(self, width=300, height=200, label="(cal)",
1795 form="itemSynopsis"):
1796 """Generate a link to a calendar pop-up window.
1798 item: HTMLProperty e.g.: context.deadline
1799 """
1800 if self.isset():
1801 date = "&date=%s"%self._value
1802 else :
1803 date = ""
1804 return ('<a class="classhelp" href="javascript:help_window('
1805 "'%s?@template=calendar&property=%s&form=%s%s', %d, %d)"
1806 '">%s</a>'%(self._classname, self._name, form, date, width,
1807 height, label))
1809 class IntervalHTMLProperty(HTMLProperty):
1810 def __init__(self, client, classname, nodeid, prop, name, value,
1811 anonymous=0):
1812 HTMLProperty.__init__(self, client, classname, nodeid, prop,
1813 name, value, anonymous)
1814 if self._value and not isinstance(self._value, (str, unicode)):
1815 self._value.setTranslator(self._client.translator)
1817 def plain(self, escape=0):
1818 """ Render a "plain" representation of the property
1819 """
1820 if not self.is_view_ok():
1821 return self._('[hidden]')
1823 if self._value is None:
1824 return ''
1825 return str(self._value)
1827 def pretty(self):
1828 """ Render the interval in a pretty format (eg. "yesterday")
1829 """
1830 if not self.is_view_ok():
1831 return self._('[hidden]')
1833 return self._value.pretty()
1835 def field(self, size=30, **kwargs):
1836 """ Render a form edit field for the property
1838 If not editable, just display the value via plain().
1839 """
1840 if not self.is_edit_ok():
1841 return self.plain(escape=1)
1843 value = self._value
1844 if value is None:
1845 value = ''
1847 return self.input(name=self._formname, value=value, size=size,
1848 **kwargs)
1850 class LinkHTMLProperty(HTMLProperty):
1851 """ Link HTMLProperty
1852 Include the above as well as being able to access the class
1853 information. Stringifying the object itself results in the value
1854 from the item being displayed. Accessing attributes of this object
1855 result in the appropriate entry from the class being queried for the
1856 property accessed (so item/assignedto/name would look up the user
1857 entry identified by the assignedto property on item, and then the
1858 name property of that user)
1859 """
1860 def __init__(self, *args, **kw):
1861 HTMLProperty.__init__(self, *args, **kw)
1862 # if we're representing a form value, then the -1 from the form really
1863 # should be a None
1864 if str(self._value) == '-1':
1865 self._value = None
1867 def __getattr__(self, attr):
1868 """ return a new HTMLItem """
1869 if not self._value:
1870 # handle a special page templates lookup
1871 if attr == '__render_with_namespace__':
1872 def nothing(*args, **kw):
1873 return ''
1874 return nothing
1875 msg = self._('Attempt to look up %(attr)s on a missing value')
1876 return MissingValue(msg%locals())
1877 i = HTMLItem(self._client, self._prop.classname, self._value)
1878 return getattr(i, attr)
1880 def plain(self, escape=0):
1881 """ Render a "plain" representation of the property
1882 """
1883 if not self.is_view_ok():
1884 return self._('[hidden]')
1886 if self._value is None:
1887 return ''
1888 linkcl = self._db.classes[self._prop.classname]
1889 k = linkcl.labelprop(1)
1890 if num_re.match(self._value):
1891 try:
1892 value = str(linkcl.get(self._value, k))
1893 except IndexError:
1894 value = self._value
1895 else :
1896 value = self._value
1897 if escape:
1898 value = cgi.escape(value)
1899 return value
1901 def field(self, showid=0, size=None, **kwargs):
1902 """ Render a form edit field for the property
1904 If not editable, just display the value via plain().
1905 """
1906 if not self.is_edit_ok():
1907 return self.plain(escape=1)
1909 # edit field
1910 linkcl = self._db.getclass(self._prop.classname)
1911 if self._value is None:
1912 value = ''
1913 else:
1914 k = linkcl.getkey()
1915 if k and num_re.match(self._value):
1916 value = linkcl.get(self._value, k)
1917 else:
1918 value = self._value
1919 return self.input(name=self._formname, value=value, size=size,
1920 **kwargs)
1922 def menu(self, size=None, height=None, showid=0, additional=[], value=None,
1923 sort_on=None, html_kwargs = {}, **conditions):
1924 """ Render a form select list for this property
1926 "size" is used to limit the length of the list labels
1927 "height" is used to set the <select> tag's "size" attribute
1928 "showid" includes the item ids in the list labels
1929 "value" specifies which item is pre-selected
1930 "additional" lists properties which should be included in the
1931 label
1932 "sort_on" indicates the property to sort the list on as
1933 (direction, property) where direction is '+' or '-'. A
1934 single string with the direction prepended may be used.
1935 For example: ('-', 'order'), '+name'.
1937 The remaining keyword arguments are used as conditions for
1938 filtering the items in the list - they're passed as the
1939 "filterspec" argument to a Class.filter() call.
1941 If not editable, just display the value via plain().
1942 """
1943 if not self.is_edit_ok():
1944 return self.plain(escape=1)
1946 # Since None indicates the default, we need another way to
1947 # indicate "no selection". We use -1 for this purpose, as
1948 # that is the value we use when submitting a form without the
1949 # value set.
1950 if value is None:
1951 value = self._value
1952 elif value == '-1':
1953 value = None
1955 linkcl = self._db.getclass(self._prop.classname)
1956 l = ['<select %s>'%cgi_escape_attrs(name = self._formname,
1957 **html_kwargs)]
1958 k = linkcl.labelprop(1)
1959 s = ''
1960 if value is None:
1961 s = 'selected="selected" '
1962 l.append(self._('<option %svalue="-1">- no selection -</option>')%s)
1964 if sort_on is not None:
1965 if not isinstance(sort_on, tuple):
1966 if sort_on[0] in '+-':
1967 sort_on = (sort_on[0], sort_on[1:])
1968 else:
1969 sort_on = ('+', sort_on)
1970 else:
1971 sort_on = ('+', linkcl.orderprop())
1973 options = [opt
1974 for opt in linkcl.filter(None, conditions, sort_on, (None, None))
1975 if self._db.security.hasPermission("View", self._client.userid,
1976 linkcl.classname, itemid=opt)]
1978 # make sure we list the current value if it's retired
1979 if value and value not in options:
1980 options.insert(0, value)
1982 if additional:
1983 additional_fns = []
1984 props = linkcl.getprops()
1985 for propname in additional:
1986 prop = props[propname]
1987 if isinstance(prop, hyperdb.Link):
1988 cl = self._db.getclass(prop.classname)
1989 labelprop = cl.labelprop()
1990 fn = lambda optionid: cl.get(linkcl.get(optionid,
1991 propname),
1992 labelprop)
1993 else:
1994 fn = lambda optionid: linkcl.get(optionid, propname)
1995 additional_fns.append(fn)
1997 for optionid in options:
1998 # get the option value, and if it's None use an empty string
1999 option = linkcl.get(optionid, k) or ''
2001 # figure if this option is selected
2002 s = ''
2003 if value in [optionid, option]:
2004 s = 'selected="selected" '
2006 # figure the label
2007 if showid:
2008 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
2009 elif not option:
2010 lab = '%s%s'%(self._prop.classname, optionid)
2011 else:
2012 lab = option
2014 # truncate if it's too long
2015 if size is not None and len(lab) > size:
2016 lab = lab[:size-3] + '...'
2017 if additional:
2018 m = []
2019 for fn in additional_fns:
2020 m.append(str(fn(optionid)))
2021 lab = lab + ' (%s)'%', '.join(m)
2023 # and generate
2024 lab = cgi.escape(self._(lab))
2025 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
2026 l.append('</select>')
2027 return '\n'.join(l)
2028 # def checklist(self, ...)
2032 class MultilinkHTMLProperty(HTMLProperty):
2033 """ Multilink HTMLProperty
2035 Also be iterable, returning a wrapper object like the Link case for
2036 each entry in the multilink.
2037 """
2038 def __init__(self, *args, **kwargs):
2039 HTMLProperty.__init__(self, *args, **kwargs)
2040 if self._value:
2041 display_value = lookupIds(self._db, self._prop, self._value,
2042 fail_ok=1, do_lookup=False)
2043 sortfun = make_sort_function(self._db, self._prop.classname)
2044 # sorting fails if the value contains
2045 # items not yet stored in the database
2046 # ignore these errors to preserve user input
2047 try:
2048 display_value.sort(sortfun)
2049 except:
2050 pass
2051 self._value = display_value
2053 def __len__(self):
2054 """ length of the multilink """
2055 return len(self._value)
2057 def __getattr__(self, attr):
2058 """ no extended attribute accesses make sense here """
2059 raise AttributeError, attr
2061 def viewableGenerator(self, values):
2062 """Used to iterate over only the View'able items in a class."""
2063 check = self._db.security.hasPermission
2064 userid = self._client.userid
2065 classname = self._prop.classname
2066 for value in values:
2067 if check('View', userid, classname, itemid=value):
2068 yield HTMLItem(self._client, classname, value)
2070 def __iter__(self):
2071 """ iterate and return a new HTMLItem
2072 """
2073 return self.viewableGenerator(self._value)
2075 def reverse(self):
2076 """ return the list in reverse order
2077 """
2078 l = self._value[:]
2079 l.reverse()
2080 return self.viewableGenerator(l)
2082 def sorted(self, property):
2083 """ Return this multilink sorted by the given property """
2084 value = list(self.__iter__())
2085 value.sort(lambda a,b:cmp(a[property], b[property]))
2086 return value
2088 def __contains__(self, value):
2089 """ Support the "in" operator. We have to make sure the passed-in
2090 value is a string first, not a HTMLProperty.
2091 """
2092 return str(value) in self._value
2094 def isset(self):
2095 """Is my _value not []?"""
2096 return self._value != []
2098 def plain(self, escape=0):
2099 """ Render a "plain" representation of the property
2100 """
2101 if not self.is_view_ok():
2102 return self._('[hidden]')
2104 linkcl = self._db.classes[self._prop.classname]
2105 k = linkcl.labelprop(1)
2106 labels = []
2107 for v in self._value:
2108 if num_re.match(v):
2109 try:
2110 label = linkcl.get(v, k)
2111 except IndexError:
2112 label = None
2113 # fall back to designator if label is None
2114 if label is None: label = '%s%s'%(self._prop.classname, k)
2115 else:
2116 label = v
2117 labels.append(label)
2118 value = ', '.join(labels)
2119 if escape:
2120 value = cgi.escape(value)
2121 return value
2123 def field(self, size=30, showid=0, **kwargs):
2124 """ Render a form edit field for the property
2126 If not editable, just display the value via plain().
2127 """
2128 if not self.is_edit_ok():
2129 return self.plain(escape=1)
2131 linkcl = self._db.getclass(self._prop.classname)
2132 value = self._value[:]
2133 # map the id to the label property
2134 if not linkcl.getkey():
2135 showid=1
2136 if not showid:
2137 k = linkcl.labelprop(1)
2138 value = lookupKeys(linkcl, k, value)
2139 value = ','.join(value)
2140 return self.input(name=self._formname, size=size, value=value,
2141 **kwargs)
2143 def menu(self, size=None, height=None, showid=0, additional=[],
2144 value=None, sort_on=None, html_kwargs = {}, **conditions):
2145 """ Render a form <select> list for this property.
2147 "size" is used to limit the length of the list labels
2148 "height" is used to set the <select> tag's "size" attribute
2149 "showid" includes the item ids in the list labels
2150 "additional" lists properties which should be included in the
2151 label
2152 "value" specifies which item is pre-selected
2153 "sort_on" indicates the property to sort the list on as
2154 (direction, property) where direction is '+' or '-'. A
2155 single string with the direction prepended may be used.
2156 For example: ('-', 'order'), '+name'.
2158 The remaining keyword arguments are used as conditions for
2159 filtering the items in the list - they're passed as the
2160 "filterspec" argument to a Class.filter() call.
2162 If not editable, just display the value via plain().
2163 """
2164 if not self.is_edit_ok():
2165 return self.plain(escape=1)
2167 if value is None:
2168 value = self._value
2170 linkcl = self._db.getclass(self._prop.classname)
2172 if sort_on is not None:
2173 if not isinstance(sort_on, tuple):
2174 if sort_on[0] in '+-':
2175 sort_on = (sort_on[0], sort_on[1:])
2176 else:
2177 sort_on = ('+', sort_on)
2178 else:
2179 sort_on = ('+', linkcl.orderprop())
2181 options = [opt
2182 for opt in linkcl.filter(None, conditions, sort_on)
2183 if self._db.security.hasPermission("View", self._client.userid,
2184 linkcl.classname, itemid=opt)]
2186 # make sure we list the current values if they're retired
2187 for val in value:
2188 if val not in options:
2189 options.insert(0, val)
2191 if not height:
2192 height = len(options)
2193 if value:
2194 # The "no selection" option.
2195 height += 1
2196 height = min(height, 7)
2197 l = ['<select multiple %s>'%cgi_escape_attrs(name = self._formname,
2198 size = height,
2199 **html_kwargs)]
2200 k = linkcl.labelprop(1)
2202 if value:
2203 l.append('<option value="%s">- no selection -</option>'
2204 % ','.join(['-' + v for v in value]))
2206 if additional:
2207 additional_fns = []
2208 props = linkcl.getprops()
2209 for propname in additional:
2210 prop = props[propname]
2211 if isinstance(prop, hyperdb.Link):
2212 cl = self._db.getclass(prop.classname)
2213 labelprop = cl.labelprop()
2214 fn = lambda optionid: cl.get(linkcl.get(optionid,
2215 propname),
2216 labelprop)
2217 else:
2218 fn = lambda optionid: linkcl.get(optionid, propname)
2219 additional_fns.append(fn)
2221 for optionid in options:
2222 # get the option value, and if it's None use an empty string
2223 option = linkcl.get(optionid, k) or ''
2225 # figure if this option is selected
2226 s = ''
2227 if optionid in value or option in value:
2228 s = 'selected="selected" '
2230 # figure the label
2231 if showid:
2232 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
2233 else:
2234 lab = option
2235 # truncate if it's too long
2236 if size is not None and len(lab) > size:
2237 lab = lab[:size-3] + '...'
2238 if additional:
2239 m = []
2240 for fn in additional_fns:
2241 m.append(str(fn(optionid)))
2242 lab = lab + ' (%s)'%', '.join(m)
2244 # and generate
2245 lab = cgi.escape(self._(lab))
2246 l.append('<option %svalue="%s">%s</option>'%(s, optionid,
2247 lab))
2248 l.append('</select>')
2249 return '\n'.join(l)
2251 # set the propclasses for HTMLItem
2252 propclasses = (
2253 (hyperdb.String, StringHTMLProperty),
2254 (hyperdb.Number, NumberHTMLProperty),
2255 (hyperdb.Boolean, BooleanHTMLProperty),
2256 (hyperdb.Date, DateHTMLProperty),
2257 (hyperdb.Interval, IntervalHTMLProperty),
2258 (hyperdb.Password, PasswordHTMLProperty),
2259 (hyperdb.Link, LinkHTMLProperty),
2260 (hyperdb.Multilink, MultilinkHTMLProperty),
2261 )
2263 def make_sort_function(db, classname, sort_on=None):
2264 """Make a sort function for a given class
2265 """
2266 linkcl = db.getclass(classname)
2267 if sort_on is None:
2268 sort_on = linkcl.orderprop()
2269 def sortfunc(a, b):
2270 return cmp(linkcl.get(a, sort_on), linkcl.get(b, sort_on))
2271 return sortfunc
2273 def handleListCGIValue(value):
2274 """ Value is either a single item or a list of items. Each item has a
2275 .value that we're actually interested in.
2276 """
2277 if isinstance(value, type([])):
2278 return [value.value for value in value]
2279 else:
2280 value = value.value.strip()
2281 if not value:
2282 return []
2283 return [v.strip() for v in value.split(',')]
2285 class HTMLRequest(HTMLInputMixin):
2286 """The *request*, holding the CGI form and environment.
2288 - "form" the CGI form as a cgi.FieldStorage
2289 - "env" the CGI environment variables
2290 - "base" the base URL for this instance
2291 - "user" a HTMLItem instance for this user
2292 - "language" as determined by the browser or config
2293 - "classname" the current classname (possibly None)
2294 - "template" the current template (suffix, also possibly None)
2296 Index args:
2298 - "columns" dictionary of the columns to display in an index page
2299 - "show" a convenience access to columns - request/show/colname will
2300 be true if the columns should be displayed, false otherwise
2301 - "sort" index sort column (direction, column name)
2302 - "group" index grouping property (direction, column name)
2303 - "filter" properties to filter the index on
2304 - "filterspec" values to filter the index on
2305 - "search_text" text to perform a full-text search on for an index
2306 """
2307 def __repr__(self):
2308 return '<HTMLRequest %r>'%self.__dict__
2310 def __init__(self, client):
2311 # _client is needed by HTMLInputMixin
2312 self._client = self.client = client
2314 # easier access vars
2315 self.form = client.form
2316 self.env = client.env
2317 self.base = client.base
2318 self.user = HTMLItem(client, 'user', client.userid)
2319 self.language = client.language
2321 # store the current class name and action
2322 self.classname = client.classname
2323 self.nodeid = client.nodeid
2324 self.template = client.template
2326 # the special char to use for special vars
2327 self.special_char = '@'
2329 HTMLInputMixin.__init__(self)
2331 self._post_init()
2333 def current_url(self):
2334 url = self.base
2335 if self.classname:
2336 url += self.classname
2337 if self.nodeid:
2338 url += self.nodeid
2339 args = {}
2340 if self.template:
2341 args['@template'] = self.template
2342 return self.indexargs_url(url, args)
2344 def _parse_sort(self, var, name):
2345 """ Parse sort/group options. Append to var
2346 """
2347 fields = []
2348 dirs = []
2349 for special in '@:':
2350 idx = 0
2351 key = '%s%s%d'%(special, name, idx)
2352 while key in self.form:
2353 self.special_char = special
2354 fields.append(self.form.getfirst(key))
2355 dirkey = '%s%sdir%d'%(special, name, idx)
2356 if dirkey in self.form:
2357 dirs.append(self.form.getfirst(dirkey))
2358 else:
2359 dirs.append(None)
2360 idx += 1
2361 key = '%s%s%d'%(special, name, idx)
2362 # backward compatible (and query) URL format
2363 key = special + name
2364 dirkey = key + 'dir'
2365 if key in self.form and not fields:
2366 fields = handleListCGIValue(self.form[key])
2367 if dirkey in self.form:
2368 dirs.append(self.form.getfirst(dirkey))
2369 if fields: # only try other special char if nothing found
2370 break
2371 for f, d in map(None, fields, dirs):
2372 if f.startswith('-'):
2373 var.append(('-', f[1:]))
2374 elif d:
2375 var.append(('-', f))
2376 else:
2377 var.append(('+', f))
2379 def _post_init(self):
2380 """ Set attributes based on self.form
2381 """
2382 # extract the index display information from the form
2383 self.columns = []
2384 for name in ':columns @columns'.split():
2385 if self.form.has_key(name):
2386 self.special_char = name[0]
2387 self.columns = handleListCGIValue(self.form[name])
2388 break
2389 self.show = support.TruthDict(self.columns)
2391 # sorting and grouping
2392 self.sort = []
2393 self.group = []
2394 self._parse_sort(self.sort, 'sort')
2395 self._parse_sort(self.group, 'group')
2397 # filtering
2398 self.filter = []
2399 for name in ':filter @filter'.split():
2400 if self.form.has_key(name):
2401 self.special_char = name[0]
2402 self.filter = handleListCGIValue(self.form[name])
2404 self.filterspec = {}
2405 db = self.client.db
2406 if self.classname is not None:
2407 cls = db.getclass (self.classname)
2408 for name in self.filter:
2409 if not self.form.has_key(name):
2410 continue
2411 prop = cls.get_transitive_prop (name)
2412 fv = self.form[name]
2413 if (isinstance(prop, hyperdb.Link) or
2414 isinstance(prop, hyperdb.Multilink)):
2415 self.filterspec[name] = lookupIds(db, prop,
2416 handleListCGIValue(fv))
2417 else:
2418 if isinstance(fv, type([])):
2419 self.filterspec[name] = [v.value for v in fv]
2420 elif name == 'id':
2421 # special case "id" property
2422 self.filterspec[name] = handleListCGIValue(fv)
2423 else:
2424 self.filterspec[name] = fv.value
2426 # full-text search argument
2427 self.search_text = None
2428 for name in ':search_text @search_text'.split():
2429 if self.form.has_key(name):
2430 self.special_char = name[0]
2431 self.search_text = self.form.getfirst(name)
2433 # pagination - size and start index
2434 # figure batch args
2435 self.pagesize = 50
2436 for name in ':pagesize @pagesize'.split():
2437 if self.form.has_key(name):
2438 self.special_char = name[0]
2439 try:
2440 self.pagesize = int(self.form.getfirst(name))
2441 except ValueError:
2442 # not an integer - ignore
2443 pass
2445 self.startwith = 0
2446 for name in ':startwith @startwith'.split():
2447 if self.form.has_key(name):
2448 self.special_char = name[0]
2449 try:
2450 self.startwith = int(self.form.getfirst(name))
2451 except ValueError:
2452 # not an integer - ignore
2453 pass
2455 # dispname
2456 if self.form.has_key('@dispname'):
2457 self.dispname = self.form.getfirst('@dispname')
2458 else:
2459 self.dispname = None
2461 def updateFromURL(self, url):
2462 """ Parse the URL for query args, and update my attributes using the
2463 values.
2464 """
2465 env = {'QUERY_STRING': url}
2466 self.form = cgi.FieldStorage(environ=env)
2468 self._post_init()
2470 def update(self, kwargs):
2471 """ Update my attributes using the keyword args
2472 """
2473 self.__dict__.update(kwargs)
2474 if kwargs.has_key('columns'):
2475 self.show = support.TruthDict(self.columns)
2477 def description(self):
2478 """ Return a description of the request - handle for the page title.
2479 """
2480 s = [self.client.db.config.TRACKER_NAME]
2481 if self.classname:
2482 if self.client.nodeid:
2483 s.append('- %s%s'%(self.classname, self.client.nodeid))
2484 else:
2485 if self.template == 'item':
2486 s.append('- new %s'%self.classname)
2487 elif self.template == 'index':
2488 s.append('- %s index'%self.classname)
2489 else:
2490 s.append('- %s %s'%(self.classname, self.template))
2491 else:
2492 s.append('- home')
2493 return ' '.join(s)
2495 def __str__(self):
2496 d = {}
2497 d.update(self.__dict__)
2498 f = ''
2499 for k in self.form.keys():
2500 f += '\n %r=%r'%(k,handleListCGIValue(self.form[k]))
2501 d['form'] = f
2502 e = ''
2503 for k,v in self.env.items():
2504 e += '\n %r=%r'%(k, v)
2505 d['env'] = e
2506 return """
2507 form: %(form)s
2508 base: %(base)r
2509 classname: %(classname)r
2510 template: %(template)r
2511 columns: %(columns)r
2512 sort: %(sort)r
2513 group: %(group)r
2514 filter: %(filter)r
2515 search_text: %(search_text)r
2516 pagesize: %(pagesize)r
2517 startwith: %(startwith)r
2518 env: %(env)s
2519 """%d
2521 def indexargs_form(self, columns=1, sort=1, group=1, filter=1,
2522 filterspec=1, search_text=1):
2523 """ return the current index args as form elements """
2524 l = []
2525 sc = self.special_char
2526 def add(k, v):
2527 l.append(self.input(type="hidden", name=k, value=v))
2528 if columns and self.columns:
2529 add(sc+'columns', ','.join(self.columns))
2530 if sort:
2531 val = []
2532 for dir, attr in self.sort:
2533 if dir == '-':
2534 val.append('-'+attr)
2535 else:
2536 val.append(attr)
2537 add(sc+'sort', ','.join (val))
2538 if group:
2539 val = []
2540 for dir, attr in self.group:
2541 if dir == '-':
2542 val.append('-'+attr)
2543 else:
2544 val.append(attr)
2545 add(sc+'group', ','.join (val))
2546 if filter and self.filter:
2547 add(sc+'filter', ','.join(self.filter))
2548 if self.classname and filterspec:
2549 cls = self.client.db.getclass(self.classname)
2550 for k,v in self.filterspec.items():
2551 if type(v) == type([]):
2552 if isinstance(cls.get_transitive_prop(k), hyperdb.String):
2553 add(k, ' '.join(v))
2554 else:
2555 add(k, ','.join(v))
2556 else:
2557 add(k, v)
2558 if search_text and self.search_text:
2559 add(sc+'search_text', self.search_text)
2560 add(sc+'pagesize', self.pagesize)
2561 add(sc+'startwith', self.startwith)
2562 return '\n'.join(l)
2564 def indexargs_url(self, url, args):
2565 """ Embed the current index args in a URL
2566 """
2567 q = urllib.quote
2568 sc = self.special_char
2569 l = ['%s=%s'%(k,v) for k,v in args.items()]
2571 # pull out the special values (prefixed by @ or :)
2572 specials = {}
2573 for key in args.keys():
2574 if key[0] in '@:':
2575 specials[key[1:]] = args[key]
2577 # ok, now handle the specials we received in the request
2578 if self.columns and not specials.has_key('columns'):
2579 l.append(sc+'columns=%s'%(','.join(self.columns)))
2580 if self.sort and not specials.has_key('sort'):
2581 val = []
2582 for dir, attr in self.sort:
2583 if dir == '-':
2584 val.append('-'+attr)
2585 else:
2586 val.append(attr)
2587 l.append(sc+'sort=%s'%(','.join(val)))
2588 if self.group and not specials.has_key('group'):
2589 val = []
2590 for dir, attr in self.group:
2591 if dir == '-':
2592 val.append('-'+attr)
2593 else:
2594 val.append(attr)
2595 l.append(sc+'group=%s'%(','.join(val)))
2596 if self.filter and not specials.has_key('filter'):
2597 l.append(sc+'filter=%s'%(','.join(self.filter)))
2598 if self.search_text and not specials.has_key('search_text'):
2599 l.append(sc+'search_text=%s'%q(self.search_text))
2600 if not specials.has_key('pagesize'):
2601 l.append(sc+'pagesize=%s'%self.pagesize)
2602 if not specials.has_key('startwith'):
2603 l.append(sc+'startwith=%s'%self.startwith)
2605 # finally, the remainder of the filter args in the request
2606 if self.classname and self.filterspec:
2607 cls = self.client.db.getclass(self.classname)
2608 for k,v in self.filterspec.items():
2609 if not args.has_key(k):
2610 if type(v) == type([]):
2611 prop = cls.get_transitive_prop(k)
2612 if k != 'id' and isinstance(prop, hyperdb.String):
2613 l.append('%s=%s'%(k, '%20'.join([q(i) for i in v])))
2614 else:
2615 l.append('%s=%s'%(k, ','.join([q(i) for i in v])))
2616 else:
2617 l.append('%s=%s'%(k, q(v)))
2618 return '%s?%s'%(url, '&'.join(l))
2619 indexargs_href = indexargs_url
2621 def base_javascript(self):
2622 return """
2623 <script type="text/javascript">
2624 submitted = false;
2625 function submit_once() {
2626 if (submitted) {
2627 alert("Your request is being processed.\\nPlease be patient.");
2628 event.returnValue = 0; // work-around for IE
2629 return 0;
2630 }
2631 submitted = true;
2632 return 1;
2633 }
2635 function help_window(helpurl, width, height) {
2636 HelpWin = window.open('%s' + helpurl, 'RoundupHelpWindow', 'scrollbars=yes,resizable=yes,toolbar=no,height='+height+',width='+width);
2637 }
2638 </script>
2639 """%self.base
2641 def batch(self):
2642 """ Return a batch object for results from the "current search"
2643 """
2644 filterspec = self.filterspec
2645 sort = self.sort
2646 group = self.group
2648 # get the list of ids we're batching over
2649 klass = self.client.db.getclass(self.classname)
2650 if self.search_text:
2651 matches = self.client.db.indexer.search(
2652 [w.upper().encode("utf-8", "replace") for w in re.findall(
2653 r'(?u)\b\w{2,25}\b',
2654 unicode(self.search_text, "utf-8", "replace")
2655 )], klass)
2656 else:
2657 matches = None
2659 # filter for visibility
2660 check = self._client.db.security.hasPermission
2661 userid = self._client.userid
2662 l = [id for id in klass.filter(matches, filterspec, sort, group)
2663 if check('View', userid, self.classname, itemid=id)]
2665 # return the batch object, using IDs only
2666 return Batch(self.client, l, self.pagesize, self.startwith,
2667 classname=self.classname)
2669 # extend the standard ZTUtils Batch object to remove dependency on
2670 # Acquisition and add a couple of useful methods
2671 class Batch(ZTUtils.Batch):
2672 """ Use me to turn a list of items, or item ids of a given class, into a
2673 series of batches.
2675 ========= ========================================================
2676 Parameter Usage
2677 ========= ========================================================
2678 sequence a list of HTMLItems or item ids
2679 classname if sequence is a list of ids, this is the class of item
2680 size how big to make the sequence.
2681 start where to start (0-indexed) in the sequence.
2682 end where to end (0-indexed) in the sequence.
2683 orphan if the next batch would contain less items than this
2684 value, then it is combined with this batch
2685 overlap the number of items shared between adjacent batches
2686 ========= ========================================================
2688 Attributes: Note that the "start" attribute, unlike the
2689 argument, is a 1-based index (I know, lame). "first" is the
2690 0-based index. "length" is the actual number of elements in
2691 the batch.
2693 "sequence_length" is the length of the original, unbatched, sequence.
2694 """
2695 def __init__(self, client, sequence, size, start, end=0, orphan=0,
2696 overlap=0, classname=None):
2697 self.client = client
2698 self.last_index = self.last_item = None
2699 self.current_item = None
2700 self.classname = classname
2701 self.sequence_length = len(sequence)
2702 ZTUtils.Batch.__init__(self, sequence, size, start, end, orphan,
2703 overlap)
2705 # overwrite so we can late-instantiate the HTMLItem instance
2706 def __getitem__(self, index):
2707 if index < 0:
2708 if index + self.end < self.first: raise IndexError, index
2709 return self._sequence[index + self.end]
2711 if index >= self.length:
2712 raise IndexError, index
2714 # move the last_item along - but only if the fetched index changes
2715 # (for some reason, index 0 is fetched twice)
2716 if index != self.last_index:
2717 self.last_item = self.current_item
2718 self.last_index = index
2720 item = self._sequence[index + self.first]
2721 if self.classname:
2722 # map the item ids to instances
2723 item = HTMLItem(self.client, self.classname, item)
2724 self.current_item = item
2725 return item
2727 def propchanged(self, *properties):
2728 """ Detect if one of the properties marked as being a group
2729 property changed in the last iteration fetch
2730 """
2731 # we poke directly at the _value here since MissingValue can screw
2732 # us up and cause Nones to compare strangely
2733 if self.last_item is None:
2734 return 1
2735 for property in properties:
2736 if property == 'id' or isinstance (self.last_item[property], list):
2737 if (str(self.last_item[property]) !=
2738 str(self.current_item[property])):
2739 return 1
2740 else:
2741 if (self.last_item[property]._value !=
2742 self.current_item[property]._value):
2743 return 1
2744 return 0
2746 # override these 'cos we don't have access to acquisition
2747 def previous(self):
2748 if self.start == 1:
2749 return None
2750 return Batch(self.client, self._sequence, self._size,
2751 self.first - self._size + self.overlap, 0, self.orphan,
2752 self.overlap)
2754 def next(self):
2755 try:
2756 self._sequence[self.end]
2757 except IndexError:
2758 return None
2759 return Batch(self.client, self._sequence, self._size,
2760 self.end - self.overlap, 0, self.orphan, self.overlap)
2762 class TemplatingUtils:
2763 """ Utilities for templating
2764 """
2765 def __init__(self, client):
2766 self.client = client
2767 def Batch(self, sequence, size, start, end=0, orphan=0, overlap=0):
2768 return Batch(self.client, sequence, size, start, end, orphan,
2769 overlap)
2771 def url_quote(self, url):
2772 """URL-quote the supplied text."""
2773 return urllib.quote(url)
2775 def html_quote(self, html):
2776 """HTML-quote the supplied text."""
2777 return cgi.escape(html)
2779 def __getattr__(self, name):
2780 """Try the tracker's templating_utils."""
2781 if not hasattr(self.client.instance, 'templating_utils'):
2782 # backwards-compatibility
2783 raise AttributeError, name
2784 if not self.client.instance.templating_utils.has_key(name):
2785 raise AttributeError, name
2786 return self.client.instance.templating_utils[name]
2788 def html_calendar(self, request):
2789 """Generate a HTML calendar.
2791 `request` the roundup.request object
2792 - @template : name of the template
2793 - form : name of the form to store back the date
2794 - property : name of the property of the form to store
2795 back the date
2796 - date : current date
2797 - display : when browsing, specifies year and month
2799 html will simply be a table.
2800 """
2801 date_str = request.form.getfirst("date", ".")
2802 display = request.form.getfirst("display", date_str)
2803 template = request.form.getfirst("@template", "calendar")
2804 form = request.form.getfirst("form")
2805 property = request.form.getfirst("property")
2806 curr_date = date.Date(date_str) # to highlight
2807 display = date.Date(display) # to show
2808 day = display.day
2810 # for navigation
2811 date_prev_month = display + date.Interval("-1m")
2812 date_next_month = display + date.Interval("+1m")
2813 date_prev_year = display + date.Interval("-1y")
2814 date_next_year = display + date.Interval("+1y")
2816 res = []
2818 base_link = "%s?@template=%s&property=%s&form=%s&date=%s" % \
2819 (request.classname, template, property, form, curr_date)
2821 # navigation
2822 # month
2823 res.append('<table class="calendar"><tr><td>')
2824 res.append(' <table width="100%" class="calendar_nav"><tr>')
2825 link = "&display=%s"%date_prev_month
2826 res.append(' <td><a href="%s&display=%s"><</a></td>'%(base_link,
2827 date_prev_month))
2828 res.append(' <td>%s</td>'%calendar.month_name[display.month])
2829 res.append(' <td><a href="%s&display=%s">></a></td>'%(base_link,
2830 date_next_month))
2831 # spacer
2832 res.append(' <td width="100%"></td>')
2833 # year
2834 res.append(' <td><a href="%s&display=%s"><</a></td>'%(base_link,
2835 date_prev_year))
2836 res.append(' <td>%s</td>'%display.year)
2837 res.append(' <td><a href="%s&display=%s">></a></td>'%(base_link,
2838 date_next_year))
2839 res.append(' </tr></table>')
2840 res.append(' </td></tr>')
2842 # the calendar
2843 res.append(' <tr><td><table class="calendar_display">')
2844 res.append(' <tr class="weekdays">')
2845 for day in calendar.weekheader(3).split():
2846 res.append(' <td>%s</td>'%day)
2847 res.append(' </tr>')
2848 for week in calendar.monthcalendar(display.year, display.month):
2849 res.append(' <tr>')
2850 for day in week:
2851 link = "javascript:form[field].value = '%d-%02d-%02d'; " \
2852 "window.close ();"%(display.year, display.month, day)
2853 if (day == curr_date.day and display.month == curr_date.month
2854 and display.year == curr_date.year):
2855 # highlight
2856 style = "today"
2857 else :
2858 style = ""
2859 if day:
2860 res.append(' <td class="%s"><a href="%s">%s</a></td>'%(
2861 style, link, day))
2862 else :
2863 res.append(' <td></td>')
2864 res.append(' </tr>')
2865 res.append('</table></td></tr></table>')
2866 return "\n".join(res)
2868 class MissingValue:
2869 def __init__(self, description, **kwargs):
2870 self.__description = description
2871 for key, value in kwargs.items():
2872 self.__dict__[key] = value
2874 def __call__(self, *args, **kwargs): return MissingValue(self.__description)
2875 def __getattr__(self, name):
2876 # This allows assignments which assume all intermediate steps are Null
2877 # objects if they don't exist yet.
2878 #
2879 # For example (with just 'client' defined):
2880 #
2881 # client.db.config.TRACKER_WEB = 'BASE/'
2882 self.__dict__[name] = MissingValue(self.__description)
2883 return getattr(self, name)
2885 def __getitem__(self, key): return self
2886 def __nonzero__(self): return 0
2887 def __str__(self): return '[%s]'%self.__description
2888 def __repr__(self): return '<MissingValue 0x%x "%s">'%(id(self),
2889 self.__description)
2890 def gettext(self, str): return str
2891 _ = gettext
2893 # vim: set et sts=4 sw=4 :