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 from KeywordsExpr import render_keywords_expression_editor
32 try:
33 import cPickle as pickle
34 except ImportError:
35 import pickle
36 try:
37 import cStringIO as StringIO
38 except ImportError:
39 import StringIO
40 try:
41 from StructuredText.StructuredText import HTML as StructuredText
42 except ImportError:
43 try: # older version
44 import StructuredText
45 except ImportError:
46 StructuredText = None
47 try:
48 from docutils.core import publish_parts as ReStructuredText
49 except ImportError:
50 ReStructuredText = None
52 # bring in the templating support
53 from roundup.cgi.PageTemplates import PageTemplate, GlobalTranslationService
54 from roundup.cgi.PageTemplates.Expressions import getEngine
55 from roundup.cgi.TAL import TALInterpreter
56 from roundup.cgi import TranslationService, ZTUtils
58 ### i18n services
59 # this global translation service is not thread-safe.
60 # it is left here for backward compatibility
61 # until all Web UI translations are done via client.translator object
62 translationService = TranslationService.get_translation()
63 GlobalTranslationService.setGlobalTranslationService(translationService)
65 ### templating
67 class NoTemplate(Exception):
68 pass
70 class Unauthorised(Exception):
71 def __init__(self, action, klass, translator=None):
72 self.action = action
73 self.klass = klass
74 if translator:
75 self._ = translator.gettext
76 else:
77 self._ = TranslationService.get_translation().gettext
78 def __str__(self):
79 return self._('You are not allowed to %(action)s '
80 'items of class %(class)s') % {
81 'action': self.action, 'class': self.klass}
83 def find_template(dir, name, view):
84 """ Find a template in the nominated dir
85 """
86 # find the source
87 if view:
88 filename = '%s.%s'%(name, view)
89 else:
90 filename = name
92 # try old-style
93 src = os.path.join(dir, filename)
94 if os.path.exists(src):
95 return (src, filename)
97 # try with a .html or .xml extension (new-style)
98 for extension in '.html', '.xml':
99 f = filename + extension
100 src = os.path.join(dir, f)
101 if os.path.exists(src):
102 return (src, f)
104 # no view == no generic template is possible
105 if not view:
106 raise NoTemplate, 'Template file "%s" doesn\'t exist'%name
108 # try for a _generic template
109 generic = '_generic.%s'%view
110 src = os.path.join(dir, generic)
111 if os.path.exists(src):
112 return (src, generic)
114 # finally, try _generic.html
115 generic = generic + '.html'
116 src = os.path.join(dir, generic)
117 if os.path.exists(src):
118 return (src, generic)
120 raise NoTemplate('No template file exists for templating "%s" '
121 'with template "%s" (neither "%s" nor "%s")'%(name, view,
122 filename, generic))
124 class Templates:
125 templates = {}
127 def __init__(self, dir):
128 self.dir = dir
130 def precompileTemplates(self):
131 """ Go through a directory and precompile all the templates therein
132 """
133 for filename in os.listdir(self.dir):
134 # skip subdirs
135 if os.path.isdir(filename):
136 continue
138 # skip files without ".html" or ".xml" extension - .css, .js etc.
139 for extension in '.html', '.xml':
140 if filename.endswith(extension):
141 break
142 else:
143 continue
145 # remove extension
146 filename = filename[:-len(extension)]
148 # load the template
149 if '.' in filename:
150 name, extension = filename.split('.', 1)
151 self.get(name, extension)
152 else:
153 self.get(filename, None)
155 def get(self, name, extension=None):
156 """ Interface to get a template, possibly loading a compiled template.
158 "name" and "extension" indicate the template we're after, which in
159 most cases will be "name.extension". If "extension" is None, then
160 we look for a template just called "name" with no extension.
162 If the file "name.extension" doesn't exist, we look for
163 "_generic.extension" as a fallback.
164 """
165 # default the name to "home"
166 if name is None:
167 name = 'home'
168 elif extension is None and '.' in name:
169 # split name
170 name, extension = name.split('.')
172 # find the source
173 src, filename = find_template(self.dir, name, extension)
175 # has it changed?
176 try:
177 stime = os.stat(src)[os.path.stat.ST_MTIME]
178 except os.error, error:
179 if error.errno != errno.ENOENT:
180 raise
182 if self.templates.has_key(src) and \
183 stime <= self.templates[src].mtime:
184 # compiled template is up to date
185 return self.templates[src]
187 # compile the template
188 pt = RoundupPageTemplate()
189 # use pt_edit so we can pass the content_type guess too
190 content_type = mimetypes.guess_type(filename)[0] or 'text/html'
191 pt.pt_edit(open(src).read(), content_type)
192 pt.id = filename
193 pt.mtime = stime
194 # Add it to the cache. We cannot do this until the template
195 # is fully initialized, as we could otherwise have a race
196 # condition when running with multiple threads:
197 #
198 # 1. Thread A notices the template is not in the cache,
199 # adds it, but has not yet set "mtime".
200 #
201 # 2. Thread B notices the template is in the cache, checks
202 # "mtime" (above) and crashes.
203 #
204 # Since Python dictionary access is atomic, as long as we
205 # insert "pt" only after it is fully initialized, we avoid
206 # this race condition. It's possible that two separate
207 # threads will both do the work of initializing the template,
208 # but the risk of wasted work is offset by avoiding a lock.
209 self.templates[src] = pt
210 return pt
212 def __getitem__(self, name):
213 name, extension = os.path.splitext(name)
214 if extension:
215 extension = extension[1:]
216 try:
217 return self.get(name, extension)
218 except NoTemplate, message:
219 raise KeyError, message
221 def context(client, template=None, classname=None, request=None):
222 """Return the rendering context dictionary
224 The dictionary includes following symbols:
226 *context*
227 this is one of three things:
229 1. None - we're viewing a "home" page
230 2. The current class of item being displayed. This is an HTMLClass
231 instance.
232 3. The current item from the database, if we're viewing a specific
233 item, as an HTMLItem instance.
235 *request*
236 Includes information about the current request, including:
238 - the url
239 - the current index information (``filterspec``, ``filter`` args,
240 ``properties``, etc) parsed out of the form.
241 - methods for easy filterspec link generation
242 - *user*, the current user node as an HTMLItem instance
243 - *form*, the current CGI form information as a FieldStorage
245 *config*
246 The current tracker config.
248 *db*
249 The current database, used to access arbitrary database items.
251 *utils*
252 This is a special class that has its base in the TemplatingUtils
253 class in this file. If the tracker interfaces module defines a
254 TemplatingUtils class then it is mixed in, overriding the methods
255 in the base class.
257 *templates*
258 Access to all the tracker templates by name.
259 Used mainly in *use-macro* commands.
261 *template*
262 Current rendering template.
264 *true*
265 Logical True value.
267 *false*
268 Logical False value.
270 *i18n*
271 Internationalization service, providing string translation
272 methods ``gettext`` and ``ngettext``.
274 """
275 # construct the TemplatingUtils class
276 utils = TemplatingUtils
277 if (hasattr(client.instance, 'interfaces') and
278 hasattr(client.instance.interfaces, 'TemplatingUtils')):
279 class utils(client.instance.interfaces.TemplatingUtils, utils):
280 pass
282 # if template, classname and/or request are not passed explicitely,
283 # compute form client
284 if template is None:
285 template = client.template
286 if classname is None:
287 classname = client.classname
288 if request is None:
289 request = HTMLRequest(client)
291 c = {
292 'context': None,
293 'options': {},
294 'nothing': None,
295 'request': request,
296 'db': HTMLDatabase(client),
297 'config': client.instance.config,
298 'tracker': client.instance,
299 'utils': utils(client),
300 'templates': client.instance.templates,
301 'template': template,
302 'true': 1,
303 'false': 0,
304 'i18n': client.translator
305 }
306 # add in the item if there is one
307 if client.nodeid:
308 c['context'] = HTMLItem(client, classname, client.nodeid,
309 anonymous=1)
310 elif client.db.classes.has_key(classname):
311 c['context'] = HTMLClass(client, classname, anonymous=1)
312 return c
314 class RoundupPageTemplate(PageTemplate.PageTemplate):
315 """A Roundup-specific PageTemplate.
317 Interrogate the client to set up Roundup-specific template variables
318 to be available. See 'context' function for the list of variables.
320 """
322 # 06-jun-2004 [als] i am not sure if this method is used yet
323 def getContext(self, client, classname, request):
324 return context(client, self, classname, request)
326 def render(self, client, classname, request, **options):
327 """Render this Page Template"""
329 if not self._v_cooked:
330 self._cook()
332 __traceback_supplement__ = (PageTemplate.PageTemplateTracebackSupplement, self)
334 if self._v_errors:
335 raise PageTemplate.PTRuntimeError, \
336 'Page Template %s has errors.'%self.id
338 # figure the context
339 c = context(client, self, classname, request)
340 c.update({'options': options})
342 # and go
343 output = StringIO.StringIO()
344 TALInterpreter.TALInterpreter(self._v_program, self.macros,
345 getEngine().getContext(c), output, tal=1, strictinsert=0)()
346 return output.getvalue()
348 def __repr__(self):
349 return '<Roundup PageTemplate %r>'%self.id
351 class HTMLDatabase:
352 """ Return HTMLClasses for valid class fetches
353 """
354 def __init__(self, client):
355 self._client = client
356 self._ = client._
357 self._db = client.db
359 # we want config to be exposed
360 self.config = client.db.config
362 def __getitem__(self, item, desre=re.compile(r'(?P<cl>[a-zA-Z_]+)(?P<id>[-\d]+)')):
363 # check to see if we're actually accessing an item
364 m = desre.match(item)
365 if m:
366 cl = m.group('cl')
367 self._client.db.getclass(cl)
368 return HTMLItem(self._client, cl, m.group('id'))
369 else:
370 self._client.db.getclass(item)
371 return HTMLClass(self._client, item)
373 def __getattr__(self, attr):
374 try:
375 return self[attr]
376 except KeyError:
377 raise AttributeError, attr
379 def classes(self):
380 l = self._client.db.classes.keys()
381 l.sort()
382 m = []
383 for item in l:
384 m.append(HTMLClass(self._client, item))
385 return m
387 num_re = re.compile('^-?\d+$')
389 def lookupIds(db, prop, ids, fail_ok=0, num_re=num_re, do_lookup=True):
390 """ "fail_ok" should be specified if we wish to pass through bad values
391 (most likely form values that we wish to represent back to the user)
392 "do_lookup" is there for preventing lookup by key-value (if we
393 know that the value passed *is* an id)
394 """
395 cl = db.getclass(prop.classname)
396 l = []
397 for entry in ids:
398 if do_lookup:
399 try:
400 item = cl.lookup(entry)
401 except (TypeError, KeyError):
402 pass
403 else:
404 l.append(item)
405 continue
406 # if fail_ok, ignore lookup error
407 # otherwise entry must be existing object id rather than key value
408 if fail_ok or num_re.match(entry):
409 l.append(entry)
410 return l
412 def lookupKeys(linkcl, key, ids, num_re=num_re):
413 """ Look up the "key" values for "ids" list - though some may already
414 be key values, not ids.
415 """
416 l = []
417 for entry in ids:
418 if num_re.match(entry):
419 label = linkcl.get(entry, key)
420 # fall back to designator if label is None
421 if label is None: label = '%s%s'%(linkcl.classname, entry)
422 l.append(label)
423 else:
424 l.append(entry)
425 return l
427 def _set_input_default_args(dic):
428 # 'text' is the default value anyway --
429 # but for CSS usage it should be present
430 dic.setdefault('type', 'text')
431 # useful e.g for HTML LABELs:
432 if not dic.has_key('id'):
433 try:
434 if dic['text'] in ('radio', 'checkbox'):
435 dic['id'] = '%(name)s-%(value)s' % dic
436 else:
437 dic['id'] = dic['name']
438 except KeyError:
439 pass
441 def cgi_escape_attrs(**attrs):
442 return ' '.join(['%s="%s"'%(k,cgi.escape(str(v), True))
443 for k,v in attrs.items()])
445 def input_html4(**attrs):
446 """Generate an 'input' (html4) element with given attributes"""
447 _set_input_default_args(attrs)
448 return '<input %s>'%cgi_escape_attrs(**attrs)
450 def input_xhtml(**attrs):
451 """Generate an 'input' (xhtml) element with given attributes"""
452 _set_input_default_args(attrs)
453 return '<input %s/>'%cgi_escape_attrs(**attrs)
455 class HTMLInputMixin:
456 """ requires a _client property """
457 def __init__(self):
458 html_version = 'html4'
459 if hasattr(self._client.instance.config, 'HTML_VERSION'):
460 html_version = self._client.instance.config.HTML_VERSION
461 if html_version == 'xhtml':
462 self.input = input_xhtml
463 else:
464 self.input = input_html4
465 # self._context is used for translations.
466 # will be initialized by the first call to .gettext()
467 self._context = None
469 def gettext(self, msgid):
470 """Return the localized translation of msgid"""
471 if self._context is None:
472 self._context = context(self._client)
473 return self._client.translator.translate(domain="roundup",
474 msgid=msgid, context=self._context)
476 _ = gettext
478 class HTMLPermissions:
480 def view_check(self):
481 """ Raise the Unauthorised exception if the user's not permitted to
482 view this class.
483 """
484 if not self.is_view_ok():
485 raise Unauthorised("view", self._classname,
486 translator=self._client.translator)
488 def edit_check(self):
489 """ Raise the Unauthorised exception if the user's not permitted to
490 edit items of this class.
491 """
492 if not self.is_edit_ok():
493 raise Unauthorised("edit", self._classname,
494 translator=self._client.translator)
496 def retire_check(self):
497 """ Raise the Unauthorised exception if the user's not permitted to
498 retire items of this class.
499 """
500 if not self.is_retire_ok():
501 raise Unauthorised("retire", self._classname,
502 translator=self._client.translator)
505 class HTMLClass(HTMLInputMixin, HTMLPermissions):
506 """ Accesses through a class (either through *class* or *db.<classname>*)
507 """
508 def __init__(self, client, classname, anonymous=0):
509 self._client = client
510 self._ = client._
511 self._db = client.db
512 self._anonymous = anonymous
514 # we want classname to be exposed, but _classname gives a
515 # consistent API for extending Class/Item
516 self._classname = self.classname = classname
517 self._klass = self._db.getclass(self.classname)
518 self._props = self._klass.getprops()
520 HTMLInputMixin.__init__(self)
522 def is_edit_ok(self):
523 """ Is the user allowed to Create the current class?
524 """
525 perm = self._db.security.hasPermission
526 return perm('Web Access', self._client.userid) and perm('Create',
527 self._client.userid, self._classname)
529 def is_retire_ok(self):
530 """ Is the user allowed to retire items of the current class?
531 """
532 perm = self._db.security.hasPermission
533 return perm('Web Access', self._client.userid) and perm('Retire',
534 self._client.userid, self._classname)
536 def is_view_ok(self):
537 """ Is the user allowed to View the current class?
538 """
539 perm = self._db.security.hasPermission
540 return perm('Web Access', self._client.userid) and perm('View',
541 self._client.userid, self._classname)
543 def is_only_view_ok(self):
544 """ Is the user only allowed to View (ie. not Create) the current class?
545 """
546 return self.is_view_ok() and not self.is_edit_ok()
548 def __repr__(self):
549 return '<HTMLClass(0x%x) %s>'%(id(self), self.classname)
551 def __getitem__(self, item):
552 """ return an HTMLProperty instance
553 """
555 # we don't exist
556 if item == 'id':
557 return None
559 # get the property
560 try:
561 prop = self._props[item]
562 except KeyError:
563 raise KeyError, 'No such property "%s" on %s'%(item, self.classname)
565 # look up the correct HTMLProperty class
566 form = self._client.form
567 for klass, htmlklass in propclasses:
568 if not isinstance(prop, klass):
569 continue
570 value = prop.get_default_value()
571 return htmlklass(self._client, self._classname, None, prop, item,
572 value, self._anonymous)
574 # no good
575 raise KeyError, item
577 def __getattr__(self, attr):
578 """ convenience access """
579 try:
580 return self[attr]
581 except KeyError:
582 raise AttributeError, attr
584 def designator(self):
585 """ Return this class' designator (classname) """
586 return self._classname
588 def getItem(self, itemid, num_re=num_re):
589 """ Get an item of this class by its item id.
590 """
591 # make sure we're looking at an itemid
592 if not isinstance(itemid, type(1)) and not num_re.match(itemid):
593 itemid = self._klass.lookup(itemid)
595 return HTMLItem(self._client, self.classname, itemid)
597 def properties(self, sort=1):
598 """ Return HTMLProperty for all of this class' properties.
599 """
600 l = []
601 for name, prop in self._props.items():
602 for klass, htmlklass in propclasses:
603 if isinstance(prop, klass):
604 value = prop.get_default_value()
605 l.append(htmlklass(self._client, self._classname, '',
606 prop, name, value, self._anonymous))
607 if sort:
608 l.sort(lambda a,b:cmp(a._name, b._name))
609 return l
611 def list(self, sort_on=None):
612 """ List all items in this class.
613 """
614 # get the list and sort it nicely
615 l = self._klass.list()
616 sortfunc = make_sort_function(self._db, self._classname, sort_on)
617 l.sort(sortfunc)
619 # check perms
620 check = self._client.db.security.hasPermission
621 userid = self._client.userid
622 if not check('Web Access', userid):
623 return []
625 l = [HTMLItem(self._client, self._classname, id) for id in l
626 if check('View', userid, self._classname, itemid=id)]
628 return l
630 def csv(self):
631 """ Return the items of this class as a chunk of CSV text.
632 """
633 props = self.propnames()
634 s = StringIO.StringIO()
635 writer = csv.writer(s)
636 writer.writerow(props)
637 check = self._client.db.security.hasPermission
638 userid = self._client.userid
639 if not check('Web Access', userid):
640 return ''
641 for nodeid in self._klass.list():
642 l = []
643 for name in props:
644 # check permission to view this property on this item
645 if not check('View', userid, itemid=nodeid,
646 classname=self._klass.classname, property=name):
647 raise Unauthorised('view', self._klass.classname,
648 translator=self._client.translator)
649 value = self._klass.get(nodeid, name)
650 if value is None:
651 l.append('')
652 elif isinstance(value, type([])):
653 l.append(':'.join(map(str, value)))
654 else:
655 l.append(str(self._klass.get(nodeid, name)))
656 writer.writerow(l)
657 return s.getvalue()
659 def propnames(self):
660 """ Return the list of the names of the properties of this class.
661 """
662 idlessprops = self._klass.getprops(protected=0).keys()
663 idlessprops.sort()
664 return ['id'] + idlessprops
666 def filter(self, request=None, filterspec={}, sort=[], group=[]):
667 """ Return a list of items from this class, filtered and sorted
668 by the current requested filterspec/filter/sort/group args
670 "request" takes precedence over the other three arguments.
671 """
672 security = self._db.security
673 userid = self._client.userid
674 if request is not None:
675 # for a request we asume it has already been
676 # security-filtered
677 filterspec = request.filterspec
678 sort = request.sort
679 group = request.group
680 else:
681 cn = self.classname
682 filterspec = security.filterFilterspec(userid, cn, filterspec)
683 sort = security.filterSortspec(userid, cn, sort)
684 group = security.filterSortspec(userid, cn, group)
686 check = security.hasPermission
687 if not check('Web Access', userid):
688 return []
690 l = [HTMLItem(self._client, self.classname, id)
691 for id in self._klass.filter(None, filterspec, sort, group)
692 if check('View', userid, self.classname, itemid=id)]
693 return l
695 def classhelp(self, properties=None, label=''"(list)", width='500',
696 height='400', property='', form='itemSynopsis',
697 pagesize=50, inputtype="checkbox", sort=None, filter=None):
698 """Pop up a javascript window with class help
700 This generates a link to a popup window which displays the
701 properties indicated by "properties" of the class named by
702 "classname". The "properties" should be a comma-separated list
703 (eg. 'id,name,description'). Properties defaults to all the
704 properties of a class (excluding id, creator, created and
705 activity).
707 You may optionally override the label displayed, the width,
708 the height, the number of items per page and the field on which
709 the list is sorted (defaults to username if in the displayed
710 properties).
712 With the "filter" arg it is possible to specify a filter for
713 which items are supposed to be displayed. It has to be of
714 the format "<field>=<values>;<field>=<values>;...".
716 The popup window will be resizable and scrollable.
718 If the "property" arg is given, it's passed through to the
719 javascript help_window function.
721 You can use inputtype="radio" to display a radio box instead
722 of the default checkbox (useful for entering Link-properties)
724 If the "form" arg is given, it's passed through to the
725 javascript help_window function. - it's the name of the form
726 the "property" belongs to.
727 """
728 if properties is None:
729 properties = self._klass.getprops(protected=0).keys()
730 properties.sort()
731 properties = ','.join(properties)
732 if sort is None:
733 if 'username' in properties.split( ',' ):
734 sort = 'username'
735 else:
736 sort = self._klass.orderprop()
737 sort = '&@sort=' + sort
738 if property:
739 property = '&property=%s'%property
740 if form:
741 form = '&form=%s'%form
742 if inputtype:
743 type= '&type=%s'%inputtype
744 if filter:
745 filterprops = filter.split(';')
746 filtervalues = []
747 names = []
748 for x in filterprops:
749 (name, values) = x.split('=')
750 names.append(name)
751 filtervalues.append('&%s=%s' % (name, urllib.quote(values)))
752 filter = '&@filter=%s%s' % (','.join(names), ''.join(filtervalues))
753 else:
754 filter = ''
755 help_url = "%s?@startwith=0&@template=help&"\
756 "properties=%s%s%s%s%s&@pagesize=%s%s" % \
757 (self.classname, properties, property, form, type,
758 sort, pagesize, filter)
759 onclick = "javascript:help_window('%s', '%s', '%s');return false;" % \
760 (help_url, width, height)
761 return '<a class="classhelp" href="%s" onclick="%s">%s</a>' % \
762 (help_url, onclick, self._(label))
764 def submit(self, label=''"Submit New Entry", action="new"):
765 """ Generate a submit button (and action hidden element)
767 Generate nothing if we're not editable.
768 """
769 if not self.is_edit_ok():
770 return ''
772 return self.input(type="hidden", name="@action", value=action) + \
773 '\n' + \
774 self.input(type="submit", name="submit_button", value=self._(label))
776 def history(self):
777 if not self.is_view_ok():
778 return self._('[hidden]')
779 return self._('New node - no history')
781 def renderWith(self, name, **kwargs):
782 """ Render this class with the given template.
783 """
784 # create a new request and override the specified args
785 req = HTMLRequest(self._client)
786 req.classname = self.classname
787 req.update(kwargs)
789 # new template, using the specified classname and request
790 pt = self._client.instance.templates.get(self.classname, name)
792 # use our fabricated request
793 args = {
794 'ok_message': self._client.ok_message,
795 'error_message': self._client.error_message
796 }
797 return pt.render(self._client, self.classname, req, **args)
799 class _HTMLItem(HTMLInputMixin, HTMLPermissions):
800 """ Accesses through an *item*
801 """
802 def __init__(self, client, classname, nodeid, anonymous=0):
803 self._client = client
804 self._db = client.db
805 self._classname = classname
806 self._nodeid = nodeid
807 self._klass = self._db.getclass(classname)
808 self._props = self._klass.getprops()
810 # do we prefix the form items with the item's identification?
811 self._anonymous = anonymous
813 HTMLInputMixin.__init__(self)
815 def is_edit_ok(self):
816 """ Is the user allowed to Edit this item?
817 """
818 perm = self._db.security.hasPermission
819 return perm('Web Access', self._client.userid) and perm('Edit',
820 self._client.userid, self._classname, itemid=self._nodeid)
822 def is_retire_ok(self):
823 """ Is the user allowed to Reture this item?
824 """
825 perm = self._db.security.hasPermission
826 return perm('Web Access', self._client.userid) and perm('Retire',
827 self._client.userid, self._classname, itemid=self._nodeid)
829 def is_view_ok(self):
830 """ Is the user allowed to View this item?
831 """
832 perm = self._db.security.hasPermission
833 if perm('Web Access', self._client.userid) and perm('View',
834 self._client.userid, self._classname, itemid=self._nodeid):
835 return 1
836 return self.is_edit_ok()
838 def is_only_view_ok(self):
839 """ Is the user only allowed to View (ie. not Edit) this item?
840 """
841 return self.is_view_ok() and not self.is_edit_ok()
843 def __repr__(self):
844 return '<HTMLItem(0x%x) %s %s>'%(id(self), self._classname,
845 self._nodeid)
847 def __getitem__(self, item):
848 """ return an HTMLProperty instance
849 this now can handle transitive lookups where item is of the
850 form x.y.z
851 """
852 if item == 'id':
853 return self._nodeid
855 items = item.split('.', 1)
856 has_rest = len(items) > 1
858 # get the property
859 prop = self._props[items[0]]
861 if has_rest and not isinstance(prop, (hyperdb.Link, hyperdb.Multilink)):
862 raise KeyError, item
864 # get the value, handling missing values
865 value = None
866 if int(self._nodeid) > 0:
867 value = self._klass.get(self._nodeid, items[0], None)
868 if value is None:
869 if isinstance(prop, hyperdb.Multilink):
870 value = []
872 # look up the correct HTMLProperty class
873 htmlprop = None
874 for klass, htmlklass in propclasses:
875 if isinstance(prop, klass):
876 htmlprop = htmlklass(self._client, self._classname,
877 self._nodeid, prop, items[0], value, self._anonymous)
878 if htmlprop is not None:
879 if has_rest:
880 if isinstance(htmlprop, MultilinkHTMLProperty):
881 return [h[items[1]] for h in htmlprop]
882 return htmlprop[items[1]]
883 return htmlprop
885 raise KeyError, item
887 def __getattr__(self, attr):
888 """ convenience access to properties """
889 try:
890 return self[attr]
891 except KeyError:
892 raise AttributeError, attr
894 def designator(self):
895 """Return this item's designator (classname + id)."""
896 return '%s%s'%(self._classname, self._nodeid)
898 def is_retired(self):
899 """Is this item retired?"""
900 return self._klass.is_retired(self._nodeid)
902 def submit(self, label=''"Submit Changes", action="edit"):
903 """Generate a submit button.
905 Also sneak in the lastactivity and action hidden elements.
906 """
907 return self.input(type="hidden", name="@lastactivity",
908 value=self.activity.local(0)) + '\n' + \
909 self.input(type="hidden", name="@action", value=action) + '\n' + \
910 self.input(type="submit", name="submit_button", value=self._(label))
912 def journal(self, direction='descending'):
913 """ Return a list of HTMLJournalEntry instances.
914 """
915 # XXX do this
916 return []
918 def history(self, direction='descending', dre=re.compile('^\d+$'),
919 limit=None):
920 if not self.is_view_ok():
921 return self._('[hidden]')
923 # pre-load the history with the current state
924 current = {}
925 for prop_n in self._props.keys():
926 prop = self[prop_n]
927 if not isinstance(prop, HTMLProperty):
928 continue
929 current[prop_n] = prop.plain(escape=1)
930 # make link if hrefable
931 if (self._props.has_key(prop_n) and
932 isinstance(self._props[prop_n], hyperdb.Link)):
933 classname = self._props[prop_n].classname
934 try:
935 template = find_template(self._db.config.TEMPLATES,
936 classname, 'item')
937 if template[1].startswith('_generic'):
938 raise NoTemplate, 'not really...'
939 except NoTemplate:
940 pass
941 else:
942 id = self._klass.get(self._nodeid, prop_n, None)
943 current[prop_n] = '<a href="%s%s">%s</a>'%(
944 classname, id, current[prop_n])
946 # get the journal, sort and reverse
947 history = self._klass.history(self._nodeid)
948 history.sort()
949 history.reverse()
951 # restrict the volume
952 if limit:
953 history = history[:limit]
955 timezone = self._db.getUserTimezone()
956 l = []
957 comments = {}
958 for id, evt_date, user, action, args in history:
959 date_s = str(evt_date.local(timezone)).replace("."," ")
960 arg_s = ''
961 if action == 'link' and type(args) == type(()):
962 if len(args) == 3:
963 linkcl, linkid, key = args
964 arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
965 linkcl, linkid, key)
966 else:
967 arg_s = str(args)
969 elif action == 'unlink' and type(args) == type(()):
970 if len(args) == 3:
971 linkcl, linkid, key = args
972 arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
973 linkcl, linkid, key)
974 else:
975 arg_s = str(args)
977 elif type(args) == type({}):
978 cell = []
979 for k in args.keys():
980 # try to get the relevant property and treat it
981 # specially
982 try:
983 prop = self._props[k]
984 except KeyError:
985 prop = None
986 if prop is None:
987 # property no longer exists
988 comments['no_exist'] = self._(
989 "<em>The indicated property no longer exists</em>")
990 cell.append(self._('<em>%s: %s</em>\n')
991 % (self._(k), str(args[k])))
992 continue
994 if args[k] and (isinstance(prop, hyperdb.Multilink) or
995 isinstance(prop, hyperdb.Link)):
996 # figure what the link class is
997 classname = prop.classname
998 try:
999 linkcl = self._db.getclass(classname)
1000 except KeyError:
1001 labelprop = None
1002 comments[classname] = self._(
1003 "The linked class %(classname)s no longer exists"
1004 ) % locals()
1005 labelprop = linkcl.labelprop(1)
1006 try:
1007 template = find_template(self._db.config.TEMPLATES,
1008 classname, 'item')
1009 if template[1].startswith('_generic'):
1010 raise NoTemplate, 'not really...'
1011 hrefable = 1
1012 except NoTemplate:
1013 hrefable = 0
1015 if isinstance(prop, hyperdb.Multilink) and args[k]:
1016 ml = []
1017 for linkid in args[k]:
1018 if isinstance(linkid, type(())):
1019 sublabel = linkid[0] + ' '
1020 linkids = linkid[1]
1021 else:
1022 sublabel = ''
1023 linkids = [linkid]
1024 subml = []
1025 for linkid in linkids:
1026 label = classname + linkid
1027 # if we have a label property, try to use it
1028 # TODO: test for node existence even when
1029 # there's no labelprop!
1030 try:
1031 if labelprop is not None and \
1032 labelprop != 'id':
1033 label = linkcl.get(linkid, labelprop)
1034 label = cgi.escape(label)
1035 except IndexError:
1036 comments['no_link'] = self._(
1037 "<strike>The linked node"
1038 " no longer exists</strike>")
1039 subml.append('<strike>%s</strike>'%label)
1040 else:
1041 if hrefable:
1042 subml.append('<a href="%s%s">%s</a>'%(
1043 classname, linkid, label))
1044 elif label is None:
1045 subml.append('%s%s'%(classname,
1046 linkid))
1047 else:
1048 subml.append(label)
1049 ml.append(sublabel + ', '.join(subml))
1050 cell.append('%s:\n %s'%(self._(k), ', '.join(ml)))
1051 elif isinstance(prop, hyperdb.Link) and args[k]:
1052 label = classname + args[k]
1053 # if we have a label property, try to use it
1054 # TODO: test for node existence even when
1055 # there's no labelprop!
1056 if labelprop is not None and labelprop != 'id':
1057 try:
1058 label = cgi.escape(linkcl.get(args[k],
1059 labelprop))
1060 except IndexError:
1061 comments['no_link'] = self._(
1062 "<strike>The linked node"
1063 " no longer exists</strike>")
1064 cell.append(' <strike>%s</strike>,\n'%label)
1065 # "flag" this is done .... euwww
1066 label = None
1067 if label is not None:
1068 if hrefable:
1069 old = '<a href="%s%s">%s</a>'%(classname,
1070 args[k], label)
1071 else:
1072 old = label;
1073 cell.append('%s: %s' % (self._(k), old))
1074 if current.has_key(k):
1075 cell[-1] += ' -> %s'%current[k]
1076 current[k] = old
1078 elif isinstance(prop, hyperdb.Date) and args[k]:
1079 if args[k] is None:
1080 d = ''
1081 else:
1082 d = date.Date(args[k],
1083 translator=self._client).local(timezone)
1084 cell.append('%s: %s'%(self._(k), str(d)))
1085 if current.has_key(k):
1086 cell[-1] += ' -> %s' % current[k]
1087 current[k] = str(d)
1089 elif isinstance(prop, hyperdb.Interval) and args[k]:
1090 val = str(date.Interval(args[k],
1091 translator=self._client))
1092 cell.append('%s: %s'%(self._(k), val))
1093 if current.has_key(k):
1094 cell[-1] += ' -> %s'%current[k]
1095 current[k] = val
1097 elif isinstance(prop, hyperdb.String) and args[k]:
1098 val = cgi.escape(args[k])
1099 cell.append('%s: %s'%(self._(k), val))
1100 if current.has_key(k):
1101 cell[-1] += ' -> %s'%current[k]
1102 current[k] = val
1104 elif isinstance(prop, hyperdb.Boolean) and args[k] is not None:
1105 val = args[k] and ''"Yes" or ''"No"
1106 cell.append('%s: %s'%(self._(k), val))
1107 if current.has_key(k):
1108 cell[-1] += ' -> %s'%current[k]
1109 current[k] = val
1111 elif isinstance(prop, hyperdb.Password) and args[k] is not None:
1112 val = args[k].dummystr()
1113 cell.append('%s: %s'%(self._(k), val))
1114 if current.has_key(k):
1115 cell[-1] += ' -> %s'%current[k]
1116 current[k] = val
1118 elif not args[k]:
1119 if current.has_key(k):
1120 cell.append('%s: %s'%(self._(k), current[k]))
1121 current[k] = '(no value)'
1122 else:
1123 cell.append(self._('%s: (no value)')%self._(k))
1125 else:
1126 cell.append('%s: %s'%(self._(k), str(args[k])))
1127 if current.has_key(k):
1128 cell[-1] += ' -> %s'%current[k]
1129 current[k] = str(args[k])
1131 arg_s = '<br />'.join(cell)
1132 else:
1133 # unkown event!!
1134 comments['unknown'] = self._(
1135 "<strong><em>This event is not handled"
1136 " by the history display!</em></strong>")
1137 arg_s = '<strong><em>' + str(args) + '</em></strong>'
1138 date_s = date_s.replace(' ', ' ')
1139 # if the user's an itemid, figure the username (older journals
1140 # have the username)
1141 if dre.match(user):
1142 user = self._db.user.get(user, 'username')
1143 l.append('<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>'%(
1144 date_s, user, self._(action), arg_s))
1145 if comments:
1146 l.append(self._(
1147 '<tr><td colspan=4><strong>Note:</strong></td></tr>'))
1148 for entry in comments.values():
1149 l.append('<tr><td colspan=4>%s</td></tr>'%entry)
1151 if direction == 'ascending':
1152 l.reverse()
1154 l[0:0] = ['<table class="history">'
1155 '<tr><th colspan="4" class="header">',
1156 self._('History'),
1157 '</th></tr><tr>',
1158 self._('<th>Date</th>'),
1159 self._('<th>User</th>'),
1160 self._('<th>Action</th>'),
1161 self._('<th>Args</th>'),
1162 '</tr>']
1163 l.append('</table>')
1164 return '\n'.join(l)
1166 def renderQueryForm(self):
1167 """ Render this item, which is a query, as a search form.
1168 """
1169 # create a new request and override the specified args
1170 req = HTMLRequest(self._client)
1171 req.classname = self._klass.get(self._nodeid, 'klass')
1172 name = self._klass.get(self._nodeid, 'name')
1173 req.updateFromURL(self._klass.get(self._nodeid, 'url') +
1174 '&@queryname=%s'%urllib.quote(name))
1176 # new template, using the specified classname and request
1177 pt = self._client.instance.templates.get(req.classname, 'search')
1178 # The context for a search page should be the class, not any
1179 # node.
1180 self._client.nodeid = None
1182 # use our fabricated request
1183 return pt.render(self._client, req.classname, req)
1185 def download_url(self):
1186 """ Assume that this item is a FileClass and that it has a name
1187 and content. Construct a URL for the download of the content.
1188 """
1189 name = self._klass.get(self._nodeid, 'name')
1190 url = '%s%s/%s'%(self._classname, self._nodeid, name)
1191 return urllib.quote(url)
1193 def copy_url(self, exclude=("messages", "files")):
1194 """Construct a URL for creating a copy of this item
1196 "exclude" is an optional list of properties that should
1197 not be copied to the new object. By default, this list
1198 includes "messages" and "files" properties. Note that
1199 "id" property cannot be copied.
1201 """
1202 exclude = ("id", "activity", "actor", "creation", "creator") \
1203 + tuple(exclude)
1204 query = {
1205 "@template": "item",
1206 "@note": self._("Copy of %(class)s %(id)s") % {
1207 "class": self._(self._classname), "id": self._nodeid},
1208 }
1209 for name in self._props.keys():
1210 if name not in exclude:
1211 query[name] = self[name].plain()
1212 return self._classname + "?" + "&".join(
1213 ["%s=%s" % (key, urllib.quote(value))
1214 for key, value in query.items()])
1216 class _HTMLUser(_HTMLItem):
1217 """Add ability to check for permissions on users.
1218 """
1219 _marker = []
1220 def hasPermission(self, permission, classname=_marker,
1221 property=None, itemid=None):
1222 """Determine if the user has the Permission.
1224 The class being tested defaults to the template's class, but may
1225 be overidden for this test by suppling an alternate classname.
1226 """
1227 if classname is self._marker:
1228 classname = self._client.classname
1229 return self._db.security.hasPermission(permission,
1230 self._nodeid, classname, property, itemid)
1232 def hasRole(self, *rolenames):
1233 """Determine whether the user has any role in rolenames."""
1234 return self._db.user.has_role(self._nodeid, *rolenames)
1236 def HTMLItem(client, classname, nodeid, anonymous=0):
1237 if classname == 'user':
1238 return _HTMLUser(client, classname, nodeid, anonymous)
1239 else:
1240 return _HTMLItem(client, classname, nodeid, anonymous)
1242 class HTMLProperty(HTMLInputMixin, HTMLPermissions):
1243 """ String, Number, Date, Interval HTMLProperty
1245 Has useful attributes:
1247 _name the name of the property
1248 _value the value of the property if any
1250 A wrapper object which may be stringified for the plain() behaviour.
1251 """
1252 def __init__(self, client, classname, nodeid, prop, name, value,
1253 anonymous=0):
1254 self._client = client
1255 self._db = client.db
1256 self._ = client._
1257 self._classname = classname
1258 self._nodeid = nodeid
1259 self._prop = prop
1260 self._value = value
1261 self._anonymous = anonymous
1262 self._name = name
1263 if not anonymous:
1264 if nodeid:
1265 self._formname = '%s%s@%s'%(classname, nodeid, name)
1266 else:
1267 # This case occurs when creating a property for a
1268 # non-anonymous class.
1269 self._formname = '%s@%s'%(classname, name)
1270 else:
1271 self._formname = name
1273 # If no value is already present for this property, see if one
1274 # is specified in the current form.
1275 form = self._client.form
1276 if not self._value and form.has_key(self._formname):
1277 if isinstance(prop, hyperdb.Multilink):
1278 value = lookupIds(self._db, prop,
1279 handleListCGIValue(form[self._formname]),
1280 fail_ok=1)
1281 elif isinstance(prop, hyperdb.Link):
1282 value = form.getfirst(self._formname).strip()
1283 if value:
1284 value = lookupIds(self._db, prop, [value],
1285 fail_ok=1)[0]
1286 else:
1287 value = None
1288 else:
1289 value = form.getfirst(self._formname).strip() or None
1290 self._value = value
1292 HTMLInputMixin.__init__(self)
1294 def __repr__(self):
1295 classname = self.__class__.__name__
1296 return '<%s(0x%x) %s %r %r>'%(classname, id(self), self._formname,
1297 self._prop, self._value)
1298 def __str__(self):
1299 return self.plain()
1300 def __cmp__(self, other):
1301 if isinstance(other, HTMLProperty):
1302 return cmp(self._value, other._value)
1303 return cmp(self._value, other)
1305 def __nonzero__(self):
1306 return not not self._value
1308 def isset(self):
1309 """Is my _value not None?"""
1310 return self._value is not None
1312 def is_edit_ok(self):
1313 """Should the user be allowed to use an edit form field for this
1314 property. Check "Create" for new items, or "Edit" for existing
1315 ones.
1316 """
1317 perm = self._db.security.hasPermission
1318 userid = self._client.userid
1319 if self._nodeid:
1320 if not perm('Web Access', userid):
1321 return False
1322 return perm('Edit', userid, self._classname, self._name,
1323 self._nodeid)
1324 return perm('Create', userid, self._classname, self._name) or \
1325 perm('Register', userid, self._classname, self._name)
1327 def is_view_ok(self):
1328 """ Is the user allowed to View the current class?
1329 """
1330 perm = self._db.security.hasPermission
1331 if perm('Web Access', self._client.userid) and perm('View',
1332 self._client.userid, self._classname, self._name, self._nodeid):
1333 return 1
1334 return self.is_edit_ok()
1336 class StringHTMLProperty(HTMLProperty):
1337 hyper_re = re.compile(r'''(
1338 (?P<url>
1339 (
1340 (ht|f)tp(s?):// # protocol
1341 ([\w]+(:\w+)?@)? # username/password
1342 ([\w\-]+) # hostname
1343 ((\.[\w-]+)+)? # .domain.etc
1344 | # ... or ...
1345 ([\w]+(:\w+)?@)? # username/password
1346 www\. # "www."
1347 ([\w\-]+\.)+ # hostname
1348 [\w]{2,5} # TLD
1349 )
1350 (:[\d]{1,5})? # port
1351 (/[\w\-$.+!*(),;:@&=?/~\\#%]*)? # path etc.
1352 )|
1353 (?P<email>[-+=%/\w\.]+@[\w\.\-]+)|
1354 (?P<item>(?P<class>[A-Za-z_]+)(\s*)(?P<id>\d+))
1355 )''', re.X | re.I)
1356 protocol_re = re.compile('^(ht|f)tp(s?)://', re.I)
1360 def _hyper_repl(self, match):
1361 if match.group('url'):
1362 return self._hyper_repl_url(match, '<a href="%s">%s</a>%s')
1363 elif match.group('email'):
1364 return self._hyper_repl_email(match, '<a href="mailto:%s">%s</a>')
1365 elif len(match.group('id')) < 10:
1366 return self._hyper_repl_item(match,
1367 '<a href="%(cls)s%(id)s">%(item)s</a>')
1368 else:
1369 # just return the matched text
1370 return match.group(0)
1372 def _hyper_repl_url(self, match, replacement):
1373 u = s = match.group('url')
1374 if not self.protocol_re.search(s):
1375 u = 'http://' + s
1376 end = ''
1377 if '>' in s:
1378 # catch an escaped ">" in the URL
1379 pos = s.find('>')
1380 end = s[pos:]
1381 u = s = s[:pos]
1382 if ')' in s and s.count('(') != s.count(')'):
1383 # don't include extraneous ')' in the link
1384 pos = s.rfind(')')
1385 end = s[pos:] + end
1386 u = s = s[:pos]
1387 return replacement % (u, s, end)
1389 def _hyper_repl_email(self, match, replacement):
1390 s = match.group('email')
1391 return replacement % (s, s)
1393 def _hyper_repl_item(self, match, replacement):
1394 item = match.group('item')
1395 cls = match.group('class').lower()
1396 id = match.group('id')
1397 try:
1398 # make sure cls is a valid tracker classname
1399 cl = self._db.getclass(cls)
1400 if not cl.hasnode(id):
1401 return item
1402 return replacement % locals()
1403 except KeyError:
1404 return item
1407 def _hyper_repl_rst(self, match):
1408 if match.group('url'):
1409 s = match.group('url')
1410 return '`%s <%s>`_'%(s, s)
1411 elif match.group('email'):
1412 s = match.group('email')
1413 return '`%s <mailto:%s>`_'%(s, s)
1414 elif len(match.group('id')) < 10:
1415 return self._hyper_repl_item(match,'`%(item)s <%(cls)s%(id)s>`_')
1416 else:
1417 # just return the matched text
1418 return match.group(0)
1420 def hyperlinked(self):
1421 """ Render a "hyperlinked" version of the text """
1422 return self.plain(hyperlink=1)
1424 def plain(self, escape=0, hyperlink=0):
1425 """Render a "plain" representation of the property
1427 - "escape" turns on/off HTML quoting
1428 - "hyperlink" turns on/off in-text hyperlinking of URLs, email
1429 addresses and designators
1430 """
1431 if not self.is_view_ok():
1432 return self._('[hidden]')
1434 if self._value is None:
1435 return ''
1436 if escape:
1437 s = cgi.escape(str(self._value))
1438 else:
1439 s = str(self._value)
1440 if hyperlink:
1441 # no, we *must* escape this text
1442 if not escape:
1443 s = cgi.escape(s)
1444 s = self.hyper_re.sub(self._hyper_repl, s)
1445 return s
1447 def wrapped(self, escape=1, hyperlink=1):
1448 """Render a "wrapped" representation of the property.
1450 We wrap long lines at 80 columns on the nearest whitespace. Lines
1451 with no whitespace are not broken to force wrapping.
1453 Note that unlike plain() we default wrapped() to have the escaping
1454 and hyperlinking turned on since that's the most common usage.
1456 - "escape" turns on/off HTML quoting
1457 - "hyperlink" turns on/off in-text hyperlinking of URLs, email
1458 addresses and designators
1459 """
1460 if not self.is_view_ok():
1461 return self._('[hidden]')
1463 if self._value is None:
1464 return ''
1465 s = support.wrap(str(self._value), width=80)
1466 if escape:
1467 s = cgi.escape(s)
1468 if hyperlink:
1469 # no, we *must* escape this text
1470 if not escape:
1471 s = cgi.escape(s)
1472 s = self.hyper_re.sub(self._hyper_repl, s)
1473 return s
1475 def stext(self, escape=0, hyperlink=1):
1476 """ Render the value of the property as StructuredText.
1478 This requires the StructureText module to be installed separately.
1479 """
1480 if not self.is_view_ok():
1481 return self._('[hidden]')
1483 s = self.plain(escape=escape, hyperlink=hyperlink)
1484 if not StructuredText:
1485 return s
1486 return StructuredText(s,level=1,header=0)
1488 def rst(self, hyperlink=1):
1489 """ Render the value of the property as ReStructuredText.
1491 This requires docutils to be installed separately.
1492 """
1493 if not self.is_view_ok():
1494 return self._('[hidden]')
1496 if not ReStructuredText:
1497 return self.plain(escape=0, hyperlink=hyperlink)
1498 s = self.plain(escape=0, hyperlink=0)
1499 if hyperlink:
1500 s = self.hyper_re.sub(self._hyper_repl_rst, s)
1501 return ReStructuredText(s, writer_name="html")["html_body"].encode("utf-8",
1502 "replace")
1504 def field(self, **kwargs):
1505 """ Render the property as a field in HTML.
1507 If not editable, just display the value via plain().
1508 """
1509 if not self.is_edit_ok():
1510 return self.plain(escape=1)
1512 value = self._value
1513 if value is None:
1514 value = ''
1516 kwargs.setdefault("size", 30)
1517 kwargs.update({"name": self._formname, "value": value})
1518 return self.input(**kwargs)
1520 def multiline(self, escape=0, rows=5, cols=40, **kwargs):
1521 """ Render a multiline form edit field for the property.
1523 If not editable, just display the plain() value in a <pre> tag.
1524 """
1525 if not self.is_edit_ok():
1526 return '<pre>%s</pre>'%self.plain()
1528 if self._value is None:
1529 value = ''
1530 else:
1531 value = cgi.escape(str(self._value))
1533 value = '"'.join(value.split('"'))
1534 name = self._formname
1535 passthrough_args = cgi_escape_attrs(**kwargs)
1536 return ('<textarea %(passthrough_args)s name="%(name)s" id="%(name)s"'
1537 ' rows="%(rows)s" cols="%(cols)s">'
1538 '%(value)s</textarea>') % locals()
1540 def email(self, escape=1):
1541 """ Render the value of the property as an obscured email address
1542 """
1543 if not self.is_view_ok():
1544 return self._('[hidden]')
1546 if self._value is None:
1547 value = ''
1548 else:
1549 value = str(self._value)
1550 split = value.split('@')
1551 if len(split) == 2:
1552 name, domain = split
1553 domain = ' '.join(domain.split('.')[:-1])
1554 name = name.replace('.', ' ')
1555 value = '%s at %s ...'%(name, domain)
1556 else:
1557 value = value.replace('.', ' ')
1558 if escape:
1559 value = cgi.escape(value)
1560 return value
1562 class PasswordHTMLProperty(HTMLProperty):
1563 def plain(self, escape=0):
1564 """ Render a "plain" representation of the property
1565 """
1566 if not self.is_view_ok():
1567 return self._('[hidden]')
1569 if self._value is None:
1570 return ''
1571 value = self._value.dummystr()
1572 if escape:
1573 value = cgi.escape(value)
1574 return value
1576 def field(self, size=30, **kwargs):
1577 """ Render a form edit field for the property.
1579 If not editable, just display the value via plain().
1580 """
1581 if not self.is_edit_ok():
1582 return self.plain(escape=1)
1584 return self.input(type="password", name=self._formname, size=size,
1585 **kwargs)
1587 def confirm(self, size=30):
1588 """ Render a second form edit field for the property, used for
1589 confirmation that the user typed the password correctly. Generates
1590 a field with name "@confirm@name".
1592 If not editable, display nothing.
1593 """
1594 if not self.is_edit_ok():
1595 return ''
1597 return self.input(type="password",
1598 name="@confirm@%s"%self._formname,
1599 id="%s-confirm"%self._formname,
1600 size=size)
1602 class NumberHTMLProperty(HTMLProperty):
1603 def plain(self, escape=0):
1604 """ Render a "plain" representation of the property
1605 """
1606 if not self.is_view_ok():
1607 return self._('[hidden]')
1609 if self._value is None:
1610 return ''
1612 return str(self._value)
1614 def field(self, size=30, **kwargs):
1615 """ Render a form edit field for the property.
1617 If not editable, just display the value via plain().
1618 """
1619 if not self.is_edit_ok():
1620 return self.plain(escape=1)
1622 value = self._value
1623 if value is None:
1624 value = ''
1626 return self.input(name=self._formname, value=value, size=size,
1627 **kwargs)
1629 def __int__(self):
1630 """ Return an int of me
1631 """
1632 return int(self._value)
1634 def __float__(self):
1635 """ Return a float of me
1636 """
1637 return float(self._value)
1640 class BooleanHTMLProperty(HTMLProperty):
1641 def plain(self, escape=0):
1642 """ Render a "plain" representation of the property
1643 """
1644 if not self.is_view_ok():
1645 return self._('[hidden]')
1647 if self._value is None:
1648 return ''
1649 return self._value and self._("Yes") or self._("No")
1651 def field(self, **kwargs):
1652 """ Render a form edit field for the property
1654 If not editable, just display the value via plain().
1655 """
1656 if not self.is_edit_ok():
1657 return self.plain(escape=1)
1659 value = self._value
1660 if isinstance(value, str) or isinstance(value, unicode):
1661 value = value.strip().lower() in ('checked', 'yes', 'true',
1662 'on', '1')
1664 checked = value and "checked" or ""
1665 if value:
1666 s = self.input(type="radio", name=self._formname, value="yes",
1667 checked="checked", **kwargs)
1668 s += self._('Yes')
1669 s +=self.input(type="radio", name=self._formname, value="no",
1670 **kwargs)
1671 s += self._('No')
1672 else:
1673 s = self.input(type="radio", name=self._formname, value="yes",
1674 **kwargs)
1675 s += self._('Yes')
1676 s +=self.input(type="radio", name=self._formname, value="no",
1677 checked="checked", **kwargs)
1678 s += self._('No')
1679 return s
1681 class DateHTMLProperty(HTMLProperty):
1683 _marker = []
1685 def __init__(self, client, classname, nodeid, prop, name, value,
1686 anonymous=0, offset=None):
1687 HTMLProperty.__init__(self, client, classname, nodeid, prop, name,
1688 value, anonymous=anonymous)
1689 if self._value and not (isinstance(self._value, str) or
1690 isinstance(self._value, unicode)):
1691 self._value.setTranslator(self._client.translator)
1692 self._offset = offset
1693 if self._offset is None :
1694 self._offset = self._prop.offset (self._db)
1696 def plain(self, escape=0):
1697 """ Render a "plain" representation of the property
1698 """
1699 if not self.is_view_ok():
1700 return self._('[hidden]')
1702 if self._value is None:
1703 return ''
1704 if self._offset is None:
1705 offset = self._db.getUserTimezone()
1706 else:
1707 offset = self._offset
1708 return str(self._value.local(offset))
1710 def now(self, str_interval=None):
1711 """ Return the current time.
1713 This is useful for defaulting a new value. Returns a
1714 DateHTMLProperty.
1715 """
1716 if not self.is_view_ok():
1717 return self._('[hidden]')
1719 ret = date.Date('.', translator=self._client)
1721 if isinstance(str_interval, basestring):
1722 sign = 1
1723 if str_interval[0] == '-':
1724 sign = -1
1725 str_interval = str_interval[1:]
1726 interval = date.Interval(str_interval, translator=self._client)
1727 if sign > 0:
1728 ret = ret + interval
1729 else:
1730 ret = ret - interval
1732 return DateHTMLProperty(self._client, self._classname, self._nodeid,
1733 self._prop, self._formname, ret)
1735 def field(self, size=30, default=None, format=_marker, popcal=True,
1736 **kwargs):
1737 """Render a form edit field for the property
1739 If not editable, just display the value via plain().
1741 If "popcal" then include the Javascript calendar editor.
1742 Default=yes.
1744 The format string is a standard python strftime format string.
1745 """
1746 if not self.is_edit_ok():
1747 if format is self._marker:
1748 return self.plain(escape=1)
1749 else:
1750 return self.pretty(format)
1752 value = self._value
1754 if value is None:
1755 if default is None:
1756 raw_value = None
1757 else:
1758 if isinstance(default, basestring):
1759 raw_value = date.Date(default, translator=self._client)
1760 elif isinstance(default, date.Date):
1761 raw_value = default
1762 elif isinstance(default, DateHTMLProperty):
1763 raw_value = default._value
1764 else:
1765 raise ValueError, self._('default value for '
1766 'DateHTMLProperty must be either DateHTMLProperty '
1767 'or string date representation.')
1768 elif isinstance(value, str) or isinstance(value, unicode):
1769 # most likely erroneous input to be passed back to user
1770 if isinstance(value, unicode): value = value.encode('utf8')
1771 return self.input(name=self._formname, value=value, size=size,
1772 **kwargs)
1773 else:
1774 raw_value = value
1776 if raw_value is None:
1777 value = ''
1778 elif isinstance(raw_value, str) or isinstance(raw_value, unicode):
1779 if format is self._marker:
1780 value = raw_value
1781 else:
1782 value = date.Date(raw_value).pretty(format)
1783 else:
1784 if self._offset is None :
1785 offset = self._db.getUserTimezone()
1786 else :
1787 offset = self._offset
1788 value = raw_value.local(offset)
1789 if format is not self._marker:
1790 value = value.pretty(format)
1792 s = self.input(name=self._formname, value=value, size=size,
1793 **kwargs)
1794 if popcal:
1795 s += self.popcal()
1796 return s
1798 def reldate(self, pretty=1):
1799 """ Render the interval between the date and now.
1801 If the "pretty" flag is true, then make the display pretty.
1802 """
1803 if not self.is_view_ok():
1804 return self._('[hidden]')
1806 if not self._value:
1807 return ''
1809 # figure the interval
1810 interval = self._value - date.Date('.', translator=self._client)
1811 if pretty:
1812 return interval.pretty()
1813 return str(interval)
1815 def pretty(self, format=_marker):
1816 """ Render the date in a pretty format (eg. month names, spaces).
1818 The format string is a standard python strftime format string.
1819 Note that if the day is zero, and appears at the start of the
1820 string, then it'll be stripped from the output. This is handy
1821 for the situation when a date only specifies a month and a year.
1822 """
1823 if not self.is_view_ok():
1824 return self._('[hidden]')
1826 if self._offset is None:
1827 offset = self._db.getUserTimezone()
1828 else:
1829 offset = self._offset
1831 if not self._value:
1832 return ''
1833 elif format is not self._marker:
1834 return self._value.local(offset).pretty(format)
1835 else:
1836 return self._value.local(offset).pretty()
1838 def local(self, offset):
1839 """ Return the date/time as a local (timezone offset) date/time.
1840 """
1841 if not self.is_view_ok():
1842 return self._('[hidden]')
1844 return DateHTMLProperty(self._client, self._classname, self._nodeid,
1845 self._prop, self._formname, self._value, offset=offset)
1847 def popcal(self, width=300, height=200, label="(cal)",
1848 form="itemSynopsis"):
1849 """Generate a link to a calendar pop-up window.
1851 item: HTMLProperty e.g.: context.deadline
1852 """
1853 if self.isset():
1854 date = "&date=%s"%self._value
1855 else :
1856 date = ""
1857 return ('<a class="classhelp" href="javascript:help_window('
1858 "'%s?@template=calendar&property=%s&form=%s%s', %d, %d)"
1859 '">%s</a>'%(self._classname, self._name, form, date, width,
1860 height, label))
1862 class IntervalHTMLProperty(HTMLProperty):
1863 def __init__(self, client, classname, nodeid, prop, name, value,
1864 anonymous=0):
1865 HTMLProperty.__init__(self, client, classname, nodeid, prop,
1866 name, value, anonymous)
1867 if self._value and not isinstance(self._value, (str, unicode)):
1868 self._value.setTranslator(self._client.translator)
1870 def plain(self, escape=0):
1871 """ Render a "plain" representation of the property
1872 """
1873 if not self.is_view_ok():
1874 return self._('[hidden]')
1876 if self._value is None:
1877 return ''
1878 return str(self._value)
1880 def pretty(self):
1881 """ Render the interval in a pretty format (eg. "yesterday")
1882 """
1883 if not self.is_view_ok():
1884 return self._('[hidden]')
1886 return self._value.pretty()
1888 def field(self, size=30, **kwargs):
1889 """ Render a form edit field for the property
1891 If not editable, just display the value via plain().
1892 """
1893 if not self.is_edit_ok():
1894 return self.plain(escape=1)
1896 value = self._value
1897 if value is None:
1898 value = ''
1900 return self.input(name=self._formname, value=value, size=size,
1901 **kwargs)
1903 class LinkHTMLProperty(HTMLProperty):
1904 """ Link HTMLProperty
1905 Include the above as well as being able to access the class
1906 information. Stringifying the object itself results in the value
1907 from the item being displayed. Accessing attributes of this object
1908 result in the appropriate entry from the class being queried for the
1909 property accessed (so item/assignedto/name would look up the user
1910 entry identified by the assignedto property on item, and then the
1911 name property of that user)
1912 """
1913 def __init__(self, *args, **kw):
1914 HTMLProperty.__init__(self, *args, **kw)
1915 # if we're representing a form value, then the -1 from the form really
1916 # should be a None
1917 if str(self._value) == '-1':
1918 self._value = None
1920 def __getattr__(self, attr):
1921 """ return a new HTMLItem """
1922 if not self._value:
1923 # handle a special page templates lookup
1924 if attr == '__render_with_namespace__':
1925 def nothing(*args, **kw):
1926 return ''
1927 return nothing
1928 msg = self._('Attempt to look up %(attr)s on a missing value')
1929 return MissingValue(msg%locals())
1930 i = HTMLItem(self._client, self._prop.classname, self._value)
1931 return getattr(i, attr)
1933 def plain(self, escape=0):
1934 """ Render a "plain" representation of the property
1935 """
1936 if not self.is_view_ok():
1937 return self._('[hidden]')
1939 if self._value is None:
1940 return ''
1941 linkcl = self._db.classes[self._prop.classname]
1942 k = linkcl.labelprop(1)
1943 if num_re.match(self._value):
1944 try:
1945 value = str(linkcl.get(self._value, k))
1946 except IndexError:
1947 value = self._value
1948 else :
1949 value = self._value
1950 if escape:
1951 value = cgi.escape(value)
1952 return value
1954 def field(self, showid=0, size=None, **kwargs):
1955 """ Render a form edit field for the property
1957 If not editable, just display the value via plain().
1958 """
1959 if not self.is_edit_ok():
1960 return self.plain(escape=1)
1962 # edit field
1963 linkcl = self._db.getclass(self._prop.classname)
1964 if self._value is None:
1965 value = ''
1966 else:
1967 k = linkcl.getkey()
1968 if k and num_re.match(self._value):
1969 value = linkcl.get(self._value, k)
1970 else:
1971 value = self._value
1972 return self.input(name=self._formname, value=value, size=size,
1973 **kwargs)
1975 def menu(self, size=None, height=None, showid=0, additional=[], value=None,
1976 sort_on=None, html_kwargs={}, translate=True, **conditions):
1977 """ Render a form select list for this property
1979 "size" is used to limit the length of the list labels
1980 "height" is used to set the <select> tag's "size" attribute
1981 "showid" includes the item ids in the list labels
1982 "value" specifies which item is pre-selected
1983 "additional" lists properties which should be included in the
1984 label
1985 "sort_on" indicates the property to sort the list on as
1986 (direction, property) where direction is '+' or '-'. A
1987 single string with the direction prepended may be used.
1988 For example: ('-', 'order'), '+name'.
1989 "html_kwargs" specified additional html args for the
1990 generated html <select>
1991 "translate" indicates if we should do translation of labels
1992 using gettext -- this is often desired (e.g. for status
1993 labels) but sometimes not.
1995 The remaining keyword arguments are used as conditions for
1996 filtering the items in the list - they're passed as the
1997 "filterspec" argument to a Class.filter() call.
1999 If not editable, just display the value via plain().
2000 """
2001 if not self.is_edit_ok():
2002 return self.plain(escape=1)
2004 # Since None indicates the default, we need another way to
2005 # indicate "no selection". We use -1 for this purpose, as
2006 # that is the value we use when submitting a form without the
2007 # value set.
2008 if value is None:
2009 value = self._value
2010 elif value == '-1':
2011 value = None
2013 linkcl = self._db.getclass(self._prop.classname)
2014 l = ['<select %s>'%cgi_escape_attrs(name = self._formname,
2015 **html_kwargs)]
2016 k = linkcl.labelprop(1)
2017 s = ''
2018 if value is None:
2019 s = 'selected="selected" '
2020 l.append(self._('<option %svalue="-1">- no selection -</option>')%s)
2022 if sort_on is not None:
2023 if not isinstance(sort_on, tuple):
2024 if sort_on[0] in '+-':
2025 sort_on = (sort_on[0], sort_on[1:])
2026 else:
2027 sort_on = ('+', sort_on)
2028 else:
2029 sort_on = ('+', linkcl.orderprop())
2031 options = [opt
2032 for opt in linkcl.filter(None, conditions, sort_on, (None, None))
2033 if self._db.security.hasPermission("View", self._client.userid,
2034 linkcl.classname, itemid=opt)]
2036 # make sure we list the current value if it's retired
2037 if value and value not in options:
2038 options.insert(0, value)
2040 if additional:
2041 additional_fns = []
2042 props = linkcl.getprops()
2043 for propname in additional:
2044 prop = props[propname]
2045 if isinstance(prop, hyperdb.Link):
2046 cl = self._db.getclass(prop.classname)
2047 labelprop = cl.labelprop()
2048 fn = lambda optionid: cl.get(linkcl.get(optionid,
2049 propname),
2050 labelprop)
2051 else:
2052 fn = lambda optionid: linkcl.get(optionid, propname)
2053 additional_fns.append(fn)
2055 for optionid in options:
2056 # get the option value, and if it's None use an empty string
2057 option = linkcl.get(optionid, k) or ''
2059 # figure if this option is selected
2060 s = ''
2061 if value in [optionid, option]:
2062 s = 'selected="selected" '
2064 # figure the label
2065 if showid:
2066 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
2067 elif not option:
2068 lab = '%s%s'%(self._prop.classname, optionid)
2069 else:
2070 lab = option
2072 # truncate if it's too long
2073 if size is not None and len(lab) > size:
2074 lab = lab[:size-3] + '...'
2075 if additional:
2076 m = []
2077 for fn in additional_fns:
2078 m.append(str(fn(optionid)))
2079 lab = lab + ' (%s)'%', '.join(m)
2081 # and generate
2082 tr = str
2083 if translate:
2084 tr = self._
2085 lab = cgi.escape(tr(lab))
2086 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
2087 l.append('</select>')
2088 return '\n'.join(l)
2089 # def checklist(self, ...)
2093 class MultilinkHTMLProperty(HTMLProperty):
2094 """ Multilink HTMLProperty
2096 Also be iterable, returning a wrapper object like the Link case for
2097 each entry in the multilink.
2098 """
2099 def __init__(self, *args, **kwargs):
2100 HTMLProperty.__init__(self, *args, **kwargs)
2101 if self._value:
2102 display_value = lookupIds(self._db, self._prop, self._value,
2103 fail_ok=1, do_lookup=False)
2104 sortfun = make_sort_function(self._db, self._prop.classname)
2105 # sorting fails if the value contains
2106 # items not yet stored in the database
2107 # ignore these errors to preserve user input
2108 try:
2109 display_value.sort(sortfun)
2110 except:
2111 pass
2112 self._value = display_value
2114 def __len__(self):
2115 """ length of the multilink """
2116 return len(self._value)
2118 def __getattr__(self, attr):
2119 """ no extended attribute accesses make sense here """
2120 raise AttributeError, attr
2122 def viewableGenerator(self, values):
2123 """Used to iterate over only the View'able items in a class."""
2124 check = self._db.security.hasPermission
2125 userid = self._client.userid
2126 classname = self._prop.classname
2127 if check('Web Access', userid):
2128 for value in values:
2129 if check('View', userid, classname, itemid=value):
2130 yield HTMLItem(self._client, classname, value)
2132 def __iter__(self):
2133 """ iterate and return a new HTMLItem
2134 """
2135 return self.viewableGenerator(self._value)
2137 def reverse(self):
2138 """ return the list in reverse order
2139 """
2140 l = self._value[:]
2141 l.reverse()
2142 return self.viewableGenerator(l)
2144 def sorted(self, property):
2145 """ Return this multilink sorted by the given property """
2146 value = list(self.__iter__())
2147 value.sort(lambda a,b:cmp(a[property], b[property]))
2148 return value
2150 def __contains__(self, value):
2151 """ Support the "in" operator. We have to make sure the passed-in
2152 value is a string first, not a HTMLProperty.
2153 """
2154 return str(value) in self._value
2156 def isset(self):
2157 """Is my _value not []?"""
2158 return self._value != []
2160 def plain(self, escape=0):
2161 """ Render a "plain" representation of the property
2162 """
2163 if not self.is_view_ok():
2164 return self._('[hidden]')
2166 linkcl = self._db.classes[self._prop.classname]
2167 k = linkcl.labelprop(1)
2168 labels = []
2169 for v in self._value:
2170 if num_re.match(v):
2171 try:
2172 label = linkcl.get(v, k)
2173 except IndexError:
2174 label = None
2175 # fall back to designator if label is None
2176 if label is None: label = '%s%s'%(self._prop.classname, k)
2177 else:
2178 label = v
2179 labels.append(label)
2180 value = ', '.join(labels)
2181 if escape:
2182 value = cgi.escape(value)
2183 return value
2185 def field(self, size=30, showid=0, **kwargs):
2186 """ Render a form edit field for the property
2188 If not editable, just display the value via plain().
2189 """
2190 if not self.is_edit_ok():
2191 return self.plain(escape=1)
2193 linkcl = self._db.getclass(self._prop.classname)
2195 if 'value' not in kwargs:
2196 value = self._value[:]
2197 # map the id to the label property
2198 if not linkcl.getkey():
2199 showid=1
2200 if not showid:
2201 k = linkcl.labelprop(1)
2202 value = lookupKeys(linkcl, k, value)
2203 value = ','.join(value)
2204 kwargs["value"] = value
2206 return self.input(name=self._formname, size=size, **kwargs)
2208 def menu(self, size=None, height=None, showid=0, additional=[],
2209 value=None, sort_on=None, html_kwargs={}, translate=True,
2210 **conditions):
2211 """ Render a form <select> list for this property.
2213 "size" is used to limit the length of the list labels
2214 "height" is used to set the <select> tag's "size" attribute
2215 "showid" includes the item ids in the list labels
2216 "additional" lists properties which should be included in the
2217 label
2218 "value" specifies which item is pre-selected
2219 "sort_on" indicates the property to sort the list on as
2220 (direction, property) where direction is '+' or '-'. A
2221 single string with the direction prepended may be used.
2222 For example: ('-', 'order'), '+name'.
2224 The remaining keyword arguments are used as conditions for
2225 filtering the items in the list - they're passed as the
2226 "filterspec" argument to a Class.filter() call.
2228 If not editable, just display the value via plain().
2229 """
2230 if not self.is_edit_ok():
2231 return self.plain(escape=1)
2233 if value is None:
2234 value = self._value
2236 linkcl = self._db.getclass(self._prop.classname)
2238 if sort_on is not None:
2239 if not isinstance(sort_on, tuple):
2240 if sort_on[0] in '+-':
2241 sort_on = (sort_on[0], sort_on[1:])
2242 else:
2243 sort_on = ('+', sort_on)
2244 else:
2245 sort_on = ('+', linkcl.orderprop())
2247 options = [opt
2248 for opt in linkcl.filter(None, conditions, sort_on)
2249 if self._db.security.hasPermission("View", self._client.userid,
2250 linkcl.classname, itemid=opt)]
2252 # make sure we list the current values if they're retired
2253 for val in value:
2254 if val not in options:
2255 options.insert(0, val)
2257 if not height:
2258 height = len(options)
2259 if value:
2260 # The "no selection" option.
2261 height += 1
2262 height = min(height, 7)
2263 l = ['<select multiple %s>'%cgi_escape_attrs(name = self._formname,
2264 size = height,
2265 **html_kwargs)]
2266 k = linkcl.labelprop(1)
2268 if value:
2269 l.append('<option value="%s">- no selection -</option>'
2270 % ','.join(['-' + v for v in value]))
2272 if additional:
2273 additional_fns = []
2274 props = linkcl.getprops()
2275 for propname in additional:
2276 prop = props[propname]
2277 if isinstance(prop, hyperdb.Link):
2278 cl = self._db.getclass(prop.classname)
2279 labelprop = cl.labelprop()
2280 fn = lambda optionid: cl.get(linkcl.get(optionid,
2281 propname),
2282 labelprop)
2283 else:
2284 fn = lambda optionid: linkcl.get(optionid, propname)
2285 additional_fns.append(fn)
2287 for optionid in options:
2288 # get the option value, and if it's None use an empty string
2289 option = linkcl.get(optionid, k) or ''
2291 # figure if this option is selected
2292 s = ''
2293 if optionid in value or option in value:
2294 s = 'selected="selected" '
2296 # figure the label
2297 if showid:
2298 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
2299 else:
2300 lab = option
2301 # truncate if it's too long
2302 if size is not None and len(lab) > size:
2303 lab = lab[:size-3] + '...'
2304 if additional:
2305 m = []
2306 for fn in additional_fns:
2307 m.append(str(fn(optionid)))
2308 lab = lab + ' (%s)'%', '.join(m)
2310 # and generate
2311 tr = str
2312 if translate:
2313 tr = self._
2314 lab = cgi.escape(tr(lab))
2315 l.append('<option %svalue="%s">%s</option>'%(s, optionid,
2316 lab))
2317 l.append('</select>')
2318 return '\n'.join(l)
2321 # set the propclasses for HTMLItem
2322 propclasses = [
2323 (hyperdb.String, StringHTMLProperty),
2324 (hyperdb.Number, NumberHTMLProperty),
2325 (hyperdb.Boolean, BooleanHTMLProperty),
2326 (hyperdb.Date, DateHTMLProperty),
2327 (hyperdb.Interval, IntervalHTMLProperty),
2328 (hyperdb.Password, PasswordHTMLProperty),
2329 (hyperdb.Link, LinkHTMLProperty),
2330 (hyperdb.Multilink, MultilinkHTMLProperty),
2331 ]
2333 def register_propclass(prop, cls):
2334 for index,propclass in enumerate(propclasses):
2335 p, c = propclass
2336 if prop == p:
2337 propclasses[index] = (prop, cls)
2338 break
2339 else:
2340 propclasses.append((prop, cls))
2343 def make_sort_function(db, classname, sort_on=None):
2344 """Make a sort function for a given class.
2346 The list being sorted may contain mixed ids and labels.
2347 """
2348 linkcl = db.getclass(classname)
2349 if sort_on is None:
2350 sort_on = linkcl.orderprop()
2351 def sortfunc(a, b):
2352 if num_re.match(a):
2353 a = linkcl.get(a, sort_on)
2354 if num_re.match(b):
2355 b = linkcl.get(b, sort_on)
2356 return cmp(a, b)
2357 return sortfunc
2359 def handleListCGIValue(value):
2360 """ Value is either a single item or a list of items. Each item has a
2361 .value that we're actually interested in.
2362 """
2363 if isinstance(value, type([])):
2364 return [value.value for value in value]
2365 else:
2366 value = value.value.strip()
2367 if not value:
2368 return []
2369 return [v.strip() for v in value.split(',')]
2371 class HTMLRequest(HTMLInputMixin):
2372 """The *request*, holding the CGI form and environment.
2374 - "form" the CGI form as a cgi.FieldStorage
2375 - "env" the CGI environment variables
2376 - "base" the base URL for this instance
2377 - "user" a HTMLItem instance for this user
2378 - "language" as determined by the browser or config
2379 - "classname" the current classname (possibly None)
2380 - "template" the current template (suffix, also possibly None)
2382 Index args:
2384 - "columns" dictionary of the columns to display in an index page
2385 - "show" a convenience access to columns - request/show/colname will
2386 be true if the columns should be displayed, false otherwise
2387 - "sort" index sort column (direction, column name)
2388 - "group" index grouping property (direction, column name)
2389 - "filter" properties to filter the index on
2390 - "filterspec" values to filter the index on
2391 - "search_text" text to perform a full-text search on for an index
2392 """
2393 def __repr__(self):
2394 return '<HTMLRequest %r>'%self.__dict__
2396 def __init__(self, client):
2397 # _client is needed by HTMLInputMixin
2398 self._client = self.client = client
2400 # easier access vars
2401 self.form = client.form
2402 self.env = client.env
2403 self.base = client.base
2404 self.user = HTMLItem(client, 'user', client.userid)
2405 self.language = client.language
2407 # store the current class name and action
2408 self.classname = client.classname
2409 self.nodeid = client.nodeid
2410 self.template = client.template
2412 # the special char to use for special vars
2413 self.special_char = '@'
2415 HTMLInputMixin.__init__(self)
2417 self._post_init()
2419 def current_url(self):
2420 url = self.base
2421 if self.classname:
2422 url += self.classname
2423 if self.nodeid:
2424 url += self.nodeid
2425 args = {}
2426 if self.template:
2427 args['@template'] = self.template
2428 return self.indexargs_url(url, args)
2430 def _parse_sort(self, var, name):
2431 """ Parse sort/group options. Append to var
2432 """
2433 fields = []
2434 dirs = []
2435 for special in '@:':
2436 idx = 0
2437 key = '%s%s%d'%(special, name, idx)
2438 while key in self.form:
2439 self.special_char = special
2440 fields.append(self.form.getfirst(key))
2441 dirkey = '%s%sdir%d'%(special, name, idx)
2442 if dirkey in self.form:
2443 dirs.append(self.form.getfirst(dirkey))
2444 else:
2445 dirs.append(None)
2446 idx += 1
2447 key = '%s%s%d'%(special, name, idx)
2448 # backward compatible (and query) URL format
2449 key = special + name
2450 dirkey = key + 'dir'
2451 if key in self.form and not fields:
2452 fields = handleListCGIValue(self.form[key])
2453 if dirkey in self.form:
2454 dirs.append(self.form.getfirst(dirkey))
2455 if fields: # only try other special char if nothing found
2456 break
2457 for f, d in map(None, fields, dirs):
2458 if f.startswith('-'):
2459 var.append(('-', f[1:]))
2460 elif d:
2461 var.append(('-', f))
2462 else:
2463 var.append(('+', f))
2465 def _post_init(self):
2466 """ Set attributes based on self.form
2467 """
2468 # extract the index display information from the form
2469 self.columns = []
2470 for name in ':columns @columns'.split():
2471 if self.form.has_key(name):
2472 self.special_char = name[0]
2473 self.columns = handleListCGIValue(self.form[name])
2474 break
2475 self.show = support.TruthDict(self.columns)
2476 security = self._client.db.security
2477 userid = self._client.userid
2479 # sorting and grouping
2480 self.sort = []
2481 self.group = []
2482 self._parse_sort(self.sort, 'sort')
2483 self._parse_sort(self.group, 'group')
2484 self.sort = security.filterSortspec(userid, self.classname, self.sort)
2485 self.group = security.filterSortspec(userid, self.classname, self.group)
2487 # filtering
2488 self.filter = []
2489 for name in ':filter @filter'.split():
2490 if self.form.has_key(name):
2491 self.special_char = name[0]
2492 self.filter = handleListCGIValue(self.form[name])
2494 self.filterspec = {}
2495 db = self.client.db
2496 if self.classname is not None:
2497 cls = db.getclass (self.classname)
2498 for name in self.filter:
2499 if not self.form.has_key(name):
2500 continue
2501 prop = cls.get_transitive_prop (name)
2502 fv = self.form[name]
2503 if (isinstance(prop, hyperdb.Link) or
2504 isinstance(prop, hyperdb.Multilink)):
2505 self.filterspec[name] = lookupIds(db, prop,
2506 handleListCGIValue(fv))
2507 else:
2508 if isinstance(fv, type([])):
2509 self.filterspec[name] = [v.value for v in fv]
2510 elif name == 'id':
2511 # special case "id" property
2512 self.filterspec[name] = handleListCGIValue(fv)
2513 else:
2514 self.filterspec[name] = fv.value
2515 self.filterspec = security.filterFilterspec(userid, self.classname,
2516 self.filterspec)
2518 # full-text search argument
2519 self.search_text = None
2520 for name in ':search_text @search_text'.split():
2521 if self.form.has_key(name):
2522 self.special_char = name[0]
2523 self.search_text = self.form.getfirst(name)
2525 # pagination - size and start index
2526 # figure batch args
2527 self.pagesize = 50
2528 for name in ':pagesize @pagesize'.split():
2529 if self.form.has_key(name):
2530 self.special_char = name[0]
2531 try:
2532 self.pagesize = int(self.form.getfirst(name))
2533 except ValueError:
2534 # not an integer - ignore
2535 pass
2537 self.startwith = 0
2538 for name in ':startwith @startwith'.split():
2539 if self.form.has_key(name):
2540 self.special_char = name[0]
2541 try:
2542 self.startwith = int(self.form.getfirst(name))
2543 except ValueError:
2544 # not an integer - ignore
2545 pass
2547 # dispname
2548 if self.form.has_key('@dispname'):
2549 self.dispname = self.form.getfirst('@dispname')
2550 else:
2551 self.dispname = None
2553 def updateFromURL(self, url):
2554 """ Parse the URL for query args, and update my attributes using the
2555 values.
2556 """
2557 env = {'QUERY_STRING': url}
2558 self.form = cgi.FieldStorage(environ=env)
2560 self._post_init()
2562 def update(self, kwargs):
2563 """ Update my attributes using the keyword args
2564 """
2565 self.__dict__.update(kwargs)
2566 if kwargs.has_key('columns'):
2567 self.show = support.TruthDict(self.columns)
2569 def description(self):
2570 """ Return a description of the request - handle for the page title.
2571 """
2572 s = [self.client.db.config.TRACKER_NAME]
2573 if self.classname:
2574 if self.client.nodeid:
2575 s.append('- %s%s'%(self.classname, self.client.nodeid))
2576 else:
2577 if self.template == 'item':
2578 s.append('- new %s'%self.classname)
2579 elif self.template == 'index':
2580 s.append('- %s index'%self.classname)
2581 else:
2582 s.append('- %s %s'%(self.classname, self.template))
2583 else:
2584 s.append('- home')
2585 return ' '.join(s)
2587 def __str__(self):
2588 d = {}
2589 d.update(self.__dict__)
2590 f = ''
2591 for k in self.form.keys():
2592 f += '\n %r=%r'%(k,handleListCGIValue(self.form[k]))
2593 d['form'] = f
2594 e = ''
2595 for k,v in self.env.items():
2596 e += '\n %r=%r'%(k, v)
2597 d['env'] = e
2598 return """
2599 form: %(form)s
2600 base: %(base)r
2601 classname: %(classname)r
2602 template: %(template)r
2603 columns: %(columns)r
2604 sort: %(sort)r
2605 group: %(group)r
2606 filter: %(filter)r
2607 search_text: %(search_text)r
2608 pagesize: %(pagesize)r
2609 startwith: %(startwith)r
2610 env: %(env)s
2611 """%d
2613 def indexargs_form(self, columns=1, sort=1, group=1, filter=1,
2614 filterspec=1, search_text=1):
2615 """ return the current index args as form elements """
2616 l = []
2617 sc = self.special_char
2618 def add(k, v):
2619 l.append(self.input(type="hidden", name=k, value=v))
2620 if columns and self.columns:
2621 add(sc+'columns', ','.join(self.columns))
2622 if sort:
2623 val = []
2624 for dir, attr in self.sort:
2625 if dir == '-':
2626 val.append('-'+attr)
2627 else:
2628 val.append(attr)
2629 add(sc+'sort', ','.join (val))
2630 if group:
2631 val = []
2632 for dir, attr in self.group:
2633 if dir == '-':
2634 val.append('-'+attr)
2635 else:
2636 val.append(attr)
2637 add(sc+'group', ','.join (val))
2638 if filter and self.filter:
2639 add(sc+'filter', ','.join(self.filter))
2640 if self.classname and filterspec:
2641 cls = self.client.db.getclass(self.classname)
2642 for k,v in self.filterspec.items():
2643 if type(v) == type([]):
2644 if isinstance(cls.get_transitive_prop(k), hyperdb.String):
2645 add(k, ' '.join(v))
2646 else:
2647 add(k, ','.join(v))
2648 else:
2649 add(k, v)
2650 if search_text and self.search_text:
2651 add(sc+'search_text', self.search_text)
2652 add(sc+'pagesize', self.pagesize)
2653 add(sc+'startwith', self.startwith)
2654 return '\n'.join(l)
2656 def indexargs_url(self, url, args):
2657 """ Embed the current index args in a URL
2658 """
2659 q = urllib.quote
2660 sc = self.special_char
2661 l = ['%s=%s'%(k,v) for k,v in args.items()]
2663 # pull out the special values (prefixed by @ or :)
2664 specials = {}
2665 for key in args.keys():
2666 if key[0] in '@:':
2667 specials[key[1:]] = args[key]
2669 # ok, now handle the specials we received in the request
2670 if self.columns and not specials.has_key('columns'):
2671 l.append(sc+'columns=%s'%(','.join(self.columns)))
2672 if self.sort and not specials.has_key('sort'):
2673 val = []
2674 for dir, attr in self.sort:
2675 if dir == '-':
2676 val.append('-'+attr)
2677 else:
2678 val.append(attr)
2679 l.append(sc+'sort=%s'%(','.join(val)))
2680 if self.group and not specials.has_key('group'):
2681 val = []
2682 for dir, attr in self.group:
2683 if dir == '-':
2684 val.append('-'+attr)
2685 else:
2686 val.append(attr)
2687 l.append(sc+'group=%s'%(','.join(val)))
2688 if self.filter and not specials.has_key('filter'):
2689 l.append(sc+'filter=%s'%(','.join(self.filter)))
2690 if self.search_text and not specials.has_key('search_text'):
2691 l.append(sc+'search_text=%s'%q(self.search_text))
2692 if not specials.has_key('pagesize'):
2693 l.append(sc+'pagesize=%s'%self.pagesize)
2694 if not specials.has_key('startwith'):
2695 l.append(sc+'startwith=%s'%self.startwith)
2697 # finally, the remainder of the filter args in the request
2698 if self.classname and self.filterspec:
2699 cls = self.client.db.getclass(self.classname)
2700 for k,v in self.filterspec.items():
2701 if not args.has_key(k):
2702 if type(v) == type([]):
2703 prop = cls.get_transitive_prop(k)
2704 if k != 'id' and isinstance(prop, hyperdb.String):
2705 l.append('%s=%s'%(k, '%20'.join([q(i) for i in v])))
2706 else:
2707 l.append('%s=%s'%(k, ','.join([q(i) for i in v])))
2708 else:
2709 l.append('%s=%s'%(k, q(v)))
2710 return '%s?%s'%(url, '&'.join(l))
2711 indexargs_href = indexargs_url
2713 def base_javascript(self):
2714 return """
2715 <script type="text/javascript">
2716 submitted = false;
2717 function submit_once() {
2718 if (submitted) {
2719 alert("Your request is being processed.\\nPlease be patient.");
2720 event.returnValue = 0; // work-around for IE
2721 return 0;
2722 }
2723 submitted = true;
2724 return 1;
2725 }
2727 function help_window(helpurl, width, height) {
2728 HelpWin = window.open('%s' + helpurl, 'RoundupHelpWindow', 'scrollbars=yes,resizable=yes,toolbar=no,height='+height+',width='+width);
2729 }
2730 </script>
2731 """%self.base
2733 def batch(self, permission='View'):
2734 """ Return a batch object for results from the "current search"
2735 """
2736 check = self._client.db.security.hasPermission
2737 userid = self._client.userid
2738 if not check('Web Access', userid):
2739 return Batch(self.client, [], self.pagesize, self.startwith,
2740 classname=self.classname)
2742 filterspec = self.filterspec
2743 sort = self.sort
2744 group = self.group
2746 # get the list of ids we're batching over
2747 klass = self.client.db.getclass(self.classname)
2748 if self.search_text:
2749 matches = self.client.db.indexer.search(
2750 [w.upper().encode("utf-8", "replace") for w in re.findall(
2751 r'(?u)\b\w{2,25}\b',
2752 unicode(self.search_text, "utf-8", "replace")
2753 )], klass)
2754 else:
2755 matches = None
2757 # filter for visibility
2758 l = [id for id in klass.filter(matches, filterspec, sort, group)
2759 if check(permission, userid, self.classname, itemid=id)]
2761 # return the batch object, using IDs only
2762 return Batch(self.client, l, self.pagesize, self.startwith,
2763 classname=self.classname)
2765 # extend the standard ZTUtils Batch object to remove dependency on
2766 # Acquisition and add a couple of useful methods
2767 class Batch(ZTUtils.Batch):
2768 """ Use me to turn a list of items, or item ids of a given class, into a
2769 series of batches.
2771 ========= ========================================================
2772 Parameter Usage
2773 ========= ========================================================
2774 sequence a list of HTMLItems or item ids
2775 classname if sequence is a list of ids, this is the class of item
2776 size how big to make the sequence.
2777 start where to start (0-indexed) in the sequence.
2778 end where to end (0-indexed) in the sequence.
2779 orphan if the next batch would contain less items than this
2780 value, then it is combined with this batch
2781 overlap the number of items shared between adjacent batches
2782 ========= ========================================================
2784 Attributes: Note that the "start" attribute, unlike the
2785 argument, is a 1-based index (I know, lame). "first" is the
2786 0-based index. "length" is the actual number of elements in
2787 the batch.
2789 "sequence_length" is the length of the original, unbatched, sequence.
2790 """
2791 def __init__(self, client, sequence, size, start, end=0, orphan=0,
2792 overlap=0, classname=None):
2793 self.client = client
2794 self.last_index = self.last_item = None
2795 self.current_item = None
2796 self.classname = classname
2797 self.sequence_length = len(sequence)
2798 ZTUtils.Batch.__init__(self, sequence, size, start, end, orphan,
2799 overlap)
2801 # overwrite so we can late-instantiate the HTMLItem instance
2802 def __getitem__(self, index):
2803 if index < 0:
2804 if index + self.end < self.first: raise IndexError, index
2805 return self._sequence[index + self.end]
2807 if index >= self.length:
2808 raise IndexError, index
2810 # move the last_item along - but only if the fetched index changes
2811 # (for some reason, index 0 is fetched twice)
2812 if index != self.last_index:
2813 self.last_item = self.current_item
2814 self.last_index = index
2816 item = self._sequence[index + self.first]
2817 if self.classname:
2818 # map the item ids to instances
2819 item = HTMLItem(self.client, self.classname, item)
2820 self.current_item = item
2821 return item
2823 def propchanged(self, *properties):
2824 """ Detect if one of the properties marked as being a group
2825 property changed in the last iteration fetch
2826 """
2827 # we poke directly at the _value here since MissingValue can screw
2828 # us up and cause Nones to compare strangely
2829 if self.last_item is None:
2830 return 1
2831 for property in properties:
2832 if property == 'id' or isinstance (self.last_item[property], list):
2833 if (str(self.last_item[property]) !=
2834 str(self.current_item[property])):
2835 return 1
2836 else:
2837 if (self.last_item[property]._value !=
2838 self.current_item[property]._value):
2839 return 1
2840 return 0
2842 # override these 'cos we don't have access to acquisition
2843 def previous(self):
2844 if self.start == 1:
2845 return None
2846 return Batch(self.client, self._sequence, self._size,
2847 self.first - self._size + self.overlap, 0, self.orphan,
2848 self.overlap)
2850 def next(self):
2851 try:
2852 self._sequence[self.end]
2853 except IndexError:
2854 return None
2855 return Batch(self.client, self._sequence, self._size,
2856 self.end - self.overlap, 0, self.orphan, self.overlap)
2858 class TemplatingUtils:
2859 """ Utilities for templating
2860 """
2861 def __init__(self, client):
2862 self.client = client
2863 def Batch(self, sequence, size, start, end=0, orphan=0, overlap=0):
2864 return Batch(self.client, sequence, size, start, end, orphan,
2865 overlap)
2867 def url_quote(self, url):
2868 """URL-quote the supplied text."""
2869 return urllib.quote(url)
2871 def html_quote(self, html):
2872 """HTML-quote the supplied text."""
2873 return cgi.escape(html)
2875 def __getattr__(self, name):
2876 """Try the tracker's templating_utils."""
2877 if not hasattr(self.client.instance, 'templating_utils'):
2878 # backwards-compatibility
2879 raise AttributeError, name
2880 if not self.client.instance.templating_utils.has_key(name):
2881 raise AttributeError, name
2882 return self.client.instance.templating_utils[name]
2884 def keywords_expressions(self, request):
2885 return render_keywords_expression_editor(request)
2887 def html_calendar(self, request):
2888 """Generate a HTML calendar.
2890 `request` the roundup.request object
2891 - @template : name of the template
2892 - form : name of the form to store back the date
2893 - property : name of the property of the form to store
2894 back the date
2895 - date : current date
2896 - display : when browsing, specifies year and month
2898 html will simply be a table.
2899 """
2900 tz = request.client.db.getUserTimezone()
2901 current_date = date.Date(".").local(tz)
2902 date_str = request.form.getfirst("date", current_date)
2903 display = request.form.getfirst("display", date_str)
2904 template = request.form.getfirst("@template", "calendar")
2905 form = request.form.getfirst("form")
2906 property = request.form.getfirst("property")
2907 curr_date = date.Date(date_str) # to highlight
2908 display = date.Date(display) # to show
2909 day = display.day
2911 # for navigation
2912 date_prev_month = display + date.Interval("-1m")
2913 date_next_month = display + date.Interval("+1m")
2914 date_prev_year = display + date.Interval("-1y")
2915 date_next_year = display + date.Interval("+1y")
2917 res = []
2919 base_link = "%s?@template=%s&property=%s&form=%s&date=%s" % \
2920 (request.classname, template, property, form, curr_date)
2922 # navigation
2923 # month
2924 res.append('<table class="calendar"><tr><td>')
2925 res.append(' <table width="100%" class="calendar_nav"><tr>')
2926 link = "&display=%s"%date_prev_month
2927 res.append(' <td><a href="%s&display=%s"><</a></td>'%(base_link,
2928 date_prev_month))
2929 res.append(' <td>%s</td>'%calendar.month_name[display.month])
2930 res.append(' <td><a href="%s&display=%s">></a></td>'%(base_link,
2931 date_next_month))
2932 # spacer
2933 res.append(' <td width="100%"></td>')
2934 # year
2935 res.append(' <td><a href="%s&display=%s"><</a></td>'%(base_link,
2936 date_prev_year))
2937 res.append(' <td>%s</td>'%display.year)
2938 res.append(' <td><a href="%s&display=%s">></a></td>'%(base_link,
2939 date_next_year))
2940 res.append(' </tr></table>')
2941 res.append(' </td></tr>')
2943 # the calendar
2944 res.append(' <tr><td><table class="calendar_display">')
2945 res.append(' <tr class="weekdays">')
2946 for day in calendar.weekheader(3).split():
2947 res.append(' <td>%s</td>'%day)
2948 res.append(' </tr>')
2949 for week in calendar.monthcalendar(display.year, display.month):
2950 res.append(' <tr>')
2951 for day in week:
2952 link = "javascript:form[field].value = '%d-%02d-%02d'; " \
2953 "window.close ();"%(display.year, display.month, day)
2954 if (day == curr_date.day and display.month == curr_date.month
2955 and display.year == curr_date.year):
2956 # highlight
2957 style = "today"
2958 else :
2959 style = ""
2960 if day:
2961 res.append(' <td class="%s"><a href="%s">%s</a></td>'%(
2962 style, link, day))
2963 else :
2964 res.append(' <td></td>')
2965 res.append(' </tr>')
2966 res.append('</table></td></tr></table>')
2967 return "\n".join(res)
2969 class MissingValue:
2970 def __init__(self, description, **kwargs):
2971 self.__description = description
2972 for key, value in kwargs.items():
2973 self.__dict__[key] = value
2975 def __call__(self, *args, **kwargs): return MissingValue(self.__description)
2976 def __getattr__(self, name):
2977 # This allows assignments which assume all intermediate steps are Null
2978 # objects if they don't exist yet.
2979 #
2980 # For example (with just 'client' defined):
2981 #
2982 # client.db.config.TRACKER_WEB = 'BASE/'
2983 self.__dict__[name] = MissingValue(self.__description)
2984 return getattr(self, name)
2986 def __getitem__(self, key): return self
2987 def __nonzero__(self): return 0
2988 def __str__(self): return '[%s]'%self.__description
2989 def __repr__(self): return '<MissingValue 0x%x "%s">'%(id(self),
2990 self.__description)
2991 def gettext(self, str): return str
2992 _ = gettext
2994 # vim: set et sts=4 sw=4 :