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 input_html4(**attrs):
424 """Generate an 'input' (html4) element with given attributes"""
425 _set_input_default_args(attrs)
426 return '<input %s>'%' '.join(['%s="%s"'%(k,cgi.escape(str(v), True))
427 for k,v in attrs.items()])
429 def input_xhtml(**attrs):
430 """Generate an 'input' (xhtml) element with given attributes"""
431 _set_input_default_args(attrs)
432 return '<input %s/>'%' '.join(['%s="%s"'%(k,cgi.escape(str(v), True))
433 for k,v in attrs.items()])
435 class HTMLInputMixin:
436 """ requires a _client property """
437 def __init__(self):
438 html_version = 'html4'
439 if hasattr(self._client.instance.config, 'HTML_VERSION'):
440 html_version = self._client.instance.config.HTML_VERSION
441 if html_version == 'xhtml':
442 self.input = input_xhtml
443 else:
444 self.input = input_html4
445 # self._context is used for translations.
446 # will be initialized by the first call to .gettext()
447 self._context = None
449 def gettext(self, msgid):
450 """Return the localized translation of msgid"""
451 if self._context is None:
452 self._context = context(self._client)
453 return self._client.translator.translate(domain="roundup",
454 msgid=msgid, context=self._context)
456 _ = gettext
458 class HTMLPermissions:
460 def view_check(self):
461 """ Raise the Unauthorised exception if the user's not permitted to
462 view this class.
463 """
464 if not self.is_view_ok():
465 raise Unauthorised("view", self._classname,
466 translator=self._client.translator)
468 def edit_check(self):
469 """ Raise the Unauthorised exception if the user's not permitted to
470 edit items of this class.
471 """
472 if not self.is_edit_ok():
473 raise Unauthorised("edit", self._classname,
474 translator=self._client.translator)
476 def retire_check(self):
477 """ Raise the Unauthorised exception if the user's not permitted to
478 retire items of this class.
479 """
480 if not self.is_retire_ok():
481 raise Unauthorised("retire", self._classname,
482 translator=self._client.translator)
485 class HTMLClass(HTMLInputMixin, HTMLPermissions):
486 """ Accesses through a class (either through *class* or *db.<classname>*)
487 """
488 def __init__(self, client, classname, anonymous=0):
489 self._client = client
490 self._ = client._
491 self._db = client.db
492 self._anonymous = anonymous
494 # we want classname to be exposed, but _classname gives a
495 # consistent API for extending Class/Item
496 self._classname = self.classname = classname
497 self._klass = self._db.getclass(self.classname)
498 self._props = self._klass.getprops()
500 HTMLInputMixin.__init__(self)
502 def is_edit_ok(self):
503 """ Is the user allowed to Create the current class?
504 """
505 return self._db.security.hasPermission('Create', self._client.userid,
506 self._classname)
508 def is_retire_ok(self):
509 """ Is the user allowed to retire items of the current class?
510 """
511 return self._db.security.hasPermission('Retire', self._client.userid,
512 self._classname)
514 def is_view_ok(self):
515 """ Is the user allowed to View the current class?
516 """
517 return self._db.security.hasPermission('View', self._client.userid,
518 self._classname)
520 def is_only_view_ok(self):
521 """ Is the user only allowed to View (ie. not Create) the current class?
522 """
523 return self.is_view_ok() and not self.is_edit_ok()
525 def __repr__(self):
526 return '<HTMLClass(0x%x) %s>'%(id(self), self.classname)
528 def __getitem__(self, item):
529 """ return an HTMLProperty instance
530 """
532 # we don't exist
533 if item == 'id':
534 return None
536 # get the property
537 try:
538 prop = self._props[item]
539 except KeyError:
540 raise KeyError, 'No such property "%s" on %s'%(item, self.classname)
542 # look up the correct HTMLProperty class
543 form = self._client.form
544 for klass, htmlklass in propclasses:
545 if not isinstance(prop, klass):
546 continue
547 if isinstance(prop, hyperdb.Multilink):
548 value = []
549 else:
550 value = None
551 return htmlklass(self._client, self._classname, None, prop, item,
552 value, self._anonymous)
554 # no good
555 raise KeyError, item
557 def __getattr__(self, attr):
558 """ convenience access """
559 try:
560 return self[attr]
561 except KeyError:
562 raise AttributeError, attr
564 def designator(self):
565 """ Return this class' designator (classname) """
566 return self._classname
568 def getItem(self, itemid, num_re=num_re):
569 """ Get an item of this class by its item id.
570 """
571 # make sure we're looking at an itemid
572 if not isinstance(itemid, type(1)) and not num_re.match(itemid):
573 itemid = self._klass.lookup(itemid)
575 return HTMLItem(self._client, self.classname, itemid)
577 def properties(self, sort=1):
578 """ Return HTMLProperty for all of this class' properties.
579 """
580 l = []
581 for name, prop in self._props.items():
582 for klass, htmlklass in propclasses:
583 if isinstance(prop, hyperdb.Multilink):
584 value = []
585 else:
586 value = None
587 if isinstance(prop, klass):
588 l.append(htmlklass(self._client, self._classname, '',
589 prop, name, value, self._anonymous))
590 if sort:
591 l.sort(lambda a,b:cmp(a._name, b._name))
592 return l
594 def list(self, sort_on=None):
595 """ List all items in this class.
596 """
597 # get the list and sort it nicely
598 l = self._klass.list()
599 sortfunc = make_sort_function(self._db, self._classname, sort_on)
600 l.sort(sortfunc)
602 # check perms
603 check = self._client.db.security.hasPermission
604 userid = self._client.userid
606 l = [HTMLItem(self._client, self._classname, id) for id in l
607 if check('View', userid, self._classname, itemid=id)]
609 return l
611 def csv(self):
612 """ Return the items of this class as a chunk of CSV text.
613 """
614 props = self.propnames()
615 s = StringIO.StringIO()
616 writer = csv.writer(s)
617 writer.writerow(props)
618 check = self._client.db.security.hasPermission
619 for nodeid in self._klass.list():
620 l = []
621 for name in props:
622 # check permission to view this property on this item
623 if not check('View', self._client.userid, itemid=nodeid,
624 classname=self._klass.classname, property=name):
625 raise Unauthorised('view', self._klass.classname,
626 translator=self._client.translator)
627 value = self._klass.get(nodeid, name)
628 if value is None:
629 l.append('')
630 elif isinstance(value, type([])):
631 l.append(':'.join(map(str, value)))
632 else:
633 l.append(str(self._klass.get(nodeid, name)))
634 writer.writerow(l)
635 return s.getvalue()
637 def propnames(self):
638 """ Return the list of the names of the properties of this class.
639 """
640 idlessprops = self._klass.getprops(protected=0).keys()
641 idlessprops.sort()
642 return ['id'] + idlessprops
644 def filter(self, request=None, filterspec={}, sort=[], group=[]):
645 """ Return a list of items from this class, filtered and sorted
646 by the current requested filterspec/filter/sort/group args
648 "request" takes precedence over the other three arguments.
649 """
650 if request is not None:
651 filterspec = request.filterspec
652 sort = request.sort
653 group = request.group
655 check = self._db.security.hasPermission
656 userid = self._client.userid
658 l = [HTMLItem(self._client, self.classname, id)
659 for id in self._klass.filter(None, filterspec, sort, group)
660 if check('View', userid, self.classname, itemid=id)]
661 return l
663 def classhelp(self, properties=None, label=''"(list)", width='500',
664 height='400', property='', form='itemSynopsis',
665 pagesize=50, inputtype="checkbox", sort=None, filter=None):
666 """Pop up a javascript window with class help
668 This generates a link to a popup window which displays the
669 properties indicated by "properties" of the class named by
670 "classname". The "properties" should be a comma-separated list
671 (eg. 'id,name,description'). Properties defaults to all the
672 properties of a class (excluding id, creator, created and
673 activity).
675 You may optionally override the label displayed, the width,
676 the height, the number of items per page and the field on which
677 the list is sorted (defaults to username if in the displayed
678 properties).
680 With the "filter" arg it is possible to specify a filter for
681 which items are supposed to be displayed. It has to be of
682 the format "<field>=<values>;<field>=<values>;...".
684 The popup window will be resizable and scrollable.
686 If the "property" arg is given, it's passed through to the
687 javascript help_window function.
689 You can use inputtype="radio" to display a radio box instead
690 of the default checkbox (useful for entering Link-properties)
692 If the "form" arg is given, it's passed through to the
693 javascript help_window function. - it's the name of the form
694 the "property" belongs to.
695 """
696 if properties is None:
697 properties = self._klass.getprops(protected=0).keys()
698 properties.sort()
699 properties = ','.join(properties)
700 if sort is None:
701 if 'username' in properties.split( ',' ):
702 sort = 'username'
703 else:
704 sort = self._klass.orderprop()
705 sort = '&@sort=' + sort
706 if property:
707 property = '&property=%s'%property
708 if form:
709 form = '&form=%s'%form
710 if inputtype:
711 type= '&type=%s'%inputtype
712 if filter:
713 filterprops = filter.split(';')
714 filtervalues = []
715 names = []
716 for x in filterprops:
717 (name, values) = x.split('=')
718 names.append(name)
719 filtervalues.append('&%s=%s' % (name, urllib.quote(values)))
720 filter = '&@filter=%s%s' % (','.join(names), ''.join(filtervalues))
721 else:
722 filter = ''
723 help_url = "%s?@startwith=0&@template=help&"\
724 "properties=%s%s%s%s%s&@pagesize=%s%s" % \
725 (self.classname, properties, property, form, type,
726 sort, pagesize, filter)
727 onclick = "javascript:help_window('%s', '%s', '%s');return false;" % \
728 (help_url, width, height)
729 return '<a class="classhelp" href="%s" onclick="%s">%s</a>' % \
730 (help_url, onclick, self._(label))
732 def submit(self, label=''"Submit New Entry", action="new"):
733 """ Generate a submit button (and action hidden element)
735 Generate nothing if we're not editable.
736 """
737 if not self.is_edit_ok():
738 return ''
740 return self.input(type="hidden", name="@action", value=action) + \
741 '\n' + \
742 self.input(type="submit", name="submit_button", value=self._(label))
744 def history(self):
745 if not self.is_view_ok():
746 return self._('[hidden]')
747 return self._('New node - no history')
749 def renderWith(self, name, **kwargs):
750 """ Render this class with the given template.
751 """
752 # create a new request and override the specified args
753 req = HTMLRequest(self._client)
754 req.classname = self.classname
755 req.update(kwargs)
757 # new template, using the specified classname and request
758 pt = self._client.instance.templates.get(self.classname, name)
760 # use our fabricated request
761 args = {
762 'ok_message': self._client.ok_message,
763 'error_message': self._client.error_message
764 }
765 return pt.render(self._client, self.classname, req, **args)
767 class _HTMLItem(HTMLInputMixin, HTMLPermissions):
768 """ Accesses through an *item*
769 """
770 def __init__(self, client, classname, nodeid, anonymous=0):
771 self._client = client
772 self._db = client.db
773 self._classname = classname
774 self._nodeid = nodeid
775 self._klass = self._db.getclass(classname)
776 self._props = self._klass.getprops()
778 # do we prefix the form items with the item's identification?
779 self._anonymous = anonymous
781 HTMLInputMixin.__init__(self)
783 def is_edit_ok(self):
784 """ Is the user allowed to Edit this item?
785 """
786 return self._db.security.hasPermission('Edit', self._client.userid,
787 self._classname, itemid=self._nodeid)
789 def is_retire_ok(self):
790 """ Is the user allowed to Reture this item?
791 """
792 return self._db.security.hasPermission('Retire', self._client.userid,
793 self._classname, itemid=self._nodeid)
795 def is_view_ok(self):
796 """ Is the user allowed to View this item?
797 """
798 if self._db.security.hasPermission('View', self._client.userid,
799 self._classname, itemid=self._nodeid):
800 return 1
801 return self.is_edit_ok()
803 def is_only_view_ok(self):
804 """ Is the user only allowed to View (ie. not Edit) this item?
805 """
806 return self.is_view_ok() and not self.is_edit_ok()
808 def __repr__(self):
809 return '<HTMLItem(0x%x) %s %s>'%(id(self), self._classname,
810 self._nodeid)
812 def __getitem__(self, item):
813 """ return an HTMLProperty instance
814 this now can handle transitive lookups where item is of the
815 form x.y.z
816 """
817 if item == 'id':
818 return self._nodeid
820 items = item.split('.', 1)
821 has_rest = len(items) > 1
823 # get the property
824 prop = self._props[items[0]]
826 if has_rest and not isinstance(prop, (hyperdb.Link, hyperdb.Multilink)):
827 raise KeyError, item
829 # get the value, handling missing values
830 value = None
831 if int(self._nodeid) > 0:
832 value = self._klass.get(self._nodeid, items[0], None)
833 if value is None:
834 if isinstance(prop, hyperdb.Multilink):
835 value = []
837 # look up the correct HTMLProperty class
838 htmlprop = None
839 for klass, htmlklass in propclasses:
840 if isinstance(prop, klass):
841 htmlprop = htmlklass(self._client, self._classname,
842 self._nodeid, prop, items[0], value, self._anonymous)
843 if htmlprop is not None:
844 if has_rest:
845 if isinstance(htmlprop, MultilinkHTMLProperty):
846 return [h[items[1]] for h in htmlprop]
847 return htmlprop[items[1]]
848 return htmlprop
850 raise KeyError, item
852 def __getattr__(self, attr):
853 """ convenience access to properties """
854 try:
855 return self[attr]
856 except KeyError:
857 raise AttributeError, attr
859 def designator(self):
860 """Return this item's designator (classname + id)."""
861 return '%s%s'%(self._classname, self._nodeid)
863 def is_retired(self):
864 """Is this item retired?"""
865 return self._klass.is_retired(self._nodeid)
867 def submit(self, label=''"Submit Changes", action="edit"):
868 """Generate a submit button.
870 Also sneak in the lastactivity and action hidden elements.
871 """
872 return self.input(type="hidden", name="@lastactivity",
873 value=self.activity.local(0)) + '\n' + \
874 self.input(type="hidden", name="@action", value=action) + '\n' + \
875 self.input(type="submit", name="submit_button", value=self._(label))
877 def journal(self, direction='descending'):
878 """ Return a list of HTMLJournalEntry instances.
879 """
880 # XXX do this
881 return []
883 def history(self, direction='descending', dre=re.compile('^\d+$'),
884 limit=None):
885 if not self.is_view_ok():
886 return self._('[hidden]')
888 # pre-load the history with the current state
889 current = {}
890 for prop_n in self._props.keys():
891 prop = self[prop_n]
892 if not isinstance(prop, HTMLProperty):
893 continue
894 current[prop_n] = prop.plain(escape=1)
895 # make link if hrefable
896 if (self._props.has_key(prop_n) and
897 isinstance(self._props[prop_n], hyperdb.Link)):
898 classname = self._props[prop_n].classname
899 try:
900 template = find_template(self._db.config.TEMPLATES,
901 classname, 'item')
902 if template[1].startswith('_generic'):
903 raise NoTemplate, 'not really...'
904 except NoTemplate:
905 pass
906 else:
907 id = self._klass.get(self._nodeid, prop_n, None)
908 current[prop_n] = '<a href="%s%s">%s</a>'%(
909 classname, id, current[prop_n])
911 # get the journal, sort and reverse
912 history = self._klass.history(self._nodeid)
913 history.sort()
914 history.reverse()
916 # restrict the volume
917 if limit:
918 history = history[:limit]
920 timezone = self._db.getUserTimezone()
921 l = []
922 comments = {}
923 for id, evt_date, user, action, args in history:
924 date_s = str(evt_date.local(timezone)).replace("."," ")
925 arg_s = ''
926 if action == 'link' and type(args) == type(()):
927 if len(args) == 3:
928 linkcl, linkid, key = args
929 arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
930 linkcl, linkid, key)
931 else:
932 arg_s = str(args)
934 elif action == 'unlink' and type(args) == type(()):
935 if len(args) == 3:
936 linkcl, linkid, key = args
937 arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
938 linkcl, linkid, key)
939 else:
940 arg_s = str(args)
942 elif type(args) == type({}):
943 cell = []
944 for k in args.keys():
945 # try to get the relevant property and treat it
946 # specially
947 try:
948 prop = self._props[k]
949 except KeyError:
950 prop = None
951 if prop is None:
952 # property no longer exists
953 comments['no_exist'] = self._(
954 "<em>The indicated property no longer exists</em>")
955 cell.append(self._('<em>%s: %s</em>\n')
956 % (self._(k), str(args[k])))
957 continue
959 if args[k] and (isinstance(prop, hyperdb.Multilink) or
960 isinstance(prop, hyperdb.Link)):
961 # figure what the link class is
962 classname = prop.classname
963 try:
964 linkcl = self._db.getclass(classname)
965 except KeyError:
966 labelprop = None
967 comments[classname] = self._(
968 "The linked class %(classname)s no longer exists"
969 ) % locals()
970 labelprop = linkcl.labelprop(1)
971 try:
972 template = find_template(self._db.config.TEMPLATES,
973 classname, 'item')
974 if template[1].startswith('_generic'):
975 raise NoTemplate, 'not really...'
976 hrefable = 1
977 except NoTemplate:
978 hrefable = 0
980 if isinstance(prop, hyperdb.Multilink) and args[k]:
981 ml = []
982 for linkid in args[k]:
983 if isinstance(linkid, type(())):
984 sublabel = linkid[0] + ' '
985 linkids = linkid[1]
986 else:
987 sublabel = ''
988 linkids = [linkid]
989 subml = []
990 for linkid in linkids:
991 label = classname + linkid
992 # if we have a label property, try to use it
993 # TODO: test for node existence even when
994 # there's no labelprop!
995 try:
996 if labelprop is not None and \
997 labelprop != 'id':
998 label = linkcl.get(linkid, labelprop)
999 label = cgi.escape(label)
1000 except IndexError:
1001 comments['no_link'] = self._(
1002 "<strike>The linked node"
1003 " no longer exists</strike>")
1004 subml.append('<strike>%s</strike>'%label)
1005 else:
1006 if hrefable:
1007 subml.append('<a href="%s%s">%s</a>'%(
1008 classname, linkid, label))
1009 elif label is None:
1010 subml.append('%s%s'%(classname,
1011 linkid))
1012 else:
1013 subml.append(label)
1014 ml.append(sublabel + ', '.join(subml))
1015 cell.append('%s:\n %s'%(self._(k), ', '.join(ml)))
1016 elif isinstance(prop, hyperdb.Link) and args[k]:
1017 label = classname + args[k]
1018 # if we have a label property, try to use it
1019 # TODO: test for node existence even when
1020 # there's no labelprop!
1021 if labelprop is not None and labelprop != 'id':
1022 try:
1023 label = cgi.escape(linkcl.get(args[k],
1024 labelprop))
1025 except IndexError:
1026 comments['no_link'] = self._(
1027 "<strike>The linked node"
1028 " no longer exists</strike>")
1029 cell.append(' <strike>%s</strike>,\n'%label)
1030 # "flag" this is done .... euwww
1031 label = None
1032 if label is not None:
1033 if hrefable:
1034 old = '<a href="%s%s">%s</a>'%(classname,
1035 args[k], label)
1036 else:
1037 old = label;
1038 cell.append('%s: %s' % (self._(k), old))
1039 if current.has_key(k):
1040 cell[-1] += ' -> %s'%current[k]
1041 current[k] = old
1043 elif isinstance(prop, hyperdb.Date) and args[k]:
1044 if args[k] is None:
1045 d = ''
1046 else:
1047 d = date.Date(args[k],
1048 translator=self._client).local(timezone)
1049 cell.append('%s: %s'%(self._(k), str(d)))
1050 if current.has_key(k):
1051 cell[-1] += ' -> %s' % current[k]
1052 current[k] = str(d)
1054 elif isinstance(prop, hyperdb.Interval) and args[k]:
1055 val = str(date.Interval(args[k],
1056 translator=self._client))
1057 cell.append('%s: %s'%(self._(k), val))
1058 if current.has_key(k):
1059 cell[-1] += ' -> %s'%current[k]
1060 current[k] = val
1062 elif isinstance(prop, hyperdb.String) and args[k]:
1063 val = cgi.escape(args[k])
1064 cell.append('%s: %s'%(self._(k), val))
1065 if current.has_key(k):
1066 cell[-1] += ' -> %s'%current[k]
1067 current[k] = val
1069 elif isinstance(prop, hyperdb.Boolean) and args[k] is not None:
1070 val = args[k] and ''"Yes" or ''"No"
1071 cell.append('%s: %s'%(self._(k), val))
1072 if current.has_key(k):
1073 cell[-1] += ' -> %s'%current[k]
1074 current[k] = val
1076 elif not args[k]:
1077 if current.has_key(k):
1078 cell.append('%s: %s'%(self._(k), current[k]))
1079 current[k] = '(no value)'
1080 else:
1081 cell.append(self._('%s: (no value)')%self._(k))
1083 else:
1084 cell.append('%s: %s'%(self._(k), str(args[k])))
1085 if current.has_key(k):
1086 cell[-1] += ' -> %s'%current[k]
1087 current[k] = str(args[k])
1089 arg_s = '<br />'.join(cell)
1090 else:
1091 # unkown event!!
1092 comments['unknown'] = self._(
1093 "<strong><em>This event is not handled"
1094 " by the history display!</em></strong>")
1095 arg_s = '<strong><em>' + str(args) + '</em></strong>'
1096 date_s = date_s.replace(' ', ' ')
1097 # if the user's an itemid, figure the username (older journals
1098 # have the username)
1099 if dre.match(user):
1100 user = self._db.user.get(user, 'username')
1101 l.append('<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>'%(
1102 date_s, user, self._(action), arg_s))
1103 if comments:
1104 l.append(self._(
1105 '<tr><td colspan=4><strong>Note:</strong></td></tr>'))
1106 for entry in comments.values():
1107 l.append('<tr><td colspan=4>%s</td></tr>'%entry)
1109 if direction == 'ascending':
1110 l.reverse()
1112 l[0:0] = ['<table class="history">'
1113 '<tr><th colspan="4" class="header">',
1114 self._('History'),
1115 '</th></tr><tr>',
1116 self._('<th>Date</th>'),
1117 self._('<th>User</th>'),
1118 self._('<th>Action</th>'),
1119 self._('<th>Args</th>'),
1120 '</tr>']
1121 l.append('</table>')
1122 return '\n'.join(l)
1124 def renderQueryForm(self):
1125 """ Render this item, which is a query, as a search form.
1126 """
1127 # create a new request and override the specified args
1128 req = HTMLRequest(self._client)
1129 req.classname = self._klass.get(self._nodeid, 'klass')
1130 name = self._klass.get(self._nodeid, 'name')
1131 req.updateFromURL(self._klass.get(self._nodeid, 'url') +
1132 '&@queryname=%s'%urllib.quote(name))
1134 # new template, using the specified classname and request
1135 pt = self._client.instance.templates.get(req.classname, 'search')
1136 # The context for a search page should be the class, not any
1137 # node.
1138 self._client.nodeid = None
1140 # use our fabricated request
1141 return pt.render(self._client, req.classname, req)
1143 def download_url(self):
1144 """ Assume that this item is a FileClass and that it has a name
1145 and content. Construct a URL for the download of the content.
1146 """
1147 name = self._klass.get(self._nodeid, 'name')
1148 url = '%s%s/%s'%(self._classname, self._nodeid, name)
1149 return urllib.quote(url)
1151 def copy_url(self, exclude=("messages", "files")):
1152 """Construct a URL for creating a copy of this item
1154 "exclude" is an optional list of properties that should
1155 not be copied to the new object. By default, this list
1156 includes "messages" and "files" properties. Note that
1157 "id" property cannot be copied.
1159 """
1160 exclude = ("id", "activity", "actor", "creation", "creator") \
1161 + tuple(exclude)
1162 query = {
1163 "@template": "item",
1164 "@note": self._("Copy of %(class)s %(id)s") % {
1165 "class": self._(self._classname), "id": self._nodeid},
1166 }
1167 for name in self._props.keys():
1168 if name not in exclude:
1169 query[name] = self[name].plain()
1170 return self._classname + "?" + "&".join(
1171 ["%s=%s" % (key, urllib.quote(value))
1172 for key, value in query.items()])
1174 class _HTMLUser(_HTMLItem):
1175 """Add ability to check for permissions on users.
1176 """
1177 _marker = []
1178 def hasPermission(self, permission, classname=_marker,
1179 property=None, itemid=None):
1180 """Determine if the user has the Permission.
1182 The class being tested defaults to the template's class, but may
1183 be overidden for this test by suppling an alternate classname.
1184 """
1185 if classname is self._marker:
1186 classname = self._client.classname
1187 return self._db.security.hasPermission(permission,
1188 self._nodeid, classname, property, itemid)
1190 def hasRole(self, rolename):
1191 """Determine whether the user has the Role."""
1192 roles = self._db.user.get(self._nodeid, 'roles').split(',')
1193 for role in roles:
1194 if role.strip() == rolename: return True
1195 return False
1197 def HTMLItem(client, classname, nodeid, anonymous=0):
1198 if classname == 'user':
1199 return _HTMLUser(client, classname, nodeid, anonymous)
1200 else:
1201 return _HTMLItem(client, classname, nodeid, anonymous)
1203 class HTMLProperty(HTMLInputMixin, HTMLPermissions):
1204 """ String, Number, Date, Interval HTMLProperty
1206 Has useful attributes:
1208 _name the name of the property
1209 _value the value of the property if any
1211 A wrapper object which may be stringified for the plain() behaviour.
1212 """
1213 def __init__(self, client, classname, nodeid, prop, name, value,
1214 anonymous=0):
1215 self._client = client
1216 self._db = client.db
1217 self._ = client._
1218 self._classname = classname
1219 self._nodeid = nodeid
1220 self._prop = prop
1221 self._value = value
1222 self._anonymous = anonymous
1223 self._name = name
1224 if not anonymous:
1225 self._formname = '%s%s@%s'%(classname, nodeid, name)
1226 else:
1227 self._formname = name
1229 # If no value is already present for this property, see if one
1230 # is specified in the current form.
1231 form = self._client.form
1232 if not self._value and form.has_key(self._formname):
1233 if isinstance(prop, hyperdb.Multilink):
1234 value = lookupIds(self._db, prop,
1235 handleListCGIValue(form[self._formname]),
1236 fail_ok=1)
1237 elif isinstance(prop, hyperdb.Link):
1238 value = form.getfirst(self._formname).strip()
1239 if value:
1240 value = lookupIds(self._db, prop, [value],
1241 fail_ok=1)[0]
1242 else:
1243 value = None
1244 else:
1245 value = form.getfirst(self._formname).strip() or None
1246 self._value = value
1248 HTMLInputMixin.__init__(self)
1250 def __repr__(self):
1251 return '<HTMLProperty(0x%x) %s %r %r>'%(id(self), self._formname,
1252 self._prop, self._value)
1253 def __str__(self):
1254 return self.plain()
1255 def __cmp__(self, other):
1256 if isinstance(other, HTMLProperty):
1257 return cmp(self._value, other._value)
1258 return cmp(self._value, other)
1260 def __nonzero__(self):
1261 return not not self._value
1263 def isset(self):
1264 """Is my _value not None?"""
1265 return self._value is not None
1267 def is_edit_ok(self):
1268 """Should the user be allowed to use an edit form field for this
1269 property. Check "Create" for new items, or "Edit" for existing
1270 ones.
1271 """
1272 if self._nodeid:
1273 return self._db.security.hasPermission('Edit', self._client.userid,
1274 self._classname, self._name, self._nodeid)
1275 return self._db.security.hasPermission('Create', self._client.userid,
1276 self._classname, self._name) or \
1277 self._db.security.hasPermission('Register', self._client.userid,
1278 self._classname, self._name)
1280 def is_view_ok(self):
1281 """ Is the user allowed to View the current class?
1282 """
1283 if self._db.security.hasPermission('View', self._client.userid,
1284 self._classname, self._name, self._nodeid):
1285 return 1
1286 return self.is_edit_ok()
1288 class StringHTMLProperty(HTMLProperty):
1289 hyper_re = re.compile(r'''(
1290 (?P<url>
1291 (
1292 (ht|f)tp(s?):// # protocol
1293 ([\w]+(:\w+)?@)? # username/password
1294 ([\w\-]+) # hostname
1295 ((\.[\w-]+)+)? # .domain.etc
1296 | # ... or ...
1297 ([\w]+(:\w+)?@)? # username/password
1298 www\. # "www."
1299 ([\w\-]+\.)+ # hostname
1300 [\w]{2,5} # TLD
1301 )
1302 (:[\d]{1,5})? # port
1303 (/[\w\-$.+!*(),;:@&=?/~\\#%]*)? # path etc.
1304 )|
1305 (?P<email>[-+=%/\w\.]+@[\w\.\-]+)|
1306 (?P<item>(?P<class>[A-Za-z_]+)(\s*)(?P<id>\d+))
1307 )''', re.X | re.I)
1308 protocol_re = re.compile('^(ht|f)tp(s?)://', re.I)
1310 def _hyper_repl_item(self,match,replacement):
1311 item = match.group('item')
1312 cls = match.group('class').lower()
1313 id = match.group('id')
1314 try:
1315 # make sure cls is a valid tracker classname
1316 cl = self._db.getclass(cls)
1317 if not cl.hasnode(id):
1318 return item
1319 return replacement % locals()
1320 except KeyError:
1321 return item
1323 def _hyper_repl(self, match):
1324 if match.group('url'):
1325 u = s = match.group('url')
1326 if not self.protocol_re.search(s):
1327 u = 'http://' + s
1328 # catch an escaped ">" at the end of the URL
1329 if s.endswith('>'):
1330 u = s = s[:-4]
1331 e = '>'
1332 else:
1333 e = ''
1334 return '<a href="%s">%s</a>%s'%(u, s, e)
1335 elif match.group('email'):
1336 s = match.group('email')
1337 return '<a href="mailto:%s">%s</a>'%(s, s)
1338 else:
1339 return self._hyper_repl_item(match,
1340 '<a href="%(cls)s%(id)s">%(item)s</a>')
1342 def _hyper_repl_rst(self, match):
1343 if match.group('url'):
1344 s = match.group('url')
1345 return '`%s <%s>`_'%(s, s)
1346 elif match.group('email'):
1347 s = match.group('email')
1348 return '`%s <mailto:%s>`_'%(s, s)
1349 else:
1350 return self._hyper_repl_item(match,'`%(item)s <%(cls)s%(id)s>`_')
1352 def hyperlinked(self):
1353 """ Render a "hyperlinked" version of the text """
1354 return self.plain(hyperlink=1)
1356 def plain(self, escape=0, hyperlink=0):
1357 """Render a "plain" representation of the property
1359 - "escape" turns on/off HTML quoting
1360 - "hyperlink" turns on/off in-text hyperlinking of URLs, email
1361 addresses and designators
1362 """
1363 if not self.is_view_ok():
1364 return self._('[hidden]')
1366 if self._value is None:
1367 return ''
1368 if escape:
1369 s = cgi.escape(str(self._value))
1370 else:
1371 s = str(self._value)
1372 if hyperlink:
1373 # no, we *must* escape this text
1374 if not escape:
1375 s = cgi.escape(s)
1376 s = self.hyper_re.sub(self._hyper_repl, s)
1377 return s
1379 def wrapped(self, escape=1, hyperlink=1):
1380 """Render a "wrapped" representation of the property.
1382 We wrap long lines at 80 columns on the nearest whitespace. Lines
1383 with no whitespace are not broken to force wrapping.
1385 Note that unlike plain() we default wrapped() to have the escaping
1386 and hyperlinking turned on since that's the most common usage.
1388 - "escape" turns on/off HTML quoting
1389 - "hyperlink" turns on/off in-text hyperlinking of URLs, email
1390 addresses and designators
1391 """
1392 if not self.is_view_ok():
1393 return self._('[hidden]')
1395 if self._value is None:
1396 return ''
1397 s = support.wrap(str(self._value), width=80)
1398 if escape:
1399 s = cgi.escape(s)
1400 if hyperlink:
1401 # no, we *must* escape this text
1402 if not escape:
1403 s = cgi.escape(s)
1404 s = self.hyper_re.sub(self._hyper_repl, s)
1405 return s
1407 def stext(self, escape=0, hyperlink=1):
1408 """ Render the value of the property as StructuredText.
1410 This requires the StructureText module to be installed separately.
1411 """
1412 if not self.is_view_ok():
1413 return self._('[hidden]')
1415 s = self.plain(escape=escape, hyperlink=hyperlink)
1416 if not StructuredText:
1417 return s
1418 return StructuredText(s,level=1,header=0)
1420 def rst(self, hyperlink=1):
1421 """ Render the value of the property as ReStructuredText.
1423 This requires docutils to be installed separately.
1424 """
1425 if not self.is_view_ok():
1426 return self._('[hidden]')
1428 if not ReStructuredText:
1429 return self.plain(escape=0, hyperlink=hyperlink)
1430 s = self.plain(escape=0, hyperlink=0)
1431 if hyperlink:
1432 s = self.hyper_re.sub(self._hyper_repl_rst, s)
1433 return ReStructuredText(s, writer_name="html")["html_body"].encode("utf-8",
1434 "replace")
1436 def field(self, **kwargs):
1437 """ Render the property as a field in HTML.
1439 If not editable, just display the value via plain().
1440 """
1441 if not self.is_edit_ok():
1442 return self.plain(escape=1)
1444 value = self._value
1445 if value is None:
1446 value = ''
1448 kwargs.setdefault("size", 30)
1449 kwargs.update({"name": self._formname, "value": value})
1450 return self.input(**kwargs)
1452 def multiline(self, escape=0, rows=5, cols=40, **kwargs):
1453 """ Render a multiline form edit field for the property.
1455 If not editable, just display the plain() value in a <pre> tag.
1456 """
1457 if not self.is_edit_ok():
1458 return '<pre>%s</pre>'%self.plain()
1460 if self._value is None:
1461 value = ''
1462 else:
1463 value = cgi.escape(str(self._value))
1465 value = '"'.join(value.split('"'))
1466 name = self._formname
1467 passthrough_args = ' '.join(['%s="%s"' % (k, cgi.escape(str(v), True))
1468 for k,v in kwargs.items()])
1469 return ('<textarea %(passthrough_args)s name="%(name)s" id="%(name)s"'
1470 ' rows="%(rows)s" cols="%(cols)s">'
1471 '%(value)s</textarea>') % locals()
1473 def email(self, escape=1):
1474 """ Render the value of the property as an obscured email address
1475 """
1476 if not self.is_view_ok():
1477 return self._('[hidden]')
1479 if self._value is None:
1480 value = ''
1481 else:
1482 value = str(self._value)
1483 split = value.split('@')
1484 if len(split) == 2:
1485 name, domain = split
1486 domain = ' '.join(domain.split('.')[:-1])
1487 name = name.replace('.', ' ')
1488 value = '%s at %s ...'%(name, domain)
1489 else:
1490 value = value.replace('.', ' ')
1491 if escape:
1492 value = cgi.escape(value)
1493 return value
1495 class PasswordHTMLProperty(HTMLProperty):
1496 def plain(self, escape=0):
1497 """ Render a "plain" representation of the property
1498 """
1499 if not self.is_view_ok():
1500 return self._('[hidden]')
1502 if self._value is None:
1503 return ''
1504 return self._('*encrypted*')
1506 def field(self, size=30):
1507 """ Render a form edit field for the property.
1509 If not editable, just display the value via plain().
1510 """
1511 if not self.is_edit_ok():
1512 return self.plain(escape=1)
1514 return self.input(type="password", name=self._formname, size=size)
1516 def confirm(self, size=30):
1517 """ Render a second form edit field for the property, used for
1518 confirmation that the user typed the password correctly. Generates
1519 a field with name "@confirm@name".
1521 If not editable, display nothing.
1522 """
1523 if not self.is_edit_ok():
1524 return ''
1526 return self.input(type="password",
1527 name="@confirm@%s"%self._formname,
1528 id="%s-confirm"%self._formname,
1529 size=size)
1531 class NumberHTMLProperty(HTMLProperty):
1532 def plain(self, escape=0):
1533 """ Render a "plain" representation of the property
1534 """
1535 if not self.is_view_ok():
1536 return self._('[hidden]')
1538 if self._value is None:
1539 return ''
1541 return str(self._value)
1543 def field(self, size=30):
1544 """ Render a form edit field for the property.
1546 If not editable, just display the value via plain().
1547 """
1548 if not self.is_edit_ok():
1549 return self.plain(escape=1)
1551 value = self._value
1552 if value is None:
1553 value = ''
1555 return self.input(name=self._formname, value=value, size=size)
1557 def __int__(self):
1558 """ Return an int of me
1559 """
1560 return int(self._value)
1562 def __float__(self):
1563 """ Return a float of me
1564 """
1565 return float(self._value)
1568 class BooleanHTMLProperty(HTMLProperty):
1569 def plain(self, escape=0):
1570 """ Render a "plain" representation of the property
1571 """
1572 if not self.is_view_ok():
1573 return self._('[hidden]')
1575 if self._value is None:
1576 return ''
1577 return self._value and self._("Yes") or self._("No")
1579 def field(self):
1580 """ Render a form edit field for the property
1582 If not editable, just display the value via plain().
1583 """
1584 if not self.is_edit_ok():
1585 return self.plain(escape=1)
1587 value = self._value
1588 if isinstance(value, str) or isinstance(value, unicode):
1589 value = value.strip().lower() in ('checked', 'yes', 'true',
1590 'on', '1')
1592 checked = value and "checked" or ""
1593 if value:
1594 s = self.input(type="radio", name=self._formname, value="yes",
1595 checked="checked")
1596 s += self._('Yes')
1597 s +=self.input(type="radio", name=self._formname, value="no")
1598 s += self._('No')
1599 else:
1600 s = self.input(type="radio", name=self._formname, value="yes")
1601 s += self._('Yes')
1602 s +=self.input(type="radio", name=self._formname, value="no",
1603 checked="checked")
1604 s += self._('No')
1605 return s
1607 class DateHTMLProperty(HTMLProperty):
1609 _marker = []
1611 def __init__(self, client, classname, nodeid, prop, name, value,
1612 anonymous=0, offset=None):
1613 HTMLProperty.__init__(self, client, classname, nodeid, prop, name,
1614 value, anonymous=anonymous)
1615 if self._value and not (isinstance(self._value, str) or
1616 isinstance(self._value, unicode)):
1617 self._value.setTranslator(self._client.translator)
1618 self._offset = offset
1619 if self._offset is None :
1620 self._offset = self._prop.offset (self._db)
1622 def plain(self, escape=0):
1623 """ Render a "plain" representation of the property
1624 """
1625 if not self.is_view_ok():
1626 return self._('[hidden]')
1628 if self._value is None:
1629 return ''
1630 if self._offset is None:
1631 offset = self._db.getUserTimezone()
1632 else:
1633 offset = self._offset
1634 return str(self._value.local(offset))
1636 def now(self, str_interval=None):
1637 """ Return the current time.
1639 This is useful for defaulting a new value. Returns a
1640 DateHTMLProperty.
1641 """
1642 if not self.is_view_ok():
1643 return self._('[hidden]')
1645 ret = date.Date('.', translator=self._client)
1647 if isinstance(str_interval, basestring):
1648 sign = 1
1649 if str_interval[0] == '-':
1650 sign = -1
1651 str_interval = str_interval[1:]
1652 interval = date.Interval(str_interval, translator=self._client)
1653 if sign > 0:
1654 ret = ret + interval
1655 else:
1656 ret = ret - interval
1658 return DateHTMLProperty(self._client, self._classname, self._nodeid,
1659 self._prop, self._formname, ret)
1661 def field(self, size=30, default=None, format=_marker, popcal=True):
1662 """Render a form edit field for the property
1664 If not editable, just display the value via plain().
1666 If "popcal" then include the Javascript calendar editor.
1667 Default=yes.
1669 The format string is a standard python strftime format string.
1670 """
1671 if not self.is_edit_ok():
1672 if format is self._marker:
1673 return self.plain(escape=1)
1674 else:
1675 return self.pretty(format)
1677 value = self._value
1679 if value is None:
1680 if default is None:
1681 raw_value = None
1682 else:
1683 if isinstance(default, basestring):
1684 raw_value = date.Date(default, translator=self._client)
1685 elif isinstance(default, date.Date):
1686 raw_value = default
1687 elif isinstance(default, DateHTMLProperty):
1688 raw_value = default._value
1689 else:
1690 raise ValueError, self._('default value for '
1691 'DateHTMLProperty must be either DateHTMLProperty '
1692 'or string date representation.')
1693 elif isinstance(value, str) or isinstance(value, unicode):
1694 # most likely erroneous input to be passed back to user
1695 if isinstance(value, unicode): value = value.encode('utf8')
1696 return self.input(name=self._formname, value=value, size=size)
1697 else:
1698 raw_value = value
1700 if raw_value is None:
1701 value = ''
1702 elif isinstance(raw_value, str) or isinstance(raw_value, unicode):
1703 if format is self._marker:
1704 value = raw_value
1705 else:
1706 value = date.Date(raw_value).pretty(format)
1707 else:
1708 if self._offset is None :
1709 offset = self._db.getUserTimezone()
1710 else :
1711 offset = self._offset
1712 value = raw_value.local(offset)
1713 if format is not self._marker:
1714 value = value.pretty(format)
1716 s = self.input(name=self._formname, value=value, size=size)
1717 if popcal:
1718 s += self.popcal()
1719 return s
1721 def reldate(self, pretty=1):
1722 """ Render the interval between the date and now.
1724 If the "pretty" flag is true, then make the display pretty.
1725 """
1726 if not self.is_view_ok():
1727 return self._('[hidden]')
1729 if not self._value:
1730 return ''
1732 # figure the interval
1733 interval = self._value - date.Date('.', translator=self._client)
1734 if pretty:
1735 return interval.pretty()
1736 return str(interval)
1738 def pretty(self, format=_marker):
1739 """ Render the date in a pretty format (eg. month names, spaces).
1741 The format string is a standard python strftime format string.
1742 Note that if the day is zero, and appears at the start of the
1743 string, then it'll be stripped from the output. This is handy
1744 for the situation when a date only specifies a month and a year.
1745 """
1746 if not self.is_view_ok():
1747 return self._('[hidden]')
1749 if self._offset is None:
1750 offset = self._db.getUserTimezone()
1751 else:
1752 offset = self._offset
1754 if not self._value:
1755 return ''
1756 elif format is not self._marker:
1757 return self._value.local(offset).pretty(format)
1758 else:
1759 return self._value.local(offset).pretty()
1761 def local(self, offset):
1762 """ Return the date/time as a local (timezone offset) date/time.
1763 """
1764 if not self.is_view_ok():
1765 return self._('[hidden]')
1767 return DateHTMLProperty(self._client, self._classname, self._nodeid,
1768 self._prop, self._formname, self._value, offset=offset)
1770 def popcal(self, width=300, height=200, label="(cal)",
1771 form="itemSynopsis"):
1772 """Generate a link to a calendar pop-up window.
1774 item: HTMLProperty e.g.: context.deadline
1775 """
1776 if self.isset():
1777 date = "&date=%s"%self._value
1778 else :
1779 date = ""
1780 return ('<a class="classhelp" href="javascript:help_window('
1781 "'%s?@template=calendar&property=%s&form=%s%s', %d, %d)"
1782 '">%s</a>'%(self._classname, self._name, form, date, width,
1783 height, label))
1785 class IntervalHTMLProperty(HTMLProperty):
1786 def __init__(self, client, classname, nodeid, prop, name, value,
1787 anonymous=0):
1788 HTMLProperty.__init__(self, client, classname, nodeid, prop,
1789 name, value, anonymous)
1790 if self._value and not isinstance(self._value, (str, unicode)):
1791 self._value.setTranslator(self._client.translator)
1793 def plain(self, escape=0):
1794 """ Render a "plain" representation of the property
1795 """
1796 if not self.is_view_ok():
1797 return self._('[hidden]')
1799 if self._value is None:
1800 return ''
1801 return str(self._value)
1803 def pretty(self):
1804 """ Render the interval in a pretty format (eg. "yesterday")
1805 """
1806 if not self.is_view_ok():
1807 return self._('[hidden]')
1809 return self._value.pretty()
1811 def field(self, size=30):
1812 """ Render a form edit field for the property
1814 If not editable, just display the value via plain().
1815 """
1816 if not self.is_edit_ok():
1817 return self.plain(escape=1)
1819 value = self._value
1820 if value is None:
1821 value = ''
1823 return self.input(name=self._formname, value=value, size=size)
1825 class LinkHTMLProperty(HTMLProperty):
1826 """ Link HTMLProperty
1827 Include the above as well as being able to access the class
1828 information. Stringifying the object itself results in the value
1829 from the item being displayed. Accessing attributes of this object
1830 result in the appropriate entry from the class being queried for the
1831 property accessed (so item/assignedto/name would look up the user
1832 entry identified by the assignedto property on item, and then the
1833 name property of that user)
1834 """
1835 def __init__(self, *args, **kw):
1836 HTMLProperty.__init__(self, *args, **kw)
1837 # if we're representing a form value, then the -1 from the form really
1838 # should be a None
1839 if str(self._value) == '-1':
1840 self._value = None
1842 def __getattr__(self, attr):
1843 """ return a new HTMLItem """
1844 if not self._value:
1845 # handle a special page templates lookup
1846 if attr == '__render_with_namespace__':
1847 def nothing(*args, **kw):
1848 return ''
1849 return nothing
1850 msg = self._('Attempt to look up %(attr)s on a missing value')
1851 return MissingValue(msg%locals())
1852 i = HTMLItem(self._client, self._prop.classname, self._value)
1853 return getattr(i, attr)
1855 def plain(self, escape=0):
1856 """ Render a "plain" representation of the property
1857 """
1858 if not self.is_view_ok():
1859 return self._('[hidden]')
1861 if self._value is None:
1862 return ''
1863 linkcl = self._db.classes[self._prop.classname]
1864 k = linkcl.labelprop(1)
1865 if num_re.match(self._value):
1866 try:
1867 value = str(linkcl.get(self._value, k))
1868 except IndexError:
1869 value = self._value
1870 else :
1871 value = self._value
1872 if escape:
1873 value = cgi.escape(value)
1874 return value
1876 def field(self, showid=0, size=None):
1877 """ Render a form edit field for the property
1879 If not editable, just display the value via plain().
1880 """
1881 if not self.is_edit_ok():
1882 return self.plain(escape=1)
1884 # edit field
1885 linkcl = self._db.getclass(self._prop.classname)
1886 if self._value is None:
1887 value = ''
1888 else:
1889 k = linkcl.getkey()
1890 if k and num_re.match(self._value):
1891 value = linkcl.get(self._value, k)
1892 else:
1893 value = self._value
1894 return self.input(name=self._formname, value=value, size=size)
1896 def menu(self, size=None, height=None, showid=0, additional=[], value=None,
1897 sort_on=None, **conditions):
1898 """ Render a form select list for this property
1900 "size" is used to limit the length of the list labels
1901 "height" is used to set the <select> tag's "size" attribute
1902 "showid" includes the item ids in the list labels
1903 "value" specifies which item is pre-selected
1904 "additional" lists properties which should be included in the
1905 label
1906 "sort_on" indicates the property to sort the list on as
1907 (direction, property) where direction is '+' or '-'. A
1908 single string with the direction prepended may be used.
1909 For example: ('-', 'order'), '+name'.
1911 The remaining keyword arguments are used as conditions for
1912 filtering the items in the list - they're passed as the
1913 "filterspec" argument to a Class.filter() call.
1915 If not editable, just display the value via plain().
1916 """
1917 if not self.is_edit_ok():
1918 return self.plain(escape=1)
1920 # Since None indicates the default, we need another way to
1921 # indicate "no selection". We use -1 for this purpose, as
1922 # that is the value we use when submitting a form without the
1923 # value set.
1924 if value is None:
1925 value = self._value
1926 elif value == '-1':
1927 value = None
1929 linkcl = self._db.getclass(self._prop.classname)
1930 l = ['<select name="%s">'%self._formname]
1931 k = linkcl.labelprop(1)
1932 s = ''
1933 if value is None:
1934 s = 'selected="selected" '
1935 l.append(self._('<option %svalue="-1">- no selection -</option>')%s)
1937 if sort_on is not None:
1938 if not isinstance(sort_on, tuple):
1939 if sort_on[0] in '+-':
1940 sort_on = (sort_on[0], sort_on[1:])
1941 else:
1942 sort_on = ('+', sort_on)
1943 else:
1944 sort_on = ('+', linkcl.orderprop())
1946 options = [opt
1947 for opt in linkcl.filter(None, conditions, sort_on, (None, None))
1948 if self._db.security.hasPermission("View", self._client.userid,
1949 linkcl.classname, itemid=opt)]
1951 # make sure we list the current value if it's retired
1952 if value and value not in options:
1953 options.insert(0, value)
1955 if additional:
1956 additional_fns = []
1957 props = linkcl.getprops()
1958 for propname in additional:
1959 prop = props[propname]
1960 if isinstance(prop, hyperdb.Link):
1961 cl = self._db.getclass(prop.classname)
1962 labelprop = cl.labelprop()
1963 fn = lambda optionid: cl.get(linkcl.get(optionid,
1964 propname),
1965 labelprop)
1966 else:
1967 fn = lambda optionid: linkcl.get(optionid, propname)
1968 additional_fns.append(fn)
1970 for optionid in options:
1971 # get the option value, and if it's None use an empty string
1972 option = linkcl.get(optionid, k) or ''
1974 # figure if this option is selected
1975 s = ''
1976 if value in [optionid, option]:
1977 s = 'selected="selected" '
1979 # figure the label
1980 if showid:
1981 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1982 elif not option:
1983 lab = '%s%s'%(self._prop.classname, optionid)
1984 else:
1985 lab = option
1987 # truncate if it's too long
1988 if size is not None and len(lab) > size:
1989 lab = lab[:size-3] + '...'
1990 if additional:
1991 m = []
1992 for fn in additional_fns:
1993 m.append(str(fn(optionid)))
1994 lab = lab + ' (%s)'%', '.join(m)
1996 # and generate
1997 lab = cgi.escape(self._(lab))
1998 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
1999 l.append('</select>')
2000 return '\n'.join(l)
2001 # def checklist(self, ...)
2005 class MultilinkHTMLProperty(HTMLProperty):
2006 """ Multilink HTMLProperty
2008 Also be iterable, returning a wrapper object like the Link case for
2009 each entry in the multilink.
2010 """
2011 def __init__(self, *args, **kwargs):
2012 HTMLProperty.__init__(self, *args, **kwargs)
2013 if self._value:
2014 display_value = lookupIds(self._db, self._prop, self._value,
2015 fail_ok=1, do_lookup=False)
2016 sortfun = make_sort_function(self._db, self._prop.classname)
2017 # sorting fails if the value contains
2018 # items not yet stored in the database
2019 # ignore these errors to preserve user input
2020 try:
2021 display_value.sort(sortfun)
2022 except:
2023 pass
2024 self._value = display_value
2026 def __len__(self):
2027 """ length of the multilink """
2028 return len(self._value)
2030 def __getattr__(self, attr):
2031 """ no extended attribute accesses make sense here """
2032 raise AttributeError, attr
2034 def viewableGenerator(self, values):
2035 """Used to iterate over only the View'able items in a class."""
2036 check = self._db.security.hasPermission
2037 userid = self._client.userid
2038 classname = self._prop.classname
2039 for value in values:
2040 if check('View', userid, classname, itemid=value):
2041 yield HTMLItem(self._client, classname, value)
2043 def __iter__(self):
2044 """ iterate and return a new HTMLItem
2045 """
2046 return self.viewableGenerator(self._value)
2048 def reverse(self):
2049 """ return the list in reverse order
2050 """
2051 l = self._value[:]
2052 l.reverse()
2053 return self.viewableGenerator(l)
2055 def sorted(self, property):
2056 """ Return this multilink sorted by the given property """
2057 value = list(self.__iter__())
2058 value.sort(lambda a,b:cmp(a[property], b[property]))
2059 return value
2061 def __contains__(self, value):
2062 """ Support the "in" operator. We have to make sure the passed-in
2063 value is a string first, not a HTMLProperty.
2064 """
2065 return str(value) in self._value
2067 def isset(self):
2068 """Is my _value not []?"""
2069 return self._value != []
2071 def plain(self, escape=0):
2072 """ Render a "plain" representation of the property
2073 """
2074 if not self.is_view_ok():
2075 return self._('[hidden]')
2077 linkcl = self._db.classes[self._prop.classname]
2078 k = linkcl.labelprop(1)
2079 labels = []
2080 for v in self._value:
2081 if num_re.match(v):
2082 try:
2083 label = linkcl.get(v, k)
2084 except IndexError:
2085 label = None
2086 # fall back to designator if label is None
2087 if label is None: label = '%s%s'%(self._prop.classname, k)
2088 else:
2089 label = v
2090 labels.append(label)
2091 value = ', '.join(labels)
2092 if escape:
2093 value = cgi.escape(value)
2094 return value
2096 def field(self, size=30, showid=0):
2097 """ Render a form edit field for the property
2099 If not editable, just display the value via plain().
2100 """
2101 if not self.is_edit_ok():
2102 return self.plain(escape=1)
2104 linkcl = self._db.getclass(self._prop.classname)
2105 value = self._value[:]
2106 # map the id to the label property
2107 if not linkcl.getkey():
2108 showid=1
2109 if not showid:
2110 k = linkcl.labelprop(1)
2111 value = lookupKeys(linkcl, k, value)
2112 value = ','.join(value)
2113 return self.input(name=self._formname, size=size, value=value)
2115 def menu(self, size=None, height=None, showid=0, additional=[],
2116 value=None, sort_on=None, **conditions):
2117 """ Render a form <select> list for this property.
2119 "size" is used to limit the length of the list labels
2120 "height" is used to set the <select> tag's "size" attribute
2121 "showid" includes the item ids in the list labels
2122 "additional" lists properties which should be included in the
2123 label
2124 "value" specifies which item is pre-selected
2125 "sort_on" indicates the property to sort the list on as
2126 (direction, property) where direction is '+' or '-'. A
2127 single string with the direction prepended may be used.
2128 For example: ('-', 'order'), '+name'.
2130 The remaining keyword arguments are used as conditions for
2131 filtering the items in the list - they're passed as the
2132 "filterspec" argument to a Class.filter() call.
2134 If not editable, just display the value via plain().
2135 """
2136 if not self.is_edit_ok():
2137 return self.plain(escape=1)
2139 if value is None:
2140 value = self._value
2142 linkcl = self._db.getclass(self._prop.classname)
2144 if sort_on is not None:
2145 if not isinstance(sort_on, tuple):
2146 if sort_on[0] in '+-':
2147 sort_on = (sort_on[0], sort_on[1:])
2148 else:
2149 sort_on = ('+', sort_on)
2150 else:
2151 sort_on = ('+', linkcl.orderprop())
2153 options = [opt
2154 for opt in linkcl.filter(None, conditions, sort_on)
2155 if self._db.security.hasPermission("View", self._client.userid,
2156 linkcl.classname, itemid=opt)]
2158 # make sure we list the current values if they're retired
2159 for val in value:
2160 if val not in options:
2161 options.insert(0, val)
2163 if not height:
2164 height = len(options)
2165 if value:
2166 # The "no selection" option.
2167 height += 1
2168 height = min(height, 7)
2169 l = ['<select multiple name="%s" size="%s">'%(self._formname, height)]
2170 k = linkcl.labelprop(1)
2172 if value:
2173 l.append('<option value="%s">- no selection -</option>'
2174 % ','.join(['-' + v for v in value]))
2176 if additional:
2177 additional_fns = []
2178 props = linkcl.getprops()
2179 for propname in additional:
2180 prop = props[propname]
2181 if isinstance(prop, hyperdb.Link):
2182 cl = self._db.getclass(prop.classname)
2183 labelprop = cl.labelprop()
2184 fn = lambda optionid: cl.get(linkcl.get(optionid,
2185 propname),
2186 labelprop)
2187 else:
2188 fn = lambda optionid: linkcl.get(optionid, propname)
2189 additional_fns.append(fn)
2191 for optionid in options:
2192 # get the option value, and if it's None use an empty string
2193 option = linkcl.get(optionid, k) or ''
2195 # figure if this option is selected
2196 s = ''
2197 if optionid in value or option in value:
2198 s = 'selected="selected" '
2200 # figure the label
2201 if showid:
2202 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
2203 else:
2204 lab = option
2205 # truncate if it's too long
2206 if size is not None and len(lab) > size:
2207 lab = lab[:size-3] + '...'
2208 if additional:
2209 m = []
2210 for fn in additional_fns:
2211 m.append(str(fn(optionid)))
2212 lab = lab + ' (%s)'%', '.join(m)
2214 # and generate
2215 lab = cgi.escape(self._(lab))
2216 l.append('<option %svalue="%s">%s</option>'%(s, optionid,
2217 lab))
2218 l.append('</select>')
2219 return '\n'.join(l)
2221 # set the propclasses for HTMLItem
2222 propclasses = (
2223 (hyperdb.String, StringHTMLProperty),
2224 (hyperdb.Number, NumberHTMLProperty),
2225 (hyperdb.Boolean, BooleanHTMLProperty),
2226 (hyperdb.Date, DateHTMLProperty),
2227 (hyperdb.Interval, IntervalHTMLProperty),
2228 (hyperdb.Password, PasswordHTMLProperty),
2229 (hyperdb.Link, LinkHTMLProperty),
2230 (hyperdb.Multilink, MultilinkHTMLProperty),
2231 )
2233 def make_sort_function(db, classname, sort_on=None):
2234 """Make a sort function for a given class
2235 """
2236 linkcl = db.getclass(classname)
2237 if sort_on is None:
2238 sort_on = linkcl.orderprop()
2239 def sortfunc(a, b):
2240 return cmp(linkcl.get(a, sort_on), linkcl.get(b, sort_on))
2241 return sortfunc
2243 def handleListCGIValue(value):
2244 """ Value is either a single item or a list of items. Each item has a
2245 .value that we're actually interested in.
2246 """
2247 if isinstance(value, type([])):
2248 return [value.value for value in value]
2249 else:
2250 value = value.value.strip()
2251 if not value:
2252 return []
2253 return [v.strip() for v in value.split(',')]
2255 class HTMLRequest(HTMLInputMixin):
2256 """The *request*, holding the CGI form and environment.
2258 - "form" the CGI form as a cgi.FieldStorage
2259 - "env" the CGI environment variables
2260 - "base" the base URL for this instance
2261 - "user" a HTMLItem instance for this user
2262 - "language" as determined by the browser or config
2263 - "classname" the current classname (possibly None)
2264 - "template" the current template (suffix, also possibly None)
2266 Index args:
2268 - "columns" dictionary of the columns to display in an index page
2269 - "show" a convenience access to columns - request/show/colname will
2270 be true if the columns should be displayed, false otherwise
2271 - "sort" index sort column (direction, column name)
2272 - "group" index grouping property (direction, column name)
2273 - "filter" properties to filter the index on
2274 - "filterspec" values to filter the index on
2275 - "search_text" text to perform a full-text search on for an index
2276 """
2277 def __repr__(self):
2278 return '<HTMLRequest %r>'%self.__dict__
2280 def __init__(self, client):
2281 # _client is needed by HTMLInputMixin
2282 self._client = self.client = client
2284 # easier access vars
2285 self.form = client.form
2286 self.env = client.env
2287 self.base = client.base
2288 self.user = HTMLItem(client, 'user', client.userid)
2289 self.language = client.language
2291 # store the current class name and action
2292 self.classname = client.classname
2293 self.nodeid = client.nodeid
2294 self.template = client.template
2296 # the special char to use for special vars
2297 self.special_char = '@'
2299 HTMLInputMixin.__init__(self)
2301 self._post_init()
2303 def current_url(self):
2304 url = self.base
2305 if self.classname:
2306 url += self.classname
2307 if self.nodeid:
2308 url += self.nodeid
2309 args = {}
2310 if self.template:
2311 args['@template'] = self.template
2312 return self.indexargs_url(url, args)
2314 def _parse_sort(self, var, name):
2315 """ Parse sort/group options. Append to var
2316 """
2317 fields = []
2318 dirs = []
2319 for special in '@:':
2320 idx = 0
2321 key = '%s%s%d'%(special, name, idx)
2322 while key in self.form:
2323 self.special_char = special
2324 fields.append(self.form.getfirst(key))
2325 dirkey = '%s%sdir%d'%(special, name, idx)
2326 if dirkey in self.form:
2327 dirs.append(self.form.getfirst(dirkey))
2328 else:
2329 dirs.append(None)
2330 idx += 1
2331 key = '%s%s%d'%(special, name, idx)
2332 # backward compatible (and query) URL format
2333 key = special + name
2334 dirkey = key + 'dir'
2335 if key in self.form and not fields:
2336 fields = handleListCGIValue(self.form[key])
2337 if dirkey in self.form:
2338 dirs.append(self.form.getfirst(dirkey))
2339 if fields: # only try other special char if nothing found
2340 break
2341 for f, d in map(None, fields, dirs):
2342 if f.startswith('-'):
2343 var.append(('-', f[1:]))
2344 elif d:
2345 var.append(('-', f))
2346 else:
2347 var.append(('+', f))
2349 def _post_init(self):
2350 """ Set attributes based on self.form
2351 """
2352 # extract the index display information from the form
2353 self.columns = []
2354 for name in ':columns @columns'.split():
2355 if self.form.has_key(name):
2356 self.special_char = name[0]
2357 self.columns = handleListCGIValue(self.form[name])
2358 break
2359 self.show = support.TruthDict(self.columns)
2361 # sorting and grouping
2362 self.sort = []
2363 self.group = []
2364 self._parse_sort(self.sort, 'sort')
2365 self._parse_sort(self.group, 'group')
2367 # filtering
2368 self.filter = []
2369 for name in ':filter @filter'.split():
2370 if self.form.has_key(name):
2371 self.special_char = name[0]
2372 self.filter = handleListCGIValue(self.form[name])
2374 self.filterspec = {}
2375 db = self.client.db
2376 if self.classname is not None:
2377 cls = db.getclass (self.classname)
2378 for name in self.filter:
2379 if not self.form.has_key(name):
2380 continue
2381 prop = cls.get_transitive_prop (name)
2382 fv = self.form[name]
2383 if (isinstance(prop, hyperdb.Link) or
2384 isinstance(prop, hyperdb.Multilink)):
2385 self.filterspec[name] = lookupIds(db, prop,
2386 handleListCGIValue(fv))
2387 else:
2388 if isinstance(fv, type([])):
2389 self.filterspec[name] = [v.value for v in fv]
2390 elif name == 'id':
2391 # special case "id" property
2392 self.filterspec[name] = handleListCGIValue(fv)
2393 else:
2394 self.filterspec[name] = fv.value
2396 # full-text search argument
2397 self.search_text = None
2398 for name in ':search_text @search_text'.split():
2399 if self.form.has_key(name):
2400 self.special_char = name[0]
2401 self.search_text = self.form.getfirst(name)
2403 # pagination - size and start index
2404 # figure batch args
2405 self.pagesize = 50
2406 for name in ':pagesize @pagesize'.split():
2407 if self.form.has_key(name):
2408 self.special_char = name[0]
2409 try:
2410 self.pagesize = int(self.form.getfirst(name))
2411 except ValueError:
2412 # not an integer - ignore
2413 pass
2415 self.startwith = 0
2416 for name in ':startwith @startwith'.split():
2417 if self.form.has_key(name):
2418 self.special_char = name[0]
2419 try:
2420 self.startwith = int(self.form.getfirst(name))
2421 except ValueError:
2422 # not an integer - ignore
2423 pass
2425 # dispname
2426 if self.form.has_key('@dispname'):
2427 self.dispname = self.form.getfirst('@dispname')
2428 else:
2429 self.dispname = None
2431 def updateFromURL(self, url):
2432 """ Parse the URL for query args, and update my attributes using the
2433 values.
2434 """
2435 env = {'QUERY_STRING': url}
2436 self.form = cgi.FieldStorage(environ=env)
2438 self._post_init()
2440 def update(self, kwargs):
2441 """ Update my attributes using the keyword args
2442 """
2443 self.__dict__.update(kwargs)
2444 if kwargs.has_key('columns'):
2445 self.show = support.TruthDict(self.columns)
2447 def description(self):
2448 """ Return a description of the request - handle for the page title.
2449 """
2450 s = [self.client.db.config.TRACKER_NAME]
2451 if self.classname:
2452 if self.client.nodeid:
2453 s.append('- %s%s'%(self.classname, self.client.nodeid))
2454 else:
2455 if self.template == 'item':
2456 s.append('- new %s'%self.classname)
2457 elif self.template == 'index':
2458 s.append('- %s index'%self.classname)
2459 else:
2460 s.append('- %s %s'%(self.classname, self.template))
2461 else:
2462 s.append('- home')
2463 return ' '.join(s)
2465 def __str__(self):
2466 d = {}
2467 d.update(self.__dict__)
2468 f = ''
2469 for k in self.form.keys():
2470 f += '\n %r=%r'%(k,handleListCGIValue(self.form[k]))
2471 d['form'] = f
2472 e = ''
2473 for k,v in self.env.items():
2474 e += '\n %r=%r'%(k, v)
2475 d['env'] = e
2476 return """
2477 form: %(form)s
2478 base: %(base)r
2479 classname: %(classname)r
2480 template: %(template)r
2481 columns: %(columns)r
2482 sort: %(sort)r
2483 group: %(group)r
2484 filter: %(filter)r
2485 search_text: %(search_text)r
2486 pagesize: %(pagesize)r
2487 startwith: %(startwith)r
2488 env: %(env)s
2489 """%d
2491 def indexargs_form(self, columns=1, sort=1, group=1, filter=1,
2492 filterspec=1, search_text=1):
2493 """ return the current index args as form elements """
2494 l = []
2495 sc = self.special_char
2496 def add(k, v):
2497 l.append(self.input(type="hidden", name=k, value=v))
2498 if columns and self.columns:
2499 add(sc+'columns', ','.join(self.columns))
2500 if sort:
2501 val = []
2502 for dir, attr in self.sort:
2503 if dir == '-':
2504 val.append('-'+attr)
2505 else:
2506 val.append(attr)
2507 add(sc+'sort', ','.join (val))
2508 if group:
2509 val = []
2510 for dir, attr in self.group:
2511 if dir == '-':
2512 val.append('-'+attr)
2513 else:
2514 val.append(attr)
2515 add(sc+'group', ','.join (val))
2516 if filter and self.filter:
2517 add(sc+'filter', ','.join(self.filter))
2518 if self.classname and filterspec:
2519 cls = self.client.db.getclass(self.classname)
2520 for k,v in self.filterspec.items():
2521 if type(v) == type([]):
2522 if isinstance(cls.get_transitive_prop(k), hyperdb.String):
2523 add(k, ' '.join(v))
2524 else:
2525 add(k, ','.join(v))
2526 else:
2527 add(k, v)
2528 if search_text and self.search_text:
2529 add(sc+'search_text', self.search_text)
2530 add(sc+'pagesize', self.pagesize)
2531 add(sc+'startwith', self.startwith)
2532 return '\n'.join(l)
2534 def indexargs_url(self, url, args):
2535 """ Embed the current index args in a URL
2536 """
2537 q = urllib.quote
2538 sc = self.special_char
2539 l = ['%s=%s'%(k,v) for k,v in args.items()]
2541 # pull out the special values (prefixed by @ or :)
2542 specials = {}
2543 for key in args.keys():
2544 if key[0] in '@:':
2545 specials[key[1:]] = args[key]
2547 # ok, now handle the specials we received in the request
2548 if self.columns and not specials.has_key('columns'):
2549 l.append(sc+'columns=%s'%(','.join(self.columns)))
2550 if self.sort and not specials.has_key('sort'):
2551 val = []
2552 for dir, attr in self.sort:
2553 if dir == '-':
2554 val.append('-'+attr)
2555 else:
2556 val.append(attr)
2557 l.append(sc+'sort=%s'%(','.join(val)))
2558 if self.group and not specials.has_key('group'):
2559 val = []
2560 for dir, attr in self.group:
2561 if dir == '-':
2562 val.append('-'+attr)
2563 else:
2564 val.append(attr)
2565 l.append(sc+'group=%s'%(','.join(val)))
2566 if self.filter and not specials.has_key('filter'):
2567 l.append(sc+'filter=%s'%(','.join(self.filter)))
2568 if self.search_text and not specials.has_key('search_text'):
2569 l.append(sc+'search_text=%s'%q(self.search_text))
2570 if not specials.has_key('pagesize'):
2571 l.append(sc+'pagesize=%s'%self.pagesize)
2572 if not specials.has_key('startwith'):
2573 l.append(sc+'startwith=%s'%self.startwith)
2575 # finally, the remainder of the filter args in the request
2576 if self.classname and self.filterspec:
2577 cls = self.client.db.getclass(self.classname)
2578 for k,v in self.filterspec.items():
2579 if not args.has_key(k):
2580 if type(v) == type([]):
2581 prop = cls.get_transitive_prop(k)
2582 if k != 'id' and isinstance(prop, hyperdb.String):
2583 l.append('%s=%s'%(k, '%20'.join([q(i) for i in v])))
2584 else:
2585 l.append('%s=%s'%(k, ','.join([q(i) for i in v])))
2586 else:
2587 l.append('%s=%s'%(k, q(v)))
2588 return '%s?%s'%(url, '&'.join(l))
2589 indexargs_href = indexargs_url
2591 def base_javascript(self):
2592 return """
2593 <script type="text/javascript">
2594 submitted = false;
2595 function submit_once() {
2596 if (submitted) {
2597 alert("Your request is being processed.\\nPlease be patient.");
2598 event.returnValue = 0; // work-around for IE
2599 return 0;
2600 }
2601 submitted = true;
2602 return 1;
2603 }
2605 function help_window(helpurl, width, height) {
2606 HelpWin = window.open('%s' + helpurl, 'RoundupHelpWindow', 'scrollbars=yes,resizable=yes,toolbar=no,height='+height+',width='+width);
2607 }
2608 </script>
2609 """%self.base
2611 def batch(self):
2612 """ Return a batch object for results from the "current search"
2613 """
2614 filterspec = self.filterspec
2615 sort = self.sort
2616 group = self.group
2618 # get the list of ids we're batching over
2619 klass = self.client.db.getclass(self.classname)
2620 if self.search_text:
2621 matches = self.client.db.indexer.search(
2622 [w.upper().encode("utf-8", "replace") for w in re.findall(
2623 r'(?u)\b\w{2,25}\b',
2624 unicode(self.search_text, "utf-8", "replace")
2625 )], klass)
2626 else:
2627 matches = None
2629 # filter for visibility
2630 check = self._client.db.security.hasPermission
2631 userid = self._client.userid
2632 l = [id for id in klass.filter(matches, filterspec, sort, group)
2633 if check('View', userid, self.classname, itemid=id)]
2635 # return the batch object, using IDs only
2636 return Batch(self.client, l, self.pagesize, self.startwith,
2637 classname=self.classname)
2639 # extend the standard ZTUtils Batch object to remove dependency on
2640 # Acquisition and add a couple of useful methods
2641 class Batch(ZTUtils.Batch):
2642 """ Use me to turn a list of items, or item ids of a given class, into a
2643 series of batches.
2645 ========= ========================================================
2646 Parameter Usage
2647 ========= ========================================================
2648 sequence a list of HTMLItems or item ids
2649 classname if sequence is a list of ids, this is the class of item
2650 size how big to make the sequence.
2651 start where to start (0-indexed) in the sequence.
2652 end where to end (0-indexed) in the sequence.
2653 orphan if the next batch would contain less items than this
2654 value, then it is combined with this batch
2655 overlap the number of items shared between adjacent batches
2656 ========= ========================================================
2658 Attributes: Note that the "start" attribute, unlike the
2659 argument, is a 1-based index (I know, lame). "first" is the
2660 0-based index. "length" is the actual number of elements in
2661 the batch.
2663 "sequence_length" is the length of the original, unbatched, sequence.
2664 """
2665 def __init__(self, client, sequence, size, start, end=0, orphan=0,
2666 overlap=0, classname=None):
2667 self.client = client
2668 self.last_index = self.last_item = None
2669 self.current_item = None
2670 self.classname = classname
2671 self.sequence_length = len(sequence)
2672 ZTUtils.Batch.__init__(self, sequence, size, start, end, orphan,
2673 overlap)
2675 # overwrite so we can late-instantiate the HTMLItem instance
2676 def __getitem__(self, index):
2677 if index < 0:
2678 if index + self.end < self.first: raise IndexError, index
2679 return self._sequence[index + self.end]
2681 if index >= self.length:
2682 raise IndexError, index
2684 # move the last_item along - but only if the fetched index changes
2685 # (for some reason, index 0 is fetched twice)
2686 if index != self.last_index:
2687 self.last_item = self.current_item
2688 self.last_index = index
2690 item = self._sequence[index + self.first]
2691 if self.classname:
2692 # map the item ids to instances
2693 item = HTMLItem(self.client, self.classname, item)
2694 self.current_item = item
2695 return item
2697 def propchanged(self, *properties):
2698 """ Detect if one of the properties marked as being a group
2699 property changed in the last iteration fetch
2700 """
2701 # we poke directly at the _value here since MissingValue can screw
2702 # us up and cause Nones to compare strangely
2703 if self.last_item is None:
2704 return 1
2705 for property in properties:
2706 if property == 'id' or isinstance (self.last_item[property], list):
2707 if (str(self.last_item[property]) !=
2708 str(self.current_item[property])):
2709 return 1
2710 else:
2711 if (self.last_item[property]._value !=
2712 self.current_item[property]._value):
2713 return 1
2714 return 0
2716 # override these 'cos we don't have access to acquisition
2717 def previous(self):
2718 if self.start == 1:
2719 return None
2720 return Batch(self.client, self._sequence, self._size,
2721 self.first - self._size + self.overlap, 0, self.orphan,
2722 self.overlap)
2724 def next(self):
2725 try:
2726 self._sequence[self.end]
2727 except IndexError:
2728 return None
2729 return Batch(self.client, self._sequence, self._size,
2730 self.end - self.overlap, 0, self.orphan, self.overlap)
2732 class TemplatingUtils:
2733 """ Utilities for templating
2734 """
2735 def __init__(self, client):
2736 self.client = client
2737 def Batch(self, sequence, size, start, end=0, orphan=0, overlap=0):
2738 return Batch(self.client, sequence, size, start, end, orphan,
2739 overlap)
2741 def url_quote(self, url):
2742 """URL-quote the supplied text."""
2743 return urllib.quote(url)
2745 def html_quote(self, html):
2746 """HTML-quote the supplied text."""
2747 return cgi.escape(html)
2749 def __getattr__(self, name):
2750 """Try the tracker's templating_utils."""
2751 if not hasattr(self.client.instance, 'templating_utils'):
2752 # backwards-compatibility
2753 raise AttributeError, name
2754 if not self.client.instance.templating_utils.has_key(name):
2755 raise AttributeError, name
2756 return self.client.instance.templating_utils[name]
2758 def html_calendar(self, request):
2759 """Generate a HTML calendar.
2761 `request` the roundup.request object
2762 - @template : name of the template
2763 - form : name of the form to store back the date
2764 - property : name of the property of the form to store
2765 back the date
2766 - date : current date
2767 - display : when browsing, specifies year and month
2769 html will simply be a table.
2770 """
2771 date_str = request.form.getfirst("date", ".")
2772 display = request.form.getfirst("display", date_str)
2773 template = request.form.getfirst("@template", "calendar")
2774 form = request.form.getfirst("form")
2775 property = request.form.getfirst("property")
2776 curr_date = date.Date(date_str) # to highlight
2777 display = date.Date(display) # to show
2778 day = display.day
2780 # for navigation
2781 date_prev_month = display + date.Interval("-1m")
2782 date_next_month = display + date.Interval("+1m")
2783 date_prev_year = display + date.Interval("-1y")
2784 date_next_year = display + date.Interval("+1y")
2786 res = []
2788 base_link = "%s?@template=%s&property=%s&form=%s&date=%s" % \
2789 (request.classname, template, property, form, curr_date)
2791 # navigation
2792 # month
2793 res.append('<table class="calendar"><tr><td>')
2794 res.append(' <table width="100%" class="calendar_nav"><tr>')
2795 link = "&display=%s"%date_prev_month
2796 res.append(' <td><a href="%s&display=%s"><</a></td>'%(base_link,
2797 date_prev_month))
2798 res.append(' <td>%s</td>'%calendar.month_name[display.month])
2799 res.append(' <td><a href="%s&display=%s">></a></td>'%(base_link,
2800 date_next_month))
2801 # spacer
2802 res.append(' <td width="100%"></td>')
2803 # year
2804 res.append(' <td><a href="%s&display=%s"><</a></td>'%(base_link,
2805 date_prev_year))
2806 res.append(' <td>%s</td>'%display.year)
2807 res.append(' <td><a href="%s&display=%s">></a></td>'%(base_link,
2808 date_next_year))
2809 res.append(' </tr></table>')
2810 res.append(' </td></tr>')
2812 # the calendar
2813 res.append(' <tr><td><table class="calendar_display">')
2814 res.append(' <tr class="weekdays">')
2815 for day in calendar.weekheader(3).split():
2816 res.append(' <td>%s</td>'%day)
2817 res.append(' </tr>')
2818 for week in calendar.monthcalendar(display.year, display.month):
2819 res.append(' <tr>')
2820 for day in week:
2821 link = "javascript:form[field].value = '%d-%02d-%02d'; " \
2822 "window.close ();"%(display.year, display.month, day)
2823 if (day == curr_date.day and display.month == curr_date.month
2824 and display.year == curr_date.year):
2825 # highlight
2826 style = "today"
2827 else :
2828 style = ""
2829 if day:
2830 res.append(' <td class="%s"><a href="%s">%s</a></td>'%(
2831 style, link, day))
2832 else :
2833 res.append(' <td></td>')
2834 res.append(' </tr>')
2835 res.append('</table></td></tr></table>')
2836 return "\n".join(res)
2838 class MissingValue:
2839 def __init__(self, description, **kwargs):
2840 self.__description = description
2841 for key, value in kwargs.items():
2842 self.__dict__[key] = value
2844 def __call__(self, *args, **kwargs): return MissingValue(self.__description)
2845 def __getattr__(self, name):
2846 # This allows assignments which assume all intermediate steps are Null
2847 # objects if they don't exist yet.
2848 #
2849 # For example (with just 'client' defined):
2850 #
2851 # client.db.config.TRACKER_WEB = 'BASE/'
2852 self.__dict__[name] = MissingValue(self.__description)
2853 return getattr(self, name)
2855 def __getitem__(self, key): return self
2856 def __nonzero__(self): return 0
2857 def __str__(self): return '[%s]'%self.__description
2858 def __repr__(self): return '<MissingValue 0x%x "%s">'%(id(self),
2859 self.__description)
2860 def gettext(self, str): return str
2861 _ = gettext
2863 # vim: set et sts=4 sw=4 :