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