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 if isinstance(prop, hyperdb.Multilink):
571 value = []
572 else:
573 value = None
574 return htmlklass(self._client, self._classname, None, prop, item,
575 value, self._anonymous)
577 # no good
578 raise KeyError, item
580 def __getattr__(self, attr):
581 """ convenience access """
582 try:
583 return self[attr]
584 except KeyError:
585 raise AttributeError, attr
587 def designator(self):
588 """ Return this class' designator (classname) """
589 return self._classname
591 def getItem(self, itemid, num_re=num_re):
592 """ Get an item of this class by its item id.
593 """
594 # make sure we're looking at an itemid
595 if not isinstance(itemid, type(1)) and not num_re.match(itemid):
596 itemid = self._klass.lookup(itemid)
598 return HTMLItem(self._client, self.classname, itemid)
600 def properties(self, sort=1):
601 """ Return HTMLProperty for all of this class' properties.
602 """
603 l = []
604 for name, prop in self._props.items():
605 for klass, htmlklass in propclasses:
606 if isinstance(prop, hyperdb.Multilink):
607 value = []
608 else:
609 value = None
610 if isinstance(prop, klass):
611 l.append(htmlklass(self._client, self._classname, '',
612 prop, name, value, self._anonymous))
613 if sort:
614 l.sort(lambda a,b:cmp(a._name, b._name))
615 return l
617 def list(self, sort_on=None):
618 """ List all items in this class.
619 """
620 # get the list and sort it nicely
621 l = self._klass.list()
622 sortfunc = make_sort_function(self._db, self._classname, sort_on)
623 l.sort(sortfunc)
625 # check perms
626 check = self._client.db.security.hasPermission
627 userid = self._client.userid
628 if not check('Web Access', userid):
629 return []
631 l = [HTMLItem(self._client, self._classname, id) for id in l
632 if check('View', userid, self._classname, itemid=id)]
634 return l
636 def csv(self):
637 """ Return the items of this class as a chunk of CSV text.
638 """
639 props = self.propnames()
640 s = StringIO.StringIO()
641 writer = csv.writer(s)
642 writer.writerow(props)
643 check = self._client.db.security.hasPermission
644 userid = self._client.userid
645 if not check('Web Access', userid):
646 return ''
647 for nodeid in self._klass.list():
648 l = []
649 for name in props:
650 # check permission to view this property on this item
651 if not check('View', userid, itemid=nodeid,
652 classname=self._klass.classname, property=name):
653 raise Unauthorised('view', self._klass.classname,
654 translator=self._client.translator)
655 value = self._klass.get(nodeid, name)
656 if value is None:
657 l.append('')
658 elif isinstance(value, type([])):
659 l.append(':'.join(map(str, value)))
660 else:
661 l.append(str(self._klass.get(nodeid, name)))
662 writer.writerow(l)
663 return s.getvalue()
665 def propnames(self):
666 """ Return the list of the names of the properties of this class.
667 """
668 idlessprops = self._klass.getprops(protected=0).keys()
669 idlessprops.sort()
670 return ['id'] + idlessprops
672 def filter(self, request=None, filterspec={}, sort=[], group=[]):
673 """ Return a list of items from this class, filtered and sorted
674 by the current requested filterspec/filter/sort/group args
676 "request" takes precedence over the other three arguments.
677 """
678 security = self._db.security
679 userid = self._client.userid
680 if request is not None:
681 # for a request we asume it has already been
682 # security-filtered
683 filterspec = request.filterspec
684 sort = request.sort
685 group = request.group
686 else:
687 cn = self.classname
688 filterspec = security.filterFilterspec(userid, cn, filterspec)
689 sort = security.filterSortspec(userid, cn, sort)
690 group = security.filterSortspec(userid, cn, group)
692 check = security.hasPermission
693 if not check('Web Access', userid):
694 return []
696 l = [HTMLItem(self._client, self.classname, id)
697 for id in self._klass.filter(None, filterspec, sort, group)
698 if check('View', userid, self.classname, itemid=id)]
699 return l
701 def classhelp(self, properties=None, label=''"(list)", width='500',
702 height='400', property='', form='itemSynopsis',
703 pagesize=50, inputtype="checkbox", sort=None, filter=None):
704 """Pop up a javascript window with class help
706 This generates a link to a popup window which displays the
707 properties indicated by "properties" of the class named by
708 "classname". The "properties" should be a comma-separated list
709 (eg. 'id,name,description'). Properties defaults to all the
710 properties of a class (excluding id, creator, created and
711 activity).
713 You may optionally override the label displayed, the width,
714 the height, the number of items per page and the field on which
715 the list is sorted (defaults to username if in the displayed
716 properties).
718 With the "filter" arg it is possible to specify a filter for
719 which items are supposed to be displayed. It has to be of
720 the format "<field>=<values>;<field>=<values>;...".
722 The popup window will be resizable and scrollable.
724 If the "property" arg is given, it's passed through to the
725 javascript help_window function.
727 You can use inputtype="radio" to display a radio box instead
728 of the default checkbox (useful for entering Link-properties)
730 If the "form" arg is given, it's passed through to the
731 javascript help_window function. - it's the name of the form
732 the "property" belongs to.
733 """
734 if properties is None:
735 properties = self._klass.getprops(protected=0).keys()
736 properties.sort()
737 properties = ','.join(properties)
738 if sort is None:
739 if 'username' in properties.split( ',' ):
740 sort = 'username'
741 else:
742 sort = self._klass.orderprop()
743 sort = '&@sort=' + sort
744 if property:
745 property = '&property=%s'%property
746 if form:
747 form = '&form=%s'%form
748 if inputtype:
749 type= '&type=%s'%inputtype
750 if filter:
751 filterprops = filter.split(';')
752 filtervalues = []
753 names = []
754 for x in filterprops:
755 (name, values) = x.split('=')
756 names.append(name)
757 filtervalues.append('&%s=%s' % (name, urllib.quote(values)))
758 filter = '&@filter=%s%s' % (','.join(names), ''.join(filtervalues))
759 else:
760 filter = ''
761 help_url = "%s?@startwith=0&@template=help&"\
762 "properties=%s%s%s%s%s&@pagesize=%s%s" % \
763 (self.classname, properties, property, form, type,
764 sort, pagesize, filter)
765 onclick = "javascript:help_window('%s', '%s', '%s');return false;" % \
766 (help_url, width, height)
767 return '<a class="classhelp" href="%s" onclick="%s">%s</a>' % \
768 (help_url, onclick, self._(label))
770 def submit(self, label=''"Submit New Entry", action="new"):
771 """ Generate a submit button (and action hidden element)
773 Generate nothing if we're not editable.
774 """
775 if not self.is_edit_ok():
776 return ''
778 return self.input(type="hidden", name="@action", value=action) + \
779 '\n' + \
780 self.input(type="submit", name="submit_button", value=self._(label))
782 def history(self):
783 if not self.is_view_ok():
784 return self._('[hidden]')
785 return self._('New node - no history')
787 def renderWith(self, name, **kwargs):
788 """ Render this class with the given template.
789 """
790 # create a new request and override the specified args
791 req = HTMLRequest(self._client)
792 req.classname = self.classname
793 req.update(kwargs)
795 # new template, using the specified classname and request
796 pt = self._client.instance.templates.get(self.classname, name)
798 # use our fabricated request
799 args = {
800 'ok_message': self._client.ok_message,
801 'error_message': self._client.error_message
802 }
803 return pt.render(self._client, self.classname, req, **args)
805 class _HTMLItem(HTMLInputMixin, HTMLPermissions):
806 """ Accesses through an *item*
807 """
808 def __init__(self, client, classname, nodeid, anonymous=0):
809 self._client = client
810 self._db = client.db
811 self._classname = classname
812 self._nodeid = nodeid
813 self._klass = self._db.getclass(classname)
814 self._props = self._klass.getprops()
816 # do we prefix the form items with the item's identification?
817 self._anonymous = anonymous
819 HTMLInputMixin.__init__(self)
821 def is_edit_ok(self):
822 """ Is the user allowed to Edit this item?
823 """
824 perm = self._db.security.hasPermission
825 return perm('Web Access', self._client.userid) and perm('Edit',
826 self._client.userid, self._classname, itemid=self._nodeid)
828 def is_retire_ok(self):
829 """ Is the user allowed to Reture this item?
830 """
831 perm = self._db.security.hasPermission
832 return perm('Web Access', self._client.userid) and perm('Retire',
833 self._client.userid, self._classname, itemid=self._nodeid)
835 def is_view_ok(self):
836 """ Is the user allowed to View this item?
837 """
838 perm = self._db.security.hasPermission
839 if perm('Web Access', self._client.userid) and perm('View',
840 self._client.userid, self._classname, itemid=self._nodeid):
841 return 1
842 return self.is_edit_ok()
844 def is_only_view_ok(self):
845 """ Is the user only allowed to View (ie. not Edit) this item?
846 """
847 return self.is_view_ok() and not self.is_edit_ok()
849 def __repr__(self):
850 return '<HTMLItem(0x%x) %s %s>'%(id(self), self._classname,
851 self._nodeid)
853 def __getitem__(self, item):
854 """ return an HTMLProperty instance
855 this now can handle transitive lookups where item is of the
856 form x.y.z
857 """
858 if item == 'id':
859 return self._nodeid
861 items = item.split('.', 1)
862 has_rest = len(items) > 1
864 # get the property
865 prop = self._props[items[0]]
867 if has_rest and not isinstance(prop, (hyperdb.Link, hyperdb.Multilink)):
868 raise KeyError, item
870 # get the value, handling missing values
871 value = None
872 if int(self._nodeid) > 0:
873 value = self._klass.get(self._nodeid, items[0], None)
874 if value is None:
875 if isinstance(prop, hyperdb.Multilink):
876 value = []
878 # look up the correct HTMLProperty class
879 htmlprop = None
880 for klass, htmlklass in propclasses:
881 if isinstance(prop, klass):
882 htmlprop = htmlklass(self._client, self._classname,
883 self._nodeid, prop, items[0], value, self._anonymous)
884 if htmlprop is not None:
885 if has_rest:
886 if isinstance(htmlprop, MultilinkHTMLProperty):
887 return [h[items[1]] for h in htmlprop]
888 return htmlprop[items[1]]
889 return htmlprop
891 raise KeyError, item
893 def __getattr__(self, attr):
894 """ convenience access to properties """
895 try:
896 return self[attr]
897 except KeyError:
898 raise AttributeError, attr
900 def designator(self):
901 """Return this item's designator (classname + id)."""
902 return '%s%s'%(self._classname, self._nodeid)
904 def is_retired(self):
905 """Is this item retired?"""
906 return self._klass.is_retired(self._nodeid)
908 def submit(self, label=''"Submit Changes", action="edit"):
909 """Generate a submit button.
911 Also sneak in the lastactivity and action hidden elements.
912 """
913 return self.input(type="hidden", name="@lastactivity",
914 value=self.activity.local(0)) + '\n' + \
915 self.input(type="hidden", name="@action", value=action) + '\n' + \
916 self.input(type="submit", name="submit_button", value=self._(label))
918 def journal(self, direction='descending'):
919 """ Return a list of HTMLJournalEntry instances.
920 """
921 # XXX do this
922 return []
924 def history(self, direction='descending', dre=re.compile('^\d+$'),
925 limit=None):
926 if not self.is_view_ok():
927 return self._('[hidden]')
929 # pre-load the history with the current state
930 current = {}
931 for prop_n in self._props.keys():
932 prop = self[prop_n]
933 if not isinstance(prop, HTMLProperty):
934 continue
935 current[prop_n] = prop.plain(escape=1)
936 # make link if hrefable
937 if (self._props.has_key(prop_n) and
938 isinstance(self._props[prop_n], hyperdb.Link)):
939 classname = self._props[prop_n].classname
940 try:
941 template = find_template(self._db.config.TEMPLATES,
942 classname, 'item')
943 if template[1].startswith('_generic'):
944 raise NoTemplate, 'not really...'
945 except NoTemplate:
946 pass
947 else:
948 id = self._klass.get(self._nodeid, prop_n, None)
949 current[prop_n] = '<a href="%s%s">%s</a>'%(
950 classname, id, current[prop_n])
952 # get the journal, sort and reverse
953 history = self._klass.history(self._nodeid)
954 history.sort()
955 history.reverse()
957 # restrict the volume
958 if limit:
959 history = history[:limit]
961 timezone = self._db.getUserTimezone()
962 l = []
963 comments = {}
964 for id, evt_date, user, action, args in history:
965 date_s = str(evt_date.local(timezone)).replace("."," ")
966 arg_s = ''
967 if action == 'link' and type(args) == type(()):
968 if len(args) == 3:
969 linkcl, linkid, key = args
970 arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
971 linkcl, linkid, key)
972 else:
973 arg_s = str(args)
975 elif action == 'unlink' and type(args) == type(()):
976 if len(args) == 3:
977 linkcl, linkid, key = args
978 arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
979 linkcl, linkid, key)
980 else:
981 arg_s = str(args)
983 elif type(args) == type({}):
984 cell = []
985 for k in args.keys():
986 # try to get the relevant property and treat it
987 # specially
988 try:
989 prop = self._props[k]
990 except KeyError:
991 prop = None
992 if prop is None:
993 # property no longer exists
994 comments['no_exist'] = self._(
995 "<em>The indicated property no longer exists</em>")
996 cell.append(self._('<em>%s: %s</em>\n')
997 % (self._(k), str(args[k])))
998 continue
1000 if args[k] and (isinstance(prop, hyperdb.Multilink) or
1001 isinstance(prop, hyperdb.Link)):
1002 # figure what the link class is
1003 classname = prop.classname
1004 try:
1005 linkcl = self._db.getclass(classname)
1006 except KeyError:
1007 labelprop = None
1008 comments[classname] = self._(
1009 "The linked class %(classname)s no longer exists"
1010 ) % locals()
1011 labelprop = linkcl.labelprop(1)
1012 try:
1013 template = find_template(self._db.config.TEMPLATES,
1014 classname, 'item')
1015 if template[1].startswith('_generic'):
1016 raise NoTemplate, 'not really...'
1017 hrefable = 1
1018 except NoTemplate:
1019 hrefable = 0
1021 if isinstance(prop, hyperdb.Multilink) and args[k]:
1022 ml = []
1023 for linkid in args[k]:
1024 if isinstance(linkid, type(())):
1025 sublabel = linkid[0] + ' '
1026 linkids = linkid[1]
1027 else:
1028 sublabel = ''
1029 linkids = [linkid]
1030 subml = []
1031 for linkid in linkids:
1032 label = classname + linkid
1033 # if we have a label property, try to use it
1034 # TODO: test for node existence even when
1035 # there's no labelprop!
1036 try:
1037 if labelprop is not None and \
1038 labelprop != 'id':
1039 label = linkcl.get(linkid, labelprop)
1040 label = cgi.escape(label)
1041 except IndexError:
1042 comments['no_link'] = self._(
1043 "<strike>The linked node"
1044 " no longer exists</strike>")
1045 subml.append('<strike>%s</strike>'%label)
1046 else:
1047 if hrefable:
1048 subml.append('<a href="%s%s">%s</a>'%(
1049 classname, linkid, label))
1050 elif label is None:
1051 subml.append('%s%s'%(classname,
1052 linkid))
1053 else:
1054 subml.append(label)
1055 ml.append(sublabel + ', '.join(subml))
1056 cell.append('%s:\n %s'%(self._(k), ', '.join(ml)))
1057 elif isinstance(prop, hyperdb.Link) and args[k]:
1058 label = classname + args[k]
1059 # if we have a label property, try to use it
1060 # TODO: test for node existence even when
1061 # there's no labelprop!
1062 if labelprop is not None and labelprop != 'id':
1063 try:
1064 label = cgi.escape(linkcl.get(args[k],
1065 labelprop))
1066 except IndexError:
1067 comments['no_link'] = self._(
1068 "<strike>The linked node"
1069 " no longer exists</strike>")
1070 cell.append(' <strike>%s</strike>,\n'%label)
1071 # "flag" this is done .... euwww
1072 label = None
1073 if label is not None:
1074 if hrefable:
1075 old = '<a href="%s%s">%s</a>'%(classname,
1076 args[k], label)
1077 else:
1078 old = label;
1079 cell.append('%s: %s' % (self._(k), old))
1080 if current.has_key(k):
1081 cell[-1] += ' -> %s'%current[k]
1082 current[k] = old
1084 elif isinstance(prop, hyperdb.Date) and args[k]:
1085 if args[k] is None:
1086 d = ''
1087 else:
1088 d = date.Date(args[k],
1089 translator=self._client).local(timezone)
1090 cell.append('%s: %s'%(self._(k), str(d)))
1091 if current.has_key(k):
1092 cell[-1] += ' -> %s' % current[k]
1093 current[k] = str(d)
1095 elif isinstance(prop, hyperdb.Interval) and args[k]:
1096 val = str(date.Interval(args[k],
1097 translator=self._client))
1098 cell.append('%s: %s'%(self._(k), val))
1099 if current.has_key(k):
1100 cell[-1] += ' -> %s'%current[k]
1101 current[k] = val
1103 elif isinstance(prop, hyperdb.String) and args[k]:
1104 val = cgi.escape(args[k])
1105 cell.append('%s: %s'%(self._(k), val))
1106 if current.has_key(k):
1107 cell[-1] += ' -> %s'%current[k]
1108 current[k] = val
1110 elif isinstance(prop, hyperdb.Boolean) and args[k] is not None:
1111 val = args[k] and ''"Yes" or ''"No"
1112 cell.append('%s: %s'%(self._(k), val))
1113 if current.has_key(k):
1114 cell[-1] += ' -> %s'%current[k]
1115 current[k] = val
1117 elif not args[k]:
1118 if current.has_key(k):
1119 cell.append('%s: %s'%(self._(k), current[k]))
1120 current[k] = '(no value)'
1121 else:
1122 cell.append(self._('%s: (no value)')%self._(k))
1124 else:
1125 cell.append('%s: %s'%(self._(k), str(args[k])))
1126 if current.has_key(k):
1127 cell[-1] += ' -> %s'%current[k]
1128 current[k] = str(args[k])
1130 arg_s = '<br />'.join(cell)
1131 else:
1132 # unkown event!!
1133 comments['unknown'] = self._(
1134 "<strong><em>This event is not handled"
1135 " by the history display!</em></strong>")
1136 arg_s = '<strong><em>' + str(args) + '</em></strong>'
1137 date_s = date_s.replace(' ', ' ')
1138 # if the user's an itemid, figure the username (older journals
1139 # have the username)
1140 if dre.match(user):
1141 user = self._db.user.get(user, 'username')
1142 l.append('<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>'%(
1143 date_s, user, self._(action), arg_s))
1144 if comments:
1145 l.append(self._(
1146 '<tr><td colspan=4><strong>Note:</strong></td></tr>'))
1147 for entry in comments.values():
1148 l.append('<tr><td colspan=4>%s</td></tr>'%entry)
1150 if direction == 'ascending':
1151 l.reverse()
1153 l[0:0] = ['<table class="history">'
1154 '<tr><th colspan="4" class="header">',
1155 self._('History'),
1156 '</th></tr><tr>',
1157 self._('<th>Date</th>'),
1158 self._('<th>User</th>'),
1159 self._('<th>Action</th>'),
1160 self._('<th>Args</th>'),
1161 '</tr>']
1162 l.append('</table>')
1163 return '\n'.join(l)
1165 def renderQueryForm(self):
1166 """ Render this item, which is a query, as a search form.
1167 """
1168 # create a new request and override the specified args
1169 req = HTMLRequest(self._client)
1170 req.classname = self._klass.get(self._nodeid, 'klass')
1171 name = self._klass.get(self._nodeid, 'name')
1172 req.updateFromURL(self._klass.get(self._nodeid, 'url') +
1173 '&@queryname=%s'%urllib.quote(name))
1175 # new template, using the specified classname and request
1176 pt = self._client.instance.templates.get(req.classname, 'search')
1177 # The context for a search page should be the class, not any
1178 # node.
1179 self._client.nodeid = None
1181 # use our fabricated request
1182 return pt.render(self._client, req.classname, req)
1184 def download_url(self):
1185 """ Assume that this item is a FileClass and that it has a name
1186 and content. Construct a URL for the download of the content.
1187 """
1188 name = self._klass.get(self._nodeid, 'name')
1189 url = '%s%s/%s'%(self._classname, self._nodeid, name)
1190 return urllib.quote(url)
1192 def copy_url(self, exclude=("messages", "files")):
1193 """Construct a URL for creating a copy of this item
1195 "exclude" is an optional list of properties that should
1196 not be copied to the new object. By default, this list
1197 includes "messages" and "files" properties. Note that
1198 "id" property cannot be copied.
1200 """
1201 exclude = ("id", "activity", "actor", "creation", "creator") \
1202 + tuple(exclude)
1203 query = {
1204 "@template": "item",
1205 "@note": self._("Copy of %(class)s %(id)s") % {
1206 "class": self._(self._classname), "id": self._nodeid},
1207 }
1208 for name in self._props.keys():
1209 if name not in exclude:
1210 query[name] = self[name].plain()
1211 return self._classname + "?" + "&".join(
1212 ["%s=%s" % (key, urllib.quote(value))
1213 for key, value in query.items()])
1215 class _HTMLUser(_HTMLItem):
1216 """Add ability to check for permissions on users.
1217 """
1218 _marker = []
1219 def hasPermission(self, permission, classname=_marker,
1220 property=None, itemid=None):
1221 """Determine if the user has the Permission.
1223 The class being tested defaults to the template's class, but may
1224 be overidden for this test by suppling an alternate classname.
1225 """
1226 if classname is self._marker:
1227 classname = self._client.classname
1228 return self._db.security.hasPermission(permission,
1229 self._nodeid, classname, property, itemid)
1231 def hasRole(self, *rolenames):
1232 """Determine whether the user has any role in rolenames."""
1233 return self._db.user.has_role(self._nodeid, *rolenames)
1235 def HTMLItem(client, classname, nodeid, anonymous=0):
1236 if classname == 'user':
1237 return _HTMLUser(client, classname, nodeid, anonymous)
1238 else:
1239 return _HTMLItem(client, classname, nodeid, anonymous)
1241 class HTMLProperty(HTMLInputMixin, HTMLPermissions):
1242 """ String, Number, Date, Interval HTMLProperty
1244 Has useful attributes:
1246 _name the name of the property
1247 _value the value of the property if any
1249 A wrapper object which may be stringified for the plain() behaviour.
1250 """
1251 def __init__(self, client, classname, nodeid, prop, name, value,
1252 anonymous=0):
1253 self._client = client
1254 self._db = client.db
1255 self._ = client._
1256 self._classname = classname
1257 self._nodeid = nodeid
1258 self._prop = prop
1259 self._value = value
1260 self._anonymous = anonymous
1261 self._name = name
1262 if not anonymous:
1263 if nodeid:
1264 self._formname = '%s%s@%s'%(classname, nodeid, name)
1265 else:
1266 # This case occurs when creating a property for a
1267 # non-anonymous class.
1268 self._formname = '%s@%s'%(classname, name)
1269 else:
1270 self._formname = name
1272 # If no value is already present for this property, see if one
1273 # is specified in the current form.
1274 form = self._client.form
1275 if not self._value and form.has_key(self._formname):
1276 if isinstance(prop, hyperdb.Multilink):
1277 value = lookupIds(self._db, prop,
1278 handleListCGIValue(form[self._formname]),
1279 fail_ok=1)
1280 elif isinstance(prop, hyperdb.Link):
1281 value = form.getfirst(self._formname).strip()
1282 if value:
1283 value = lookupIds(self._db, prop, [value],
1284 fail_ok=1)[0]
1285 else:
1286 value = None
1287 else:
1288 value = form.getfirst(self._formname).strip() or None
1289 self._value = value
1291 HTMLInputMixin.__init__(self)
1293 def __repr__(self):
1294 classname = self.__class__.__name__
1295 return '<%s(0x%x) %s %r %r>'%(classname, id(self), self._formname,
1296 self._prop, self._value)
1297 def __str__(self):
1298 return self.plain()
1299 def __cmp__(self, other):
1300 if isinstance(other, HTMLProperty):
1301 return cmp(self._value, other._value)
1302 return cmp(self._value, other)
1304 def __nonzero__(self):
1305 return not not self._value
1307 def isset(self):
1308 """Is my _value not None?"""
1309 return self._value is not None
1311 def is_edit_ok(self):
1312 """Should the user be allowed to use an edit form field for this
1313 property. Check "Create" for new items, or "Edit" for existing
1314 ones.
1315 """
1316 perm = self._db.security.hasPermission
1317 userid = self._client.userid
1318 if self._nodeid:
1319 if not perm('Web Access', userid):
1320 return False
1321 return perm('Edit', userid, self._classname, self._name,
1322 self._nodeid)
1323 return perm('Create', userid, self._classname, self._name) or \
1324 perm('Register', userid, self._classname, self._name)
1326 def is_view_ok(self):
1327 """ Is the user allowed to View the current class?
1328 """
1329 perm = self._db.security.hasPermission
1330 if perm('Web Access', self._client.userid) and perm('View',
1331 self._client.userid, self._classname, self._name, self._nodeid):
1332 return 1
1333 return self.is_edit_ok()
1335 class StringHTMLProperty(HTMLProperty):
1336 hyper_re = re.compile(r'''(
1337 (?P<url>
1338 (
1339 (ht|f)tp(s?):// # protocol
1340 ([\w]+(:\w+)?@)? # username/password
1341 ([\w\-]+) # hostname
1342 ((\.[\w-]+)+)? # .domain.etc
1343 | # ... or ...
1344 ([\w]+(:\w+)?@)? # username/password
1345 www\. # "www."
1346 ([\w\-]+\.)+ # hostname
1347 [\w]{2,5} # TLD
1348 )
1349 (:[\d]{1,5})? # port
1350 (/[\w\-$.+!*(),;:@&=?/~\\#%]*)? # path etc.
1351 )|
1352 (?P<email>[-+=%/\w\.]+@[\w\.\-]+)|
1353 (?P<item>(?P<class>[A-Za-z_]+)(\s*)(?P<id>\d+))
1354 )''', re.X | re.I)
1355 protocol_re = re.compile('^(ht|f)tp(s?)://', re.I)
1359 def _hyper_repl(self, match):
1360 if match.group('url'):
1361 return self._hyper_repl_url(match, '<a href="%s">%s</a>%s')
1362 elif match.group('email'):
1363 return self._hyper_repl_email(match, '<a href="mailto:%s">%s</a>')
1364 elif len(match.group('id')) < 10:
1365 return self._hyper_repl_item(match,
1366 '<a href="%(cls)s%(id)s">%(item)s</a>')
1367 else:
1368 # just return the matched text
1369 return match.group(0)
1371 def _hyper_repl_url(self, match, replacement):
1372 u = s = match.group('url')
1373 if not self.protocol_re.search(s):
1374 u = 'http://' + s
1375 end = ''
1376 if '>' in s:
1377 # catch an escaped ">" in the URL
1378 pos = s.find('>')
1379 end = s[pos:]
1380 u = s = s[:pos]
1381 if ')' in s and s.count('(') != s.count(')'):
1382 # don't include extraneous ')' in the link
1383 pos = s.rfind(')')
1384 end = s[pos:] + end
1385 u = s = s[:pos]
1386 return replacement % (u, s, end)
1388 def _hyper_repl_email(self, match, replacement):
1389 s = match.group('email')
1390 return replacement % (s, s)
1392 def _hyper_repl_item(self, match, replacement):
1393 item = match.group('item')
1394 cls = match.group('class').lower()
1395 id = match.group('id')
1396 try:
1397 # make sure cls is a valid tracker classname
1398 cl = self._db.getclass(cls)
1399 if not cl.hasnode(id):
1400 return item
1401 return replacement % locals()
1402 except KeyError:
1403 return item
1406 def _hyper_repl_rst(self, match):
1407 if match.group('url'):
1408 s = match.group('url')
1409 return '`%s <%s>`_'%(s, s)
1410 elif match.group('email'):
1411 s = match.group('email')
1412 return '`%s <mailto:%s>`_'%(s, s)
1413 elif len(match.group('id')) < 10:
1414 return self._hyper_repl_item(match,'`%(item)s <%(cls)s%(id)s>`_')
1415 else:
1416 # just return the matched text
1417 return match.group(0)
1419 def hyperlinked(self):
1420 """ Render a "hyperlinked" version of the text """
1421 return self.plain(hyperlink=1)
1423 def plain(self, escape=0, hyperlink=0):
1424 """Render a "plain" representation of the property
1426 - "escape" turns on/off HTML quoting
1427 - "hyperlink" turns on/off in-text hyperlinking of URLs, email
1428 addresses and designators
1429 """
1430 if not self.is_view_ok():
1431 return self._('[hidden]')
1433 if self._value is None:
1434 return ''
1435 if escape:
1436 s = cgi.escape(str(self._value))
1437 else:
1438 s = str(self._value)
1439 if hyperlink:
1440 # no, we *must* escape this text
1441 if not escape:
1442 s = cgi.escape(s)
1443 s = self.hyper_re.sub(self._hyper_repl, s)
1444 return s
1446 def wrapped(self, escape=1, hyperlink=1):
1447 """Render a "wrapped" representation of the property.
1449 We wrap long lines at 80 columns on the nearest whitespace. Lines
1450 with no whitespace are not broken to force wrapping.
1452 Note that unlike plain() we default wrapped() to have the escaping
1453 and hyperlinking turned on since that's the most common usage.
1455 - "escape" turns on/off HTML quoting
1456 - "hyperlink" turns on/off in-text hyperlinking of URLs, email
1457 addresses and designators
1458 """
1459 if not self.is_view_ok():
1460 return self._('[hidden]')
1462 if self._value is None:
1463 return ''
1464 s = support.wrap(str(self._value), width=80)
1465 if escape:
1466 s = cgi.escape(s)
1467 if hyperlink:
1468 # no, we *must* escape this text
1469 if not escape:
1470 s = cgi.escape(s)
1471 s = self.hyper_re.sub(self._hyper_repl, s)
1472 return s
1474 def stext(self, escape=0, hyperlink=1):
1475 """ Render the value of the property as StructuredText.
1477 This requires the StructureText module to be installed separately.
1478 """
1479 if not self.is_view_ok():
1480 return self._('[hidden]')
1482 s = self.plain(escape=escape, hyperlink=hyperlink)
1483 if not StructuredText:
1484 return s
1485 return StructuredText(s,level=1,header=0)
1487 def rst(self, hyperlink=1):
1488 """ Render the value of the property as ReStructuredText.
1490 This requires docutils to be installed separately.
1491 """
1492 if not self.is_view_ok():
1493 return self._('[hidden]')
1495 if not ReStructuredText:
1496 return self.plain(escape=0, hyperlink=hyperlink)
1497 s = self.plain(escape=0, hyperlink=0)
1498 if hyperlink:
1499 s = self.hyper_re.sub(self._hyper_repl_rst, s)
1500 return ReStructuredText(s, writer_name="html")["html_body"].encode("utf-8",
1501 "replace")
1503 def field(self, **kwargs):
1504 """ Render the property as a field in HTML.
1506 If not editable, just display the value via plain().
1507 """
1508 if not self.is_edit_ok():
1509 return self.plain(escape=1)
1511 value = self._value
1512 if value is None:
1513 value = ''
1515 kwargs.setdefault("size", 30)
1516 kwargs.update({"name": self._formname, "value": value})
1517 return self.input(**kwargs)
1519 def multiline(self, escape=0, rows=5, cols=40, **kwargs):
1520 """ Render a multiline form edit field for the property.
1522 If not editable, just display the plain() value in a <pre> tag.
1523 """
1524 if not self.is_edit_ok():
1525 return '<pre>%s</pre>'%self.plain()
1527 if self._value is None:
1528 value = ''
1529 else:
1530 value = cgi.escape(str(self._value))
1532 value = '"'.join(value.split('"'))
1533 name = self._formname
1534 passthrough_args = cgi_escape_attrs(**kwargs)
1535 return ('<textarea %(passthrough_args)s name="%(name)s" id="%(name)s"'
1536 ' rows="%(rows)s" cols="%(cols)s">'
1537 '%(value)s</textarea>') % locals()
1539 def email(self, escape=1):
1540 """ Render the value of the property as an obscured email address
1541 """
1542 if not self.is_view_ok():
1543 return self._('[hidden]')
1545 if self._value is None:
1546 value = ''
1547 else:
1548 value = str(self._value)
1549 split = value.split('@')
1550 if len(split) == 2:
1551 name, domain = split
1552 domain = ' '.join(domain.split('.')[:-1])
1553 name = name.replace('.', ' ')
1554 value = '%s at %s ...'%(name, domain)
1555 else:
1556 value = value.replace('.', ' ')
1557 if escape:
1558 value = cgi.escape(value)
1559 return value
1561 class PasswordHTMLProperty(HTMLProperty):
1562 def plain(self, escape=0):
1563 """ Render a "plain" representation of the property
1564 """
1565 if not self.is_view_ok():
1566 return self._('[hidden]')
1568 if self._value is None:
1569 return ''
1570 return self._('*encrypted*')
1572 def field(self, size=30, **kwargs):
1573 """ Render a form edit field for the property.
1575 If not editable, just display the value via plain().
1576 """
1577 if not self.is_edit_ok():
1578 return self.plain(escape=1)
1580 return self.input(type="password", name=self._formname, size=size,
1581 **kwargs)
1583 def confirm(self, size=30):
1584 """ Render a second form edit field for the property, used for
1585 confirmation that the user typed the password correctly. Generates
1586 a field with name "@confirm@name".
1588 If not editable, display nothing.
1589 """
1590 if not self.is_edit_ok():
1591 return ''
1593 return self.input(type="password",
1594 name="@confirm@%s"%self._formname,
1595 id="%s-confirm"%self._formname,
1596 size=size)
1598 class NumberHTMLProperty(HTMLProperty):
1599 def plain(self, escape=0):
1600 """ Render a "plain" representation of the property
1601 """
1602 if not self.is_view_ok():
1603 return self._('[hidden]')
1605 if self._value is None:
1606 return ''
1608 return str(self._value)
1610 def field(self, size=30, **kwargs):
1611 """ Render a form edit field for the property.
1613 If not editable, just display the value via plain().
1614 """
1615 if not self.is_edit_ok():
1616 return self.plain(escape=1)
1618 value = self._value
1619 if value is None:
1620 value = ''
1622 return self.input(name=self._formname, value=value, size=size,
1623 **kwargs)
1625 def __int__(self):
1626 """ Return an int of me
1627 """
1628 return int(self._value)
1630 def __float__(self):
1631 """ Return a float of me
1632 """
1633 return float(self._value)
1636 class BooleanHTMLProperty(HTMLProperty):
1637 def plain(self, escape=0):
1638 """ Render a "plain" representation of the property
1639 """
1640 if not self.is_view_ok():
1641 return self._('[hidden]')
1643 if self._value is None:
1644 return ''
1645 return self._value and self._("Yes") or self._("No")
1647 def field(self, **kwargs):
1648 """ Render a form edit field for the property
1650 If not editable, just display the value via plain().
1651 """
1652 if not self.is_edit_ok():
1653 return self.plain(escape=1)
1655 value = self._value
1656 if isinstance(value, str) or isinstance(value, unicode):
1657 value = value.strip().lower() in ('checked', 'yes', 'true',
1658 'on', '1')
1660 checked = value and "checked" or ""
1661 if value:
1662 s = self.input(type="radio", name=self._formname, value="yes",
1663 checked="checked", **kwargs)
1664 s += self._('Yes')
1665 s +=self.input(type="radio", name=self._formname, value="no",
1666 **kwargs)
1667 s += self._('No')
1668 else:
1669 s = self.input(type="radio", name=self._formname, value="yes",
1670 **kwargs)
1671 s += self._('Yes')
1672 s +=self.input(type="radio", name=self._formname, value="no",
1673 checked="checked", **kwargs)
1674 s += self._('No')
1675 return s
1677 class DateHTMLProperty(HTMLProperty):
1679 _marker = []
1681 def __init__(self, client, classname, nodeid, prop, name, value,
1682 anonymous=0, offset=None):
1683 HTMLProperty.__init__(self, client, classname, nodeid, prop, name,
1684 value, anonymous=anonymous)
1685 if self._value and not (isinstance(self._value, str) or
1686 isinstance(self._value, unicode)):
1687 self._value.setTranslator(self._client.translator)
1688 self._offset = offset
1689 if self._offset is None :
1690 self._offset = self._prop.offset (self._db)
1692 def plain(self, escape=0):
1693 """ Render a "plain" representation of the property
1694 """
1695 if not self.is_view_ok():
1696 return self._('[hidden]')
1698 if self._value is None:
1699 return ''
1700 if self._offset is None:
1701 offset = self._db.getUserTimezone()
1702 else:
1703 offset = self._offset
1704 return str(self._value.local(offset))
1706 def now(self, str_interval=None):
1707 """ Return the current time.
1709 This is useful for defaulting a new value. Returns a
1710 DateHTMLProperty.
1711 """
1712 if not self.is_view_ok():
1713 return self._('[hidden]')
1715 ret = date.Date('.', translator=self._client)
1717 if isinstance(str_interval, basestring):
1718 sign = 1
1719 if str_interval[0] == '-':
1720 sign = -1
1721 str_interval = str_interval[1:]
1722 interval = date.Interval(str_interval, translator=self._client)
1723 if sign > 0:
1724 ret = ret + interval
1725 else:
1726 ret = ret - interval
1728 return DateHTMLProperty(self._client, self._classname, self._nodeid,
1729 self._prop, self._formname, ret)
1731 def field(self, size=30, default=None, format=_marker, popcal=True,
1732 **kwargs):
1733 """Render a form edit field for the property
1735 If not editable, just display the value via plain().
1737 If "popcal" then include the Javascript calendar editor.
1738 Default=yes.
1740 The format string is a standard python strftime format string.
1741 """
1742 if not self.is_edit_ok():
1743 if format is self._marker:
1744 return self.plain(escape=1)
1745 else:
1746 return self.pretty(format)
1748 value = self._value
1750 if value is None:
1751 if default is None:
1752 raw_value = None
1753 else:
1754 if isinstance(default, basestring):
1755 raw_value = date.Date(default, translator=self._client)
1756 elif isinstance(default, date.Date):
1757 raw_value = default
1758 elif isinstance(default, DateHTMLProperty):
1759 raw_value = default._value
1760 else:
1761 raise ValueError, self._('default value for '
1762 'DateHTMLProperty must be either DateHTMLProperty '
1763 'or string date representation.')
1764 elif isinstance(value, str) or isinstance(value, unicode):
1765 # most likely erroneous input to be passed back to user
1766 if isinstance(value, unicode): value = value.encode('utf8')
1767 return self.input(name=self._formname, value=value, size=size,
1768 **kwargs)
1769 else:
1770 raw_value = value
1772 if raw_value is None:
1773 value = ''
1774 elif isinstance(raw_value, str) or isinstance(raw_value, unicode):
1775 if format is self._marker:
1776 value = raw_value
1777 else:
1778 value = date.Date(raw_value).pretty(format)
1779 else:
1780 if self._offset is None :
1781 offset = self._db.getUserTimezone()
1782 else :
1783 offset = self._offset
1784 value = raw_value.local(offset)
1785 if format is not self._marker:
1786 value = value.pretty(format)
1788 s = self.input(name=self._formname, value=value, size=size,
1789 **kwargs)
1790 if popcal:
1791 s += self.popcal()
1792 return s
1794 def reldate(self, pretty=1):
1795 """ Render the interval between the date and now.
1797 If the "pretty" flag is true, then make the display pretty.
1798 """
1799 if not self.is_view_ok():
1800 return self._('[hidden]')
1802 if not self._value:
1803 return ''
1805 # figure the interval
1806 interval = self._value - date.Date('.', translator=self._client)
1807 if pretty:
1808 return interval.pretty()
1809 return str(interval)
1811 def pretty(self, format=_marker):
1812 """ Render the date in a pretty format (eg. month names, spaces).
1814 The format string is a standard python strftime format string.
1815 Note that if the day is zero, and appears at the start of the
1816 string, then it'll be stripped from the output. This is handy
1817 for the situation when a date only specifies a month and a year.
1818 """
1819 if not self.is_view_ok():
1820 return self._('[hidden]')
1822 if self._offset is None:
1823 offset = self._db.getUserTimezone()
1824 else:
1825 offset = self._offset
1827 if not self._value:
1828 return ''
1829 elif format is not self._marker:
1830 return self._value.local(offset).pretty(format)
1831 else:
1832 return self._value.local(offset).pretty()
1834 def local(self, offset):
1835 """ Return the date/time as a local (timezone offset) date/time.
1836 """
1837 if not self.is_view_ok():
1838 return self._('[hidden]')
1840 return DateHTMLProperty(self._client, self._classname, self._nodeid,
1841 self._prop, self._formname, self._value, offset=offset)
1843 def popcal(self, width=300, height=200, label="(cal)",
1844 form="itemSynopsis"):
1845 """Generate a link to a calendar pop-up window.
1847 item: HTMLProperty e.g.: context.deadline
1848 """
1849 if self.isset():
1850 date = "&date=%s"%self._value
1851 else :
1852 date = ""
1853 return ('<a class="classhelp" href="javascript:help_window('
1854 "'%s?@template=calendar&property=%s&form=%s%s', %d, %d)"
1855 '">%s</a>'%(self._classname, self._name, form, date, width,
1856 height, label))
1858 class IntervalHTMLProperty(HTMLProperty):
1859 def __init__(self, client, classname, nodeid, prop, name, value,
1860 anonymous=0):
1861 HTMLProperty.__init__(self, client, classname, nodeid, prop,
1862 name, value, anonymous)
1863 if self._value and not isinstance(self._value, (str, unicode)):
1864 self._value.setTranslator(self._client.translator)
1866 def plain(self, escape=0):
1867 """ Render a "plain" representation of the property
1868 """
1869 if not self.is_view_ok():
1870 return self._('[hidden]')
1872 if self._value is None:
1873 return ''
1874 return str(self._value)
1876 def pretty(self):
1877 """ Render the interval in a pretty format (eg. "yesterday")
1878 """
1879 if not self.is_view_ok():
1880 return self._('[hidden]')
1882 return self._value.pretty()
1884 def field(self, size=30, **kwargs):
1885 """ Render a form edit field for the property
1887 If not editable, just display the value via plain().
1888 """
1889 if not self.is_edit_ok():
1890 return self.plain(escape=1)
1892 value = self._value
1893 if value is None:
1894 value = ''
1896 return self.input(name=self._formname, value=value, size=size,
1897 **kwargs)
1899 class LinkHTMLProperty(HTMLProperty):
1900 """ Link HTMLProperty
1901 Include the above as well as being able to access the class
1902 information. Stringifying the object itself results in the value
1903 from the item being displayed. Accessing attributes of this object
1904 result in the appropriate entry from the class being queried for the
1905 property accessed (so item/assignedto/name would look up the user
1906 entry identified by the assignedto property on item, and then the
1907 name property of that user)
1908 """
1909 def __init__(self, *args, **kw):
1910 HTMLProperty.__init__(self, *args, **kw)
1911 # if we're representing a form value, then the -1 from the form really
1912 # should be a None
1913 if str(self._value) == '-1':
1914 self._value = None
1916 def __getattr__(self, attr):
1917 """ return a new HTMLItem """
1918 if not self._value:
1919 # handle a special page templates lookup
1920 if attr == '__render_with_namespace__':
1921 def nothing(*args, **kw):
1922 return ''
1923 return nothing
1924 msg = self._('Attempt to look up %(attr)s on a missing value')
1925 return MissingValue(msg%locals())
1926 i = HTMLItem(self._client, self._prop.classname, self._value)
1927 return getattr(i, attr)
1929 def plain(self, escape=0):
1930 """ Render a "plain" representation of the property
1931 """
1932 if not self.is_view_ok():
1933 return self._('[hidden]')
1935 if self._value is None:
1936 return ''
1937 linkcl = self._db.classes[self._prop.classname]
1938 k = linkcl.labelprop(1)
1939 if num_re.match(self._value):
1940 try:
1941 value = str(linkcl.get(self._value, k))
1942 except IndexError:
1943 value = self._value
1944 else :
1945 value = self._value
1946 if escape:
1947 value = cgi.escape(value)
1948 return value
1950 def field(self, showid=0, size=None, **kwargs):
1951 """ Render a form edit field for the property
1953 If not editable, just display the value via plain().
1954 """
1955 if not self.is_edit_ok():
1956 return self.plain(escape=1)
1958 # edit field
1959 linkcl = self._db.getclass(self._prop.classname)
1960 if self._value is None:
1961 value = ''
1962 else:
1963 k = linkcl.getkey()
1964 if k and num_re.match(self._value):
1965 value = linkcl.get(self._value, k)
1966 else:
1967 value = self._value
1968 return self.input(name=self._formname, value=value, size=size,
1969 **kwargs)
1971 def menu(self, size=None, height=None, showid=0, additional=[], value=None,
1972 sort_on=None, html_kwargs = {}, **conditions):
1973 """ Render a form select list for this property
1975 "size" is used to limit the length of the list labels
1976 "height" is used to set the <select> tag's "size" attribute
1977 "showid" includes the item ids in the list labels
1978 "value" specifies which item is pre-selected
1979 "additional" lists properties which should be included in the
1980 label
1981 "sort_on" indicates the property to sort the list on as
1982 (direction, property) where direction is '+' or '-'. A
1983 single string with the direction prepended may be used.
1984 For example: ('-', 'order'), '+name'.
1986 The remaining keyword arguments are used as conditions for
1987 filtering the items in the list - they're passed as the
1988 "filterspec" argument to a Class.filter() call.
1990 If not editable, just display the value via plain().
1991 """
1992 if not self.is_edit_ok():
1993 return self.plain(escape=1)
1995 # Since None indicates the default, we need another way to
1996 # indicate "no selection". We use -1 for this purpose, as
1997 # that is the value we use when submitting a form without the
1998 # value set.
1999 if value is None:
2000 value = self._value
2001 elif value == '-1':
2002 value = None
2004 linkcl = self._db.getclass(self._prop.classname)
2005 l = ['<select %s>'%cgi_escape_attrs(name = self._formname,
2006 **html_kwargs)]
2007 k = linkcl.labelprop(1)
2008 s = ''
2009 if value is None:
2010 s = 'selected="selected" '
2011 l.append(self._('<option %svalue="-1">- no selection -</option>')%s)
2013 if sort_on is not None:
2014 if not isinstance(sort_on, tuple):
2015 if sort_on[0] in '+-':
2016 sort_on = (sort_on[0], sort_on[1:])
2017 else:
2018 sort_on = ('+', sort_on)
2019 else:
2020 sort_on = ('+', linkcl.orderprop())
2022 options = [opt
2023 for opt in linkcl.filter(None, conditions, sort_on, (None, None))
2024 if self._db.security.hasPermission("View", self._client.userid,
2025 linkcl.classname, itemid=opt)]
2027 # make sure we list the current value if it's retired
2028 if value and value not in options:
2029 options.insert(0, value)
2031 if additional:
2032 additional_fns = []
2033 props = linkcl.getprops()
2034 for propname in additional:
2035 prop = props[propname]
2036 if isinstance(prop, hyperdb.Link):
2037 cl = self._db.getclass(prop.classname)
2038 labelprop = cl.labelprop()
2039 fn = lambda optionid: cl.get(linkcl.get(optionid,
2040 propname),
2041 labelprop)
2042 else:
2043 fn = lambda optionid: linkcl.get(optionid, propname)
2044 additional_fns.append(fn)
2046 for optionid in options:
2047 # get the option value, and if it's None use an empty string
2048 option = linkcl.get(optionid, k) or ''
2050 # figure if this option is selected
2051 s = ''
2052 if value in [optionid, option]:
2053 s = 'selected="selected" '
2055 # figure the label
2056 if showid:
2057 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
2058 elif not option:
2059 lab = '%s%s'%(self._prop.classname, optionid)
2060 else:
2061 lab = option
2063 # truncate if it's too long
2064 if size is not None and len(lab) > size:
2065 lab = lab[:size-3] + '...'
2066 if additional:
2067 m = []
2068 for fn in additional_fns:
2069 m.append(str(fn(optionid)))
2070 lab = lab + ' (%s)'%', '.join(m)
2072 # and generate
2073 lab = cgi.escape(self._(lab))
2074 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
2075 l.append('</select>')
2076 return '\n'.join(l)
2077 # def checklist(self, ...)
2081 class MultilinkHTMLProperty(HTMLProperty):
2082 """ Multilink HTMLProperty
2084 Also be iterable, returning a wrapper object like the Link case for
2085 each entry in the multilink.
2086 """
2087 def __init__(self, *args, **kwargs):
2088 HTMLProperty.__init__(self, *args, **kwargs)
2089 if self._value:
2090 display_value = lookupIds(self._db, self._prop, self._value,
2091 fail_ok=1, do_lookup=False)
2092 sortfun = make_sort_function(self._db, self._prop.classname)
2093 # sorting fails if the value contains
2094 # items not yet stored in the database
2095 # ignore these errors to preserve user input
2096 try:
2097 display_value.sort(sortfun)
2098 except:
2099 pass
2100 self._value = display_value
2102 def __len__(self):
2103 """ length of the multilink """
2104 return len(self._value)
2106 def __getattr__(self, attr):
2107 """ no extended attribute accesses make sense here """
2108 raise AttributeError, attr
2110 def viewableGenerator(self, values):
2111 """Used to iterate over only the View'able items in a class."""
2112 check = self._db.security.hasPermission
2113 userid = self._client.userid
2114 classname = self._prop.classname
2115 if check('Web Access', userid):
2116 for value in values:
2117 if check('View', userid, classname, itemid=value):
2118 yield HTMLItem(self._client, classname, value)
2120 def __iter__(self):
2121 """ iterate and return a new HTMLItem
2122 """
2123 return self.viewableGenerator(self._value)
2125 def reverse(self):
2126 """ return the list in reverse order
2127 """
2128 l = self._value[:]
2129 l.reverse()
2130 return self.viewableGenerator(l)
2132 def sorted(self, property):
2133 """ Return this multilink sorted by the given property """
2134 value = list(self.__iter__())
2135 value.sort(lambda a,b:cmp(a[property], b[property]))
2136 return value
2138 def __contains__(self, value):
2139 """ Support the "in" operator. We have to make sure the passed-in
2140 value is a string first, not a HTMLProperty.
2141 """
2142 return str(value) in self._value
2144 def isset(self):
2145 """Is my _value not []?"""
2146 return self._value != []
2148 def plain(self, escape=0):
2149 """ Render a "plain" representation of the property
2150 """
2151 if not self.is_view_ok():
2152 return self._('[hidden]')
2154 linkcl = self._db.classes[self._prop.classname]
2155 k = linkcl.labelprop(1)
2156 labels = []
2157 for v in self._value:
2158 if num_re.match(v):
2159 try:
2160 label = linkcl.get(v, k)
2161 except IndexError:
2162 label = None
2163 # fall back to designator if label is None
2164 if label is None: label = '%s%s'%(self._prop.classname, k)
2165 else:
2166 label = v
2167 labels.append(label)
2168 value = ', '.join(labels)
2169 if escape:
2170 value = cgi.escape(value)
2171 return value
2173 def field(self, size=30, showid=0, **kwargs):
2174 """ Render a form edit field for the property
2176 If not editable, just display the value via plain().
2177 """
2178 if not self.is_edit_ok():
2179 return self.plain(escape=1)
2181 linkcl = self._db.getclass(self._prop.classname)
2183 if 'value' not in kwargs:
2184 value = self._value[:]
2185 # map the id to the label property
2186 if not linkcl.getkey():
2187 showid=1
2188 if not showid:
2189 k = linkcl.labelprop(1)
2190 value = lookupKeys(linkcl, k, value)
2191 value = ','.join(value)
2192 kwargs["value"] = value
2194 return self.input(name=self._formname, size=size, **kwargs)
2196 def menu(self, size=None, height=None, showid=0, additional=[],
2197 value=None, sort_on=None, html_kwargs = {}, **conditions):
2198 """ Render a form <select> list for this property.
2200 "size" is used to limit the length of the list labels
2201 "height" is used to set the <select> tag's "size" attribute
2202 "showid" includes the item ids in the list labels
2203 "additional" lists properties which should be included in the
2204 label
2205 "value" specifies which item is pre-selected
2206 "sort_on" indicates the property to sort the list on as
2207 (direction, property) where direction is '+' or '-'. A
2208 single string with the direction prepended may be used.
2209 For example: ('-', 'order'), '+name'.
2211 The remaining keyword arguments are used as conditions for
2212 filtering the items in the list - they're passed as the
2213 "filterspec" argument to a Class.filter() call.
2215 If not editable, just display the value via plain().
2216 """
2217 if not self.is_edit_ok():
2218 return self.plain(escape=1)
2220 if value is None:
2221 value = self._value
2223 linkcl = self._db.getclass(self._prop.classname)
2225 if sort_on is not None:
2226 if not isinstance(sort_on, tuple):
2227 if sort_on[0] in '+-':
2228 sort_on = (sort_on[0], sort_on[1:])
2229 else:
2230 sort_on = ('+', sort_on)
2231 else:
2232 sort_on = ('+', linkcl.orderprop())
2234 options = [opt
2235 for opt in linkcl.filter(None, conditions, sort_on)
2236 if self._db.security.hasPermission("View", self._client.userid,
2237 linkcl.classname, itemid=opt)]
2239 # make sure we list the current values if they're retired
2240 for val in value:
2241 if val not in options:
2242 options.insert(0, val)
2244 if not height:
2245 height = len(options)
2246 if value:
2247 # The "no selection" option.
2248 height += 1
2249 height = min(height, 7)
2250 l = ['<select multiple %s>'%cgi_escape_attrs(name = self._formname,
2251 size = height,
2252 **html_kwargs)]
2253 k = linkcl.labelprop(1)
2255 if value:
2256 l.append('<option value="%s">- no selection -</option>'
2257 % ','.join(['-' + v for v in value]))
2259 if additional:
2260 additional_fns = []
2261 props = linkcl.getprops()
2262 for propname in additional:
2263 prop = props[propname]
2264 if isinstance(prop, hyperdb.Link):
2265 cl = self._db.getclass(prop.classname)
2266 labelprop = cl.labelprop()
2267 fn = lambda optionid: cl.get(linkcl.get(optionid,
2268 propname),
2269 labelprop)
2270 else:
2271 fn = lambda optionid: linkcl.get(optionid, propname)
2272 additional_fns.append(fn)
2274 for optionid in options:
2275 # get the option value, and if it's None use an empty string
2276 option = linkcl.get(optionid, k) or ''
2278 # figure if this option is selected
2279 s = ''
2280 if optionid in value or option in value:
2281 s = 'selected="selected" '
2283 # figure the label
2284 if showid:
2285 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
2286 else:
2287 lab = option
2288 # truncate if it's too long
2289 if size is not None and len(lab) > size:
2290 lab = lab[:size-3] + '...'
2291 if additional:
2292 m = []
2293 for fn in additional_fns:
2294 m.append(str(fn(optionid)))
2295 lab = lab + ' (%s)'%', '.join(m)
2297 # and generate
2298 lab = cgi.escape(self._(lab))
2299 l.append('<option %svalue="%s">%s</option>'%(s, optionid,
2300 lab))
2301 l.append('</select>')
2302 return '\n'.join(l)
2305 # set the propclasses for HTMLItem
2306 propclasses = [
2307 (hyperdb.String, StringHTMLProperty),
2308 (hyperdb.Number, NumberHTMLProperty),
2309 (hyperdb.Boolean, BooleanHTMLProperty),
2310 (hyperdb.Date, DateHTMLProperty),
2311 (hyperdb.Interval, IntervalHTMLProperty),
2312 (hyperdb.Password, PasswordHTMLProperty),
2313 (hyperdb.Link, LinkHTMLProperty),
2314 (hyperdb.Multilink, MultilinkHTMLProperty),
2315 ]
2317 def register_propclass(prop, cls):
2318 for index,propclass in enumerate(propclasses):
2319 p, c = propclass
2320 if prop == p:
2321 propclasses[index] = (prop, cls)
2322 break
2323 else:
2324 propclasses.append((prop, cls))
2327 def make_sort_function(db, classname, sort_on=None):
2328 """Make a sort function for a given class.
2330 The list being sorted may contain mixed ids and labels.
2331 """
2332 linkcl = db.getclass(classname)
2333 if sort_on is None:
2334 sort_on = linkcl.orderprop()
2335 def sortfunc(a, b):
2336 if num_re.match(a):
2337 a = linkcl.get(a, sort_on)
2338 if num_re.match(b):
2339 b = linkcl.get(b, sort_on)
2340 return cmp(a, b)
2341 return sortfunc
2343 def handleListCGIValue(value):
2344 """ Value is either a single item or a list of items. Each item has a
2345 .value that we're actually interested in.
2346 """
2347 if isinstance(value, type([])):
2348 return [value.value for value in value]
2349 else:
2350 value = value.value.strip()
2351 if not value:
2352 return []
2353 return [v.strip() for v in value.split(',')]
2355 class HTMLRequest(HTMLInputMixin):
2356 """The *request*, holding the CGI form and environment.
2358 - "form" the CGI form as a cgi.FieldStorage
2359 - "env" the CGI environment variables
2360 - "base" the base URL for this instance
2361 - "user" a HTMLItem instance for this user
2362 - "language" as determined by the browser or config
2363 - "classname" the current classname (possibly None)
2364 - "template" the current template (suffix, also possibly None)
2366 Index args:
2368 - "columns" dictionary of the columns to display in an index page
2369 - "show" a convenience access to columns - request/show/colname will
2370 be true if the columns should be displayed, false otherwise
2371 - "sort" index sort column (direction, column name)
2372 - "group" index grouping property (direction, column name)
2373 - "filter" properties to filter the index on
2374 - "filterspec" values to filter the index on
2375 - "search_text" text to perform a full-text search on for an index
2376 """
2377 def __repr__(self):
2378 return '<HTMLRequest %r>'%self.__dict__
2380 def __init__(self, client):
2381 # _client is needed by HTMLInputMixin
2382 self._client = self.client = client
2384 # easier access vars
2385 self.form = client.form
2386 self.env = client.env
2387 self.base = client.base
2388 self.user = HTMLItem(client, 'user', client.userid)
2389 self.language = client.language
2391 # store the current class name and action
2392 self.classname = client.classname
2393 self.nodeid = client.nodeid
2394 self.template = client.template
2396 # the special char to use for special vars
2397 self.special_char = '@'
2399 HTMLInputMixin.__init__(self)
2401 self._post_init()
2403 def current_url(self):
2404 url = self.base
2405 if self.classname:
2406 url += self.classname
2407 if self.nodeid:
2408 url += self.nodeid
2409 args = {}
2410 if self.template:
2411 args['@template'] = self.template
2412 return self.indexargs_url(url, args)
2414 def _parse_sort(self, var, name):
2415 """ Parse sort/group options. Append to var
2416 """
2417 fields = []
2418 dirs = []
2419 for special in '@:':
2420 idx = 0
2421 key = '%s%s%d'%(special, name, idx)
2422 while key in self.form:
2423 self.special_char = special
2424 fields.append(self.form.getfirst(key))
2425 dirkey = '%s%sdir%d'%(special, name, idx)
2426 if dirkey in self.form:
2427 dirs.append(self.form.getfirst(dirkey))
2428 else:
2429 dirs.append(None)
2430 idx += 1
2431 key = '%s%s%d'%(special, name, idx)
2432 # backward compatible (and query) URL format
2433 key = special + name
2434 dirkey = key + 'dir'
2435 if key in self.form and not fields:
2436 fields = handleListCGIValue(self.form[key])
2437 if dirkey in self.form:
2438 dirs.append(self.form.getfirst(dirkey))
2439 if fields: # only try other special char if nothing found
2440 break
2441 for f, d in map(None, fields, dirs):
2442 if f.startswith('-'):
2443 var.append(('-', f[1:]))
2444 elif d:
2445 var.append(('-', f))
2446 else:
2447 var.append(('+', f))
2449 def _post_init(self):
2450 """ Set attributes based on self.form
2451 """
2452 # extract the index display information from the form
2453 self.columns = []
2454 for name in ':columns @columns'.split():
2455 if self.form.has_key(name):
2456 self.special_char = name[0]
2457 self.columns = handleListCGIValue(self.form[name])
2458 break
2459 self.show = support.TruthDict(self.columns)
2460 security = self._client.db.security
2461 userid = self._client.userid
2463 # sorting and grouping
2464 self.sort = []
2465 self.group = []
2466 self._parse_sort(self.sort, 'sort')
2467 self._parse_sort(self.group, 'group')
2468 self.sort = security.filterSortspec(userid, self.classname, self.sort)
2469 self.group = security.filterSortspec(userid, self.classname, self.group)
2471 # filtering
2472 self.filter = []
2473 for name in ':filter @filter'.split():
2474 if self.form.has_key(name):
2475 self.special_char = name[0]
2476 self.filter = handleListCGIValue(self.form[name])
2478 self.filterspec = {}
2479 db = self.client.db
2480 if self.classname is not None:
2481 cls = db.getclass (self.classname)
2482 for name in self.filter:
2483 if not self.form.has_key(name):
2484 continue
2485 prop = cls.get_transitive_prop (name)
2486 fv = self.form[name]
2487 if (isinstance(prop, hyperdb.Link) or
2488 isinstance(prop, hyperdb.Multilink)):
2489 self.filterspec[name] = lookupIds(db, prop,
2490 handleListCGIValue(fv))
2491 else:
2492 if isinstance(fv, type([])):
2493 self.filterspec[name] = [v.value for v in fv]
2494 elif name == 'id':
2495 # special case "id" property
2496 self.filterspec[name] = handleListCGIValue(fv)
2497 else:
2498 self.filterspec[name] = fv.value
2499 self.filterspec = security.filterFilterspec(userid, self.classname,
2500 self.filterspec)
2502 # full-text search argument
2503 self.search_text = None
2504 for name in ':search_text @search_text'.split():
2505 if self.form.has_key(name):
2506 self.special_char = name[0]
2507 self.search_text = self.form.getfirst(name)
2509 # pagination - size and start index
2510 # figure batch args
2511 self.pagesize = 50
2512 for name in ':pagesize @pagesize'.split():
2513 if self.form.has_key(name):
2514 self.special_char = name[0]
2515 try:
2516 self.pagesize = int(self.form.getfirst(name))
2517 except ValueError:
2518 # not an integer - ignore
2519 pass
2521 self.startwith = 0
2522 for name in ':startwith @startwith'.split():
2523 if self.form.has_key(name):
2524 self.special_char = name[0]
2525 try:
2526 self.startwith = int(self.form.getfirst(name))
2527 except ValueError:
2528 # not an integer - ignore
2529 pass
2531 # dispname
2532 if self.form.has_key('@dispname'):
2533 self.dispname = self.form.getfirst('@dispname')
2534 else:
2535 self.dispname = None
2537 def updateFromURL(self, url):
2538 """ Parse the URL for query args, and update my attributes using the
2539 values.
2540 """
2541 env = {'QUERY_STRING': url}
2542 self.form = cgi.FieldStorage(environ=env)
2544 self._post_init()
2546 def update(self, kwargs):
2547 """ Update my attributes using the keyword args
2548 """
2549 self.__dict__.update(kwargs)
2550 if kwargs.has_key('columns'):
2551 self.show = support.TruthDict(self.columns)
2553 def description(self):
2554 """ Return a description of the request - handle for the page title.
2555 """
2556 s = [self.client.db.config.TRACKER_NAME]
2557 if self.classname:
2558 if self.client.nodeid:
2559 s.append('- %s%s'%(self.classname, self.client.nodeid))
2560 else:
2561 if self.template == 'item':
2562 s.append('- new %s'%self.classname)
2563 elif self.template == 'index':
2564 s.append('- %s index'%self.classname)
2565 else:
2566 s.append('- %s %s'%(self.classname, self.template))
2567 else:
2568 s.append('- home')
2569 return ' '.join(s)
2571 def __str__(self):
2572 d = {}
2573 d.update(self.__dict__)
2574 f = ''
2575 for k in self.form.keys():
2576 f += '\n %r=%r'%(k,handleListCGIValue(self.form[k]))
2577 d['form'] = f
2578 e = ''
2579 for k,v in self.env.items():
2580 e += '\n %r=%r'%(k, v)
2581 d['env'] = e
2582 return """
2583 form: %(form)s
2584 base: %(base)r
2585 classname: %(classname)r
2586 template: %(template)r
2587 columns: %(columns)r
2588 sort: %(sort)r
2589 group: %(group)r
2590 filter: %(filter)r
2591 search_text: %(search_text)r
2592 pagesize: %(pagesize)r
2593 startwith: %(startwith)r
2594 env: %(env)s
2595 """%d
2597 def indexargs_form(self, columns=1, sort=1, group=1, filter=1,
2598 filterspec=1, search_text=1):
2599 """ return the current index args as form elements """
2600 l = []
2601 sc = self.special_char
2602 def add(k, v):
2603 l.append(self.input(type="hidden", name=k, value=v))
2604 if columns and self.columns:
2605 add(sc+'columns', ','.join(self.columns))
2606 if sort:
2607 val = []
2608 for dir, attr in self.sort:
2609 if dir == '-':
2610 val.append('-'+attr)
2611 else:
2612 val.append(attr)
2613 add(sc+'sort', ','.join (val))
2614 if group:
2615 val = []
2616 for dir, attr in self.group:
2617 if dir == '-':
2618 val.append('-'+attr)
2619 else:
2620 val.append(attr)
2621 add(sc+'group', ','.join (val))
2622 if filter and self.filter:
2623 add(sc+'filter', ','.join(self.filter))
2624 if self.classname and filterspec:
2625 cls = self.client.db.getclass(self.classname)
2626 for k,v in self.filterspec.items():
2627 if type(v) == type([]):
2628 if isinstance(cls.get_transitive_prop(k), hyperdb.String):
2629 add(k, ' '.join(v))
2630 else:
2631 add(k, ','.join(v))
2632 else:
2633 add(k, v)
2634 if search_text and self.search_text:
2635 add(sc+'search_text', self.search_text)
2636 add(sc+'pagesize', self.pagesize)
2637 add(sc+'startwith', self.startwith)
2638 return '\n'.join(l)
2640 def indexargs_url(self, url, args):
2641 """ Embed the current index args in a URL
2642 """
2643 q = urllib.quote
2644 sc = self.special_char
2645 l = ['%s=%s'%(k,v) for k,v in args.items()]
2647 # pull out the special values (prefixed by @ or :)
2648 specials = {}
2649 for key in args.keys():
2650 if key[0] in '@:':
2651 specials[key[1:]] = args[key]
2653 # ok, now handle the specials we received in the request
2654 if self.columns and not specials.has_key('columns'):
2655 l.append(sc+'columns=%s'%(','.join(self.columns)))
2656 if self.sort and not specials.has_key('sort'):
2657 val = []
2658 for dir, attr in self.sort:
2659 if dir == '-':
2660 val.append('-'+attr)
2661 else:
2662 val.append(attr)
2663 l.append(sc+'sort=%s'%(','.join(val)))
2664 if self.group and not specials.has_key('group'):
2665 val = []
2666 for dir, attr in self.group:
2667 if dir == '-':
2668 val.append('-'+attr)
2669 else:
2670 val.append(attr)
2671 l.append(sc+'group=%s'%(','.join(val)))
2672 if self.filter and not specials.has_key('filter'):
2673 l.append(sc+'filter=%s'%(','.join(self.filter)))
2674 if self.search_text and not specials.has_key('search_text'):
2675 l.append(sc+'search_text=%s'%q(self.search_text))
2676 if not specials.has_key('pagesize'):
2677 l.append(sc+'pagesize=%s'%self.pagesize)
2678 if not specials.has_key('startwith'):
2679 l.append(sc+'startwith=%s'%self.startwith)
2681 # finally, the remainder of the filter args in the request
2682 if self.classname and self.filterspec:
2683 cls = self.client.db.getclass(self.classname)
2684 for k,v in self.filterspec.items():
2685 if not args.has_key(k):
2686 if type(v) == type([]):
2687 prop = cls.get_transitive_prop(k)
2688 if k != 'id' and isinstance(prop, hyperdb.String):
2689 l.append('%s=%s'%(k, '%20'.join([q(i) for i in v])))
2690 else:
2691 l.append('%s=%s'%(k, ','.join([q(i) for i in v])))
2692 else:
2693 l.append('%s=%s'%(k, q(v)))
2694 return '%s?%s'%(url, '&'.join(l))
2695 indexargs_href = indexargs_url
2697 def base_javascript(self):
2698 return """
2699 <script type="text/javascript">
2700 submitted = false;
2701 function submit_once() {
2702 if (submitted) {
2703 alert("Your request is being processed.\\nPlease be patient.");
2704 event.returnValue = 0; // work-around for IE
2705 return 0;
2706 }
2707 submitted = true;
2708 return 1;
2709 }
2711 function help_window(helpurl, width, height) {
2712 HelpWin = window.open('%s' + helpurl, 'RoundupHelpWindow', 'scrollbars=yes,resizable=yes,toolbar=no,height='+height+',width='+width);
2713 }
2714 </script>
2715 """%self.base
2717 def batch(self):
2718 """ Return a batch object for results from the "current search"
2719 """
2720 check = self._client.db.security.hasPermission
2721 userid = self._client.userid
2722 if not check('Web Access', userid):
2723 return Batch(self.client, [], self.pagesize, self.startwith,
2724 classname=self.classname)
2726 filterspec = self.filterspec
2727 sort = self.sort
2728 group = self.group
2730 # get the list of ids we're batching over
2731 klass = self.client.db.getclass(self.classname)
2732 if self.search_text:
2733 matches = self.client.db.indexer.search(
2734 [w.upper().encode("utf-8", "replace") for w in re.findall(
2735 r'(?u)\b\w{2,25}\b',
2736 unicode(self.search_text, "utf-8", "replace")
2737 )], klass)
2738 else:
2739 matches = None
2741 # filter for visibility
2742 l = [id for id in klass.filter(matches, filterspec, sort, group)
2743 if check('View', userid, self.classname, itemid=id)]
2745 # return the batch object, using IDs only
2746 return Batch(self.client, l, self.pagesize, self.startwith,
2747 classname=self.classname)
2749 # extend the standard ZTUtils Batch object to remove dependency on
2750 # Acquisition and add a couple of useful methods
2751 class Batch(ZTUtils.Batch):
2752 """ Use me to turn a list of items, or item ids of a given class, into a
2753 series of batches.
2755 ========= ========================================================
2756 Parameter Usage
2757 ========= ========================================================
2758 sequence a list of HTMLItems or item ids
2759 classname if sequence is a list of ids, this is the class of item
2760 size how big to make the sequence.
2761 start where to start (0-indexed) in the sequence.
2762 end where to end (0-indexed) in the sequence.
2763 orphan if the next batch would contain less items than this
2764 value, then it is combined with this batch
2765 overlap the number of items shared between adjacent batches
2766 ========= ========================================================
2768 Attributes: Note that the "start" attribute, unlike the
2769 argument, is a 1-based index (I know, lame). "first" is the
2770 0-based index. "length" is the actual number of elements in
2771 the batch.
2773 "sequence_length" is the length of the original, unbatched, sequence.
2774 """
2775 def __init__(self, client, sequence, size, start, end=0, orphan=0,
2776 overlap=0, classname=None):
2777 self.client = client
2778 self.last_index = self.last_item = None
2779 self.current_item = None
2780 self.classname = classname
2781 self.sequence_length = len(sequence)
2782 ZTUtils.Batch.__init__(self, sequence, size, start, end, orphan,
2783 overlap)
2785 # overwrite so we can late-instantiate the HTMLItem instance
2786 def __getitem__(self, index):
2787 if index < 0:
2788 if index + self.end < self.first: raise IndexError, index
2789 return self._sequence[index + self.end]
2791 if index >= self.length:
2792 raise IndexError, index
2794 # move the last_item along - but only if the fetched index changes
2795 # (for some reason, index 0 is fetched twice)
2796 if index != self.last_index:
2797 self.last_item = self.current_item
2798 self.last_index = index
2800 item = self._sequence[index + self.first]
2801 if self.classname:
2802 # map the item ids to instances
2803 item = HTMLItem(self.client, self.classname, item)
2804 self.current_item = item
2805 return item
2807 def propchanged(self, *properties):
2808 """ Detect if one of the properties marked as being a group
2809 property changed in the last iteration fetch
2810 """
2811 # we poke directly at the _value here since MissingValue can screw
2812 # us up and cause Nones to compare strangely
2813 if self.last_item is None:
2814 return 1
2815 for property in properties:
2816 if property == 'id' or isinstance (self.last_item[property], list):
2817 if (str(self.last_item[property]) !=
2818 str(self.current_item[property])):
2819 return 1
2820 else:
2821 if (self.last_item[property]._value !=
2822 self.current_item[property]._value):
2823 return 1
2824 return 0
2826 # override these 'cos we don't have access to acquisition
2827 def previous(self):
2828 if self.start == 1:
2829 return None
2830 return Batch(self.client, self._sequence, self._size,
2831 self.first - self._size + self.overlap, 0, self.orphan,
2832 self.overlap)
2834 def next(self):
2835 try:
2836 self._sequence[self.end]
2837 except IndexError:
2838 return None
2839 return Batch(self.client, self._sequence, self._size,
2840 self.end - self.overlap, 0, self.orphan, self.overlap)
2842 class TemplatingUtils:
2843 """ Utilities for templating
2844 """
2845 def __init__(self, client):
2846 self.client = client
2847 def Batch(self, sequence, size, start, end=0, orphan=0, overlap=0):
2848 return Batch(self.client, sequence, size, start, end, orphan,
2849 overlap)
2851 def url_quote(self, url):
2852 """URL-quote the supplied text."""
2853 return urllib.quote(url)
2855 def html_quote(self, html):
2856 """HTML-quote the supplied text."""
2857 return cgi.escape(html)
2859 def __getattr__(self, name):
2860 """Try the tracker's templating_utils."""
2861 if not hasattr(self.client.instance, 'templating_utils'):
2862 # backwards-compatibility
2863 raise AttributeError, name
2864 if not self.client.instance.templating_utils.has_key(name):
2865 raise AttributeError, name
2866 return self.client.instance.templating_utils[name]
2868 def keywords_expressions(self, request):
2869 return render_keywords_expression_editor(request)
2871 def html_calendar(self, request):
2872 """Generate a HTML calendar.
2874 `request` the roundup.request object
2875 - @template : name of the template
2876 - form : name of the form to store back the date
2877 - property : name of the property of the form to store
2878 back the date
2879 - date : current date
2880 - display : when browsing, specifies year and month
2882 html will simply be a table.
2883 """
2884 tz = request.client.db.getUserTimezone()
2885 current_date = date.Date(".").local(tz)
2886 date_str = request.form.getfirst("date", current_date)
2887 display = request.form.getfirst("display", date_str)
2888 template = request.form.getfirst("@template", "calendar")
2889 form = request.form.getfirst("form")
2890 property = request.form.getfirst("property")
2891 curr_date = date.Date(date_str) # to highlight
2892 display = date.Date(display) # to show
2893 day = display.day
2895 # for navigation
2896 date_prev_month = display + date.Interval("-1m")
2897 date_next_month = display + date.Interval("+1m")
2898 date_prev_year = display + date.Interval("-1y")
2899 date_next_year = display + date.Interval("+1y")
2901 res = []
2903 base_link = "%s?@template=%s&property=%s&form=%s&date=%s" % \
2904 (request.classname, template, property, form, curr_date)
2906 # navigation
2907 # month
2908 res.append('<table class="calendar"><tr><td>')
2909 res.append(' <table width="100%" class="calendar_nav"><tr>')
2910 link = "&display=%s"%date_prev_month
2911 res.append(' <td><a href="%s&display=%s"><</a></td>'%(base_link,
2912 date_prev_month))
2913 res.append(' <td>%s</td>'%calendar.month_name[display.month])
2914 res.append(' <td><a href="%s&display=%s">></a></td>'%(base_link,
2915 date_next_month))
2916 # spacer
2917 res.append(' <td width="100%"></td>')
2918 # year
2919 res.append(' <td><a href="%s&display=%s"><</a></td>'%(base_link,
2920 date_prev_year))
2921 res.append(' <td>%s</td>'%display.year)
2922 res.append(' <td><a href="%s&display=%s">></a></td>'%(base_link,
2923 date_next_year))
2924 res.append(' </tr></table>')
2925 res.append(' </td></tr>')
2927 # the calendar
2928 res.append(' <tr><td><table class="calendar_display">')
2929 res.append(' <tr class="weekdays">')
2930 for day in calendar.weekheader(3).split():
2931 res.append(' <td>%s</td>'%day)
2932 res.append(' </tr>')
2933 for week in calendar.monthcalendar(display.year, display.month):
2934 res.append(' <tr>')
2935 for day in week:
2936 link = "javascript:form[field].value = '%d-%02d-%02d'; " \
2937 "window.close ();"%(display.year, display.month, day)
2938 if (day == curr_date.day and display.month == curr_date.month
2939 and display.year == curr_date.year):
2940 # highlight
2941 style = "today"
2942 else :
2943 style = ""
2944 if day:
2945 res.append(' <td class="%s"><a href="%s">%s</a></td>'%(
2946 style, link, day))
2947 else :
2948 res.append(' <td></td>')
2949 res.append(' </tr>')
2950 res.append('</table></td></tr></table>')
2951 return "\n".join(res)
2953 class MissingValue:
2954 def __init__(self, description, **kwargs):
2955 self.__description = description
2956 for key, value in kwargs.items():
2957 self.__dict__[key] = value
2959 def __call__(self, *args, **kwargs): return MissingValue(self.__description)
2960 def __getattr__(self, name):
2961 # This allows assignments which assume all intermediate steps are Null
2962 # objects if they don't exist yet.
2963 #
2964 # For example (with just 'client' defined):
2965 #
2966 # client.db.config.TRACKER_WEB = 'BASE/'
2967 self.__dict__[name] = MissingValue(self.__description)
2968 return getattr(self, name)
2970 def __getitem__(self, key): return self
2971 def __nonzero__(self): return 0
2972 def __str__(self): return '[%s]'%self.__description
2973 def __repr__(self): return '<MissingValue 0x%x "%s">'%(id(self),
2974 self.__description)
2975 def gettext(self, str): return str
2976 _ = gettext
2978 # vim: set et sts=4 sw=4 :