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