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 not args[k]:
1112 if current.has_key(k):
1113 cell.append('%s: %s'%(self._(k), current[k]))
1114 current[k] = '(no value)'
1115 else:
1116 cell.append(self._('%s: (no value)')%self._(k))
1118 else:
1119 cell.append('%s: %s'%(self._(k), str(args[k])))
1120 if current.has_key(k):
1121 cell[-1] += ' -> %s'%current[k]
1122 current[k] = str(args[k])
1124 arg_s = '<br />'.join(cell)
1125 else:
1126 # unkown event!!
1127 comments['unknown'] = self._(
1128 "<strong><em>This event is not handled"
1129 " by the history display!</em></strong>")
1130 arg_s = '<strong><em>' + str(args) + '</em></strong>'
1131 date_s = date_s.replace(' ', ' ')
1132 # if the user's an itemid, figure the username (older journals
1133 # have the username)
1134 if dre.match(user):
1135 user = self._db.user.get(user, 'username')
1136 l.append('<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>'%(
1137 date_s, user, self._(action), arg_s))
1138 if comments:
1139 l.append(self._(
1140 '<tr><td colspan=4><strong>Note:</strong></td></tr>'))
1141 for entry in comments.values():
1142 l.append('<tr><td colspan=4>%s</td></tr>'%entry)
1144 if direction == 'ascending':
1145 l.reverse()
1147 l[0:0] = ['<table class="history">'
1148 '<tr><th colspan="4" class="header">',
1149 self._('History'),
1150 '</th></tr><tr>',
1151 self._('<th>Date</th>'),
1152 self._('<th>User</th>'),
1153 self._('<th>Action</th>'),
1154 self._('<th>Args</th>'),
1155 '</tr>']
1156 l.append('</table>')
1157 return '\n'.join(l)
1159 def renderQueryForm(self):
1160 """ Render this item, which is a query, as a search form.
1161 """
1162 # create a new request and override the specified args
1163 req = HTMLRequest(self._client)
1164 req.classname = self._klass.get(self._nodeid, 'klass')
1165 name = self._klass.get(self._nodeid, 'name')
1166 req.updateFromURL(self._klass.get(self._nodeid, 'url') +
1167 '&@queryname=%s'%urllib.quote(name))
1169 # new template, using the specified classname and request
1170 pt = self._client.instance.templates.get(req.classname, 'search')
1171 # The context for a search page should be the class, not any
1172 # node.
1173 self._client.nodeid = None
1175 # use our fabricated request
1176 return pt.render(self._client, req.classname, req)
1178 def download_url(self):
1179 """ Assume that this item is a FileClass and that it has a name
1180 and content. Construct a URL for the download of the content.
1181 """
1182 name = self._klass.get(self._nodeid, 'name')
1183 url = '%s%s/%s'%(self._classname, self._nodeid, name)
1184 return urllib.quote(url)
1186 def copy_url(self, exclude=("messages", "files")):
1187 """Construct a URL for creating a copy of this item
1189 "exclude" is an optional list of properties that should
1190 not be copied to the new object. By default, this list
1191 includes "messages" and "files" properties. Note that
1192 "id" property cannot be copied.
1194 """
1195 exclude = ("id", "activity", "actor", "creation", "creator") \
1196 + tuple(exclude)
1197 query = {
1198 "@template": "item",
1199 "@note": self._("Copy of %(class)s %(id)s") % {
1200 "class": self._(self._classname), "id": self._nodeid},
1201 }
1202 for name in self._props.keys():
1203 if name not in exclude:
1204 query[name] = self[name].plain()
1205 return self._classname + "?" + "&".join(
1206 ["%s=%s" % (key, urllib.quote(value))
1207 for key, value in query.items()])
1209 class _HTMLUser(_HTMLItem):
1210 """Add ability to check for permissions on users.
1211 """
1212 _marker = []
1213 def hasPermission(self, permission, classname=_marker,
1214 property=None, itemid=None):
1215 """Determine if the user has the Permission.
1217 The class being tested defaults to the template's class, but may
1218 be overidden for this test by suppling an alternate classname.
1219 """
1220 if classname is self._marker:
1221 classname = self._client.classname
1222 return self._db.security.hasPermission(permission,
1223 self._nodeid, classname, property, itemid)
1225 def hasRole(self, *rolenames):
1226 """Determine whether the user has any role in rolenames."""
1227 return self._db.user.has_role(self._nodeid, *rolenames)
1229 def HTMLItem(client, classname, nodeid, anonymous=0):
1230 if classname == 'user':
1231 return _HTMLUser(client, classname, nodeid, anonymous)
1232 else:
1233 return _HTMLItem(client, classname, nodeid, anonymous)
1235 class HTMLProperty(HTMLInputMixin, HTMLPermissions):
1236 """ String, Number, Date, Interval HTMLProperty
1238 Has useful attributes:
1240 _name the name of the property
1241 _value the value of the property if any
1243 A wrapper object which may be stringified for the plain() behaviour.
1244 """
1245 def __init__(self, client, classname, nodeid, prop, name, value,
1246 anonymous=0):
1247 self._client = client
1248 self._db = client.db
1249 self._ = client._
1250 self._classname = classname
1251 self._nodeid = nodeid
1252 self._prop = prop
1253 self._value = value
1254 self._anonymous = anonymous
1255 self._name = name
1256 if not anonymous:
1257 if nodeid:
1258 self._formname = '%s%s@%s'%(classname, nodeid, name)
1259 else:
1260 # This case occurs when creating a property for a
1261 # non-anonymous class.
1262 self._formname = '%s@%s'%(classname, name)
1263 else:
1264 self._formname = name
1266 # If no value is already present for this property, see if one
1267 # is specified in the current form.
1268 form = self._client.form
1269 if not self._value and form.has_key(self._formname):
1270 if isinstance(prop, hyperdb.Multilink):
1271 value = lookupIds(self._db, prop,
1272 handleListCGIValue(form[self._formname]),
1273 fail_ok=1)
1274 elif isinstance(prop, hyperdb.Link):
1275 value = form.getfirst(self._formname).strip()
1276 if value:
1277 value = lookupIds(self._db, prop, [value],
1278 fail_ok=1)[0]
1279 else:
1280 value = None
1281 else:
1282 value = form.getfirst(self._formname).strip() or None
1283 self._value = value
1285 HTMLInputMixin.__init__(self)
1287 def __repr__(self):
1288 classname = self.__class__.__name__
1289 return '<%s(0x%x) %s %r %r>'%(classname, id(self), self._formname,
1290 self._prop, self._value)
1291 def __str__(self):
1292 return self.plain()
1293 def __cmp__(self, other):
1294 if isinstance(other, HTMLProperty):
1295 return cmp(self._value, other._value)
1296 return cmp(self._value, other)
1298 def __nonzero__(self):
1299 return not not self._value
1301 def isset(self):
1302 """Is my _value not None?"""
1303 return self._value is not None
1305 def is_edit_ok(self):
1306 """Should the user be allowed to use an edit form field for this
1307 property. Check "Create" for new items, or "Edit" for existing
1308 ones.
1309 """
1310 perm = self._db.security.hasPermission
1311 userid = self._client.userid
1312 if self._nodeid:
1313 if not perm('Web Access', userid):
1314 return False
1315 return perm('Edit', userid, self._classname, self._name,
1316 self._nodeid)
1317 return perm('Create', userid, self._classname, self._name) or \
1318 perm('Register', userid, self._classname, self._name)
1320 def is_view_ok(self):
1321 """ Is the user allowed to View the current class?
1322 """
1323 perm = self._db.security.hasPermission
1324 if perm('Web Access', self._client.userid) and perm('View',
1325 self._client.userid, self._classname, self._name, self._nodeid):
1326 return 1
1327 return self.is_edit_ok()
1329 class StringHTMLProperty(HTMLProperty):
1330 hyper_re = re.compile(r'''(
1331 (?P<url>
1332 (
1333 (ht|f)tp(s?):// # protocol
1334 ([\w]+(:\w+)?@)? # username/password
1335 ([\w\-]+) # hostname
1336 ((\.[\w-]+)+)? # .domain.etc
1337 | # ... or ...
1338 ([\w]+(:\w+)?@)? # username/password
1339 www\. # "www."
1340 ([\w\-]+\.)+ # hostname
1341 [\w]{2,5} # TLD
1342 )
1343 (:[\d]{1,5})? # port
1344 (/[\w\-$.+!*(),;:@&=?/~\\#%]*)? # path etc.
1345 )|
1346 (?P<email>[-+=%/\w\.]+@[\w\.\-]+)|
1347 (?P<item>(?P<class>[A-Za-z_]+)(\s*)(?P<id>\d+))
1348 )''', re.X | re.I)
1349 protocol_re = re.compile('^(ht|f)tp(s?)://', re.I)
1353 def _hyper_repl(self, match):
1354 if match.group('url'):
1355 return self._hyper_repl_url(match, '<a href="%s">%s</a>%s')
1356 elif match.group('email'):
1357 return self._hyper_repl_email(match, '<a href="mailto:%s">%s</a>')
1358 elif len(match.group('id')) < 10:
1359 return self._hyper_repl_item(match,
1360 '<a href="%(cls)s%(id)s">%(item)s</a>')
1361 else:
1362 # just return the matched text
1363 return match.group(0)
1365 def _hyper_repl_url(self, match, replacement):
1366 u = s = match.group('url')
1367 if not self.protocol_re.search(s):
1368 u = 'http://' + s
1369 end = ''
1370 if '>' in s:
1371 # catch an escaped ">" in the URL
1372 pos = s.find('>')
1373 end = s[pos:]
1374 u = s = s[:pos]
1375 if ')' in s and s.count('(') != s.count(')'):
1376 # don't include extraneous ')' in the link
1377 pos = s.rfind(')')
1378 end = s[pos:] + end
1379 u = s = s[:pos]
1380 return replacement % (u, s, end)
1382 def _hyper_repl_email(self, match, replacement):
1383 s = match.group('email')
1384 return replacement % (s, s)
1386 def _hyper_repl_item(self, match, replacement):
1387 item = match.group('item')
1388 cls = match.group('class').lower()
1389 id = match.group('id')
1390 try:
1391 # make sure cls is a valid tracker classname
1392 cl = self._db.getclass(cls)
1393 if not cl.hasnode(id):
1394 return item
1395 return replacement % locals()
1396 except KeyError:
1397 return item
1400 def _hyper_repl_rst(self, match):
1401 if match.group('url'):
1402 s = match.group('url')
1403 return '`%s <%s>`_'%(s, s)
1404 elif match.group('email'):
1405 s = match.group('email')
1406 return '`%s <mailto:%s>`_'%(s, s)
1407 elif len(match.group('id')) < 10:
1408 return self._hyper_repl_item(match,'`%(item)s <%(cls)s%(id)s>`_')
1409 else:
1410 # just return the matched text
1411 return match.group(0)
1413 def hyperlinked(self):
1414 """ Render a "hyperlinked" version of the text """
1415 return self.plain(hyperlink=1)
1417 def plain(self, escape=0, hyperlink=0):
1418 """Render a "plain" representation of the property
1420 - "escape" turns on/off HTML quoting
1421 - "hyperlink" turns on/off in-text hyperlinking of URLs, email
1422 addresses and designators
1423 """
1424 if not self.is_view_ok():
1425 return self._('[hidden]')
1427 if self._value is None:
1428 return ''
1429 if escape:
1430 s = cgi.escape(str(self._value))
1431 else:
1432 s = str(self._value)
1433 if hyperlink:
1434 # no, we *must* escape this text
1435 if not escape:
1436 s = cgi.escape(s)
1437 s = self.hyper_re.sub(self._hyper_repl, s)
1438 return s
1440 def wrapped(self, escape=1, hyperlink=1):
1441 """Render a "wrapped" representation of the property.
1443 We wrap long lines at 80 columns on the nearest whitespace. Lines
1444 with no whitespace are not broken to force wrapping.
1446 Note that unlike plain() we default wrapped() to have the escaping
1447 and hyperlinking turned on since that's the most common usage.
1449 - "escape" turns on/off HTML quoting
1450 - "hyperlink" turns on/off in-text hyperlinking of URLs, email
1451 addresses and designators
1452 """
1453 if not self.is_view_ok():
1454 return self._('[hidden]')
1456 if self._value is None:
1457 return ''
1458 s = support.wrap(str(self._value), width=80)
1459 if escape:
1460 s = cgi.escape(s)
1461 if hyperlink:
1462 # no, we *must* escape this text
1463 if not escape:
1464 s = cgi.escape(s)
1465 s = self.hyper_re.sub(self._hyper_repl, s)
1466 return s
1468 def stext(self, escape=0, hyperlink=1):
1469 """ Render the value of the property as StructuredText.
1471 This requires the StructureText module to be installed separately.
1472 """
1473 if not self.is_view_ok():
1474 return self._('[hidden]')
1476 s = self.plain(escape=escape, hyperlink=hyperlink)
1477 if not StructuredText:
1478 return s
1479 return StructuredText(s,level=1,header=0)
1481 def rst(self, hyperlink=1):
1482 """ Render the value of the property as ReStructuredText.
1484 This requires docutils to be installed separately.
1485 """
1486 if not self.is_view_ok():
1487 return self._('[hidden]')
1489 if not ReStructuredText:
1490 return self.plain(escape=0, hyperlink=hyperlink)
1491 s = self.plain(escape=0, hyperlink=0)
1492 if hyperlink:
1493 s = self.hyper_re.sub(self._hyper_repl_rst, s)
1494 return ReStructuredText(s, writer_name="html")["html_body"].encode("utf-8",
1495 "replace")
1497 def field(self, **kwargs):
1498 """ Render the property as a field in HTML.
1500 If not editable, just display the value via plain().
1501 """
1502 if not self.is_edit_ok():
1503 return self.plain(escape=1)
1505 value = self._value
1506 if value is None:
1507 value = ''
1509 kwargs.setdefault("size", 30)
1510 kwargs.update({"name": self._formname, "value": value})
1511 return self.input(**kwargs)
1513 def multiline(self, escape=0, rows=5, cols=40, **kwargs):
1514 """ Render a multiline form edit field for the property.
1516 If not editable, just display the plain() value in a <pre> tag.
1517 """
1518 if not self.is_edit_ok():
1519 return '<pre>%s</pre>'%self.plain()
1521 if self._value is None:
1522 value = ''
1523 else:
1524 value = cgi.escape(str(self._value))
1526 value = '"'.join(value.split('"'))
1527 name = self._formname
1528 passthrough_args = cgi_escape_attrs(**kwargs)
1529 return ('<textarea %(passthrough_args)s name="%(name)s" id="%(name)s"'
1530 ' rows="%(rows)s" cols="%(cols)s">'
1531 '%(value)s</textarea>') % locals()
1533 def email(self, escape=1):
1534 """ Render the value of the property as an obscured email address
1535 """
1536 if not self.is_view_ok():
1537 return self._('[hidden]')
1539 if self._value is None:
1540 value = ''
1541 else:
1542 value = str(self._value)
1543 split = value.split('@')
1544 if len(split) == 2:
1545 name, domain = split
1546 domain = ' '.join(domain.split('.')[:-1])
1547 name = name.replace('.', ' ')
1548 value = '%s at %s ...'%(name, domain)
1549 else:
1550 value = value.replace('.', ' ')
1551 if escape:
1552 value = cgi.escape(value)
1553 return value
1555 class PasswordHTMLProperty(HTMLProperty):
1556 def plain(self, escape=0):
1557 """ Render a "plain" representation of the property
1558 """
1559 if not self.is_view_ok():
1560 return self._('[hidden]')
1562 if self._value is None:
1563 return ''
1564 return self._('*encrypted*')
1566 def field(self, size=30, **kwargs):
1567 """ Render a form edit field for the property.
1569 If not editable, just display the value via plain().
1570 """
1571 if not self.is_edit_ok():
1572 return self.plain(escape=1)
1574 return self.input(type="password", name=self._formname, size=size,
1575 **kwargs)
1577 def confirm(self, size=30):
1578 """ Render a second form edit field for the property, used for
1579 confirmation that the user typed the password correctly. Generates
1580 a field with name "@confirm@name".
1582 If not editable, display nothing.
1583 """
1584 if not self.is_edit_ok():
1585 return ''
1587 return self.input(type="password",
1588 name="@confirm@%s"%self._formname,
1589 id="%s-confirm"%self._formname,
1590 size=size)
1592 class NumberHTMLProperty(HTMLProperty):
1593 def plain(self, escape=0):
1594 """ Render a "plain" representation of the property
1595 """
1596 if not self.is_view_ok():
1597 return self._('[hidden]')
1599 if self._value is None:
1600 return ''
1602 return str(self._value)
1604 def field(self, size=30, **kwargs):
1605 """ Render a form edit field for the property.
1607 If not editable, just display the value via plain().
1608 """
1609 if not self.is_edit_ok():
1610 return self.plain(escape=1)
1612 value = self._value
1613 if value is None:
1614 value = ''
1616 return self.input(name=self._formname, value=value, size=size,
1617 **kwargs)
1619 def __int__(self):
1620 """ Return an int of me
1621 """
1622 return int(self._value)
1624 def __float__(self):
1625 """ Return a float of me
1626 """
1627 return float(self._value)
1630 class BooleanHTMLProperty(HTMLProperty):
1631 def plain(self, escape=0):
1632 """ Render a "plain" representation of the property
1633 """
1634 if not self.is_view_ok():
1635 return self._('[hidden]')
1637 if self._value is None:
1638 return ''
1639 return self._value and self._("Yes") or self._("No")
1641 def field(self, **kwargs):
1642 """ Render a form edit field for the property
1644 If not editable, just display the value via plain().
1645 """
1646 if not self.is_edit_ok():
1647 return self.plain(escape=1)
1649 value = self._value
1650 if isinstance(value, str) or isinstance(value, unicode):
1651 value = value.strip().lower() in ('checked', 'yes', 'true',
1652 'on', '1')
1654 checked = value and "checked" or ""
1655 if value:
1656 s = self.input(type="radio", name=self._formname, value="yes",
1657 checked="checked", **kwargs)
1658 s += self._('Yes')
1659 s +=self.input(type="radio", name=self._formname, value="no",
1660 **kwargs)
1661 s += self._('No')
1662 else:
1663 s = self.input(type="radio", name=self._formname, value="yes",
1664 **kwargs)
1665 s += self._('Yes')
1666 s +=self.input(type="radio", name=self._formname, value="no",
1667 checked="checked", **kwargs)
1668 s += self._('No')
1669 return s
1671 class DateHTMLProperty(HTMLProperty):
1673 _marker = []
1675 def __init__(self, client, classname, nodeid, prop, name, value,
1676 anonymous=0, offset=None):
1677 HTMLProperty.__init__(self, client, classname, nodeid, prop, name,
1678 value, anonymous=anonymous)
1679 if self._value and not (isinstance(self._value, str) or
1680 isinstance(self._value, unicode)):
1681 self._value.setTranslator(self._client.translator)
1682 self._offset = offset
1683 if self._offset is None :
1684 self._offset = self._prop.offset (self._db)
1686 def plain(self, escape=0):
1687 """ Render a "plain" representation of the property
1688 """
1689 if not self.is_view_ok():
1690 return self._('[hidden]')
1692 if self._value is None:
1693 return ''
1694 if self._offset is None:
1695 offset = self._db.getUserTimezone()
1696 else:
1697 offset = self._offset
1698 return str(self._value.local(offset))
1700 def now(self, str_interval=None):
1701 """ Return the current time.
1703 This is useful for defaulting a new value. Returns a
1704 DateHTMLProperty.
1705 """
1706 if not self.is_view_ok():
1707 return self._('[hidden]')
1709 ret = date.Date('.', translator=self._client)
1711 if isinstance(str_interval, basestring):
1712 sign = 1
1713 if str_interval[0] == '-':
1714 sign = -1
1715 str_interval = str_interval[1:]
1716 interval = date.Interval(str_interval, translator=self._client)
1717 if sign > 0:
1718 ret = ret + interval
1719 else:
1720 ret = ret - interval
1722 return DateHTMLProperty(self._client, self._classname, self._nodeid,
1723 self._prop, self._formname, ret)
1725 def field(self, size=30, default=None, format=_marker, popcal=True,
1726 **kwargs):
1727 """Render a form edit field for the property
1729 If not editable, just display the value via plain().
1731 If "popcal" then include the Javascript calendar editor.
1732 Default=yes.
1734 The format string is a standard python strftime format string.
1735 """
1736 if not self.is_edit_ok():
1737 if format is self._marker:
1738 return self.plain(escape=1)
1739 else:
1740 return self.pretty(format)
1742 value = self._value
1744 if value is None:
1745 if default is None:
1746 raw_value = None
1747 else:
1748 if isinstance(default, basestring):
1749 raw_value = date.Date(default, translator=self._client)
1750 elif isinstance(default, date.Date):
1751 raw_value = default
1752 elif isinstance(default, DateHTMLProperty):
1753 raw_value = default._value
1754 else:
1755 raise ValueError, self._('default value for '
1756 'DateHTMLProperty must be either DateHTMLProperty '
1757 'or string date representation.')
1758 elif isinstance(value, str) or isinstance(value, unicode):
1759 # most likely erroneous input to be passed back to user
1760 if isinstance(value, unicode): value = value.encode('utf8')
1761 return self.input(name=self._formname, value=value, size=size,
1762 **kwargs)
1763 else:
1764 raw_value = value
1766 if raw_value is None:
1767 value = ''
1768 elif isinstance(raw_value, str) or isinstance(raw_value, unicode):
1769 if format is self._marker:
1770 value = raw_value
1771 else:
1772 value = date.Date(raw_value).pretty(format)
1773 else:
1774 if self._offset is None :
1775 offset = self._db.getUserTimezone()
1776 else :
1777 offset = self._offset
1778 value = raw_value.local(offset)
1779 if format is not self._marker:
1780 value = value.pretty(format)
1782 s = self.input(name=self._formname, value=value, size=size,
1783 **kwargs)
1784 if popcal:
1785 s += self.popcal()
1786 return s
1788 def reldate(self, pretty=1):
1789 """ Render the interval between the date and now.
1791 If the "pretty" flag is true, then make the display pretty.
1792 """
1793 if not self.is_view_ok():
1794 return self._('[hidden]')
1796 if not self._value:
1797 return ''
1799 # figure the interval
1800 interval = self._value - date.Date('.', translator=self._client)
1801 if pretty:
1802 return interval.pretty()
1803 return str(interval)
1805 def pretty(self, format=_marker):
1806 """ Render the date in a pretty format (eg. month names, spaces).
1808 The format string is a standard python strftime format string.
1809 Note that if the day is zero, and appears at the start of the
1810 string, then it'll be stripped from the output. This is handy
1811 for the situation when a date only specifies a month and a year.
1812 """
1813 if not self.is_view_ok():
1814 return self._('[hidden]')
1816 if self._offset is None:
1817 offset = self._db.getUserTimezone()
1818 else:
1819 offset = self._offset
1821 if not self._value:
1822 return ''
1823 elif format is not self._marker:
1824 return self._value.local(offset).pretty(format)
1825 else:
1826 return self._value.local(offset).pretty()
1828 def local(self, offset):
1829 """ Return the date/time as a local (timezone offset) date/time.
1830 """
1831 if not self.is_view_ok():
1832 return self._('[hidden]')
1834 return DateHTMLProperty(self._client, self._classname, self._nodeid,
1835 self._prop, self._formname, self._value, offset=offset)
1837 def popcal(self, width=300, height=200, label="(cal)",
1838 form="itemSynopsis"):
1839 """Generate a link to a calendar pop-up window.
1841 item: HTMLProperty e.g.: context.deadline
1842 """
1843 if self.isset():
1844 date = "&date=%s"%self._value
1845 else :
1846 date = ""
1847 return ('<a class="classhelp" href="javascript:help_window('
1848 "'%s?@template=calendar&property=%s&form=%s%s', %d, %d)"
1849 '">%s</a>'%(self._classname, self._name, form, date, width,
1850 height, label))
1852 class IntervalHTMLProperty(HTMLProperty):
1853 def __init__(self, client, classname, nodeid, prop, name, value,
1854 anonymous=0):
1855 HTMLProperty.__init__(self, client, classname, nodeid, prop,
1856 name, value, anonymous)
1857 if self._value and not isinstance(self._value, (str, unicode)):
1858 self._value.setTranslator(self._client.translator)
1860 def plain(self, escape=0):
1861 """ Render a "plain" representation of the property
1862 """
1863 if not self.is_view_ok():
1864 return self._('[hidden]')
1866 if self._value is None:
1867 return ''
1868 return str(self._value)
1870 def pretty(self):
1871 """ Render the interval in a pretty format (eg. "yesterday")
1872 """
1873 if not self.is_view_ok():
1874 return self._('[hidden]')
1876 return self._value.pretty()
1878 def field(self, size=30, **kwargs):
1879 """ Render a form edit field for the property
1881 If not editable, just display the value via plain().
1882 """
1883 if not self.is_edit_ok():
1884 return self.plain(escape=1)
1886 value = self._value
1887 if value is None:
1888 value = ''
1890 return self.input(name=self._formname, value=value, size=size,
1891 **kwargs)
1893 class LinkHTMLProperty(HTMLProperty):
1894 """ Link HTMLProperty
1895 Include the above as well as being able to access the class
1896 information. Stringifying the object itself results in the value
1897 from the item being displayed. Accessing attributes of this object
1898 result in the appropriate entry from the class being queried for the
1899 property accessed (so item/assignedto/name would look up the user
1900 entry identified by the assignedto property on item, and then the
1901 name property of that user)
1902 """
1903 def __init__(self, *args, **kw):
1904 HTMLProperty.__init__(self, *args, **kw)
1905 # if we're representing a form value, then the -1 from the form really
1906 # should be a None
1907 if str(self._value) == '-1':
1908 self._value = None
1910 def __getattr__(self, attr):
1911 """ return a new HTMLItem """
1912 if not self._value:
1913 # handle a special page templates lookup
1914 if attr == '__render_with_namespace__':
1915 def nothing(*args, **kw):
1916 return ''
1917 return nothing
1918 msg = self._('Attempt to look up %(attr)s on a missing value')
1919 return MissingValue(msg%locals())
1920 i = HTMLItem(self._client, self._prop.classname, self._value)
1921 return getattr(i, attr)
1923 def plain(self, escape=0):
1924 """ Render a "plain" representation of the property
1925 """
1926 if not self.is_view_ok():
1927 return self._('[hidden]')
1929 if self._value is None:
1930 return ''
1931 linkcl = self._db.classes[self._prop.classname]
1932 k = linkcl.labelprop(1)
1933 if num_re.match(self._value):
1934 try:
1935 value = str(linkcl.get(self._value, k))
1936 except IndexError:
1937 value = self._value
1938 else :
1939 value = self._value
1940 if escape:
1941 value = cgi.escape(value)
1942 return value
1944 def field(self, showid=0, size=None, **kwargs):
1945 """ Render a form edit field for the property
1947 If not editable, just display the value via plain().
1948 """
1949 if not self.is_edit_ok():
1950 return self.plain(escape=1)
1952 # edit field
1953 linkcl = self._db.getclass(self._prop.classname)
1954 if self._value is None:
1955 value = ''
1956 else:
1957 k = linkcl.getkey()
1958 if k and num_re.match(self._value):
1959 value = linkcl.get(self._value, k)
1960 else:
1961 value = self._value
1962 return self.input(name=self._formname, value=value, size=size,
1963 **kwargs)
1965 def menu(self, size=None, height=None, showid=0, additional=[], value=None,
1966 sort_on=None, html_kwargs = {}, **conditions):
1967 """ Render a form select list for this property
1969 "size" is used to limit the length of the list labels
1970 "height" is used to set the <select> tag's "size" attribute
1971 "showid" includes the item ids in the list labels
1972 "value" specifies which item is pre-selected
1973 "additional" lists properties which should be included in the
1974 label
1975 "sort_on" indicates the property to sort the list on as
1976 (direction, property) where direction is '+' or '-'. A
1977 single string with the direction prepended may be used.
1978 For example: ('-', 'order'), '+name'.
1980 The remaining keyword arguments are used as conditions for
1981 filtering the items in the list - they're passed as the
1982 "filterspec" argument to a Class.filter() call.
1984 If not editable, just display the value via plain().
1985 """
1986 if not self.is_edit_ok():
1987 return self.plain(escape=1)
1989 # Since None indicates the default, we need another way to
1990 # indicate "no selection". We use -1 for this purpose, as
1991 # that is the value we use when submitting a form without the
1992 # value set.
1993 if value is None:
1994 value = self._value
1995 elif value == '-1':
1996 value = None
1998 linkcl = self._db.getclass(self._prop.classname)
1999 l = ['<select %s>'%cgi_escape_attrs(name = self._formname,
2000 **html_kwargs)]
2001 k = linkcl.labelprop(1)
2002 s = ''
2003 if value is None:
2004 s = 'selected="selected" '
2005 l.append(self._('<option %svalue="-1">- no selection -</option>')%s)
2007 if sort_on is not None:
2008 if not isinstance(sort_on, tuple):
2009 if sort_on[0] in '+-':
2010 sort_on = (sort_on[0], sort_on[1:])
2011 else:
2012 sort_on = ('+', sort_on)
2013 else:
2014 sort_on = ('+', linkcl.orderprop())
2016 options = [opt
2017 for opt in linkcl.filter(None, conditions, sort_on, (None, None))
2018 if self._db.security.hasPermission("View", self._client.userid,
2019 linkcl.classname, itemid=opt)]
2021 # make sure we list the current value if it's retired
2022 if value and value not in options:
2023 options.insert(0, value)
2025 if additional:
2026 additional_fns = []
2027 props = linkcl.getprops()
2028 for propname in additional:
2029 prop = props[propname]
2030 if isinstance(prop, hyperdb.Link):
2031 cl = self._db.getclass(prop.classname)
2032 labelprop = cl.labelprop()
2033 fn = lambda optionid: cl.get(linkcl.get(optionid,
2034 propname),
2035 labelprop)
2036 else:
2037 fn = lambda optionid: linkcl.get(optionid, propname)
2038 additional_fns.append(fn)
2040 for optionid in options:
2041 # get the option value, and if it's None use an empty string
2042 option = linkcl.get(optionid, k) or ''
2044 # figure if this option is selected
2045 s = ''
2046 if value in [optionid, option]:
2047 s = 'selected="selected" '
2049 # figure the label
2050 if showid:
2051 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
2052 elif not option:
2053 lab = '%s%s'%(self._prop.classname, optionid)
2054 else:
2055 lab = option
2057 # truncate if it's too long
2058 if size is not None and len(lab) > size:
2059 lab = lab[:size-3] + '...'
2060 if additional:
2061 m = []
2062 for fn in additional_fns:
2063 m.append(str(fn(optionid)))
2064 lab = lab + ' (%s)'%', '.join(m)
2066 # and generate
2067 lab = cgi.escape(self._(lab))
2068 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
2069 l.append('</select>')
2070 return '\n'.join(l)
2071 # def checklist(self, ...)
2075 class MultilinkHTMLProperty(HTMLProperty):
2076 """ Multilink HTMLProperty
2078 Also be iterable, returning a wrapper object like the Link case for
2079 each entry in the multilink.
2080 """
2081 def __init__(self, *args, **kwargs):
2082 HTMLProperty.__init__(self, *args, **kwargs)
2083 if self._value:
2084 display_value = lookupIds(self._db, self._prop, self._value,
2085 fail_ok=1, do_lookup=False)
2086 sortfun = make_sort_function(self._db, self._prop.classname)
2087 # sorting fails if the value contains
2088 # items not yet stored in the database
2089 # ignore these errors to preserve user input
2090 try:
2091 display_value.sort(sortfun)
2092 except:
2093 pass
2094 self._value = display_value
2096 def __len__(self):
2097 """ length of the multilink """
2098 return len(self._value)
2100 def __getattr__(self, attr):
2101 """ no extended attribute accesses make sense here """
2102 raise AttributeError, attr
2104 def viewableGenerator(self, values):
2105 """Used to iterate over only the View'able items in a class."""
2106 check = self._db.security.hasPermission
2107 userid = self._client.userid
2108 classname = self._prop.classname
2109 if check('Web Access', userid):
2110 for value in values:
2111 if check('View', userid, classname, itemid=value):
2112 yield HTMLItem(self._client, classname, value)
2114 def __iter__(self):
2115 """ iterate and return a new HTMLItem
2116 """
2117 return self.viewableGenerator(self._value)
2119 def reverse(self):
2120 """ return the list in reverse order
2121 """
2122 l = self._value[:]
2123 l.reverse()
2124 return self.viewableGenerator(l)
2126 def sorted(self, property):
2127 """ Return this multilink sorted by the given property """
2128 value = list(self.__iter__())
2129 value.sort(lambda a,b:cmp(a[property], b[property]))
2130 return value
2132 def __contains__(self, value):
2133 """ Support the "in" operator. We have to make sure the passed-in
2134 value is a string first, not a HTMLProperty.
2135 """
2136 return str(value) in self._value
2138 def isset(self):
2139 """Is my _value not []?"""
2140 return self._value != []
2142 def plain(self, escape=0):
2143 """ Render a "plain" representation of the property
2144 """
2145 if not self.is_view_ok():
2146 return self._('[hidden]')
2148 linkcl = self._db.classes[self._prop.classname]
2149 k = linkcl.labelprop(1)
2150 labels = []
2151 for v in self._value:
2152 if num_re.match(v):
2153 try:
2154 label = linkcl.get(v, k)
2155 except IndexError:
2156 label = None
2157 # fall back to designator if label is None
2158 if label is None: label = '%s%s'%(self._prop.classname, k)
2159 else:
2160 label = v
2161 labels.append(label)
2162 value = ', '.join(labels)
2163 if escape:
2164 value = cgi.escape(value)
2165 return value
2167 def field(self, size=30, showid=0, **kwargs):
2168 """ Render a form edit field for the property
2170 If not editable, just display the value via plain().
2171 """
2172 if not self.is_edit_ok():
2173 return self.plain(escape=1)
2175 linkcl = self._db.getclass(self._prop.classname)
2177 if 'value' not in kwargs:
2178 value = self._value[:]
2179 # map the id to the label property
2180 if not linkcl.getkey():
2181 showid=1
2182 if not showid:
2183 k = linkcl.labelprop(1)
2184 value = lookupKeys(linkcl, k, value)
2185 value = ','.join(value)
2186 kwargs["value"] = value
2188 return self.input(name=self._formname, size=size, **kwargs)
2190 def menu(self, size=None, height=None, showid=0, additional=[],
2191 value=None, sort_on=None, html_kwargs = {}, **conditions):
2192 """ Render a form <select> list for this property.
2194 "size" is used to limit the length of the list labels
2195 "height" is used to set the <select> tag's "size" attribute
2196 "showid" includes the item ids in the list labels
2197 "additional" lists properties which should be included in the
2198 label
2199 "value" specifies which item is pre-selected
2200 "sort_on" indicates the property to sort the list on as
2201 (direction, property) where direction is '+' or '-'. A
2202 single string with the direction prepended may be used.
2203 For example: ('-', 'order'), '+name'.
2205 The remaining keyword arguments are used as conditions for
2206 filtering the items in the list - they're passed as the
2207 "filterspec" argument to a Class.filter() call.
2209 If not editable, just display the value via plain().
2210 """
2211 if not self.is_edit_ok():
2212 return self.plain(escape=1)
2214 if value is None:
2215 value = self._value
2217 linkcl = self._db.getclass(self._prop.classname)
2219 if sort_on is not None:
2220 if not isinstance(sort_on, tuple):
2221 if sort_on[0] in '+-':
2222 sort_on = (sort_on[0], sort_on[1:])
2223 else:
2224 sort_on = ('+', sort_on)
2225 else:
2226 sort_on = ('+', linkcl.orderprop())
2228 options = [opt
2229 for opt in linkcl.filter(None, conditions, sort_on)
2230 if self._db.security.hasPermission("View", self._client.userid,
2231 linkcl.classname, itemid=opt)]
2233 # make sure we list the current values if they're retired
2234 for val in value:
2235 if val not in options:
2236 options.insert(0, val)
2238 if not height:
2239 height = len(options)
2240 if value:
2241 # The "no selection" option.
2242 height += 1
2243 height = min(height, 7)
2244 l = ['<select multiple %s>'%cgi_escape_attrs(name = self._formname,
2245 size = height,
2246 **html_kwargs)]
2247 k = linkcl.labelprop(1)
2249 if value:
2250 l.append('<option value="%s">- no selection -</option>'
2251 % ','.join(['-' + v for v in value]))
2253 if additional:
2254 additional_fns = []
2255 props = linkcl.getprops()
2256 for propname in additional:
2257 prop = props[propname]
2258 if isinstance(prop, hyperdb.Link):
2259 cl = self._db.getclass(prop.classname)
2260 labelprop = cl.labelprop()
2261 fn = lambda optionid: cl.get(linkcl.get(optionid,
2262 propname),
2263 labelprop)
2264 else:
2265 fn = lambda optionid: linkcl.get(optionid, propname)
2266 additional_fns.append(fn)
2268 for optionid in options:
2269 # get the option value, and if it's None use an empty string
2270 option = linkcl.get(optionid, k) or ''
2272 # figure if this option is selected
2273 s = ''
2274 if optionid in value or option in value:
2275 s = 'selected="selected" '
2277 # figure the label
2278 if showid:
2279 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
2280 else:
2281 lab = option
2282 # truncate if it's too long
2283 if size is not None and len(lab) > size:
2284 lab = lab[:size-3] + '...'
2285 if additional:
2286 m = []
2287 for fn in additional_fns:
2288 m.append(str(fn(optionid)))
2289 lab = lab + ' (%s)'%', '.join(m)
2291 # and generate
2292 lab = cgi.escape(self._(lab))
2293 l.append('<option %svalue="%s">%s</option>'%(s, optionid,
2294 lab))
2295 l.append('</select>')
2296 return '\n'.join(l)
2299 # set the propclasses for HTMLItem
2300 propclasses = [
2301 (hyperdb.String, StringHTMLProperty),
2302 (hyperdb.Number, NumberHTMLProperty),
2303 (hyperdb.Boolean, BooleanHTMLProperty),
2304 (hyperdb.Date, DateHTMLProperty),
2305 (hyperdb.Interval, IntervalHTMLProperty),
2306 (hyperdb.Password, PasswordHTMLProperty),
2307 (hyperdb.Link, LinkHTMLProperty),
2308 (hyperdb.Multilink, MultilinkHTMLProperty),
2309 ]
2311 def register_propclass(prop, cls):
2312 for index,propclass in enumerate(propclasses):
2313 p, c = propclass
2314 if prop == p:
2315 propclasses[index] = (prop, cls)
2316 break
2317 else:
2318 propclasses.append((prop, cls))
2321 def make_sort_function(db, classname, sort_on=None):
2322 """Make a sort function for a given class.
2324 The list being sorted may contain mixed ids and labels.
2325 """
2326 linkcl = db.getclass(classname)
2327 if sort_on is None:
2328 sort_on = linkcl.orderprop()
2329 def sortfunc(a, b):
2330 if num_re.match(a):
2331 a = linkcl.get(a, sort_on)
2332 if num_re.match(b):
2333 b = linkcl.get(b, sort_on)
2334 return cmp(a, b)
2335 return sortfunc
2337 def handleListCGIValue(value):
2338 """ Value is either a single item or a list of items. Each item has a
2339 .value that we're actually interested in.
2340 """
2341 if isinstance(value, type([])):
2342 return [value.value for value in value]
2343 else:
2344 value = value.value.strip()
2345 if not value:
2346 return []
2347 return [v.strip() for v in value.split(',')]
2349 class HTMLRequest(HTMLInputMixin):
2350 """The *request*, holding the CGI form and environment.
2352 - "form" the CGI form as a cgi.FieldStorage
2353 - "env" the CGI environment variables
2354 - "base" the base URL for this instance
2355 - "user" a HTMLItem instance for this user
2356 - "language" as determined by the browser or config
2357 - "classname" the current classname (possibly None)
2358 - "template" the current template (suffix, also possibly None)
2360 Index args:
2362 - "columns" dictionary of the columns to display in an index page
2363 - "show" a convenience access to columns - request/show/colname will
2364 be true if the columns should be displayed, false otherwise
2365 - "sort" index sort column (direction, column name)
2366 - "group" index grouping property (direction, column name)
2367 - "filter" properties to filter the index on
2368 - "filterspec" values to filter the index on
2369 - "search_text" text to perform a full-text search on for an index
2370 """
2371 def __repr__(self):
2372 return '<HTMLRequest %r>'%self.__dict__
2374 def __init__(self, client):
2375 # _client is needed by HTMLInputMixin
2376 self._client = self.client = client
2378 # easier access vars
2379 self.form = client.form
2380 self.env = client.env
2381 self.base = client.base
2382 self.user = HTMLItem(client, 'user', client.userid)
2383 self.language = client.language
2385 # store the current class name and action
2386 self.classname = client.classname
2387 self.nodeid = client.nodeid
2388 self.template = client.template
2390 # the special char to use for special vars
2391 self.special_char = '@'
2393 HTMLInputMixin.__init__(self)
2395 self._post_init()
2397 def current_url(self):
2398 url = self.base
2399 if self.classname:
2400 url += self.classname
2401 if self.nodeid:
2402 url += self.nodeid
2403 args = {}
2404 if self.template:
2405 args['@template'] = self.template
2406 return self.indexargs_url(url, args)
2408 def _parse_sort(self, var, name):
2409 """ Parse sort/group options. Append to var
2410 """
2411 fields = []
2412 dirs = []
2413 for special in '@:':
2414 idx = 0
2415 key = '%s%s%d'%(special, name, idx)
2416 while key in self.form:
2417 self.special_char = special
2418 fields.append(self.form.getfirst(key))
2419 dirkey = '%s%sdir%d'%(special, name, idx)
2420 if dirkey in self.form:
2421 dirs.append(self.form.getfirst(dirkey))
2422 else:
2423 dirs.append(None)
2424 idx += 1
2425 key = '%s%s%d'%(special, name, idx)
2426 # backward compatible (and query) URL format
2427 key = special + name
2428 dirkey = key + 'dir'
2429 if key in self.form and not fields:
2430 fields = handleListCGIValue(self.form[key])
2431 if dirkey in self.form:
2432 dirs.append(self.form.getfirst(dirkey))
2433 if fields: # only try other special char if nothing found
2434 break
2435 for f, d in map(None, fields, dirs):
2436 if f.startswith('-'):
2437 var.append(('-', f[1:]))
2438 elif d:
2439 var.append(('-', f))
2440 else:
2441 var.append(('+', f))
2443 def _post_init(self):
2444 """ Set attributes based on self.form
2445 """
2446 # extract the index display information from the form
2447 self.columns = []
2448 for name in ':columns @columns'.split():
2449 if self.form.has_key(name):
2450 self.special_char = name[0]
2451 self.columns = handleListCGIValue(self.form[name])
2452 break
2453 self.show = support.TruthDict(self.columns)
2454 security = self._client.db.security
2455 userid = self._client.userid
2457 # sorting and grouping
2458 self.sort = []
2459 self.group = []
2460 self._parse_sort(self.sort, 'sort')
2461 self._parse_sort(self.group, 'group')
2462 self.sort = security.filterSortspec(userid, self.classname, self.sort)
2463 self.group = security.filterSortspec(userid, self.classname, self.group)
2465 # filtering
2466 self.filter = []
2467 for name in ':filter @filter'.split():
2468 if self.form.has_key(name):
2469 self.special_char = name[0]
2470 self.filter = handleListCGIValue(self.form[name])
2472 self.filterspec = {}
2473 db = self.client.db
2474 if self.classname is not None:
2475 cls = db.getclass (self.classname)
2476 for name in self.filter:
2477 if not self.form.has_key(name):
2478 continue
2479 prop = cls.get_transitive_prop (name)
2480 fv = self.form[name]
2481 if (isinstance(prop, hyperdb.Link) or
2482 isinstance(prop, hyperdb.Multilink)):
2483 self.filterspec[name] = lookupIds(db, prop,
2484 handleListCGIValue(fv))
2485 else:
2486 if isinstance(fv, type([])):
2487 self.filterspec[name] = [v.value for v in fv]
2488 elif name == 'id':
2489 # special case "id" property
2490 self.filterspec[name] = handleListCGIValue(fv)
2491 else:
2492 self.filterspec[name] = fv.value
2493 self.filterspec = security.filterFilterspec(userid, self.classname,
2494 self.filterspec)
2496 # full-text search argument
2497 self.search_text = None
2498 for name in ':search_text @search_text'.split():
2499 if self.form.has_key(name):
2500 self.special_char = name[0]
2501 self.search_text = self.form.getfirst(name)
2503 # pagination - size and start index
2504 # figure batch args
2505 self.pagesize = 50
2506 for name in ':pagesize @pagesize'.split():
2507 if self.form.has_key(name):
2508 self.special_char = name[0]
2509 try:
2510 self.pagesize = int(self.form.getfirst(name))
2511 except ValueError:
2512 # not an integer - ignore
2513 pass
2515 self.startwith = 0
2516 for name in ':startwith @startwith'.split():
2517 if self.form.has_key(name):
2518 self.special_char = name[0]
2519 try:
2520 self.startwith = int(self.form.getfirst(name))
2521 except ValueError:
2522 # not an integer - ignore
2523 pass
2525 # dispname
2526 if self.form.has_key('@dispname'):
2527 self.dispname = self.form.getfirst('@dispname')
2528 else:
2529 self.dispname = None
2531 def updateFromURL(self, url):
2532 """ Parse the URL for query args, and update my attributes using the
2533 values.
2534 """
2535 env = {'QUERY_STRING': url}
2536 self.form = cgi.FieldStorage(environ=env)
2538 self._post_init()
2540 def update(self, kwargs):
2541 """ Update my attributes using the keyword args
2542 """
2543 self.__dict__.update(kwargs)
2544 if kwargs.has_key('columns'):
2545 self.show = support.TruthDict(self.columns)
2547 def description(self):
2548 """ Return a description of the request - handle for the page title.
2549 """
2550 s = [self.client.db.config.TRACKER_NAME]
2551 if self.classname:
2552 if self.client.nodeid:
2553 s.append('- %s%s'%(self.classname, self.client.nodeid))
2554 else:
2555 if self.template == 'item':
2556 s.append('- new %s'%self.classname)
2557 elif self.template == 'index':
2558 s.append('- %s index'%self.classname)
2559 else:
2560 s.append('- %s %s'%(self.classname, self.template))
2561 else:
2562 s.append('- home')
2563 return ' '.join(s)
2565 def __str__(self):
2566 d = {}
2567 d.update(self.__dict__)
2568 f = ''
2569 for k in self.form.keys():
2570 f += '\n %r=%r'%(k,handleListCGIValue(self.form[k]))
2571 d['form'] = f
2572 e = ''
2573 for k,v in self.env.items():
2574 e += '\n %r=%r'%(k, v)
2575 d['env'] = e
2576 return """
2577 form: %(form)s
2578 base: %(base)r
2579 classname: %(classname)r
2580 template: %(template)r
2581 columns: %(columns)r
2582 sort: %(sort)r
2583 group: %(group)r
2584 filter: %(filter)r
2585 search_text: %(search_text)r
2586 pagesize: %(pagesize)r
2587 startwith: %(startwith)r
2588 env: %(env)s
2589 """%d
2591 def indexargs_form(self, columns=1, sort=1, group=1, filter=1,
2592 filterspec=1, search_text=1):
2593 """ return the current index args as form elements """
2594 l = []
2595 sc = self.special_char
2596 def add(k, v):
2597 l.append(self.input(type="hidden", name=k, value=v))
2598 if columns and self.columns:
2599 add(sc+'columns', ','.join(self.columns))
2600 if sort:
2601 val = []
2602 for dir, attr in self.sort:
2603 if dir == '-':
2604 val.append('-'+attr)
2605 else:
2606 val.append(attr)
2607 add(sc+'sort', ','.join (val))
2608 if group:
2609 val = []
2610 for dir, attr in self.group:
2611 if dir == '-':
2612 val.append('-'+attr)
2613 else:
2614 val.append(attr)
2615 add(sc+'group', ','.join (val))
2616 if filter and self.filter:
2617 add(sc+'filter', ','.join(self.filter))
2618 if self.classname and filterspec:
2619 cls = self.client.db.getclass(self.classname)
2620 for k,v in self.filterspec.items():
2621 if type(v) == type([]):
2622 if isinstance(cls.get_transitive_prop(k), hyperdb.String):
2623 add(k, ' '.join(v))
2624 else:
2625 add(k, ','.join(v))
2626 else:
2627 add(k, v)
2628 if search_text and self.search_text:
2629 add(sc+'search_text', self.search_text)
2630 add(sc+'pagesize', self.pagesize)
2631 add(sc+'startwith', self.startwith)
2632 return '\n'.join(l)
2634 def indexargs_url(self, url, args):
2635 """ Embed the current index args in a URL
2636 """
2637 q = urllib.quote
2638 sc = self.special_char
2639 l = ['%s=%s'%(k,v) for k,v in args.items()]
2641 # pull out the special values (prefixed by @ or :)
2642 specials = {}
2643 for key in args.keys():
2644 if key[0] in '@:':
2645 specials[key[1:]] = args[key]
2647 # ok, now handle the specials we received in the request
2648 if self.columns and not specials.has_key('columns'):
2649 l.append(sc+'columns=%s'%(','.join(self.columns)))
2650 if self.sort and not specials.has_key('sort'):
2651 val = []
2652 for dir, attr in self.sort:
2653 if dir == '-':
2654 val.append('-'+attr)
2655 else:
2656 val.append(attr)
2657 l.append(sc+'sort=%s'%(','.join(val)))
2658 if self.group and not specials.has_key('group'):
2659 val = []
2660 for dir, attr in self.group:
2661 if dir == '-':
2662 val.append('-'+attr)
2663 else:
2664 val.append(attr)
2665 l.append(sc+'group=%s'%(','.join(val)))
2666 if self.filter and not specials.has_key('filter'):
2667 l.append(sc+'filter=%s'%(','.join(self.filter)))
2668 if self.search_text and not specials.has_key('search_text'):
2669 l.append(sc+'search_text=%s'%q(self.search_text))
2670 if not specials.has_key('pagesize'):
2671 l.append(sc+'pagesize=%s'%self.pagesize)
2672 if not specials.has_key('startwith'):
2673 l.append(sc+'startwith=%s'%self.startwith)
2675 # finally, the remainder of the filter args in the request
2676 if self.classname and self.filterspec:
2677 cls = self.client.db.getclass(self.classname)
2678 for k,v in self.filterspec.items():
2679 if not args.has_key(k):
2680 if type(v) == type([]):
2681 prop = cls.get_transitive_prop(k)
2682 if k != 'id' and isinstance(prop, hyperdb.String):
2683 l.append('%s=%s'%(k, '%20'.join([q(i) for i in v])))
2684 else:
2685 l.append('%s=%s'%(k, ','.join([q(i) for i in v])))
2686 else:
2687 l.append('%s=%s'%(k, q(v)))
2688 return '%s?%s'%(url, '&'.join(l))
2689 indexargs_href = indexargs_url
2691 def base_javascript(self):
2692 return """
2693 <script type="text/javascript">
2694 submitted = false;
2695 function submit_once() {
2696 if (submitted) {
2697 alert("Your request is being processed.\\nPlease be patient.");
2698 event.returnValue = 0; // work-around for IE
2699 return 0;
2700 }
2701 submitted = true;
2702 return 1;
2703 }
2705 function help_window(helpurl, width, height) {
2706 HelpWin = window.open('%s' + helpurl, 'RoundupHelpWindow', 'scrollbars=yes,resizable=yes,toolbar=no,height='+height+',width='+width);
2707 }
2708 </script>
2709 """%self.base
2711 def batch(self):
2712 """ Return a batch object for results from the "current search"
2713 """
2714 check = self._client.db.security.hasPermission
2715 userid = self._client.userid
2716 if not check('Web Access', userid):
2717 return Batch(self.client, [], self.pagesize, self.startwith,
2718 classname=self.classname)
2720 filterspec = self.filterspec
2721 sort = self.sort
2722 group = self.group
2724 # get the list of ids we're batching over
2725 klass = self.client.db.getclass(self.classname)
2726 if self.search_text:
2727 matches = self.client.db.indexer.search(
2728 [w.upper().encode("utf-8", "replace") for w in re.findall(
2729 r'(?u)\b\w{2,25}\b',
2730 unicode(self.search_text, "utf-8", "replace")
2731 )], klass)
2732 else:
2733 matches = None
2735 # filter for visibility
2736 l = [id for id in klass.filter(matches, filterspec, sort, group)
2737 if check('View', userid, self.classname, itemid=id)]
2739 # return the batch object, using IDs only
2740 return Batch(self.client, l, self.pagesize, self.startwith,
2741 classname=self.classname)
2743 # extend the standard ZTUtils Batch object to remove dependency on
2744 # Acquisition and add a couple of useful methods
2745 class Batch(ZTUtils.Batch):
2746 """ Use me to turn a list of items, or item ids of a given class, into a
2747 series of batches.
2749 ========= ========================================================
2750 Parameter Usage
2751 ========= ========================================================
2752 sequence a list of HTMLItems or item ids
2753 classname if sequence is a list of ids, this is the class of item
2754 size how big to make the sequence.
2755 start where to start (0-indexed) in the sequence.
2756 end where to end (0-indexed) in the sequence.
2757 orphan if the next batch would contain less items than this
2758 value, then it is combined with this batch
2759 overlap the number of items shared between adjacent batches
2760 ========= ========================================================
2762 Attributes: Note that the "start" attribute, unlike the
2763 argument, is a 1-based index (I know, lame). "first" is the
2764 0-based index. "length" is the actual number of elements in
2765 the batch.
2767 "sequence_length" is the length of the original, unbatched, sequence.
2768 """
2769 def __init__(self, client, sequence, size, start, end=0, orphan=0,
2770 overlap=0, classname=None):
2771 self.client = client
2772 self.last_index = self.last_item = None
2773 self.current_item = None
2774 self.classname = classname
2775 self.sequence_length = len(sequence)
2776 ZTUtils.Batch.__init__(self, sequence, size, start, end, orphan,
2777 overlap)
2779 # overwrite so we can late-instantiate the HTMLItem instance
2780 def __getitem__(self, index):
2781 if index < 0:
2782 if index + self.end < self.first: raise IndexError, index
2783 return self._sequence[index + self.end]
2785 if index >= self.length:
2786 raise IndexError, index
2788 # move the last_item along - but only if the fetched index changes
2789 # (for some reason, index 0 is fetched twice)
2790 if index != self.last_index:
2791 self.last_item = self.current_item
2792 self.last_index = index
2794 item = self._sequence[index + self.first]
2795 if self.classname:
2796 # map the item ids to instances
2797 item = HTMLItem(self.client, self.classname, item)
2798 self.current_item = item
2799 return item
2801 def propchanged(self, *properties):
2802 """ Detect if one of the properties marked as being a group
2803 property changed in the last iteration fetch
2804 """
2805 # we poke directly at the _value here since MissingValue can screw
2806 # us up and cause Nones to compare strangely
2807 if self.last_item is None:
2808 return 1
2809 for property in properties:
2810 if property == 'id' or isinstance (self.last_item[property], list):
2811 if (str(self.last_item[property]) !=
2812 str(self.current_item[property])):
2813 return 1
2814 else:
2815 if (self.last_item[property]._value !=
2816 self.current_item[property]._value):
2817 return 1
2818 return 0
2820 # override these 'cos we don't have access to acquisition
2821 def previous(self):
2822 if self.start == 1:
2823 return None
2824 return Batch(self.client, self._sequence, self._size,
2825 self.first - self._size + self.overlap, 0, self.orphan,
2826 self.overlap)
2828 def next(self):
2829 try:
2830 self._sequence[self.end]
2831 except IndexError:
2832 return None
2833 return Batch(self.client, self._sequence, self._size,
2834 self.end - self.overlap, 0, self.orphan, self.overlap)
2836 class TemplatingUtils:
2837 """ Utilities for templating
2838 """
2839 def __init__(self, client):
2840 self.client = client
2841 def Batch(self, sequence, size, start, end=0, orphan=0, overlap=0):
2842 return Batch(self.client, sequence, size, start, end, orphan,
2843 overlap)
2845 def url_quote(self, url):
2846 """URL-quote the supplied text."""
2847 return urllib.quote(url)
2849 def html_quote(self, html):
2850 """HTML-quote the supplied text."""
2851 return cgi.escape(html)
2853 def __getattr__(self, name):
2854 """Try the tracker's templating_utils."""
2855 if not hasattr(self.client.instance, 'templating_utils'):
2856 # backwards-compatibility
2857 raise AttributeError, name
2858 if not self.client.instance.templating_utils.has_key(name):
2859 raise AttributeError, name
2860 return self.client.instance.templating_utils[name]
2862 def keywords_expressions(self, request):
2863 return render_keywords_expression_editor(request)
2865 def html_calendar(self, request):
2866 """Generate a HTML calendar.
2868 `request` the roundup.request object
2869 - @template : name of the template
2870 - form : name of the form to store back the date
2871 - property : name of the property of the form to store
2872 back the date
2873 - date : current date
2874 - display : when browsing, specifies year and month
2876 html will simply be a table.
2877 """
2878 tz = request.client.db.getUserTimezone()
2879 current_date = date.Date(".").local(tz)
2880 date_str = request.form.getfirst("date", current_date)
2881 display = request.form.getfirst("display", date_str)
2882 template = request.form.getfirst("@template", "calendar")
2883 form = request.form.getfirst("form")
2884 property = request.form.getfirst("property")
2885 curr_date = date.Date(date_str) # to highlight
2886 display = date.Date(display) # to show
2887 day = display.day
2889 # for navigation
2890 date_prev_month = display + date.Interval("-1m")
2891 date_next_month = display + date.Interval("+1m")
2892 date_prev_year = display + date.Interval("-1y")
2893 date_next_year = display + date.Interval("+1y")
2895 res = []
2897 base_link = "%s?@template=%s&property=%s&form=%s&date=%s" % \
2898 (request.classname, template, property, form, curr_date)
2900 # navigation
2901 # month
2902 res.append('<table class="calendar"><tr><td>')
2903 res.append(' <table width="100%" class="calendar_nav"><tr>')
2904 link = "&display=%s"%date_prev_month
2905 res.append(' <td><a href="%s&display=%s"><</a></td>'%(base_link,
2906 date_prev_month))
2907 res.append(' <td>%s</td>'%calendar.month_name[display.month])
2908 res.append(' <td><a href="%s&display=%s">></a></td>'%(base_link,
2909 date_next_month))
2910 # spacer
2911 res.append(' <td width="100%"></td>')
2912 # year
2913 res.append(' <td><a href="%s&display=%s"><</a></td>'%(base_link,
2914 date_prev_year))
2915 res.append(' <td>%s</td>'%display.year)
2916 res.append(' <td><a href="%s&display=%s">></a></td>'%(base_link,
2917 date_next_year))
2918 res.append(' </tr></table>')
2919 res.append(' </td></tr>')
2921 # the calendar
2922 res.append(' <tr><td><table class="calendar_display">')
2923 res.append(' <tr class="weekdays">')
2924 for day in calendar.weekheader(3).split():
2925 res.append(' <td>%s</td>'%day)
2926 res.append(' </tr>')
2927 for week in calendar.monthcalendar(display.year, display.month):
2928 res.append(' <tr>')
2929 for day in week:
2930 link = "javascript:form[field].value = '%d-%02d-%02d'; " \
2931 "window.close ();"%(display.year, display.month, day)
2932 if (day == curr_date.day and display.month == curr_date.month
2933 and display.year == curr_date.year):
2934 # highlight
2935 style = "today"
2936 else :
2937 style = ""
2938 if day:
2939 res.append(' <td class="%s"><a href="%s">%s</a></td>'%(
2940 style, link, day))
2941 else :
2942 res.append(' <td></td>')
2943 res.append(' </tr>')
2944 res.append('</table></td></tr></table>')
2945 return "\n".join(res)
2947 class MissingValue:
2948 def __init__(self, description, **kwargs):
2949 self.__description = description
2950 for key, value in kwargs.items():
2951 self.__dict__[key] = value
2953 def __call__(self, *args, **kwargs): return MissingValue(self.__description)
2954 def __getattr__(self, name):
2955 # This allows assignments which assume all intermediate steps are Null
2956 # objects if they don't exist yet.
2957 #
2958 # For example (with just 'client' defined):
2959 #
2960 # client.db.config.TRACKER_WEB = 'BASE/'
2961 self.__dict__[name] = MissingValue(self.__description)
2962 return getattr(self, name)
2964 def __getitem__(self, key): return self
2965 def __nonzero__(self): return 0
2966 def __str__(self): return '[%s]'%self.__description
2967 def __repr__(self): return '<MissingValue 0x%x "%s">'%(id(self),
2968 self.__description)
2969 def gettext(self, str): return str
2970 _ = gettext
2972 # vim: set et sts=4 sw=4 :