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 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 # Add it to the cache. We cannot do this until the template
193 # is fully initialized, as we could otherwise have a race
194 # condition when running with multiple threads:
195 #
196 # 1. Thread A notices the template is not in the cache,
197 # adds it, but has not yet set "mtime".
198 #
199 # 2. Thread B notices the template is in the cache, checks
200 # "mtime" (above) and crashes.
201 #
202 # Since Python dictionary access is atomic, as long as we
203 # insert "pt" only after it is fully initialized, we avoid
204 # this race condition. It's possible that two separate
205 # threads will both do the work of initializing the template,
206 # but the risk of wasted work is offset by avoiding a lock.
207 self.templates[src] = pt
208 return pt
210 def __getitem__(self, name):
211 name, extension = os.path.splitext(name)
212 if extension:
213 extension = extension[1:]
214 try:
215 return self.get(name, extension)
216 except NoTemplate, message:
217 raise KeyError, message
219 def context(client, template=None, classname=None, request=None):
220 """Return the rendering context dictionary
222 The dictionary includes following symbols:
224 *context*
225 this is one of three things:
227 1. None - we're viewing a "home" page
228 2. The current class of item being displayed. This is an HTMLClass
229 instance.
230 3. The current item from the database, if we're viewing a specific
231 item, as an HTMLItem instance.
233 *request*
234 Includes information about the current request, including:
236 - the url
237 - the current index information (``filterspec``, ``filter`` args,
238 ``properties``, etc) parsed out of the form.
239 - methods for easy filterspec link generation
240 - *user*, the current user node as an HTMLItem instance
241 - *form*, the current CGI form information as a FieldStorage
243 *config*
244 The current tracker config.
246 *db*
247 The current database, used to access arbitrary database items.
249 *utils*
250 This is a special class that has its base in the TemplatingUtils
251 class in this file. If the tracker interfaces module defines a
252 TemplatingUtils class then it is mixed in, overriding the methods
253 in the base class.
255 *templates*
256 Access to all the tracker templates by name.
257 Used mainly in *use-macro* commands.
259 *template*
260 Current rendering template.
262 *true*
263 Logical True value.
265 *false*
266 Logical False value.
268 *i18n*
269 Internationalization service, providing string translation
270 methods ``gettext`` and ``ngettext``.
272 """
273 # construct the TemplatingUtils class
274 utils = TemplatingUtils
275 if (hasattr(client.instance, 'interfaces') and
276 hasattr(client.instance.interfaces, 'TemplatingUtils')):
277 class utils(client.instance.interfaces.TemplatingUtils, utils):
278 pass
280 # if template, classname and/or request are not passed explicitely,
281 # compute form client
282 if template is None:
283 template = client.template
284 if classname is None:
285 classname = client.classname
286 if request is None:
287 request = HTMLRequest(client)
289 c = {
290 'context': None,
291 'options': {},
292 'nothing': None,
293 'request': request,
294 'db': HTMLDatabase(client),
295 'config': client.instance.config,
296 'tracker': client.instance,
297 'utils': utils(client),
298 'templates': client.instance.templates,
299 'template': template,
300 'true': 1,
301 'false': 0,
302 'i18n': client.translator
303 }
304 # add in the item if there is one
305 if client.nodeid:
306 c['context'] = HTMLItem(client, classname, client.nodeid,
307 anonymous=1)
308 elif client.db.classes.has_key(classname):
309 c['context'] = HTMLClass(client, classname, anonymous=1)
310 return c
312 class RoundupPageTemplate(PageTemplate.PageTemplate):
313 """A Roundup-specific PageTemplate.
315 Interrogate the client to set up Roundup-specific template variables
316 to be available. See 'context' function for the list of variables.
318 """
320 # 06-jun-2004 [als] i am not sure if this method is used yet
321 def getContext(self, client, classname, request):
322 return context(client, self, classname, request)
324 def render(self, client, classname, request, **options):
325 """Render this Page Template"""
327 if not self._v_cooked:
328 self._cook()
330 __traceback_supplement__ = (PageTemplate.PageTemplateTracebackSupplement, self)
332 if self._v_errors:
333 raise PageTemplate.PTRuntimeError, \
334 'Page Template %s has errors.'%self.id
336 # figure the context
337 c = context(client, self, classname, request)
338 c.update({'options': options})
340 # and go
341 output = StringIO.StringIO()
342 TALInterpreter.TALInterpreter(self._v_program, self.macros,
343 getEngine().getContext(c), output, tal=1, strictinsert=0)()
344 return output.getvalue()
346 def __repr__(self):
347 return '<Roundup PageTemplate %r>'%self.id
349 class HTMLDatabase:
350 """ Return HTMLClasses for valid class fetches
351 """
352 def __init__(self, client):
353 self._client = client
354 self._ = client._
355 self._db = client.db
357 # we want config to be exposed
358 self.config = client.db.config
360 def __getitem__(self, item, desre=re.compile(r'(?P<cl>[a-zA-Z_]+)(?P<id>[-\d]+)')):
361 # check to see if we're actually accessing an item
362 m = desre.match(item)
363 if m:
364 cl = m.group('cl')
365 self._client.db.getclass(cl)
366 return HTMLItem(self._client, cl, m.group('id'))
367 else:
368 self._client.db.getclass(item)
369 return HTMLClass(self._client, item)
371 def __getattr__(self, attr):
372 try:
373 return self[attr]
374 except KeyError:
375 raise AttributeError, attr
377 def classes(self):
378 l = self._client.db.classes.keys()
379 l.sort()
380 m = []
381 for item in l:
382 m.append(HTMLClass(self._client, item))
383 return m
385 num_re = re.compile('^-?\d+$')
387 def lookupIds(db, prop, ids, fail_ok=0, num_re=num_re, do_lookup=True):
388 """ "fail_ok" should be specified if we wish to pass through bad values
389 (most likely form values that we wish to represent back to the user)
390 "do_lookup" is there for preventing lookup by key-value (if we
391 know that the value passed *is* an id)
392 """
393 cl = db.getclass(prop.classname)
394 l = []
395 for entry in ids:
396 if do_lookup:
397 try:
398 item = cl.lookup(entry)
399 except (TypeError, KeyError):
400 pass
401 else:
402 l.append(item)
403 continue
404 # if fail_ok, ignore lookup error
405 # otherwise entry must be existing object id rather than key value
406 if fail_ok or num_re.match(entry):
407 l.append(entry)
408 return l
410 def lookupKeys(linkcl, key, ids, num_re=num_re):
411 """ Look up the "key" values for "ids" list - though some may already
412 be key values, not ids.
413 """
414 l = []
415 for entry in ids:
416 if num_re.match(entry):
417 label = linkcl.get(entry, key)
418 # fall back to designator if label is None
419 if label is None: label = '%s%s'%(linkcl.classname, entry)
420 l.append(label)
421 else:
422 l.append(entry)
423 return l
425 def _set_input_default_args(dic):
426 # 'text' is the default value anyway --
427 # but for CSS usage it should be present
428 dic.setdefault('type', 'text')
429 # useful e.g for HTML LABELs:
430 if not dic.has_key('id'):
431 try:
432 if dic['text'] in ('radio', 'checkbox'):
433 dic['id'] = '%(name)s-%(value)s' % dic
434 else:
435 dic['id'] = dic['name']
436 except KeyError:
437 pass
439 def cgi_escape_attrs(**attrs):
440 return ' '.join(['%s="%s"'%(k,cgi.escape(str(v), True))
441 for k,v in attrs.items()])
443 def input_html4(**attrs):
444 """Generate an 'input' (html4) element with given attributes"""
445 _set_input_default_args(attrs)
446 return '<input %s>'%cgi_escape_attrs(**attrs)
448 def input_xhtml(**attrs):
449 """Generate an 'input' (xhtml) element with given attributes"""
450 _set_input_default_args(attrs)
451 return '<input %s/>'%cgi_escape_attrs(**attrs)
453 class HTMLInputMixin:
454 """ requires a _client property """
455 def __init__(self):
456 html_version = 'html4'
457 if hasattr(self._client.instance.config, 'HTML_VERSION'):
458 html_version = self._client.instance.config.HTML_VERSION
459 if html_version == 'xhtml':
460 self.input = input_xhtml
461 else:
462 self.input = input_html4
463 # self._context is used for translations.
464 # will be initialized by the first call to .gettext()
465 self._context = None
467 def gettext(self, msgid):
468 """Return the localized translation of msgid"""
469 if self._context is None:
470 self._context = context(self._client)
471 return self._client.translator.translate(domain="roundup",
472 msgid=msgid, context=self._context)
474 _ = gettext
476 class HTMLPermissions:
478 def view_check(self):
479 """ Raise the Unauthorised exception if the user's not permitted to
480 view this class.
481 """
482 if not self.is_view_ok():
483 raise Unauthorised("view", self._classname,
484 translator=self._client.translator)
486 def edit_check(self):
487 """ Raise the Unauthorised exception if the user's not permitted to
488 edit items of this class.
489 """
490 if not self.is_edit_ok():
491 raise Unauthorised("edit", self._classname,
492 translator=self._client.translator)
494 def retire_check(self):
495 """ Raise the Unauthorised exception if the user's not permitted to
496 retire items of this class.
497 """
498 if not self.is_retire_ok():
499 raise Unauthorised("retire", self._classname,
500 translator=self._client.translator)
503 class HTMLClass(HTMLInputMixin, HTMLPermissions):
504 """ Accesses through a class (either through *class* or *db.<classname>*)
505 """
506 def __init__(self, client, classname, anonymous=0):
507 self._client = client
508 self._ = client._
509 self._db = client.db
510 self._anonymous = anonymous
512 # we want classname to be exposed, but _classname gives a
513 # consistent API for extending Class/Item
514 self._classname = self.classname = classname
515 self._klass = self._db.getclass(self.classname)
516 self._props = self._klass.getprops()
518 HTMLInputMixin.__init__(self)
520 def is_edit_ok(self):
521 """ Is the user allowed to Create the current class?
522 """
523 return self._db.security.hasPermission('Create', self._client.userid,
524 self._classname)
526 def is_retire_ok(self):
527 """ Is the user allowed to retire items of the current class?
528 """
529 return self._db.security.hasPermission('Retire', self._client.userid,
530 self._classname)
532 def is_view_ok(self):
533 """ Is the user allowed to View the current class?
534 """
535 return self._db.security.hasPermission('View', self._client.userid,
536 self._classname)
538 def is_only_view_ok(self):
539 """ Is the user only allowed to View (ie. not Create) the current class?
540 """
541 return self.is_view_ok() and not self.is_edit_ok()
543 def __repr__(self):
544 return '<HTMLClass(0x%x) %s>'%(id(self), self.classname)
546 def __getitem__(self, item):
547 """ return an HTMLProperty instance
548 """
550 # we don't exist
551 if item == 'id':
552 return None
554 # get the property
555 try:
556 prop = self._props[item]
557 except KeyError:
558 raise KeyError, 'No such property "%s" on %s'%(item, self.classname)
560 # look up the correct HTMLProperty class
561 form = self._client.form
562 for klass, htmlklass in propclasses:
563 if not isinstance(prop, klass):
564 continue
565 if isinstance(prop, hyperdb.Multilink):
566 value = []
567 else:
568 value = None
569 return htmlklass(self._client, self._classname, None, prop, item,
570 value, self._anonymous)
572 # no good
573 raise KeyError, item
575 def __getattr__(self, attr):
576 """ convenience access """
577 try:
578 return self[attr]
579 except KeyError:
580 raise AttributeError, attr
582 def designator(self):
583 """ Return this class' designator (classname) """
584 return self._classname
586 def getItem(self, itemid, num_re=num_re):
587 """ Get an item of this class by its item id.
588 """
589 # make sure we're looking at an itemid
590 if not isinstance(itemid, type(1)) and not num_re.match(itemid):
591 itemid = self._klass.lookup(itemid)
593 return HTMLItem(self._client, self.classname, itemid)
595 def properties(self, sort=1):
596 """ Return HTMLProperty for all of this class' properties.
597 """
598 l = []
599 for name, prop in self._props.items():
600 for klass, htmlklass in propclasses:
601 if isinstance(prop, hyperdb.Multilink):
602 value = []
603 else:
604 value = None
605 if isinstance(prop, klass):
606 l.append(htmlklass(self._client, self._classname, '',
607 prop, name, value, self._anonymous))
608 if sort:
609 l.sort(lambda a,b:cmp(a._name, b._name))
610 return l
612 def list(self, sort_on=None):
613 """ List all items in this class.
614 """
615 # get the list and sort it nicely
616 l = self._klass.list()
617 sortfunc = make_sort_function(self._db, self._classname, sort_on)
618 l.sort(sortfunc)
620 # check perms
621 check = self._client.db.security.hasPermission
622 userid = self._client.userid
624 l = [HTMLItem(self._client, self._classname, id) for id in l
625 if check('View', userid, self._classname, itemid=id)]
627 return l
629 def csv(self):
630 """ Return the items of this class as a chunk of CSV text.
631 """
632 props = self.propnames()
633 s = StringIO.StringIO()
634 writer = csv.writer(s)
635 writer.writerow(props)
636 check = self._client.db.security.hasPermission
637 for nodeid in self._klass.list():
638 l = []
639 for name in props:
640 # check permission to view this property on this item
641 if not check('View', self._client.userid, itemid=nodeid,
642 classname=self._klass.classname, property=name):
643 raise Unauthorised('view', self._klass.classname,
644 translator=self._client.translator)
645 value = self._klass.get(nodeid, name)
646 if value is None:
647 l.append('')
648 elif isinstance(value, type([])):
649 l.append(':'.join(map(str, value)))
650 else:
651 l.append(str(self._klass.get(nodeid, name)))
652 writer.writerow(l)
653 return s.getvalue()
655 def propnames(self):
656 """ Return the list of the names of the properties of this class.
657 """
658 idlessprops = self._klass.getprops(protected=0).keys()
659 idlessprops.sort()
660 return ['id'] + idlessprops
662 def filter(self, request=None, filterspec={}, sort=[], group=[]):
663 """ Return a list of items from this class, filtered and sorted
664 by the current requested filterspec/filter/sort/group args
666 "request" takes precedence over the other three arguments.
667 """
668 if request is not None:
669 filterspec = request.filterspec
670 sort = request.sort
671 group = request.group
673 check = self._db.security.hasPermission
674 userid = self._client.userid
676 l = [HTMLItem(self._client, self.classname, id)
677 for id in self._klass.filter(None, filterspec, sort, group)
678 if check('View', userid, self.classname, itemid=id)]
679 return l
681 def classhelp(self, properties=None, label=''"(list)", width='500',
682 height='400', property='', form='itemSynopsis',
683 pagesize=50, inputtype="checkbox", sort=None, filter=None):
684 """Pop up a javascript window with class help
686 This generates a link to a popup window which displays the
687 properties indicated by "properties" of the class named by
688 "classname". The "properties" should be a comma-separated list
689 (eg. 'id,name,description'). Properties defaults to all the
690 properties of a class (excluding id, creator, created and
691 activity).
693 You may optionally override the label displayed, the width,
694 the height, the number of items per page and the field on which
695 the list is sorted (defaults to username if in the displayed
696 properties).
698 With the "filter" arg it is possible to specify a filter for
699 which items are supposed to be displayed. It has to be of
700 the format "<field>=<values>;<field>=<values>;...".
702 The popup window will be resizable and scrollable.
704 If the "property" arg is given, it's passed through to the
705 javascript help_window function.
707 You can use inputtype="radio" to display a radio box instead
708 of the default checkbox (useful for entering Link-properties)
710 If the "form" arg is given, it's passed through to the
711 javascript help_window function. - it's the name of the form
712 the "property" belongs to.
713 """
714 if properties is None:
715 properties = self._klass.getprops(protected=0).keys()
716 properties.sort()
717 properties = ','.join(properties)
718 if sort is None:
719 if 'username' in properties.split( ',' ):
720 sort = 'username'
721 else:
722 sort = self._klass.orderprop()
723 sort = '&@sort=' + sort
724 if property:
725 property = '&property=%s'%property
726 if form:
727 form = '&form=%s'%form
728 if inputtype:
729 type= '&type=%s'%inputtype
730 if filter:
731 filterprops = filter.split(';')
732 filtervalues = []
733 names = []
734 for x in filterprops:
735 (name, values) = x.split('=')
736 names.append(name)
737 filtervalues.append('&%s=%s' % (name, urllib.quote(values)))
738 filter = '&@filter=%s%s' % (','.join(names), ''.join(filtervalues))
739 else:
740 filter = ''
741 help_url = "%s?@startwith=0&@template=help&"\
742 "properties=%s%s%s%s%s&@pagesize=%s%s" % \
743 (self.classname, properties, property, form, type,
744 sort, pagesize, filter)
745 onclick = "javascript:help_window('%s', '%s', '%s');return false;" % \
746 (help_url, width, height)
747 return '<a class="classhelp" href="%s" onclick="%s">%s</a>' % \
748 (help_url, onclick, self._(label))
750 def submit(self, label=''"Submit New Entry", action="new"):
751 """ Generate a submit button (and action hidden element)
753 Generate nothing if we're not editable.
754 """
755 if not self.is_edit_ok():
756 return ''
758 return self.input(type="hidden", name="@action", value=action) + \
759 '\n' + \
760 self.input(type="submit", name="submit_button", value=self._(label))
762 def history(self):
763 if not self.is_view_ok():
764 return self._('[hidden]')
765 return self._('New node - no history')
767 def renderWith(self, name, **kwargs):
768 """ Render this class with the given template.
769 """
770 # create a new request and override the specified args
771 req = HTMLRequest(self._client)
772 req.classname = self.classname
773 req.update(kwargs)
775 # new template, using the specified classname and request
776 pt = self._client.instance.templates.get(self.classname, name)
778 # use our fabricated request
779 args = {
780 'ok_message': self._client.ok_message,
781 'error_message': self._client.error_message
782 }
783 return pt.render(self._client, self.classname, req, **args)
785 class _HTMLItem(HTMLInputMixin, HTMLPermissions):
786 """ Accesses through an *item*
787 """
788 def __init__(self, client, classname, nodeid, anonymous=0):
789 self._client = client
790 self._db = client.db
791 self._classname = classname
792 self._nodeid = nodeid
793 self._klass = self._db.getclass(classname)
794 self._props = self._klass.getprops()
796 # do we prefix the form items with the item's identification?
797 self._anonymous = anonymous
799 HTMLInputMixin.__init__(self)
801 def is_edit_ok(self):
802 """ Is the user allowed to Edit this item?
803 """
804 return self._db.security.hasPermission('Edit', self._client.userid,
805 self._classname, itemid=self._nodeid)
807 def is_retire_ok(self):
808 """ Is the user allowed to Reture this item?
809 """
810 return self._db.security.hasPermission('Retire', self._client.userid,
811 self._classname, itemid=self._nodeid)
813 def is_view_ok(self):
814 """ Is the user allowed to View this item?
815 """
816 if self._db.security.hasPermission('View', self._client.userid,
817 self._classname, itemid=self._nodeid):
818 return 1
819 return self.is_edit_ok()
821 def is_only_view_ok(self):
822 """ Is the user only allowed to View (ie. not Edit) this item?
823 """
824 return self.is_view_ok() and not self.is_edit_ok()
826 def __repr__(self):
827 return '<HTMLItem(0x%x) %s %s>'%(id(self), self._classname,
828 self._nodeid)
830 def __getitem__(self, item):
831 """ return an HTMLProperty instance
832 this now can handle transitive lookups where item is of the
833 form x.y.z
834 """
835 if item == 'id':
836 return self._nodeid
838 items = item.split('.', 1)
839 has_rest = len(items) > 1
841 # get the property
842 prop = self._props[items[0]]
844 if has_rest and not isinstance(prop, (hyperdb.Link, hyperdb.Multilink)):
845 raise KeyError, item
847 # get the value, handling missing values
848 value = None
849 if int(self._nodeid) > 0:
850 value = self._klass.get(self._nodeid, items[0], None)
851 if value is None:
852 if isinstance(prop, hyperdb.Multilink):
853 value = []
855 # look up the correct HTMLProperty class
856 htmlprop = None
857 for klass, htmlklass in propclasses:
858 if isinstance(prop, klass):
859 htmlprop = htmlklass(self._client, self._classname,
860 self._nodeid, prop, items[0], value, self._anonymous)
861 if htmlprop is not None:
862 if has_rest:
863 if isinstance(htmlprop, MultilinkHTMLProperty):
864 return [h[items[1]] for h in htmlprop]
865 return htmlprop[items[1]]
866 return htmlprop
868 raise KeyError, item
870 def __getattr__(self, attr):
871 """ convenience access to properties """
872 try:
873 return self[attr]
874 except KeyError:
875 raise AttributeError, attr
877 def designator(self):
878 """Return this item's designator (classname + id)."""
879 return '%s%s'%(self._classname, self._nodeid)
881 def is_retired(self):
882 """Is this item retired?"""
883 return self._klass.is_retired(self._nodeid)
885 def submit(self, label=''"Submit Changes", action="edit"):
886 """Generate a submit button.
888 Also sneak in the lastactivity and action hidden elements.
889 """
890 return self.input(type="hidden", name="@lastactivity",
891 value=self.activity.local(0)) + '\n' + \
892 self.input(type="hidden", name="@action", value=action) + '\n' + \
893 self.input(type="submit", name="submit_button", value=self._(label))
895 def journal(self, direction='descending'):
896 """ Return a list of HTMLJournalEntry instances.
897 """
898 # XXX do this
899 return []
901 def history(self, direction='descending', dre=re.compile('^\d+$'),
902 limit=None):
903 if not self.is_view_ok():
904 return self._('[hidden]')
906 # pre-load the history with the current state
907 current = {}
908 for prop_n in self._props.keys():
909 prop = self[prop_n]
910 if not isinstance(prop, HTMLProperty):
911 continue
912 current[prop_n] = prop.plain(escape=1)
913 # make link if hrefable
914 if (self._props.has_key(prop_n) and
915 isinstance(self._props[prop_n], hyperdb.Link)):
916 classname = self._props[prop_n].classname
917 try:
918 template = find_template(self._db.config.TEMPLATES,
919 classname, 'item')
920 if template[1].startswith('_generic'):
921 raise NoTemplate, 'not really...'
922 except NoTemplate:
923 pass
924 else:
925 id = self._klass.get(self._nodeid, prop_n, None)
926 current[prop_n] = '<a href="%s%s">%s</a>'%(
927 classname, id, current[prop_n])
929 # get the journal, sort and reverse
930 history = self._klass.history(self._nodeid)
931 history.sort()
932 history.reverse()
934 # restrict the volume
935 if limit:
936 history = history[:limit]
938 timezone = self._db.getUserTimezone()
939 l = []
940 comments = {}
941 for id, evt_date, user, action, args in history:
942 date_s = str(evt_date.local(timezone)).replace("."," ")
943 arg_s = ''
944 if action == 'link' and type(args) == type(()):
945 if len(args) == 3:
946 linkcl, linkid, key = args
947 arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
948 linkcl, linkid, key)
949 else:
950 arg_s = str(args)
952 elif action == 'unlink' and type(args) == type(()):
953 if len(args) == 3:
954 linkcl, linkid, key = args
955 arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
956 linkcl, linkid, key)
957 else:
958 arg_s = str(args)
960 elif type(args) == type({}):
961 cell = []
962 for k in args.keys():
963 # try to get the relevant property and treat it
964 # specially
965 try:
966 prop = self._props[k]
967 except KeyError:
968 prop = None
969 if prop is None:
970 # property no longer exists
971 comments['no_exist'] = self._(
972 "<em>The indicated property no longer exists</em>")
973 cell.append(self._('<em>%s: %s</em>\n')
974 % (self._(k), str(args[k])))
975 continue
977 if args[k] and (isinstance(prop, hyperdb.Multilink) or
978 isinstance(prop, hyperdb.Link)):
979 # figure what the link class is
980 classname = prop.classname
981 try:
982 linkcl = self._db.getclass(classname)
983 except KeyError:
984 labelprop = None
985 comments[classname] = self._(
986 "The linked class %(classname)s no longer exists"
987 ) % locals()
988 labelprop = linkcl.labelprop(1)
989 try:
990 template = find_template(self._db.config.TEMPLATES,
991 classname, 'item')
992 if template[1].startswith('_generic'):
993 raise NoTemplate, 'not really...'
994 hrefable = 1
995 except NoTemplate:
996 hrefable = 0
998 if isinstance(prop, hyperdb.Multilink) and args[k]:
999 ml = []
1000 for linkid in args[k]:
1001 if isinstance(linkid, type(())):
1002 sublabel = linkid[0] + ' '
1003 linkids = linkid[1]
1004 else:
1005 sublabel = ''
1006 linkids = [linkid]
1007 subml = []
1008 for linkid in linkids:
1009 label = classname + linkid
1010 # if we have a label property, try to use it
1011 # TODO: test for node existence even when
1012 # there's no labelprop!
1013 try:
1014 if labelprop is not None and \
1015 labelprop != 'id':
1016 label = linkcl.get(linkid, labelprop)
1017 label = cgi.escape(label)
1018 except IndexError:
1019 comments['no_link'] = self._(
1020 "<strike>The linked node"
1021 " no longer exists</strike>")
1022 subml.append('<strike>%s</strike>'%label)
1023 else:
1024 if hrefable:
1025 subml.append('<a href="%s%s">%s</a>'%(
1026 classname, linkid, label))
1027 elif label is None:
1028 subml.append('%s%s'%(classname,
1029 linkid))
1030 else:
1031 subml.append(label)
1032 ml.append(sublabel + ', '.join(subml))
1033 cell.append('%s:\n %s'%(self._(k), ', '.join(ml)))
1034 elif isinstance(prop, hyperdb.Link) and args[k]:
1035 label = classname + args[k]
1036 # if we have a label property, try to use it
1037 # TODO: test for node existence even when
1038 # there's no labelprop!
1039 if labelprop is not None and labelprop != 'id':
1040 try:
1041 label = cgi.escape(linkcl.get(args[k],
1042 labelprop))
1043 except IndexError:
1044 comments['no_link'] = self._(
1045 "<strike>The linked node"
1046 " no longer exists</strike>")
1047 cell.append(' <strike>%s</strike>,\n'%label)
1048 # "flag" this is done .... euwww
1049 label = None
1050 if label is not None:
1051 if hrefable:
1052 old = '<a href="%s%s">%s</a>'%(classname,
1053 args[k], label)
1054 else:
1055 old = label;
1056 cell.append('%s: %s' % (self._(k), old))
1057 if current.has_key(k):
1058 cell[-1] += ' -> %s'%current[k]
1059 current[k] = old
1061 elif isinstance(prop, hyperdb.Date) and args[k]:
1062 if args[k] is None:
1063 d = ''
1064 else:
1065 d = date.Date(args[k],
1066 translator=self._client).local(timezone)
1067 cell.append('%s: %s'%(self._(k), str(d)))
1068 if current.has_key(k):
1069 cell[-1] += ' -> %s' % current[k]
1070 current[k] = str(d)
1072 elif isinstance(prop, hyperdb.Interval) and args[k]:
1073 val = str(date.Interval(args[k],
1074 translator=self._client))
1075 cell.append('%s: %s'%(self._(k), val))
1076 if current.has_key(k):
1077 cell[-1] += ' -> %s'%current[k]
1078 current[k] = val
1080 elif isinstance(prop, hyperdb.String) and args[k]:
1081 val = cgi.escape(args[k])
1082 cell.append('%s: %s'%(self._(k), val))
1083 if current.has_key(k):
1084 cell[-1] += ' -> %s'%current[k]
1085 current[k] = val
1087 elif isinstance(prop, hyperdb.Boolean) and args[k] is not None:
1088 val = args[k] and ''"Yes" or ''"No"
1089 cell.append('%s: %s'%(self._(k), val))
1090 if current.has_key(k):
1091 cell[-1] += ' -> %s'%current[k]
1092 current[k] = val
1094 elif not args[k]:
1095 if current.has_key(k):
1096 cell.append('%s: %s'%(self._(k), current[k]))
1097 current[k] = '(no value)'
1098 else:
1099 cell.append(self._('%s: (no value)')%self._(k))
1101 else:
1102 cell.append('%s: %s'%(self._(k), str(args[k])))
1103 if current.has_key(k):
1104 cell[-1] += ' -> %s'%current[k]
1105 current[k] = str(args[k])
1107 arg_s = '<br />'.join(cell)
1108 else:
1109 # unkown event!!
1110 comments['unknown'] = self._(
1111 "<strong><em>This event is not handled"
1112 " by the history display!</em></strong>")
1113 arg_s = '<strong><em>' + str(args) + '</em></strong>'
1114 date_s = date_s.replace(' ', ' ')
1115 # if the user's an itemid, figure the username (older journals
1116 # have the username)
1117 if dre.match(user):
1118 user = self._db.user.get(user, 'username')
1119 l.append('<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>'%(
1120 date_s, user, self._(action), arg_s))
1121 if comments:
1122 l.append(self._(
1123 '<tr><td colspan=4><strong>Note:</strong></td></tr>'))
1124 for entry in comments.values():
1125 l.append('<tr><td colspan=4>%s</td></tr>'%entry)
1127 if direction == 'ascending':
1128 l.reverse()
1130 l[0:0] = ['<table class="history">'
1131 '<tr><th colspan="4" class="header">',
1132 self._('History'),
1133 '</th></tr><tr>',
1134 self._('<th>Date</th>'),
1135 self._('<th>User</th>'),
1136 self._('<th>Action</th>'),
1137 self._('<th>Args</th>'),
1138 '</tr>']
1139 l.append('</table>')
1140 return '\n'.join(l)
1142 def renderQueryForm(self):
1143 """ Render this item, which is a query, as a search form.
1144 """
1145 # create a new request and override the specified args
1146 req = HTMLRequest(self._client)
1147 req.classname = self._klass.get(self._nodeid, 'klass')
1148 name = self._klass.get(self._nodeid, 'name')
1149 req.updateFromURL(self._klass.get(self._nodeid, 'url') +
1150 '&@queryname=%s'%urllib.quote(name))
1152 # new template, using the specified classname and request
1153 pt = self._client.instance.templates.get(req.classname, 'search')
1154 # The context for a search page should be the class, not any
1155 # node.
1156 self._client.nodeid = None
1158 # use our fabricated request
1159 return pt.render(self._client, req.classname, req)
1161 def download_url(self):
1162 """ Assume that this item is a FileClass and that it has a name
1163 and content. Construct a URL for the download of the content.
1164 """
1165 name = self._klass.get(self._nodeid, 'name')
1166 url = '%s%s/%s'%(self._classname, self._nodeid, name)
1167 return urllib.quote(url)
1169 def copy_url(self, exclude=("messages", "files")):
1170 """Construct a URL for creating a copy of this item
1172 "exclude" is an optional list of properties that should
1173 not be copied to the new object. By default, this list
1174 includes "messages" and "files" properties. Note that
1175 "id" property cannot be copied.
1177 """
1178 exclude = ("id", "activity", "actor", "creation", "creator") \
1179 + tuple(exclude)
1180 query = {
1181 "@template": "item",
1182 "@note": self._("Copy of %(class)s %(id)s") % {
1183 "class": self._(self._classname), "id": self._nodeid},
1184 }
1185 for name in self._props.keys():
1186 if name not in exclude:
1187 query[name] = self[name].plain()
1188 return self._classname + "?" + "&".join(
1189 ["%s=%s" % (key, urllib.quote(value))
1190 for key, value in query.items()])
1192 class _HTMLUser(_HTMLItem):
1193 """Add ability to check for permissions on users.
1194 """
1195 _marker = []
1196 def hasPermission(self, permission, classname=_marker,
1197 property=None, itemid=None):
1198 """Determine if the user has the Permission.
1200 The class being tested defaults to the template's class, but may
1201 be overidden for this test by suppling an alternate classname.
1202 """
1203 if classname is self._marker:
1204 classname = self._client.classname
1205 return self._db.security.hasPermission(permission,
1206 self._nodeid, classname, property, itemid)
1208 def hasRole(self, rolename):
1209 """Determine whether the user has the Role."""
1210 roles = self._db.user.get(self._nodeid, 'roles').split(',')
1211 for role in roles:
1212 if role.strip() == rolename: return True
1213 return False
1215 def HTMLItem(client, classname, nodeid, anonymous=0):
1216 if classname == 'user':
1217 return _HTMLUser(client, classname, nodeid, anonymous)
1218 else:
1219 return _HTMLItem(client, classname, nodeid, anonymous)
1221 class HTMLProperty(HTMLInputMixin, HTMLPermissions):
1222 """ String, Number, Date, Interval HTMLProperty
1224 Has useful attributes:
1226 _name the name of the property
1227 _value the value of the property if any
1229 A wrapper object which may be stringified for the plain() behaviour.
1230 """
1231 def __init__(self, client, classname, nodeid, prop, name, value,
1232 anonymous=0):
1233 self._client = client
1234 self._db = client.db
1235 self._ = client._
1236 self._classname = classname
1237 self._nodeid = nodeid
1238 self._prop = prop
1239 self._value = value
1240 self._anonymous = anonymous
1241 self._name = name
1242 if not anonymous:
1243 if nodeid:
1244 self._formname = '%s%s@%s'%(classname, nodeid, name)
1245 else:
1246 # This case occurs when creating a property for a
1247 # non-anonymous class.
1248 self._formname = '%s@%s'%(classname, name)
1249 else:
1250 self._formname = name
1252 # If no value is already present for this property, see if one
1253 # is specified in the current form.
1254 form = self._client.form
1255 if not self._value and form.has_key(self._formname):
1256 if isinstance(prop, hyperdb.Multilink):
1257 value = lookupIds(self._db, prop,
1258 handleListCGIValue(form[self._formname]),
1259 fail_ok=1)
1260 elif isinstance(prop, hyperdb.Link):
1261 value = form.getfirst(self._formname).strip()
1262 if value:
1263 value = lookupIds(self._db, prop, [value],
1264 fail_ok=1)[0]
1265 else:
1266 value = None
1267 else:
1268 value = form.getfirst(self._formname).strip() or None
1269 self._value = value
1271 HTMLInputMixin.__init__(self)
1273 def __repr__(self):
1274 return '<HTMLProperty(0x%x) %s %r %r>'%(id(self), self._formname,
1275 self._prop, self._value)
1276 def __str__(self):
1277 return self.plain()
1278 def __cmp__(self, other):
1279 if isinstance(other, HTMLProperty):
1280 return cmp(self._value, other._value)
1281 return cmp(self._value, other)
1283 def __nonzero__(self):
1284 return not not self._value
1286 def isset(self):
1287 """Is my _value not None?"""
1288 return self._value is not None
1290 def is_edit_ok(self):
1291 """Should the user be allowed to use an edit form field for this
1292 property. Check "Create" for new items, or "Edit" for existing
1293 ones.
1294 """
1295 if self._nodeid:
1296 return self._db.security.hasPermission('Edit', self._client.userid,
1297 self._classname, self._name, self._nodeid)
1298 return self._db.security.hasPermission('Create', self._client.userid,
1299 self._classname, self._name) or \
1300 self._db.security.hasPermission('Register', self._client.userid,
1301 self._classname, self._name)
1303 def is_view_ok(self):
1304 """ Is the user allowed to View the current class?
1305 """
1306 if self._db.security.hasPermission('View', self._client.userid,
1307 self._classname, self._name, self._nodeid):
1308 return 1
1309 return self.is_edit_ok()
1311 class StringHTMLProperty(HTMLProperty):
1312 hyper_re = re.compile(r'''(
1313 (?P<url>
1314 (
1315 (ht|f)tp(s?):// # protocol
1316 ([\w]+(:\w+)?@)? # username/password
1317 ([\w\-]+) # hostname
1318 ((\.[\w-]+)+)? # .domain.etc
1319 | # ... or ...
1320 ([\w]+(:\w+)?@)? # username/password
1321 www\. # "www."
1322 ([\w\-]+\.)+ # hostname
1323 [\w]{2,5} # TLD
1324 )
1325 (:[\d]{1,5})? # port
1326 (/[\w\-$.+!*(),;:@&=?/~\\#%]*)? # path etc.
1327 )|
1328 (?P<email>[-+=%/\w\.]+@[\w\.\-]+)|
1329 (?P<item>(?P<class>[A-Za-z_]+)(\s*)(?P<id>\d+))
1330 )''', re.X | re.I)
1331 protocol_re = re.compile('^(ht|f)tp(s?)://', re.I)
1333 def _hyper_repl_item(self,match,replacement):
1334 item = match.group('item')
1335 cls = match.group('class').lower()
1336 id = match.group('id')
1337 try:
1338 # make sure cls is a valid tracker classname
1339 cl = self._db.getclass(cls)
1340 if not cl.hasnode(id):
1341 return item
1342 return replacement % locals()
1343 except KeyError:
1344 return item
1346 def _hyper_repl(self, match):
1347 if match.group('url'):
1348 u = s = match.group('url')
1349 if not self.protocol_re.search(s):
1350 u = 'http://' + s
1351 # catch an escaped ">" at the end of the URL
1352 if s.endswith('>'):
1353 u = s = s[:-4]
1354 e = '>'
1355 else:
1356 e = ''
1357 return '<a href="%s">%s</a>%s'%(u, s, e)
1358 elif match.group('email'):
1359 s = match.group('email')
1360 return '<a href="mailto:%s">%s</a>'%(s, s)
1361 else:
1362 return self._hyper_repl_item(match,
1363 '<a href="%(cls)s%(id)s">%(item)s</a>')
1365 def _hyper_repl_rst(self, match):
1366 if match.group('url'):
1367 s = match.group('url')
1368 return '`%s <%s>`_'%(s, s)
1369 elif match.group('email'):
1370 s = match.group('email')
1371 return '`%s <mailto:%s>`_'%(s, s)
1372 else:
1373 return self._hyper_repl_item(match,'`%(item)s <%(cls)s%(id)s>`_')
1375 def hyperlinked(self):
1376 """ Render a "hyperlinked" version of the text """
1377 return self.plain(hyperlink=1)
1379 def plain(self, escape=0, hyperlink=0):
1380 """Render a "plain" representation of the property
1382 - "escape" turns on/off HTML quoting
1383 - "hyperlink" turns on/off in-text hyperlinking of URLs, email
1384 addresses and designators
1385 """
1386 if not self.is_view_ok():
1387 return self._('[hidden]')
1389 if self._value is None:
1390 return ''
1391 if escape:
1392 s = cgi.escape(str(self._value))
1393 else:
1394 s = str(self._value)
1395 if hyperlink:
1396 # no, we *must* escape this text
1397 if not escape:
1398 s = cgi.escape(s)
1399 s = self.hyper_re.sub(self._hyper_repl, s)
1400 return s
1402 def wrapped(self, escape=1, hyperlink=1):
1403 """Render a "wrapped" representation of the property.
1405 We wrap long lines at 80 columns on the nearest whitespace. Lines
1406 with no whitespace are not broken to force wrapping.
1408 Note that unlike plain() we default wrapped() to have the escaping
1409 and hyperlinking turned on since that's the most common usage.
1411 - "escape" turns on/off HTML quoting
1412 - "hyperlink" turns on/off in-text hyperlinking of URLs, email
1413 addresses and designators
1414 """
1415 if not self.is_view_ok():
1416 return self._('[hidden]')
1418 if self._value is None:
1419 return ''
1420 s = support.wrap(str(self._value), width=80)
1421 if escape:
1422 s = cgi.escape(s)
1423 if hyperlink:
1424 # no, we *must* escape this text
1425 if not escape:
1426 s = cgi.escape(s)
1427 s = self.hyper_re.sub(self._hyper_repl, s)
1428 return s
1430 def stext(self, escape=0, hyperlink=1):
1431 """ Render the value of the property as StructuredText.
1433 This requires the StructureText module to be installed separately.
1434 """
1435 if not self.is_view_ok():
1436 return self._('[hidden]')
1438 s = self.plain(escape=escape, hyperlink=hyperlink)
1439 if not StructuredText:
1440 return s
1441 return StructuredText(s,level=1,header=0)
1443 def rst(self, hyperlink=1):
1444 """ Render the value of the property as ReStructuredText.
1446 This requires docutils to be installed separately.
1447 """
1448 if not self.is_view_ok():
1449 return self._('[hidden]')
1451 if not ReStructuredText:
1452 return self.plain(escape=0, hyperlink=hyperlink)
1453 s = self.plain(escape=0, hyperlink=0)
1454 if hyperlink:
1455 s = self.hyper_re.sub(self._hyper_repl_rst, s)
1456 return ReStructuredText(s, writer_name="html")["html_body"].encode("utf-8",
1457 "replace")
1459 def field(self, **kwargs):
1460 """ Render the property as a field in HTML.
1462 If not editable, just display the value via plain().
1463 """
1464 if not self.is_edit_ok():
1465 return self.plain(escape=1)
1467 value = self._value
1468 if value is None:
1469 value = ''
1471 kwargs.setdefault("size", 30)
1472 kwargs.update({"name": self._formname, "value": value})
1473 return self.input(**kwargs)
1475 def multiline(self, escape=0, rows=5, cols=40, **kwargs):
1476 """ Render a multiline form edit field for the property.
1478 If not editable, just display the plain() value in a <pre> tag.
1479 """
1480 if not self.is_edit_ok():
1481 return '<pre>%s</pre>'%self.plain()
1483 if self._value is None:
1484 value = ''
1485 else:
1486 value = cgi.escape(str(self._value))
1488 value = '"'.join(value.split('"'))
1489 name = self._formname
1490 passthrough_args = cgi_escape_attrs(**kwargs)
1491 return ('<textarea %(passthrough_args)s name="%(name)s" id="%(name)s"'
1492 ' rows="%(rows)s" cols="%(cols)s">'
1493 '%(value)s</textarea>') % locals()
1495 def email(self, escape=1):
1496 """ Render the value of the property as an obscured email address
1497 """
1498 if not self.is_view_ok():
1499 return self._('[hidden]')
1501 if self._value is None:
1502 value = ''
1503 else:
1504 value = str(self._value)
1505 split = value.split('@')
1506 if len(split) == 2:
1507 name, domain = split
1508 domain = ' '.join(domain.split('.')[:-1])
1509 name = name.replace('.', ' ')
1510 value = '%s at %s ...'%(name, domain)
1511 else:
1512 value = value.replace('.', ' ')
1513 if escape:
1514 value = cgi.escape(value)
1515 return value
1517 class PasswordHTMLProperty(HTMLProperty):
1518 def plain(self, escape=0):
1519 """ Render a "plain" representation of the property
1520 """
1521 if not self.is_view_ok():
1522 return self._('[hidden]')
1524 if self._value is None:
1525 return ''
1526 return self._('*encrypted*')
1528 def field(self, size=30, **kwargs):
1529 """ Render a form edit field for the property.
1531 If not editable, just display the value via plain().
1532 """
1533 if not self.is_edit_ok():
1534 return self.plain(escape=1)
1536 return self.input(type="password", name=self._formname, size=size,
1537 **kwargs)
1539 def confirm(self, size=30):
1540 """ Render a second form edit field for the property, used for
1541 confirmation that the user typed the password correctly. Generates
1542 a field with name "@confirm@name".
1544 If not editable, display nothing.
1545 """
1546 if not self.is_edit_ok():
1547 return ''
1549 return self.input(type="password",
1550 name="@confirm@%s"%self._formname,
1551 id="%s-confirm"%self._formname,
1552 size=size)
1554 class NumberHTMLProperty(HTMLProperty):
1555 def plain(self, escape=0):
1556 """ Render a "plain" representation of the property
1557 """
1558 if not self.is_view_ok():
1559 return self._('[hidden]')
1561 if self._value is None:
1562 return ''
1564 return str(self._value)
1566 def field(self, size=30, **kwargs):
1567 """ Render a form edit field for the property.
1569 If not editable, just display the value via plain().
1570 """
1571 if not self.is_edit_ok():
1572 return self.plain(escape=1)
1574 value = self._value
1575 if value is None:
1576 value = ''
1578 return self.input(name=self._formname, value=value, size=size,
1579 **kwargs)
1581 def __int__(self):
1582 """ Return an int of me
1583 """
1584 return int(self._value)
1586 def __float__(self):
1587 """ Return a float of me
1588 """
1589 return float(self._value)
1592 class BooleanHTMLProperty(HTMLProperty):
1593 def plain(self, escape=0):
1594 """ Render a "plain" representation of the property
1595 """
1596 if not self.is_view_ok():
1597 return self._('[hidden]')
1599 if self._value is None:
1600 return ''
1601 return self._value and self._("Yes") or self._("No")
1603 def field(self, **kwargs):
1604 """ Render a form edit field for the property
1606 If not editable, just display the value via plain().
1607 """
1608 if not self.is_edit_ok():
1609 return self.plain(escape=1)
1611 value = self._value
1612 if isinstance(value, str) or isinstance(value, unicode):
1613 value = value.strip().lower() in ('checked', 'yes', 'true',
1614 'on', '1')
1616 checked = value and "checked" or ""
1617 if value:
1618 s = self.input(type="radio", name=self._formname, value="yes",
1619 checked="checked", **kwargs)
1620 s += self._('Yes')
1621 s +=self.input(type="radio", name=self._formname, value="no",
1622 **kwargs)
1623 s += self._('No')
1624 else:
1625 s = self.input(type="radio", name=self._formname, value="yes",
1626 **kwargs)
1627 s += self._('Yes')
1628 s +=self.input(type="radio", name=self._formname, value="no",
1629 checked="checked", **kwargs)
1630 s += self._('No')
1631 return s
1633 class DateHTMLProperty(HTMLProperty):
1635 _marker = []
1637 def __init__(self, client, classname, nodeid, prop, name, value,
1638 anonymous=0, offset=None):
1639 HTMLProperty.__init__(self, client, classname, nodeid, prop, name,
1640 value, anonymous=anonymous)
1641 if self._value and not (isinstance(self._value, str) or
1642 isinstance(self._value, unicode)):
1643 self._value.setTranslator(self._client.translator)
1644 self._offset = offset
1645 if self._offset is None :
1646 self._offset = self._prop.offset (self._db)
1648 def plain(self, escape=0):
1649 """ Render a "plain" representation of the property
1650 """
1651 if not self.is_view_ok():
1652 return self._('[hidden]')
1654 if self._value is None:
1655 return ''
1656 if self._offset is None:
1657 offset = self._db.getUserTimezone()
1658 else:
1659 offset = self._offset
1660 return str(self._value.local(offset))
1662 def now(self, str_interval=None):
1663 """ Return the current time.
1665 This is useful for defaulting a new value. Returns a
1666 DateHTMLProperty.
1667 """
1668 if not self.is_view_ok():
1669 return self._('[hidden]')
1671 ret = date.Date('.', translator=self._client)
1673 if isinstance(str_interval, basestring):
1674 sign = 1
1675 if str_interval[0] == '-':
1676 sign = -1
1677 str_interval = str_interval[1:]
1678 interval = date.Interval(str_interval, translator=self._client)
1679 if sign > 0:
1680 ret = ret + interval
1681 else:
1682 ret = ret - interval
1684 return DateHTMLProperty(self._client, self._classname, self._nodeid,
1685 self._prop, self._formname, ret)
1687 def field(self, size=30, default=None, format=_marker, popcal=True,
1688 **kwargs):
1689 """Render a form edit field for the property
1691 If not editable, just display the value via plain().
1693 If "popcal" then include the Javascript calendar editor.
1694 Default=yes.
1696 The format string is a standard python strftime format string.
1697 """
1698 if not self.is_edit_ok():
1699 if format is self._marker:
1700 return self.plain(escape=1)
1701 else:
1702 return self.pretty(format)
1704 value = self._value
1706 if value is None:
1707 if default is None:
1708 raw_value = None
1709 else:
1710 if isinstance(default, basestring):
1711 raw_value = date.Date(default, translator=self._client)
1712 elif isinstance(default, date.Date):
1713 raw_value = default
1714 elif isinstance(default, DateHTMLProperty):
1715 raw_value = default._value
1716 else:
1717 raise ValueError, self._('default value for '
1718 'DateHTMLProperty must be either DateHTMLProperty '
1719 'or string date representation.')
1720 elif isinstance(value, str) or isinstance(value, unicode):
1721 # most likely erroneous input to be passed back to user
1722 if isinstance(value, unicode): value = value.encode('utf8')
1723 return self.input(name=self._formname, value=value, size=size,
1724 **kwargs)
1725 else:
1726 raw_value = value
1728 if raw_value is None:
1729 value = ''
1730 elif isinstance(raw_value, str) or isinstance(raw_value, unicode):
1731 if format is self._marker:
1732 value = raw_value
1733 else:
1734 value = date.Date(raw_value).pretty(format)
1735 else:
1736 if self._offset is None :
1737 offset = self._db.getUserTimezone()
1738 else :
1739 offset = self._offset
1740 value = raw_value.local(offset)
1741 if format is not self._marker:
1742 value = value.pretty(format)
1744 s = self.input(name=self._formname, value=value, size=size,
1745 **kwargs)
1746 if popcal:
1747 s += self.popcal()
1748 return s
1750 def reldate(self, pretty=1):
1751 """ Render the interval between the date and now.
1753 If the "pretty" flag is true, then make the display pretty.
1754 """
1755 if not self.is_view_ok():
1756 return self._('[hidden]')
1758 if not self._value:
1759 return ''
1761 # figure the interval
1762 interval = self._value - date.Date('.', translator=self._client)
1763 if pretty:
1764 return interval.pretty()
1765 return str(interval)
1767 def pretty(self, format=_marker):
1768 """ Render the date in a pretty format (eg. month names, spaces).
1770 The format string is a standard python strftime format string.
1771 Note that if the day is zero, and appears at the start of the
1772 string, then it'll be stripped from the output. This is handy
1773 for the situation when a date only specifies a month and a year.
1774 """
1775 if not self.is_view_ok():
1776 return self._('[hidden]')
1778 if self._offset is None:
1779 offset = self._db.getUserTimezone()
1780 else:
1781 offset = self._offset
1783 if not self._value:
1784 return ''
1785 elif format is not self._marker:
1786 return self._value.local(offset).pretty(format)
1787 else:
1788 return self._value.local(offset).pretty()
1790 def local(self, offset):
1791 """ Return the date/time as a local (timezone offset) date/time.
1792 """
1793 if not self.is_view_ok():
1794 return self._('[hidden]')
1796 return DateHTMLProperty(self._client, self._classname, self._nodeid,
1797 self._prop, self._formname, self._value, offset=offset)
1799 def popcal(self, width=300, height=200, label="(cal)",
1800 form="itemSynopsis"):
1801 """Generate a link to a calendar pop-up window.
1803 item: HTMLProperty e.g.: context.deadline
1804 """
1805 if self.isset():
1806 date = "&date=%s"%self._value
1807 else :
1808 date = ""
1809 return ('<a class="classhelp" href="javascript:help_window('
1810 "'%s?@template=calendar&property=%s&form=%s%s', %d, %d)"
1811 '">%s</a>'%(self._classname, self._name, form, date, width,
1812 height, label))
1814 class IntervalHTMLProperty(HTMLProperty):
1815 def __init__(self, client, classname, nodeid, prop, name, value,
1816 anonymous=0):
1817 HTMLProperty.__init__(self, client, classname, nodeid, prop,
1818 name, value, anonymous)
1819 if self._value and not isinstance(self._value, (str, unicode)):
1820 self._value.setTranslator(self._client.translator)
1822 def plain(self, escape=0):
1823 """ Render a "plain" representation of the property
1824 """
1825 if not self.is_view_ok():
1826 return self._('[hidden]')
1828 if self._value is None:
1829 return ''
1830 return str(self._value)
1832 def pretty(self):
1833 """ Render the interval in a pretty format (eg. "yesterday")
1834 """
1835 if not self.is_view_ok():
1836 return self._('[hidden]')
1838 return self._value.pretty()
1840 def field(self, size=30, **kwargs):
1841 """ Render a form edit field for the property
1843 If not editable, just display the value via plain().
1844 """
1845 if not self.is_edit_ok():
1846 return self.plain(escape=1)
1848 value = self._value
1849 if value is None:
1850 value = ''
1852 return self.input(name=self._formname, value=value, size=size,
1853 **kwargs)
1855 class LinkHTMLProperty(HTMLProperty):
1856 """ Link HTMLProperty
1857 Include the above as well as being able to access the class
1858 information. Stringifying the object itself results in the value
1859 from the item being displayed. Accessing attributes of this object
1860 result in the appropriate entry from the class being queried for the
1861 property accessed (so item/assignedto/name would look up the user
1862 entry identified by the assignedto property on item, and then the
1863 name property of that user)
1864 """
1865 def __init__(self, *args, **kw):
1866 HTMLProperty.__init__(self, *args, **kw)
1867 # if we're representing a form value, then the -1 from the form really
1868 # should be a None
1869 if str(self._value) == '-1':
1870 self._value = None
1872 def __getattr__(self, attr):
1873 """ return a new HTMLItem """
1874 if not self._value:
1875 # handle a special page templates lookup
1876 if attr == '__render_with_namespace__':
1877 def nothing(*args, **kw):
1878 return ''
1879 return nothing
1880 msg = self._('Attempt to look up %(attr)s on a missing value')
1881 return MissingValue(msg%locals())
1882 i = HTMLItem(self._client, self._prop.classname, self._value)
1883 return getattr(i, attr)
1885 def plain(self, escape=0):
1886 """ Render a "plain" representation of the property
1887 """
1888 if not self.is_view_ok():
1889 return self._('[hidden]')
1891 if self._value is None:
1892 return ''
1893 linkcl = self._db.classes[self._prop.classname]
1894 k = linkcl.labelprop(1)
1895 if num_re.match(self._value):
1896 try:
1897 value = str(linkcl.get(self._value, k))
1898 except IndexError:
1899 value = self._value
1900 else :
1901 value = self._value
1902 if escape:
1903 value = cgi.escape(value)
1904 return value
1906 def field(self, showid=0, size=None, **kwargs):
1907 """ Render a form edit field for the property
1909 If not editable, just display the value via plain().
1910 """
1911 if not self.is_edit_ok():
1912 return self.plain(escape=1)
1914 # edit field
1915 linkcl = self._db.getclass(self._prop.classname)
1916 if self._value is None:
1917 value = ''
1918 else:
1919 k = linkcl.getkey()
1920 if k and num_re.match(self._value):
1921 value = linkcl.get(self._value, k)
1922 else:
1923 value = self._value
1924 return self.input(name=self._formname, value=value, size=size,
1925 **kwargs)
1927 def menu(self, size=None, height=None, showid=0, additional=[], value=None,
1928 sort_on=None, html_kwargs = {}, **conditions):
1929 """ Render a form select list for this property
1931 "size" is used to limit the length of the list labels
1932 "height" is used to set the <select> tag's "size" attribute
1933 "showid" includes the item ids in the list labels
1934 "value" specifies which item is pre-selected
1935 "additional" lists properties which should be included in the
1936 label
1937 "sort_on" indicates the property to sort the list on as
1938 (direction, property) where direction is '+' or '-'. A
1939 single string with the direction prepended may be used.
1940 For example: ('-', 'order'), '+name'.
1942 The remaining keyword arguments are used as conditions for
1943 filtering the items in the list - they're passed as the
1944 "filterspec" argument to a Class.filter() call.
1946 If not editable, just display the value via plain().
1947 """
1948 if not self.is_edit_ok():
1949 return self.plain(escape=1)
1951 # Since None indicates the default, we need another way to
1952 # indicate "no selection". We use -1 for this purpose, as
1953 # that is the value we use when submitting a form without the
1954 # value set.
1955 if value is None:
1956 value = self._value
1957 elif value == '-1':
1958 value = None
1960 linkcl = self._db.getclass(self._prop.classname)
1961 l = ['<select %s>'%cgi_escape_attrs(name = self._formname,
1962 **html_kwargs)]
1963 k = linkcl.labelprop(1)
1964 s = ''
1965 if value is None:
1966 s = 'selected="selected" '
1967 l.append(self._('<option %svalue="-1">- no selection -</option>')%s)
1969 if sort_on is not None:
1970 if not isinstance(sort_on, tuple):
1971 if sort_on[0] in '+-':
1972 sort_on = (sort_on[0], sort_on[1:])
1973 else:
1974 sort_on = ('+', sort_on)
1975 else:
1976 sort_on = ('+', linkcl.orderprop())
1978 options = [opt
1979 for opt in linkcl.filter(None, conditions, sort_on, (None, None))
1980 if self._db.security.hasPermission("View", self._client.userid,
1981 linkcl.classname, itemid=opt)]
1983 # make sure we list the current value if it's retired
1984 if value and value not in options:
1985 options.insert(0, value)
1987 if additional:
1988 additional_fns = []
1989 props = linkcl.getprops()
1990 for propname in additional:
1991 prop = props[propname]
1992 if isinstance(prop, hyperdb.Link):
1993 cl = self._db.getclass(prop.classname)
1994 labelprop = cl.labelprop()
1995 fn = lambda optionid: cl.get(linkcl.get(optionid,
1996 propname),
1997 labelprop)
1998 else:
1999 fn = lambda optionid: linkcl.get(optionid, propname)
2000 additional_fns.append(fn)
2002 for optionid in options:
2003 # get the option value, and if it's None use an empty string
2004 option = linkcl.get(optionid, k) or ''
2006 # figure if this option is selected
2007 s = ''
2008 if value in [optionid, option]:
2009 s = 'selected="selected" '
2011 # figure the label
2012 if showid:
2013 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
2014 elif not option:
2015 lab = '%s%s'%(self._prop.classname, optionid)
2016 else:
2017 lab = option
2019 # truncate if it's too long
2020 if size is not None and len(lab) > size:
2021 lab = lab[:size-3] + '...'
2022 if additional:
2023 m = []
2024 for fn in additional_fns:
2025 m.append(str(fn(optionid)))
2026 lab = lab + ' (%s)'%', '.join(m)
2028 # and generate
2029 lab = cgi.escape(self._(lab))
2030 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
2031 l.append('</select>')
2032 return '\n'.join(l)
2033 # def checklist(self, ...)
2037 class MultilinkHTMLProperty(HTMLProperty):
2038 """ Multilink HTMLProperty
2040 Also be iterable, returning a wrapper object like the Link case for
2041 each entry in the multilink.
2042 """
2043 def __init__(self, *args, **kwargs):
2044 HTMLProperty.__init__(self, *args, **kwargs)
2045 if self._value:
2046 display_value = lookupIds(self._db, self._prop, self._value,
2047 fail_ok=1, do_lookup=False)
2048 sortfun = make_sort_function(self._db, self._prop.classname)
2049 # sorting fails if the value contains
2050 # items not yet stored in the database
2051 # ignore these errors to preserve user input
2052 try:
2053 display_value.sort(sortfun)
2054 except:
2055 pass
2056 self._value = display_value
2058 def __len__(self):
2059 """ length of the multilink """
2060 return len(self._value)
2062 def __getattr__(self, attr):
2063 """ no extended attribute accesses make sense here """
2064 raise AttributeError, attr
2066 def viewableGenerator(self, values):
2067 """Used to iterate over only the View'able items in a class."""
2068 check = self._db.security.hasPermission
2069 userid = self._client.userid
2070 classname = self._prop.classname
2071 for value in values:
2072 if check('View', userid, classname, itemid=value):
2073 yield HTMLItem(self._client, classname, value)
2075 def __iter__(self):
2076 """ iterate and return a new HTMLItem
2077 """
2078 return self.viewableGenerator(self._value)
2080 def reverse(self):
2081 """ return the list in reverse order
2082 """
2083 l = self._value[:]
2084 l.reverse()
2085 return self.viewableGenerator(l)
2087 def sorted(self, property):
2088 """ Return this multilink sorted by the given property """
2089 value = list(self.__iter__())
2090 value.sort(lambda a,b:cmp(a[property], b[property]))
2091 return value
2093 def __contains__(self, value):
2094 """ Support the "in" operator. We have to make sure the passed-in
2095 value is a string first, not a HTMLProperty.
2096 """
2097 return str(value) in self._value
2099 def isset(self):
2100 """Is my _value not []?"""
2101 return self._value != []
2103 def plain(self, escape=0):
2104 """ Render a "plain" representation of the property
2105 """
2106 if not self.is_view_ok():
2107 return self._('[hidden]')
2109 linkcl = self._db.classes[self._prop.classname]
2110 k = linkcl.labelprop(1)
2111 labels = []
2112 for v in self._value:
2113 if num_re.match(v):
2114 try:
2115 label = linkcl.get(v, k)
2116 except IndexError:
2117 label = None
2118 # fall back to designator if label is None
2119 if label is None: label = '%s%s'%(self._prop.classname, k)
2120 else:
2121 label = v
2122 labels.append(label)
2123 value = ', '.join(labels)
2124 if escape:
2125 value = cgi.escape(value)
2126 return value
2128 def field(self, size=30, showid=0, **kwargs):
2129 """ Render a form edit field for the property
2131 If not editable, just display the value via plain().
2132 """
2133 if not self.is_edit_ok():
2134 return self.plain(escape=1)
2136 linkcl = self._db.getclass(self._prop.classname)
2137 value = self._value[:]
2138 # map the id to the label property
2139 if not linkcl.getkey():
2140 showid=1
2141 if not showid:
2142 k = linkcl.labelprop(1)
2143 value = lookupKeys(linkcl, k, value)
2144 value = ','.join(value)
2145 return self.input(name=self._formname, size=size, value=value,
2146 **kwargs)
2148 def menu(self, size=None, height=None, showid=0, additional=[],
2149 value=None, sort_on=None, html_kwargs = {}, **conditions):
2150 """ Render a form <select> list for this property.
2152 "size" is used to limit the length of the list labels
2153 "height" is used to set the <select> tag's "size" attribute
2154 "showid" includes the item ids in the list labels
2155 "additional" lists properties which should be included in the
2156 label
2157 "value" specifies which item is pre-selected
2158 "sort_on" indicates the property to sort the list on as
2159 (direction, property) where direction is '+' or '-'. A
2160 single string with the direction prepended may be used.
2161 For example: ('-', 'order'), '+name'.
2163 The remaining keyword arguments are used as conditions for
2164 filtering the items in the list - they're passed as the
2165 "filterspec" argument to a Class.filter() call.
2167 If not editable, just display the value via plain().
2168 """
2169 if not self.is_edit_ok():
2170 return self.plain(escape=1)
2172 if value is None:
2173 value = self._value
2175 linkcl = self._db.getclass(self._prop.classname)
2177 if sort_on is not None:
2178 if not isinstance(sort_on, tuple):
2179 if sort_on[0] in '+-':
2180 sort_on = (sort_on[0], sort_on[1:])
2181 else:
2182 sort_on = ('+', sort_on)
2183 else:
2184 sort_on = ('+', linkcl.orderprop())
2186 options = [opt
2187 for opt in linkcl.filter(None, conditions, sort_on)
2188 if self._db.security.hasPermission("View", self._client.userid,
2189 linkcl.classname, itemid=opt)]
2191 # make sure we list the current values if they're retired
2192 for val in value:
2193 if val not in options:
2194 options.insert(0, val)
2196 if not height:
2197 height = len(options)
2198 if value:
2199 # The "no selection" option.
2200 height += 1
2201 height = min(height, 7)
2202 l = ['<select multiple %s>'%cgi_escape_attrs(name = self._formname,
2203 size = height,
2204 **html_kwargs)]
2205 k = linkcl.labelprop(1)
2207 if value:
2208 l.append('<option value="%s">- no selection -</option>'
2209 % ','.join(['-' + v for v in value]))
2211 if additional:
2212 additional_fns = []
2213 props = linkcl.getprops()
2214 for propname in additional:
2215 prop = props[propname]
2216 if isinstance(prop, hyperdb.Link):
2217 cl = self._db.getclass(prop.classname)
2218 labelprop = cl.labelprop()
2219 fn = lambda optionid: cl.get(linkcl.get(optionid,
2220 propname),
2221 labelprop)
2222 else:
2223 fn = lambda optionid: linkcl.get(optionid, propname)
2224 additional_fns.append(fn)
2226 for optionid in options:
2227 # get the option value, and if it's None use an empty string
2228 option = linkcl.get(optionid, k) or ''
2230 # figure if this option is selected
2231 s = ''
2232 if optionid in value or option in value:
2233 s = 'selected="selected" '
2235 # figure the label
2236 if showid:
2237 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
2238 else:
2239 lab = option
2240 # truncate if it's too long
2241 if size is not None and len(lab) > size:
2242 lab = lab[:size-3] + '...'
2243 if additional:
2244 m = []
2245 for fn in additional_fns:
2246 m.append(str(fn(optionid)))
2247 lab = lab + ' (%s)'%', '.join(m)
2249 # and generate
2250 lab = cgi.escape(self._(lab))
2251 l.append('<option %svalue="%s">%s</option>'%(s, optionid,
2252 lab))
2253 l.append('</select>')
2254 return '\n'.join(l)
2256 # set the propclasses for HTMLItem
2257 propclasses = (
2258 (hyperdb.String, StringHTMLProperty),
2259 (hyperdb.Number, NumberHTMLProperty),
2260 (hyperdb.Boolean, BooleanHTMLProperty),
2261 (hyperdb.Date, DateHTMLProperty),
2262 (hyperdb.Interval, IntervalHTMLProperty),
2263 (hyperdb.Password, PasswordHTMLProperty),
2264 (hyperdb.Link, LinkHTMLProperty),
2265 (hyperdb.Multilink, MultilinkHTMLProperty),
2266 )
2268 def make_sort_function(db, classname, sort_on=None):
2269 """Make a sort function for a given class
2270 """
2271 linkcl = db.getclass(classname)
2272 if sort_on is None:
2273 sort_on = linkcl.orderprop()
2274 def sortfunc(a, b):
2275 return cmp(linkcl.get(a, sort_on), linkcl.get(b, sort_on))
2276 return sortfunc
2278 def handleListCGIValue(value):
2279 """ Value is either a single item or a list of items. Each item has a
2280 .value that we're actually interested in.
2281 """
2282 if isinstance(value, type([])):
2283 return [value.value for value in value]
2284 else:
2285 value = value.value.strip()
2286 if not value:
2287 return []
2288 return [v.strip() for v in value.split(',')]
2290 class HTMLRequest(HTMLInputMixin):
2291 """The *request*, holding the CGI form and environment.
2293 - "form" the CGI form as a cgi.FieldStorage
2294 - "env" the CGI environment variables
2295 - "base" the base URL for this instance
2296 - "user" a HTMLItem instance for this user
2297 - "language" as determined by the browser or config
2298 - "classname" the current classname (possibly None)
2299 - "template" the current template (suffix, also possibly None)
2301 Index args:
2303 - "columns" dictionary of the columns to display in an index page
2304 - "show" a convenience access to columns - request/show/colname will
2305 be true if the columns should be displayed, false otherwise
2306 - "sort" index sort column (direction, column name)
2307 - "group" index grouping property (direction, column name)
2308 - "filter" properties to filter the index on
2309 - "filterspec" values to filter the index on
2310 - "search_text" text to perform a full-text search on for an index
2311 """
2312 def __repr__(self):
2313 return '<HTMLRequest %r>'%self.__dict__
2315 def __init__(self, client):
2316 # _client is needed by HTMLInputMixin
2317 self._client = self.client = client
2319 # easier access vars
2320 self.form = client.form
2321 self.env = client.env
2322 self.base = client.base
2323 self.user = HTMLItem(client, 'user', client.userid)
2324 self.language = client.language
2326 # store the current class name and action
2327 self.classname = client.classname
2328 self.nodeid = client.nodeid
2329 self.template = client.template
2331 # the special char to use for special vars
2332 self.special_char = '@'
2334 HTMLInputMixin.__init__(self)
2336 self._post_init()
2338 def current_url(self):
2339 url = self.base
2340 if self.classname:
2341 url += self.classname
2342 if self.nodeid:
2343 url += self.nodeid
2344 args = {}
2345 if self.template:
2346 args['@template'] = self.template
2347 return self.indexargs_url(url, args)
2349 def _parse_sort(self, var, name):
2350 """ Parse sort/group options. Append to var
2351 """
2352 fields = []
2353 dirs = []
2354 for special in '@:':
2355 idx = 0
2356 key = '%s%s%d'%(special, name, idx)
2357 while key in self.form:
2358 self.special_char = special
2359 fields.append(self.form.getfirst(key))
2360 dirkey = '%s%sdir%d'%(special, name, idx)
2361 if dirkey in self.form:
2362 dirs.append(self.form.getfirst(dirkey))
2363 else:
2364 dirs.append(None)
2365 idx += 1
2366 key = '%s%s%d'%(special, name, idx)
2367 # backward compatible (and query) URL format
2368 key = special + name
2369 dirkey = key + 'dir'
2370 if key in self.form and not fields:
2371 fields = handleListCGIValue(self.form[key])
2372 if dirkey in self.form:
2373 dirs.append(self.form.getfirst(dirkey))
2374 if fields: # only try other special char if nothing found
2375 break
2376 for f, d in map(None, fields, dirs):
2377 if f.startswith('-'):
2378 var.append(('-', f[1:]))
2379 elif d:
2380 var.append(('-', f))
2381 else:
2382 var.append(('+', f))
2384 def _post_init(self):
2385 """ Set attributes based on self.form
2386 """
2387 # extract the index display information from the form
2388 self.columns = []
2389 for name in ':columns @columns'.split():
2390 if self.form.has_key(name):
2391 self.special_char = name[0]
2392 self.columns = handleListCGIValue(self.form[name])
2393 break
2394 self.show = support.TruthDict(self.columns)
2396 # sorting and grouping
2397 self.sort = []
2398 self.group = []
2399 self._parse_sort(self.sort, 'sort')
2400 self._parse_sort(self.group, 'group')
2402 # filtering
2403 self.filter = []
2404 for name in ':filter @filter'.split():
2405 if self.form.has_key(name):
2406 self.special_char = name[0]
2407 self.filter = handleListCGIValue(self.form[name])
2409 self.filterspec = {}
2410 db = self.client.db
2411 if self.classname is not None:
2412 cls = db.getclass (self.classname)
2413 for name in self.filter:
2414 if not self.form.has_key(name):
2415 continue
2416 prop = cls.get_transitive_prop (name)
2417 fv = self.form[name]
2418 if (isinstance(prop, hyperdb.Link) or
2419 isinstance(prop, hyperdb.Multilink)):
2420 self.filterspec[name] = lookupIds(db, prop,
2421 handleListCGIValue(fv))
2422 else:
2423 if isinstance(fv, type([])):
2424 self.filterspec[name] = [v.value for v in fv]
2425 elif name == 'id':
2426 # special case "id" property
2427 self.filterspec[name] = handleListCGIValue(fv)
2428 else:
2429 self.filterspec[name] = fv.value
2431 # full-text search argument
2432 self.search_text = None
2433 for name in ':search_text @search_text'.split():
2434 if self.form.has_key(name):
2435 self.special_char = name[0]
2436 self.search_text = self.form.getfirst(name)
2438 # pagination - size and start index
2439 # figure batch args
2440 self.pagesize = 50
2441 for name in ':pagesize @pagesize'.split():
2442 if self.form.has_key(name):
2443 self.special_char = name[0]
2444 try:
2445 self.pagesize = int(self.form.getfirst(name))
2446 except ValueError:
2447 # not an integer - ignore
2448 pass
2450 self.startwith = 0
2451 for name in ':startwith @startwith'.split():
2452 if self.form.has_key(name):
2453 self.special_char = name[0]
2454 try:
2455 self.startwith = int(self.form.getfirst(name))
2456 except ValueError:
2457 # not an integer - ignore
2458 pass
2460 # dispname
2461 if self.form.has_key('@dispname'):
2462 self.dispname = self.form.getfirst('@dispname')
2463 else:
2464 self.dispname = None
2466 def updateFromURL(self, url):
2467 """ Parse the URL for query args, and update my attributes using the
2468 values.
2469 """
2470 env = {'QUERY_STRING': url}
2471 self.form = cgi.FieldStorage(environ=env)
2473 self._post_init()
2475 def update(self, kwargs):
2476 """ Update my attributes using the keyword args
2477 """
2478 self.__dict__.update(kwargs)
2479 if kwargs.has_key('columns'):
2480 self.show = support.TruthDict(self.columns)
2482 def description(self):
2483 """ Return a description of the request - handle for the page title.
2484 """
2485 s = [self.client.db.config.TRACKER_NAME]
2486 if self.classname:
2487 if self.client.nodeid:
2488 s.append('- %s%s'%(self.classname, self.client.nodeid))
2489 else:
2490 if self.template == 'item':
2491 s.append('- new %s'%self.classname)
2492 elif self.template == 'index':
2493 s.append('- %s index'%self.classname)
2494 else:
2495 s.append('- %s %s'%(self.classname, self.template))
2496 else:
2497 s.append('- home')
2498 return ' '.join(s)
2500 def __str__(self):
2501 d = {}
2502 d.update(self.__dict__)
2503 f = ''
2504 for k in self.form.keys():
2505 f += '\n %r=%r'%(k,handleListCGIValue(self.form[k]))
2506 d['form'] = f
2507 e = ''
2508 for k,v in self.env.items():
2509 e += '\n %r=%r'%(k, v)
2510 d['env'] = e
2511 return """
2512 form: %(form)s
2513 base: %(base)r
2514 classname: %(classname)r
2515 template: %(template)r
2516 columns: %(columns)r
2517 sort: %(sort)r
2518 group: %(group)r
2519 filter: %(filter)r
2520 search_text: %(search_text)r
2521 pagesize: %(pagesize)r
2522 startwith: %(startwith)r
2523 env: %(env)s
2524 """%d
2526 def indexargs_form(self, columns=1, sort=1, group=1, filter=1,
2527 filterspec=1, search_text=1):
2528 """ return the current index args as form elements """
2529 l = []
2530 sc = self.special_char
2531 def add(k, v):
2532 l.append(self.input(type="hidden", name=k, value=v))
2533 if columns and self.columns:
2534 add(sc+'columns', ','.join(self.columns))
2535 if sort:
2536 val = []
2537 for dir, attr in self.sort:
2538 if dir == '-':
2539 val.append('-'+attr)
2540 else:
2541 val.append(attr)
2542 add(sc+'sort', ','.join (val))
2543 if group:
2544 val = []
2545 for dir, attr in self.group:
2546 if dir == '-':
2547 val.append('-'+attr)
2548 else:
2549 val.append(attr)
2550 add(sc+'group', ','.join (val))
2551 if filter and self.filter:
2552 add(sc+'filter', ','.join(self.filter))
2553 if self.classname and filterspec:
2554 cls = self.client.db.getclass(self.classname)
2555 for k,v in self.filterspec.items():
2556 if type(v) == type([]):
2557 if isinstance(cls.get_transitive_prop(k), hyperdb.String):
2558 add(k, ' '.join(v))
2559 else:
2560 add(k, ','.join(v))
2561 else:
2562 add(k, v)
2563 if search_text and self.search_text:
2564 add(sc+'search_text', self.search_text)
2565 add(sc+'pagesize', self.pagesize)
2566 add(sc+'startwith', self.startwith)
2567 return '\n'.join(l)
2569 def indexargs_url(self, url, args):
2570 """ Embed the current index args in a URL
2571 """
2572 q = urllib.quote
2573 sc = self.special_char
2574 l = ['%s=%s'%(k,v) for k,v in args.items()]
2576 # pull out the special values (prefixed by @ or :)
2577 specials = {}
2578 for key in args.keys():
2579 if key[0] in '@:':
2580 specials[key[1:]] = args[key]
2582 # ok, now handle the specials we received in the request
2583 if self.columns and not specials.has_key('columns'):
2584 l.append(sc+'columns=%s'%(','.join(self.columns)))
2585 if self.sort and not specials.has_key('sort'):
2586 val = []
2587 for dir, attr in self.sort:
2588 if dir == '-':
2589 val.append('-'+attr)
2590 else:
2591 val.append(attr)
2592 l.append(sc+'sort=%s'%(','.join(val)))
2593 if self.group and not specials.has_key('group'):
2594 val = []
2595 for dir, attr in self.group:
2596 if dir == '-':
2597 val.append('-'+attr)
2598 else:
2599 val.append(attr)
2600 l.append(sc+'group=%s'%(','.join(val)))
2601 if self.filter and not specials.has_key('filter'):
2602 l.append(sc+'filter=%s'%(','.join(self.filter)))
2603 if self.search_text and not specials.has_key('search_text'):
2604 l.append(sc+'search_text=%s'%q(self.search_text))
2605 if not specials.has_key('pagesize'):
2606 l.append(sc+'pagesize=%s'%self.pagesize)
2607 if not specials.has_key('startwith'):
2608 l.append(sc+'startwith=%s'%self.startwith)
2610 # finally, the remainder of the filter args in the request
2611 if self.classname and self.filterspec:
2612 cls = self.client.db.getclass(self.classname)
2613 for k,v in self.filterspec.items():
2614 if not args.has_key(k):
2615 if type(v) == type([]):
2616 prop = cls.get_transitive_prop(k)
2617 if k != 'id' and isinstance(prop, hyperdb.String):
2618 l.append('%s=%s'%(k, '%20'.join([q(i) for i in v])))
2619 else:
2620 l.append('%s=%s'%(k, ','.join([q(i) for i in v])))
2621 else:
2622 l.append('%s=%s'%(k, q(v)))
2623 return '%s?%s'%(url, '&'.join(l))
2624 indexargs_href = indexargs_url
2626 def base_javascript(self):
2627 return """
2628 <script type="text/javascript">
2629 submitted = false;
2630 function submit_once() {
2631 if (submitted) {
2632 alert("Your request is being processed.\\nPlease be patient.");
2633 event.returnValue = 0; // work-around for IE
2634 return 0;
2635 }
2636 submitted = true;
2637 return 1;
2638 }
2640 function help_window(helpurl, width, height) {
2641 HelpWin = window.open('%s' + helpurl, 'RoundupHelpWindow', 'scrollbars=yes,resizable=yes,toolbar=no,height='+height+',width='+width);
2642 }
2643 </script>
2644 """%self.base
2646 def batch(self):
2647 """ Return a batch object for results from the "current search"
2648 """
2649 filterspec = self.filterspec
2650 sort = self.sort
2651 group = self.group
2653 # get the list of ids we're batching over
2654 klass = self.client.db.getclass(self.classname)
2655 if self.search_text:
2656 matches = self.client.db.indexer.search(
2657 [w.upper().encode("utf-8", "replace") for w in re.findall(
2658 r'(?u)\b\w{2,25}\b',
2659 unicode(self.search_text, "utf-8", "replace")
2660 )], klass)
2661 else:
2662 matches = None
2664 # filter for visibility
2665 check = self._client.db.security.hasPermission
2666 userid = self._client.userid
2667 l = [id for id in klass.filter(matches, filterspec, sort, group)
2668 if check('View', userid, self.classname, itemid=id)]
2670 # return the batch object, using IDs only
2671 return Batch(self.client, l, self.pagesize, self.startwith,
2672 classname=self.classname)
2674 # extend the standard ZTUtils Batch object to remove dependency on
2675 # Acquisition and add a couple of useful methods
2676 class Batch(ZTUtils.Batch):
2677 """ Use me to turn a list of items, or item ids of a given class, into a
2678 series of batches.
2680 ========= ========================================================
2681 Parameter Usage
2682 ========= ========================================================
2683 sequence a list of HTMLItems or item ids
2684 classname if sequence is a list of ids, this is the class of item
2685 size how big to make the sequence.
2686 start where to start (0-indexed) in the sequence.
2687 end where to end (0-indexed) in the sequence.
2688 orphan if the next batch would contain less items than this
2689 value, then it is combined with this batch
2690 overlap the number of items shared between adjacent batches
2691 ========= ========================================================
2693 Attributes: Note that the "start" attribute, unlike the
2694 argument, is a 1-based index (I know, lame). "first" is the
2695 0-based index. "length" is the actual number of elements in
2696 the batch.
2698 "sequence_length" is the length of the original, unbatched, sequence.
2699 """
2700 def __init__(self, client, sequence, size, start, end=0, orphan=0,
2701 overlap=0, classname=None):
2702 self.client = client
2703 self.last_index = self.last_item = None
2704 self.current_item = None
2705 self.classname = classname
2706 self.sequence_length = len(sequence)
2707 ZTUtils.Batch.__init__(self, sequence, size, start, end, orphan,
2708 overlap)
2710 # overwrite so we can late-instantiate the HTMLItem instance
2711 def __getitem__(self, index):
2712 if index < 0:
2713 if index + self.end < self.first: raise IndexError, index
2714 return self._sequence[index + self.end]
2716 if index >= self.length:
2717 raise IndexError, index
2719 # move the last_item along - but only if the fetched index changes
2720 # (for some reason, index 0 is fetched twice)
2721 if index != self.last_index:
2722 self.last_item = self.current_item
2723 self.last_index = index
2725 item = self._sequence[index + self.first]
2726 if self.classname:
2727 # map the item ids to instances
2728 item = HTMLItem(self.client, self.classname, item)
2729 self.current_item = item
2730 return item
2732 def propchanged(self, *properties):
2733 """ Detect if one of the properties marked as being a group
2734 property changed in the last iteration fetch
2735 """
2736 # we poke directly at the _value here since MissingValue can screw
2737 # us up and cause Nones to compare strangely
2738 if self.last_item is None:
2739 return 1
2740 for property in properties:
2741 if property == 'id' or isinstance (self.last_item[property], list):
2742 if (str(self.last_item[property]) !=
2743 str(self.current_item[property])):
2744 return 1
2745 else:
2746 if (self.last_item[property]._value !=
2747 self.current_item[property]._value):
2748 return 1
2749 return 0
2751 # override these 'cos we don't have access to acquisition
2752 def previous(self):
2753 if self.start == 1:
2754 return None
2755 return Batch(self.client, self._sequence, self._size,
2756 self.first - self._size + self.overlap, 0, self.orphan,
2757 self.overlap)
2759 def next(self):
2760 try:
2761 self._sequence[self.end]
2762 except IndexError:
2763 return None
2764 return Batch(self.client, self._sequence, self._size,
2765 self.end - self.overlap, 0, self.orphan, self.overlap)
2767 class TemplatingUtils:
2768 """ Utilities for templating
2769 """
2770 def __init__(self, client):
2771 self.client = client
2772 def Batch(self, sequence, size, start, end=0, orphan=0, overlap=0):
2773 return Batch(self.client, sequence, size, start, end, orphan,
2774 overlap)
2776 def url_quote(self, url):
2777 """URL-quote the supplied text."""
2778 return urllib.quote(url)
2780 def html_quote(self, html):
2781 """HTML-quote the supplied text."""
2782 return cgi.escape(html)
2784 def __getattr__(self, name):
2785 """Try the tracker's templating_utils."""
2786 if not hasattr(self.client.instance, 'templating_utils'):
2787 # backwards-compatibility
2788 raise AttributeError, name
2789 if not self.client.instance.templating_utils.has_key(name):
2790 raise AttributeError, name
2791 return self.client.instance.templating_utils[name]
2793 def html_calendar(self, request):
2794 """Generate a HTML calendar.
2796 `request` the roundup.request object
2797 - @template : name of the template
2798 - form : name of the form to store back the date
2799 - property : name of the property of the form to store
2800 back the date
2801 - date : current date
2802 - display : when browsing, specifies year and month
2804 html will simply be a table.
2805 """
2806 date_str = request.form.getfirst("date", ".")
2807 display = request.form.getfirst("display", date_str)
2808 template = request.form.getfirst("@template", "calendar")
2809 form = request.form.getfirst("form")
2810 property = request.form.getfirst("property")
2811 curr_date = date.Date(date_str) # to highlight
2812 display = date.Date(display) # to show
2813 day = display.day
2815 # for navigation
2816 date_prev_month = display + date.Interval("-1m")
2817 date_next_month = display + date.Interval("+1m")
2818 date_prev_year = display + date.Interval("-1y")
2819 date_next_year = display + date.Interval("+1y")
2821 res = []
2823 base_link = "%s?@template=%s&property=%s&form=%s&date=%s" % \
2824 (request.classname, template, property, form, curr_date)
2826 # navigation
2827 # month
2828 res.append('<table class="calendar"><tr><td>')
2829 res.append(' <table width="100%" class="calendar_nav"><tr>')
2830 link = "&display=%s"%date_prev_month
2831 res.append(' <td><a href="%s&display=%s"><</a></td>'%(base_link,
2832 date_prev_month))
2833 res.append(' <td>%s</td>'%calendar.month_name[display.month])
2834 res.append(' <td><a href="%s&display=%s">></a></td>'%(base_link,
2835 date_next_month))
2836 # spacer
2837 res.append(' <td width="100%"></td>')
2838 # year
2839 res.append(' <td><a href="%s&display=%s"><</a></td>'%(base_link,
2840 date_prev_year))
2841 res.append(' <td>%s</td>'%display.year)
2842 res.append(' <td><a href="%s&display=%s">></a></td>'%(base_link,
2843 date_next_year))
2844 res.append(' </tr></table>')
2845 res.append(' </td></tr>')
2847 # the calendar
2848 res.append(' <tr><td><table class="calendar_display">')
2849 res.append(' <tr class="weekdays">')
2850 for day in calendar.weekheader(3).split():
2851 res.append(' <td>%s</td>'%day)
2852 res.append(' </tr>')
2853 for week in calendar.monthcalendar(display.year, display.month):
2854 res.append(' <tr>')
2855 for day in week:
2856 link = "javascript:form[field].value = '%d-%02d-%02d'; " \
2857 "window.close ();"%(display.year, display.month, day)
2858 if (day == curr_date.day and display.month == curr_date.month
2859 and display.year == curr_date.year):
2860 # highlight
2861 style = "today"
2862 else :
2863 style = ""
2864 if day:
2865 res.append(' <td class="%s"><a href="%s">%s</a></td>'%(
2866 style, link, day))
2867 else :
2868 res.append(' <td></td>')
2869 res.append(' </tr>')
2870 res.append('</table></td></tr></table>')
2871 return "\n".join(res)
2873 class MissingValue:
2874 def __init__(self, description, **kwargs):
2875 self.__description = description
2876 for key, value in kwargs.items():
2877 self.__dict__[key] = value
2879 def __call__(self, *args, **kwargs): return MissingValue(self.__description)
2880 def __getattr__(self, name):
2881 # This allows assignments which assume all intermediate steps are Null
2882 # objects if they don't exist yet.
2883 #
2884 # For example (with just 'client' defined):
2885 #
2886 # client.db.config.TRACKER_WEB = 'BASE/'
2887 self.__dict__[name] = MissingValue(self.__description)
2888 return getattr(self, name)
2890 def __getitem__(self, key): return self
2891 def __nonzero__(self): return 0
2892 def __str__(self): return '[%s]'%self.__description
2893 def __repr__(self): return '<MissingValue 0x%x "%s">'%(id(self),
2894 self.__description)
2895 def gettext(self, str): return str
2896 _ = gettext
2898 # vim: set et sts=4 sw=4 :