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 for nodeid in self._klass.list():
619 l = []
620 for name in props:
621 value = self._klass.get(nodeid, name)
622 if value is None:
623 l.append('')
624 elif isinstance(value, type([])):
625 l.append(':'.join(map(str, value)))
626 else:
627 l.append(str(self._klass.get(nodeid, name)))
628 writer.writerow(l)
629 return s.getvalue()
631 def propnames(self):
632 """ Return the list of the names of the properties of this class.
633 """
634 idlessprops = self._klass.getprops(protected=0).keys()
635 idlessprops.sort()
636 return ['id'] + idlessprops
638 def filter(self, request=None, filterspec={}, sort=[], group=[]):
639 """ Return a list of items from this class, filtered and sorted
640 by the current requested filterspec/filter/sort/group args
642 "request" takes precedence over the other three arguments.
643 """
644 if request is not None:
645 filterspec = request.filterspec
646 sort = request.sort
647 group = request.group
649 check = self._db.security.hasPermission
650 userid = self._client.userid
652 l = [HTMLItem(self._client, self.classname, id)
653 for id in self._klass.filter(None, filterspec, sort, group)
654 if check('View', userid, self.classname, itemid=id)]
655 return l
657 def classhelp(self, properties=None, label=''"(list)", width='500',
658 height='400', property='', form='itemSynopsis',
659 pagesize=50, inputtype="checkbox", sort=None, filter=None):
660 """Pop up a javascript window with class help
662 This generates a link to a popup window which displays the
663 properties indicated by "properties" of the class named by
664 "classname". The "properties" should be a comma-separated list
665 (eg. 'id,name,description'). Properties defaults to all the
666 properties of a class (excluding id, creator, created and
667 activity).
669 You may optionally override the label displayed, the width,
670 the height, the number of items per page and the field on which
671 the list is sorted (defaults to username if in the displayed
672 properties).
674 With the "filter" arg it is possible to specify a filter for
675 which items are supposed to be displayed. It has to be of
676 the format "<field>=<values>;<field>=<values>;...".
678 The popup window will be resizable and scrollable.
680 If the "property" arg is given, it's passed through to the
681 javascript help_window function.
683 You can use inputtype="radio" to display a radio box instead
684 of the default checkbox (useful for entering Link-properties)
686 If the "form" arg is given, it's passed through to the
687 javascript help_window function. - it's the name of the form
688 the "property" belongs to.
689 """
690 if properties is None:
691 properties = self._klass.getprops(protected=0).keys()
692 properties.sort()
693 properties = ','.join(properties)
694 if sort is None:
695 if 'username' in properties.split( ',' ):
696 sort = 'username'
697 else:
698 sort = self._klass.orderprop()
699 sort = '&@sort=' + sort
700 if property:
701 property = '&property=%s'%property
702 if form:
703 form = '&form=%s'%form
704 if inputtype:
705 type= '&type=%s'%inputtype
706 if filter:
707 filterprops = filter.split(';')
708 filtervalues = []
709 names = []
710 for x in filterprops:
711 (name, values) = x.split('=')
712 names.append(name)
713 filtervalues.append('&%s=%s' % (name, urllib.quote(values)))
714 filter = '&@filter=%s%s' % (','.join(names), ''.join(filtervalues))
715 else:
716 filter = ''
717 help_url = "%s?@startwith=0&@template=help&"\
718 "properties=%s%s%s%s%s&@pagesize=%s%s" % \
719 (self.classname, properties, property, form, type,
720 sort, pagesize, filter)
721 onclick = "javascript:help_window('%s', '%s', '%s');return false;" % \
722 (help_url, width, height)
723 return '<a class="classhelp" href="%s" onclick="%s">%s</a>' % \
724 (help_url, onclick, self._(label))
726 def submit(self, label=''"Submit New Entry", action="new"):
727 """ Generate a submit button (and action hidden element)
729 Generate nothing if we're not editable.
730 """
731 if not self.is_edit_ok():
732 return ''
734 return self.input(type="hidden", name="@action", value=action) + \
735 '\n' + \
736 self.input(type="submit", name="submit_button", value=self._(label))
738 def history(self):
739 if not self.is_view_ok():
740 return self._('[hidden]')
741 return self._('New node - no history')
743 def renderWith(self, name, **kwargs):
744 """ Render this class with the given template.
745 """
746 # create a new request and override the specified args
747 req = HTMLRequest(self._client)
748 req.classname = self.classname
749 req.update(kwargs)
751 # new template, using the specified classname and request
752 pt = self._client.instance.templates.get(self.classname, name)
754 # use our fabricated request
755 args = {
756 'ok_message': self._client.ok_message,
757 'error_message': self._client.error_message
758 }
759 return pt.render(self._client, self.classname, req, **args)
761 class _HTMLItem(HTMLInputMixin, HTMLPermissions):
762 """ Accesses through an *item*
763 """
764 def __init__(self, client, classname, nodeid, anonymous=0):
765 self._client = client
766 self._db = client.db
767 self._classname = classname
768 self._nodeid = nodeid
769 self._klass = self._db.getclass(classname)
770 self._props = self._klass.getprops()
772 # do we prefix the form items with the item's identification?
773 self._anonymous = anonymous
775 HTMLInputMixin.__init__(self)
777 def is_edit_ok(self):
778 """ Is the user allowed to Edit this item?
779 """
780 return self._db.security.hasPermission('Edit', self._client.userid,
781 self._classname, itemid=self._nodeid)
783 def is_retire_ok(self):
784 """ Is the user allowed to Reture this item?
785 """
786 return self._db.security.hasPermission('Retire', self._client.userid,
787 self._classname, itemid=self._nodeid)
789 def is_view_ok(self):
790 """ Is the user allowed to View this item?
791 """
792 if self._db.security.hasPermission('View', self._client.userid,
793 self._classname, itemid=self._nodeid):
794 return 1
795 return self.is_edit_ok()
797 def is_only_view_ok(self):
798 """ Is the user only allowed to View (ie. not Edit) this item?
799 """
800 return self.is_view_ok() and not self.is_edit_ok()
802 def __repr__(self):
803 return '<HTMLItem(0x%x) %s %s>'%(id(self), self._classname,
804 self._nodeid)
806 def __getitem__(self, item):
807 """ return an HTMLProperty instance
808 this now can handle transitive lookups where item is of the
809 form x.y.z
810 """
811 if item == 'id':
812 return self._nodeid
814 items = item.split('.', 1)
815 has_rest = len(items) > 1
817 # get the property
818 prop = self._props[items[0]]
820 if has_rest and not isinstance(prop, (hyperdb.Link, hyperdb.Multilink)):
821 raise KeyError, item
823 # get the value, handling missing values
824 value = None
825 if int(self._nodeid) > 0:
826 value = self._klass.get(self._nodeid, items[0], None)
827 if value is None:
828 if isinstance(prop, hyperdb.Multilink):
829 value = []
831 # look up the correct HTMLProperty class
832 htmlprop = None
833 for klass, htmlklass in propclasses:
834 if isinstance(prop, klass):
835 htmlprop = htmlklass(self._client, self._classname,
836 self._nodeid, prop, items[0], value, self._anonymous)
837 if htmlprop is not None:
838 if has_rest:
839 if isinstance(htmlprop, MultilinkHTMLProperty):
840 return [h[items[1]] for h in htmlprop]
841 return htmlprop[items[1]]
842 return htmlprop
844 raise KeyError, item
846 def __getattr__(self, attr):
847 """ convenience access to properties """
848 try:
849 return self[attr]
850 except KeyError:
851 raise AttributeError, attr
853 def designator(self):
854 """Return this item's designator (classname + id)."""
855 return '%s%s'%(self._classname, self._nodeid)
857 def is_retired(self):
858 """Is this item retired?"""
859 return self._klass.is_retired(self._nodeid)
861 def submit(self, label=''"Submit Changes", action="edit"):
862 """Generate a submit button.
864 Also sneak in the lastactivity and action hidden elements.
865 """
866 return self.input(type="hidden", name="@lastactivity",
867 value=self.activity.local(0)) + '\n' + \
868 self.input(type="hidden", name="@action", value=action) + '\n' + \
869 self.input(type="submit", name="submit_button", value=self._(label))
871 def journal(self, direction='descending'):
872 """ Return a list of HTMLJournalEntry instances.
873 """
874 # XXX do this
875 return []
877 def history(self, direction='descending', dre=re.compile('^\d+$')):
878 if not self.is_view_ok():
879 return self._('[hidden]')
881 # pre-load the history with the current state
882 current = {}
883 for prop_n in self._props.keys():
884 prop = self[prop_n]
885 if not isinstance(prop, HTMLProperty):
886 continue
887 current[prop_n] = prop.plain(escape=1)
888 # make link if hrefable
889 if (self._props.has_key(prop_n) and
890 isinstance(self._props[prop_n], hyperdb.Link)):
891 classname = self._props[prop_n].classname
892 try:
893 template = find_template(self._db.config.TEMPLATES,
894 classname, 'item')
895 if template[1].startswith('_generic'):
896 raise NoTemplate, 'not really...'
897 except NoTemplate:
898 pass
899 else:
900 id = self._klass.get(self._nodeid, prop_n, None)
901 current[prop_n] = '<a href="%s%s">%s</a>'%(
902 classname, id, current[prop_n])
904 # get the journal, sort and reverse
905 history = self._klass.history(self._nodeid)
906 history.sort()
907 history.reverse()
909 timezone = self._db.getUserTimezone()
910 l = []
911 comments = {}
912 for id, evt_date, user, action, args in history:
913 date_s = str(evt_date.local(timezone)).replace("."," ")
914 arg_s = ''
915 if action == 'link' and type(args) == type(()):
916 if len(args) == 3:
917 linkcl, linkid, key = args
918 arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
919 linkcl, linkid, key)
920 else:
921 arg_s = str(args)
923 elif action == 'unlink' and type(args) == type(()):
924 if len(args) == 3:
925 linkcl, linkid, key = args
926 arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
927 linkcl, linkid, key)
928 else:
929 arg_s = str(args)
931 elif type(args) == type({}):
932 cell = []
933 for k in args.keys():
934 # try to get the relevant property and treat it
935 # specially
936 try:
937 prop = self._props[k]
938 except KeyError:
939 prop = None
940 if prop is None:
941 # property no longer exists
942 comments['no_exist'] = self._(
943 "<em>The indicated property no longer exists</em>")
944 cell.append(self._('<em>%s: %s</em>\n')
945 % (self._(k), str(args[k])))
946 continue
948 if args[k] and (isinstance(prop, hyperdb.Multilink) or
949 isinstance(prop, hyperdb.Link)):
950 # figure what the link class is
951 classname = prop.classname
952 try:
953 linkcl = self._db.getclass(classname)
954 except KeyError:
955 labelprop = None
956 comments[classname] = self._(
957 "The linked class %(classname)s no longer exists"
958 ) % locals()
959 labelprop = linkcl.labelprop(1)
960 try:
961 template = find_template(self._db.config.TEMPLATES,
962 classname, 'item')
963 if template[1].startswith('_generic'):
964 raise NoTemplate, 'not really...'
965 hrefable = 1
966 except NoTemplate:
967 hrefable = 0
969 if isinstance(prop, hyperdb.Multilink) and args[k]:
970 ml = []
971 for linkid in args[k]:
972 if isinstance(linkid, type(())):
973 sublabel = linkid[0] + ' '
974 linkids = linkid[1]
975 else:
976 sublabel = ''
977 linkids = [linkid]
978 subml = []
979 for linkid in linkids:
980 label = classname + linkid
981 # if we have a label property, try to use it
982 # TODO: test for node existence even when
983 # there's no labelprop!
984 try:
985 if labelprop is not None and \
986 labelprop != 'id':
987 label = linkcl.get(linkid, labelprop)
988 label = cgi.escape(label)
989 except IndexError:
990 comments['no_link'] = self._(
991 "<strike>The linked node"
992 " no longer exists</strike>")
993 subml.append('<strike>%s</strike>'%label)
994 else:
995 if hrefable:
996 subml.append('<a href="%s%s">%s</a>'%(
997 classname, linkid, label))
998 elif label is None:
999 subml.append('%s%s'%(classname,
1000 linkid))
1001 else:
1002 subml.append(label)
1003 ml.append(sublabel + ', '.join(subml))
1004 cell.append('%s:\n %s'%(self._(k), ', '.join(ml)))
1005 elif isinstance(prop, hyperdb.Link) and args[k]:
1006 label = classname + args[k]
1007 # if we have a label property, try to use it
1008 # TODO: test for node existence even when
1009 # there's no labelprop!
1010 if labelprop is not None and labelprop != 'id':
1011 try:
1012 label = cgi.escape(linkcl.get(args[k],
1013 labelprop))
1014 except IndexError:
1015 comments['no_link'] = self._(
1016 "<strike>The linked node"
1017 " no longer exists</strike>")
1018 cell.append(' <strike>%s</strike>,\n'%label)
1019 # "flag" this is done .... euwww
1020 label = None
1021 if label is not None:
1022 if hrefable:
1023 old = '<a href="%s%s">%s</a>'%(classname,
1024 args[k], label)
1025 else:
1026 old = label;
1027 cell.append('%s: %s' % (self._(k), old))
1028 if current.has_key(k):
1029 cell[-1] += ' -> %s'%current[k]
1030 current[k] = old
1032 elif isinstance(prop, hyperdb.Date) and args[k]:
1033 if args[k] is None:
1034 d = ''
1035 else:
1036 d = date.Date(args[k],
1037 translator=self._client).local(timezone)
1038 cell.append('%s: %s'%(self._(k), str(d)))
1039 if current.has_key(k):
1040 cell[-1] += ' -> %s' % current[k]
1041 current[k] = str(d)
1043 elif isinstance(prop, hyperdb.Interval) and args[k]:
1044 val = str(date.Interval(args[k],
1045 translator=self._client))
1046 cell.append('%s: %s'%(self._(k), val))
1047 if current.has_key(k):
1048 cell[-1] += ' -> %s'%current[k]
1049 current[k] = val
1051 elif isinstance(prop, hyperdb.String) and args[k]:
1052 val = cgi.escape(args[k])
1053 cell.append('%s: %s'%(self._(k), val))
1054 if current.has_key(k):
1055 cell[-1] += ' -> %s'%current[k]
1056 current[k] = val
1058 elif isinstance(prop, hyperdb.Boolean) and args[k] is not None:
1059 val = args[k] and ''"Yes" or ''"No"
1060 cell.append('%s: %s'%(self._(k), val))
1061 if current.has_key(k):
1062 cell[-1] += ' -> %s'%current[k]
1063 current[k] = val
1065 elif not args[k]:
1066 if current.has_key(k):
1067 cell.append('%s: %s'%(self._(k), current[k]))
1068 current[k] = '(no value)'
1069 else:
1070 cell.append(self._('%s: (no value)')%self._(k))
1072 else:
1073 cell.append('%s: %s'%(self._(k), str(args[k])))
1074 if current.has_key(k):
1075 cell[-1] += ' -> %s'%current[k]
1076 current[k] = str(args[k])
1078 arg_s = '<br />'.join(cell)
1079 else:
1080 # unkown event!!
1081 comments['unknown'] = self._(
1082 "<strong><em>This event is not handled"
1083 " by the history display!</em></strong>")
1084 arg_s = '<strong><em>' + str(args) + '</em></strong>'
1085 date_s = date_s.replace(' ', ' ')
1086 # if the user's an itemid, figure the username (older journals
1087 # have the username)
1088 if dre.match(user):
1089 user = self._db.user.get(user, 'username')
1090 l.append('<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>'%(
1091 date_s, user, self._(action), arg_s))
1092 if comments:
1093 l.append(self._(
1094 '<tr><td colspan=4><strong>Note:</strong></td></tr>'))
1095 for entry in comments.values():
1096 l.append('<tr><td colspan=4>%s</td></tr>'%entry)
1098 if direction == 'ascending':
1099 l.reverse()
1101 l[0:0] = ['<table class="history">'
1102 '<tr><th colspan="4" class="header">',
1103 self._('History'),
1104 '</th></tr><tr>',
1105 self._('<th>Date</th>'),
1106 self._('<th>User</th>'),
1107 self._('<th>Action</th>'),
1108 self._('<th>Args</th>'),
1109 '</tr>']
1110 l.append('</table>')
1111 return '\n'.join(l)
1113 def renderQueryForm(self):
1114 """ Render this item, which is a query, as a search form.
1115 """
1116 # create a new request and override the specified args
1117 req = HTMLRequest(self._client)
1118 req.classname = self._klass.get(self._nodeid, 'klass')
1119 name = self._klass.get(self._nodeid, 'name')
1120 req.updateFromURL(self._klass.get(self._nodeid, 'url') +
1121 '&@queryname=%s'%urllib.quote(name))
1123 # new template, using the specified classname and request
1124 pt = self._client.instance.templates.get(req.classname, 'search')
1125 # The context for a search page should be the class, not any
1126 # node.
1127 self._client.nodeid = None
1129 # use our fabricated request
1130 return pt.render(self._client, req.classname, req)
1132 def download_url(self):
1133 """ Assume that this item is a FileClass and that it has a name
1134 and content. Construct a URL for the download of the content.
1135 """
1136 name = self._klass.get(self._nodeid, 'name')
1137 url = '%s%s/%s'%(self._classname, self._nodeid, name)
1138 return urllib.quote(url)
1140 def copy_url(self, exclude=("messages", "files")):
1141 """Construct a URL for creating a copy of this item
1143 "exclude" is an optional list of properties that should
1144 not be copied to the new object. By default, this list
1145 includes "messages" and "files" properties. Note that
1146 "id" property cannot be copied.
1148 """
1149 exclude = ("id", "activity", "actor", "creation", "creator") \
1150 + tuple(exclude)
1151 query = {
1152 "@template": "item",
1153 "@note": self._("Copy of %(class)s %(id)s") % {
1154 "class": self._(self._classname), "id": self._nodeid},
1155 }
1156 for name in self._props.keys():
1157 if name not in exclude:
1158 query[name] = self[name].plain()
1159 return self._classname + "?" + "&".join(
1160 ["%s=%s" % (key, urllib.quote(value))
1161 for key, value in query.items()])
1163 class _HTMLUser(_HTMLItem):
1164 """Add ability to check for permissions on users.
1165 """
1166 _marker = []
1167 def hasPermission(self, permission, classname=_marker,
1168 property=None, itemid=None):
1169 """Determine if the user has the Permission.
1171 The class being tested defaults to the template's class, but may
1172 be overidden for this test by suppling an alternate classname.
1173 """
1174 if classname is self._marker:
1175 classname = self._client.classname
1176 return self._db.security.hasPermission(permission,
1177 self._nodeid, classname, property, itemid)
1179 def hasRole(self, rolename):
1180 """Determine whether the user has the Role."""
1181 roles = self._db.user.get(self._nodeid, 'roles').split(',')
1182 for role in roles:
1183 if role.strip() == rolename: return True
1184 return False
1186 def HTMLItem(client, classname, nodeid, anonymous=0):
1187 if classname == 'user':
1188 return _HTMLUser(client, classname, nodeid, anonymous)
1189 else:
1190 return _HTMLItem(client, classname, nodeid, anonymous)
1192 class HTMLProperty(HTMLInputMixin, HTMLPermissions):
1193 """ String, Number, Date, Interval HTMLProperty
1195 Has useful attributes:
1197 _name the name of the property
1198 _value the value of the property if any
1200 A wrapper object which may be stringified for the plain() behaviour.
1201 """
1202 def __init__(self, client, classname, nodeid, prop, name, value,
1203 anonymous=0):
1204 self._client = client
1205 self._db = client.db
1206 self._ = client._
1207 self._classname = classname
1208 self._nodeid = nodeid
1209 self._prop = prop
1210 self._value = value
1211 self._anonymous = anonymous
1212 self._name = name
1213 if not anonymous:
1214 self._formname = '%s%s@%s'%(classname, nodeid, name)
1215 else:
1216 self._formname = name
1218 # If no value is already present for this property, see if one
1219 # is specified in the current form.
1220 form = self._client.form
1221 if not self._value and form.has_key(self._formname):
1222 if isinstance(prop, hyperdb.Multilink):
1223 value = lookupIds(self._db, prop,
1224 handleListCGIValue(form[self._formname]),
1225 fail_ok=1)
1226 elif isinstance(prop, hyperdb.Link):
1227 value = form.getfirst(self._formname).strip()
1228 if value:
1229 value = lookupIds(self._db, prop, [value],
1230 fail_ok=1)[0]
1231 else:
1232 value = None
1233 else:
1234 value = form.getfirst(self._formname).strip() or None
1235 self._value = value
1237 HTMLInputMixin.__init__(self)
1239 def __repr__(self):
1240 return '<HTMLProperty(0x%x) %s %r %r>'%(id(self), self._formname,
1241 self._prop, self._value)
1242 def __str__(self):
1243 return self.plain()
1244 def __cmp__(self, other):
1245 if isinstance(other, HTMLProperty):
1246 return cmp(self._value, other._value)
1247 return cmp(self._value, other)
1249 def __nonzero__(self):
1250 return not not self._value
1252 def isset(self):
1253 """Is my _value not None?"""
1254 return self._value is not None
1256 def is_edit_ok(self):
1257 """Should the user be allowed to use an edit form field for this
1258 property. Check "Create" for new items, or "Edit" for existing
1259 ones.
1260 """
1261 if self._nodeid:
1262 return self._db.security.hasPermission('Edit', self._client.userid,
1263 self._classname, self._name, self._nodeid)
1264 return self._db.security.hasPermission('Create', self._client.userid,
1265 self._classname, self._name)
1267 def is_view_ok(self):
1268 """ Is the user allowed to View the current class?
1269 """
1270 if self._db.security.hasPermission('View', self._client.userid,
1271 self._classname, self._name, self._nodeid):
1272 return 1
1273 return self.is_edit_ok()
1275 class StringHTMLProperty(HTMLProperty):
1276 hyper_re = re.compile(r'''(
1277 (?P<url>
1278 (
1279 (ht|f)tp(s?):// # protocol
1280 ([\w]+(:\w+)?@)? # username/password
1281 ([\w\-]+) # hostname
1282 ((\.[\w-]+)+)? # .domain.etc
1283 | # ... or ...
1284 ([\w]+(:\w+)?@)? # username/password
1285 www\. # "www."
1286 ([\w\-]+\.)+ # hostname
1287 [\w]{2,5} # TLD
1288 )
1289 (:[\d]{1,5})? # port
1290 (/[\w\-$.+!*(),;:@&=?/~\\#%]*)? # path etc.
1291 )|
1292 (?P<email>[-+=%/\w\.]+@[\w\.\-]+)|
1293 (?P<item>(?P<class>[A-Za-z_]+)(\s*)(?P<id>\d+))
1294 )''', re.X | re.I)
1295 protocol_re = re.compile('^(ht|f)tp(s?)://', re.I)
1297 def _hyper_repl_item(self,match,replacement):
1298 item = match.group('item')
1299 cls = match.group('class').lower()
1300 id = match.group('id')
1301 try:
1302 # make sure cls is a valid tracker classname
1303 cl = self._db.getclass(cls)
1304 if not cl.hasnode(id):
1305 return item
1306 return replacement % locals()
1307 except KeyError:
1308 return item
1310 def _hyper_repl(self, match):
1311 if match.group('url'):
1312 u = s = match.group('url')
1313 if not self.protocol_re.search(s):
1314 u = 'http://' + s
1315 # catch an escaped ">" at the end of the URL
1316 if s.endswith('>'):
1317 u = s = s[:-4]
1318 e = '>'
1319 else:
1320 e = ''
1321 return '<a href="%s">%s</a>%s'%(u, s, e)
1322 elif match.group('email'):
1323 s = match.group('email')
1324 return '<a href="mailto:%s">%s</a>'%(s, s)
1325 else:
1326 return self._hyper_repl_item(match,
1327 '<a href="%(cls)s%(id)s">%(item)s</a>')
1329 def _hyper_repl_rst(self, match):
1330 if match.group('url'):
1331 s = match.group('url')
1332 return '`%s <%s>`_'%(s, s)
1333 elif match.group('email'):
1334 s = match.group('email')
1335 return '`%s <mailto:%s>`_'%(s, s)
1336 else:
1337 return self._hyper_repl_item(match,'`%(item)s <%(cls)s%(id)s>`_')
1339 def hyperlinked(self):
1340 """ Render a "hyperlinked" version of the text """
1341 return self.plain(hyperlink=1)
1343 def plain(self, escape=0, hyperlink=0):
1344 """Render a "plain" representation of the property
1346 - "escape" turns on/off HTML quoting
1347 - "hyperlink" turns on/off in-text hyperlinking of URLs, email
1348 addresses and designators
1349 """
1350 if not self.is_view_ok():
1351 return self._('[hidden]')
1353 if self._value is None:
1354 return ''
1355 if escape:
1356 s = cgi.escape(str(self._value))
1357 else:
1358 s = str(self._value)
1359 if hyperlink:
1360 # no, we *must* escape this text
1361 if not escape:
1362 s = cgi.escape(s)
1363 s = self.hyper_re.sub(self._hyper_repl, s)
1364 return s
1366 def wrapped(self, escape=1, hyperlink=1):
1367 """Render a "wrapped" representation of the property.
1369 We wrap long lines at 80 columns on the nearest whitespace. Lines
1370 with no whitespace are not broken to force wrapping.
1372 Note that unlike plain() we default wrapped() to have the escaping
1373 and hyperlinking turned on since that's the most common usage.
1375 - "escape" turns on/off HTML quoting
1376 - "hyperlink" turns on/off in-text hyperlinking of URLs, email
1377 addresses and designators
1378 """
1379 if not self.is_view_ok():
1380 return self._('[hidden]')
1382 if self._value is None:
1383 return ''
1384 s = support.wrap(str(self._value), width=80)
1385 if escape:
1386 s = cgi.escape(s)
1387 if hyperlink:
1388 # no, we *must* escape this text
1389 if not escape:
1390 s = cgi.escape(s)
1391 s = self.hyper_re.sub(self._hyper_repl, s)
1392 return s
1394 def stext(self, escape=0, hyperlink=1):
1395 """ Render the value of the property as StructuredText.
1397 This requires the StructureText module to be installed separately.
1398 """
1399 if not self.is_view_ok():
1400 return self._('[hidden]')
1402 s = self.plain(escape=escape, hyperlink=hyperlink)
1403 if not StructuredText:
1404 return s
1405 return StructuredText(s,level=1,header=0)
1407 def rst(self, hyperlink=1):
1408 """ Render the value of the property as ReStructuredText.
1410 This requires docutils to be installed separately.
1411 """
1412 if not self.is_view_ok():
1413 return self._('[hidden]')
1415 if not ReStructuredText:
1416 return self.plain(escape=0, hyperlink=hyperlink)
1417 s = self.plain(escape=0, hyperlink=0)
1418 if hyperlink:
1419 s = self.hyper_re.sub(self._hyper_repl_rst, s)
1420 return ReStructuredText(s, writer_name="html")["body"].encode("utf-8",
1421 "replace")
1423 def field(self, **kwargs):
1424 """ Render the property as a field in HTML.
1426 If not editable, just display the value via plain().
1427 """
1428 if not self.is_edit_ok():
1429 return self.plain(escape=1)
1431 value = self._value
1432 if value is None:
1433 value = ''
1435 kwargs.setdefault("size", 30)
1436 kwargs.update({"name": self._formname, "value": value})
1437 return self.input(**kwargs)
1439 def multiline(self, escape=0, rows=5, cols=40, **kwargs):
1440 """ Render a multiline form edit field for the property.
1442 If not editable, just display the plain() value in a <pre> tag.
1443 """
1444 if not self.is_edit_ok():
1445 return '<pre>%s</pre>'%self.plain()
1447 if self._value is None:
1448 value = ''
1449 else:
1450 value = cgi.escape(str(self._value))
1452 value = '"'.join(value.split('"'))
1453 name = self._formname
1454 passthrough_args = ' '.join(['%s="%s"' % (k, cgi.escape(str(v), True))
1455 for k,v in kwargs.items()])
1456 return ('<textarea %(passthrough_args)s name="%(name)s" id="%(name)s"'
1457 ' rows="%(rows)s" cols="%(cols)s">'
1458 '%(value)s</textarea>') % locals()
1460 def email(self, escape=1):
1461 """ Render the value of the property as an obscured email address
1462 """
1463 if not self.is_view_ok():
1464 return self._('[hidden]')
1466 if self._value is None:
1467 value = ''
1468 else:
1469 value = str(self._value)
1470 split = value.split('@')
1471 if len(split) == 2:
1472 name, domain = split
1473 domain = ' '.join(domain.split('.')[:-1])
1474 name = name.replace('.', ' ')
1475 value = '%s at %s ...'%(name, domain)
1476 else:
1477 value = value.replace('.', ' ')
1478 if escape:
1479 value = cgi.escape(value)
1480 return value
1482 class PasswordHTMLProperty(HTMLProperty):
1483 def plain(self, escape=0):
1484 """ Render a "plain" representation of the property
1485 """
1486 if not self.is_view_ok():
1487 return self._('[hidden]')
1489 if self._value is None:
1490 return ''
1491 return self._('*encrypted*')
1493 def field(self, size=30):
1494 """ Render a form edit field for the property.
1496 If not editable, just display the value via plain().
1497 """
1498 if not self.is_edit_ok():
1499 return self.plain(escape=1)
1501 return self.input(type="password", name=self._formname, size=size)
1503 def confirm(self, size=30):
1504 """ Render a second form edit field for the property, used for
1505 confirmation that the user typed the password correctly. Generates
1506 a field with name "@confirm@name".
1508 If not editable, display nothing.
1509 """
1510 if not self.is_edit_ok():
1511 return ''
1513 return self.input(type="password",
1514 name="@confirm@%s"%self._formname,
1515 id="%s-confirm"%self._formname,
1516 size=size)
1518 class NumberHTMLProperty(HTMLProperty):
1519 def plain(self, escape=0):
1520 """ Render a "plain" representation of the property
1521 """
1522 if not self.is_view_ok():
1523 return self._('[hidden]')
1525 if self._value is None:
1526 return ''
1528 return str(self._value)
1530 def field(self, size=30):
1531 """ Render a form edit field for the property.
1533 If not editable, just display the value via plain().
1534 """
1535 if not self.is_edit_ok():
1536 return self.plain(escape=1)
1538 value = self._value
1539 if value is None:
1540 value = ''
1542 return self.input(name=self._formname, value=value, size=size)
1544 def __int__(self):
1545 """ Return an int of me
1546 """
1547 return int(self._value)
1549 def __float__(self):
1550 """ Return a float of me
1551 """
1552 return float(self._value)
1555 class BooleanHTMLProperty(HTMLProperty):
1556 def plain(self, escape=0):
1557 """ Render a "plain" representation of the property
1558 """
1559 if not self.is_view_ok():
1560 return self._('[hidden]')
1562 if self._value is None:
1563 return ''
1564 return self._value and self._("Yes") or self._("No")
1566 def field(self):
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 isinstance(value, str) or isinstance(value, unicode):
1576 value = value.strip().lower() in ('checked', 'yes', 'true',
1577 'on', '1')
1579 checked = value and "checked" or ""
1580 if value:
1581 s = self.input(type="radio", name=self._formname, value="yes",
1582 checked="checked")
1583 s += self._('Yes')
1584 s +=self.input(type="radio", name=self._formname, value="no")
1585 s += self._('No')
1586 else:
1587 s = self.input(type="radio", name=self._formname, value="yes")
1588 s += self._('Yes')
1589 s +=self.input(type="radio", name=self._formname, value="no",
1590 checked="checked")
1591 s += self._('No')
1592 return s
1594 class DateHTMLProperty(HTMLProperty):
1596 _marker = []
1598 def __init__(self, client, classname, nodeid, prop, name, value,
1599 anonymous=0, offset=None):
1600 HTMLProperty.__init__(self, client, classname, nodeid, prop, name,
1601 value, anonymous=anonymous)
1602 if self._value and not (isinstance(self._value, str) or
1603 isinstance(self._value, unicode)):
1604 self._value.setTranslator(self._client.translator)
1605 self._offset = offset
1606 if self._offset is None :
1607 self._offset = self._prop.offset (self._db)
1609 def plain(self, escape=0):
1610 """ Render a "plain" representation of the property
1611 """
1612 if not self.is_view_ok():
1613 return self._('[hidden]')
1615 if self._value is None:
1616 return ''
1617 if self._offset is None:
1618 offset = self._db.getUserTimezone()
1619 else:
1620 offset = self._offset
1621 return str(self._value.local(offset))
1623 def now(self, str_interval=None):
1624 """ Return the current time.
1626 This is useful for defaulting a new value. Returns a
1627 DateHTMLProperty.
1628 """
1629 if not self.is_view_ok():
1630 return self._('[hidden]')
1632 ret = date.Date('.', translator=self._client)
1634 if isinstance(str_interval, basestring):
1635 sign = 1
1636 if str_interval[0] == '-':
1637 sign = -1
1638 str_interval = str_interval[1:]
1639 interval = date.Interval(str_interval, translator=self._client)
1640 if sign > 0:
1641 ret = ret + interval
1642 else:
1643 ret = ret - interval
1645 return DateHTMLProperty(self._client, self._classname, self._nodeid,
1646 self._prop, self._formname, ret)
1648 def field(self, size=30, default=None, format=_marker, popcal=True):
1649 """Render a form edit field for the property
1651 If not editable, just display the value via plain().
1653 If "popcal" then include the Javascript calendar editor.
1654 Default=yes.
1656 The format string is a standard python strftime format string.
1657 """
1658 if not self.is_edit_ok():
1659 if format is self._marker:
1660 return self.plain(escape=1)
1661 else:
1662 return self.pretty(format)
1664 value = self._value
1666 if value is None:
1667 if default is None:
1668 raw_value = None
1669 else:
1670 if isinstance(default, basestring):
1671 raw_value = date.Date(default, translator=self._client)
1672 elif isinstance(default, date.Date):
1673 raw_value = default
1674 elif isinstance(default, DateHTMLProperty):
1675 raw_value = default._value
1676 else:
1677 raise ValueError, self._('default value for '
1678 'DateHTMLProperty must be either DateHTMLProperty '
1679 'or string date representation.')
1680 elif isinstance(value, str) or isinstance(value, unicode):
1681 # most likely erroneous input to be passed back to user
1682 if isinstance(value, unicode): value = value.encode('utf8')
1683 return self.input(name=self._formname, value=value, size=size)
1684 else:
1685 raw_value = value
1687 if raw_value is None:
1688 value = ''
1689 elif isinstance(raw_value, str) or isinstance(raw_value, unicode):
1690 if format is self._marker:
1691 value = raw_value
1692 else:
1693 value = date.Date(raw_value).pretty(format)
1694 else:
1695 if self._offset is None :
1696 offset = self._db.getUserTimezone()
1697 else :
1698 offset = self._offset
1699 value = raw_value.local(offset)
1700 if format is not self._marker:
1701 value = value.pretty(format)
1703 s = self.input(name=self._formname, value=value, size=size)
1704 if popcal:
1705 s += self.popcal()
1706 return s
1708 def reldate(self, pretty=1):
1709 """ Render the interval between the date and now.
1711 If the "pretty" flag is true, then make the display pretty.
1712 """
1713 if not self.is_view_ok():
1714 return self._('[hidden]')
1716 if not self._value:
1717 return ''
1719 # figure the interval
1720 interval = self._value - date.Date('.', translator=self._client)
1721 if pretty:
1722 return interval.pretty()
1723 return str(interval)
1725 def pretty(self, format=_marker):
1726 """ Render the date in a pretty format (eg. month names, spaces).
1728 The format string is a standard python strftime format string.
1729 Note that if the day is zero, and appears at the start of the
1730 string, then it'll be stripped from the output. This is handy
1731 for the situation when a date only specifies a month and a year.
1732 """
1733 if not self.is_view_ok():
1734 return self._('[hidden]')
1736 if self._offset is None:
1737 offset = self._db.getUserTimezone()
1738 else:
1739 offset = self._offset
1741 if not self._value:
1742 return ''
1743 elif format is not self._marker:
1744 return self._value.local(offset).pretty(format)
1745 else:
1746 return self._value.local(offset).pretty()
1748 def local(self, offset):
1749 """ Return the date/time as a local (timezone offset) date/time.
1750 """
1751 if not self.is_view_ok():
1752 return self._('[hidden]')
1754 return DateHTMLProperty(self._client, self._classname, self._nodeid,
1755 self._prop, self._formname, self._value, offset=offset)
1757 def popcal(self, width=300, height=200, label="(cal)",
1758 form="itemSynopsis"):
1759 """Generate a link to a calendar pop-up window.
1761 item: HTMLProperty e.g.: context.deadline
1762 """
1763 if self.isset():
1764 date = "&date=%s"%self._value
1765 else :
1766 date = ""
1767 return ('<a class="classhelp" href="javascript:help_window('
1768 "'%s?@template=calendar&property=%s&form=%s%s', %d, %d)"
1769 '">%s</a>'%(self._classname, self._name, form, date, width,
1770 height, label))
1772 class IntervalHTMLProperty(HTMLProperty):
1773 def __init__(self, client, classname, nodeid, prop, name, value,
1774 anonymous=0):
1775 HTMLProperty.__init__(self, client, classname, nodeid, prop,
1776 name, value, anonymous)
1777 if self._value and not isinstance(self._value, (str, unicode)):
1778 self._value.setTranslator(self._client.translator)
1780 def plain(self, escape=0):
1781 """ Render a "plain" representation of the property
1782 """
1783 if not self.is_view_ok():
1784 return self._('[hidden]')
1786 if self._value is None:
1787 return ''
1788 return str(self._value)
1790 def pretty(self):
1791 """ Render the interval in a pretty format (eg. "yesterday")
1792 """
1793 if not self.is_view_ok():
1794 return self._('[hidden]')
1796 return self._value.pretty()
1798 def field(self, size=30):
1799 """ Render a form edit field for the property
1801 If not editable, just display the value via plain().
1802 """
1803 if not self.is_edit_ok():
1804 return self.plain(escape=1)
1806 value = self._value
1807 if value is None:
1808 value = ''
1810 return self.input(name=self._formname, value=value, size=size)
1812 class LinkHTMLProperty(HTMLProperty):
1813 """ Link HTMLProperty
1814 Include the above as well as being able to access the class
1815 information. Stringifying the object itself results in the value
1816 from the item being displayed. Accessing attributes of this object
1817 result in the appropriate entry from the class being queried for the
1818 property accessed (so item/assignedto/name would look up the user
1819 entry identified by the assignedto property on item, and then the
1820 name property of that user)
1821 """
1822 def __init__(self, *args, **kw):
1823 HTMLProperty.__init__(self, *args, **kw)
1824 # if we're representing a form value, then the -1 from the form really
1825 # should be a None
1826 if str(self._value) == '-1':
1827 self._value = None
1829 def __getattr__(self, attr):
1830 """ return a new HTMLItem """
1831 if not self._value:
1832 # handle a special page templates lookup
1833 if attr == '__render_with_namespace__':
1834 def nothing(*args, **kw):
1835 return ''
1836 return nothing
1837 msg = self._('Attempt to look up %(attr)s on a missing value')
1838 return MissingValue(msg%locals())
1839 i = HTMLItem(self._client, self._prop.classname, self._value)
1840 return getattr(i, attr)
1842 def plain(self, escape=0):
1843 """ Render a "plain" representation of the property
1844 """
1845 if not self.is_view_ok():
1846 return self._('[hidden]')
1848 if self._value is None:
1849 return ''
1850 linkcl = self._db.classes[self._prop.classname]
1851 k = linkcl.labelprop(1)
1852 if num_re.match(self._value):
1853 try:
1854 value = str(linkcl.get(self._value, k))
1855 except IndexError:
1856 value = self._value
1857 else :
1858 value = self._value
1859 if escape:
1860 value = cgi.escape(value)
1861 return value
1863 def field(self, showid=0, size=None):
1864 """ Render a form edit field for the property
1866 If not editable, just display the value via plain().
1867 """
1868 if not self.is_edit_ok():
1869 return self.plain(escape=1)
1871 # edit field
1872 linkcl = self._db.getclass(self._prop.classname)
1873 if self._value is None:
1874 value = ''
1875 else:
1876 k = linkcl.getkey()
1877 if k and num_re.match(self._value):
1878 value = linkcl.get(self._value, k)
1879 else:
1880 value = self._value
1881 return self.input(name=self._formname, value=value, size=size)
1883 def menu(self, size=None, height=None, showid=0, additional=[], value=None,
1884 sort_on=None, **conditions):
1885 """ Render a form select list for this property
1887 "size" is used to limit the length of the list labels
1888 "height" is used to set the <select> tag's "size" attribute
1889 "showid" includes the item ids in the list labels
1890 "value" specifies which item is pre-selected
1891 "additional" lists properties which should be included in the
1892 label
1893 "sort_on" indicates the property to sort the list on as
1894 (direction, property) where direction is '+' or '-'. A
1895 single string with the direction prepended may be used.
1896 For example: ('-', 'order'), '+name'.
1898 The remaining keyword arguments are used as conditions for
1899 filtering the items in the list - they're passed as the
1900 "filterspec" argument to a Class.filter() call.
1902 If not editable, just display the value via plain().
1903 """
1904 if not self.is_edit_ok():
1905 return self.plain(escape=1)
1907 # Since None indicates the default, we need another way to
1908 # indicate "no selection". We use -1 for this purpose, as
1909 # that is the value we use when submitting a form without the
1910 # value set.
1911 if value is None:
1912 value = self._value
1913 elif value == '-1':
1914 value = None
1916 linkcl = self._db.getclass(self._prop.classname)
1917 l = ['<select name="%s">'%self._formname]
1918 k = linkcl.labelprop(1)
1919 s = ''
1920 if value is None:
1921 s = 'selected="selected" '
1922 l.append(self._('<option %svalue="-1">- no selection -</option>')%s)
1924 if sort_on is not None:
1925 if not isinstance(sort_on, tuple):
1926 if sort_on[0] in '+-':
1927 sort_on = (sort_on[0], sort_on[1:])
1928 else:
1929 sort_on = ('+', sort_on)
1930 else:
1931 sort_on = ('+', linkcl.orderprop())
1933 options = [opt
1934 for opt in linkcl.filter(None, conditions, sort_on, (None, None))
1935 if self._db.security.hasPermission("View", self._client.userid,
1936 linkcl.classname, itemid=opt)]
1938 # make sure we list the current value if it's retired
1939 if value and value not in options:
1940 options.insert(0, value)
1942 if additional:
1943 additional_fns = []
1944 props = linkcl.getprops()
1945 for propname in additional:
1946 prop = props[propname]
1947 if isinstance(prop, hyperdb.Link):
1948 cl = self._db.getclass(prop.classname)
1949 labelprop = cl.labelprop()
1950 fn = lambda optionid: cl.get(linkcl.get(optionid,
1951 propname),
1952 labelprop)
1953 else:
1954 fn = lambda optionid: linkcl.get(optionid, propname)
1955 additional_fns.append(fn)
1957 for optionid in options:
1958 # get the option value, and if it's None use an empty string
1959 option = linkcl.get(optionid, k) or ''
1961 # figure if this option is selected
1962 s = ''
1963 if value in [optionid, option]:
1964 s = 'selected="selected" '
1966 # figure the label
1967 if showid:
1968 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1969 elif not option:
1970 lab = '%s%s'%(self._prop.classname, optionid)
1971 else:
1972 lab = option
1974 # truncate if it's too long
1975 if size is not None and len(lab) > size:
1976 lab = lab[:size-3] + '...'
1977 if additional:
1978 m = []
1979 for fn in additional_fns:
1980 m.append(str(fn(optionid)))
1981 lab = lab + ' (%s)'%', '.join(m)
1983 # and generate
1984 lab = cgi.escape(self._(lab))
1985 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
1986 l.append('</select>')
1987 return '\n'.join(l)
1988 # def checklist(self, ...)
1992 class MultilinkHTMLProperty(HTMLProperty):
1993 """ Multilink HTMLProperty
1995 Also be iterable, returning a wrapper object like the Link case for
1996 each entry in the multilink.
1997 """
1998 def __init__(self, *args, **kwargs):
1999 HTMLProperty.__init__(self, *args, **kwargs)
2000 if self._value:
2001 display_value = lookupIds(self._db, self._prop, self._value,
2002 fail_ok=1, do_lookup=False)
2003 sortfun = make_sort_function(self._db, self._prop.classname)
2004 # sorting fails if the value contains
2005 # items not yet stored in the database
2006 # ignore these errors to preserve user input
2007 try:
2008 display_value.sort(sortfun)
2009 except:
2010 pass
2011 self._value = display_value
2013 def __len__(self):
2014 """ length of the multilink """
2015 return len(self._value)
2017 def __getattr__(self, attr):
2018 """ no extended attribute accesses make sense here """
2019 raise AttributeError, attr
2021 def viewableGenerator(self, values):
2022 """Used to iterate over only the View'able items in a class."""
2023 check = self._db.security.hasPermission
2024 userid = self._client.userid
2025 classname = self._prop.classname
2026 for value in values:
2027 if check('View', userid, classname, itemid=value):
2028 yield HTMLItem(self._client, classname, value)
2030 def __iter__(self):
2031 """ iterate and return a new HTMLItem
2032 """
2033 return self.viewableGenerator(self._value)
2035 def reverse(self):
2036 """ return the list in reverse order
2037 """
2038 l = self._value[:]
2039 l.reverse()
2040 return self.viewableGenerator(l)
2042 def sorted(self, property):
2043 """ Return this multilink sorted by the given property """
2044 value = list(self.__iter__())
2045 value.sort(lambda a,b:cmp(a[property], b[property]))
2046 return value
2048 def __contains__(self, value):
2049 """ Support the "in" operator. We have to make sure the passed-in
2050 value is a string first, not a HTMLProperty.
2051 """
2052 return str(value) in self._value
2054 def isset(self):
2055 """Is my _value not []?"""
2056 return self._value != []
2058 def plain(self, escape=0):
2059 """ Render a "plain" representation of the property
2060 """
2061 if not self.is_view_ok():
2062 return self._('[hidden]')
2064 linkcl = self._db.classes[self._prop.classname]
2065 k = linkcl.labelprop(1)
2066 labels = []
2067 for v in self._value:
2068 if num_re.match(v):
2069 try:
2070 label = linkcl.get(v, k)
2071 except IndexError:
2072 label = None
2073 # fall back to designator if label is None
2074 if label is None: label = '%s%s'%(self._prop.classname, k)
2075 else:
2076 label = v
2077 labels.append(label)
2078 value = ', '.join(labels)
2079 if escape:
2080 value = cgi.escape(value)
2081 return value
2083 def field(self, size=30, showid=0):
2084 """ Render a form edit field for the property
2086 If not editable, just display the value via plain().
2087 """
2088 if not self.is_edit_ok():
2089 return self.plain(escape=1)
2091 linkcl = self._db.getclass(self._prop.classname)
2092 value = self._value[:]
2093 # map the id to the label property
2094 if not linkcl.getkey():
2095 showid=1
2096 if not showid:
2097 k = linkcl.labelprop(1)
2098 value = lookupKeys(linkcl, k, value)
2099 value = ','.join(value)
2100 return self.input(name=self._formname, size=size, value=value)
2102 def menu(self, size=None, height=None, showid=0, additional=[],
2103 value=None, sort_on=None, **conditions):
2104 """ Render a form <select> list for this property.
2106 "size" is used to limit the length of the list labels
2107 "height" is used to set the <select> tag's "size" attribute
2108 "showid" includes the item ids in the list labels
2109 "additional" lists properties which should be included in the
2110 label
2111 "value" specifies which item is pre-selected
2112 "sort_on" indicates the property to sort the list on as
2113 (direction, property) where direction is '+' or '-'. A
2114 single string with the direction prepended may be used.
2115 For example: ('-', 'order'), '+name'.
2117 The remaining keyword arguments are used as conditions for
2118 filtering the items in the list - they're passed as the
2119 "filterspec" argument to a Class.filter() call.
2121 If not editable, just display the value via plain().
2122 """
2123 if not self.is_edit_ok():
2124 return self.plain(escape=1)
2126 if value is None:
2127 value = self._value
2129 linkcl = self._db.getclass(self._prop.classname)
2131 if sort_on is not None:
2132 if not isinstance(sort_on, tuple):
2133 if sort_on[0] in '+-':
2134 sort_on = (sort_on[0], sort_on[1:])
2135 else:
2136 sort_on = ('+', sort_on)
2137 else:
2138 sort_on = ('+', linkcl.orderprop())
2140 options = [opt
2141 for opt in linkcl.filter(None, conditions, sort_on)
2142 if self._db.security.hasPermission("View", self._client.userid,
2143 linkcl.classname, itemid=opt)]
2145 # make sure we list the current values if they're retired
2146 for val in value:
2147 if val not in options:
2148 options.insert(0, val)
2150 if not height:
2151 height = len(options)
2152 if value:
2153 # The "no selection" option.
2154 height += 1
2155 height = min(height, 7)
2156 l = ['<select multiple name="%s" size="%s">'%(self._formname, height)]
2157 k = linkcl.labelprop(1)
2159 if value:
2160 l.append('<option value="%s">- no selection -</option>'
2161 % ','.join(['-' + v for v in value]))
2163 if additional:
2164 additional_fns = []
2165 props = linkcl.getprops()
2166 for propname in additional:
2167 prop = props[propname]
2168 if isinstance(prop, hyperdb.Link):
2169 cl = self._db.getclass(prop.classname)
2170 labelprop = cl.labelprop()
2171 fn = lambda optionid: cl.get(linkcl.get(optionid,
2172 propname),
2173 labelprop)
2174 else:
2175 fn = lambda optionid: linkcl.get(optionid, propname)
2176 additional_fns.append(fn)
2178 for optionid in options:
2179 # get the option value, and if it's None use an empty string
2180 option = linkcl.get(optionid, k) or ''
2182 # figure if this option is selected
2183 s = ''
2184 if optionid in value or option in value:
2185 s = 'selected="selected" '
2187 # figure the label
2188 if showid:
2189 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
2190 else:
2191 lab = option
2192 # truncate if it's too long
2193 if size is not None and len(lab) > size:
2194 lab = lab[:size-3] + '...'
2195 if additional:
2196 m = []
2197 for fn in additional_fns:
2198 m.append(str(fn(optionid)))
2199 lab = lab + ' (%s)'%', '.join(m)
2201 # and generate
2202 lab = cgi.escape(self._(lab))
2203 l.append('<option %svalue="%s">%s</option>'%(s, optionid,
2204 lab))
2205 l.append('</select>')
2206 return '\n'.join(l)
2208 # set the propclasses for HTMLItem
2209 propclasses = (
2210 (hyperdb.String, StringHTMLProperty),
2211 (hyperdb.Number, NumberHTMLProperty),
2212 (hyperdb.Boolean, BooleanHTMLProperty),
2213 (hyperdb.Date, DateHTMLProperty),
2214 (hyperdb.Interval, IntervalHTMLProperty),
2215 (hyperdb.Password, PasswordHTMLProperty),
2216 (hyperdb.Link, LinkHTMLProperty),
2217 (hyperdb.Multilink, MultilinkHTMLProperty),
2218 )
2220 def make_sort_function(db, classname, sort_on=None):
2221 """Make a sort function for a given class
2222 """
2223 linkcl = db.getclass(classname)
2224 if sort_on is None:
2225 sort_on = linkcl.orderprop()
2226 def sortfunc(a, b):
2227 return cmp(linkcl.get(a, sort_on), linkcl.get(b, sort_on))
2228 return sortfunc
2230 def handleListCGIValue(value):
2231 """ Value is either a single item or a list of items. Each item has a
2232 .value that we're actually interested in.
2233 """
2234 if isinstance(value, type([])):
2235 return [value.value for value in value]
2236 else:
2237 value = value.value.strip()
2238 if not value:
2239 return []
2240 return [v.strip() for v in value.split(',')]
2242 class HTMLRequest(HTMLInputMixin):
2243 """The *request*, holding the CGI form and environment.
2245 - "form" the CGI form as a cgi.FieldStorage
2246 - "env" the CGI environment variables
2247 - "base" the base URL for this instance
2248 - "user" a HTMLItem instance for this user
2249 - "language" as determined by the browser or config
2250 - "classname" the current classname (possibly None)
2251 - "template" the current template (suffix, also possibly None)
2253 Index args:
2255 - "columns" dictionary of the columns to display in an index page
2256 - "show" a convenience access to columns - request/show/colname will
2257 be true if the columns should be displayed, false otherwise
2258 - "sort" index sort column (direction, column name)
2259 - "group" index grouping property (direction, column name)
2260 - "filter" properties to filter the index on
2261 - "filterspec" values to filter the index on
2262 - "search_text" text to perform a full-text search on for an index
2263 """
2264 def __repr__(self):
2265 return '<HTMLRequest %r>'%self.__dict__
2267 def __init__(self, client):
2268 # _client is needed by HTMLInputMixin
2269 self._client = self.client = client
2271 # easier access vars
2272 self.form = client.form
2273 self.env = client.env
2274 self.base = client.base
2275 self.user = HTMLItem(client, 'user', client.userid)
2276 self.language = client.language
2278 # store the current class name and action
2279 self.classname = client.classname
2280 self.nodeid = client.nodeid
2281 self.template = client.template
2283 # the special char to use for special vars
2284 self.special_char = '@'
2286 HTMLInputMixin.__init__(self)
2288 self._post_init()
2290 def current_url(self):
2291 url = self.base
2292 if self.classname:
2293 url += self.classname
2294 if self.nodeid:
2295 url += self.nodeid
2296 args = {}
2297 if self.template:
2298 args['@template'] = self.template
2299 return self.indexargs_url(url, args)
2301 def _parse_sort(self, var, name):
2302 """ Parse sort/group options. Append to var
2303 """
2304 fields = []
2305 dirs = []
2306 for special in '@:':
2307 idx = 0
2308 key = '%s%s%d'%(special, name, idx)
2309 while key in self.form:
2310 self.special_char = special
2311 fields.append(self.form.getfirst(key))
2312 dirkey = '%s%sdir%d'%(special, name, idx)
2313 if dirkey in self.form:
2314 dirs.append(self.form.getfirst(dirkey))
2315 else:
2316 dirs.append(None)
2317 idx += 1
2318 key = '%s%s%d'%(special, name, idx)
2319 # backward compatible (and query) URL format
2320 key = special + name
2321 dirkey = key + 'dir'
2322 if key in self.form and not fields:
2323 fields = handleListCGIValue(self.form[key])
2324 if dirkey in self.form:
2325 dirs.append(self.form.getfirst(dirkey))
2326 if fields: # only try other special char if nothing found
2327 break
2328 for f, d in map(None, fields, dirs):
2329 if f.startswith('-'):
2330 var.append(('-', f[1:]))
2331 elif d:
2332 var.append(('-', f))
2333 else:
2334 var.append(('+', f))
2336 def _post_init(self):
2337 """ Set attributes based on self.form
2338 """
2339 # extract the index display information from the form
2340 self.columns = []
2341 for name in ':columns @columns'.split():
2342 if self.form.has_key(name):
2343 self.special_char = name[0]
2344 self.columns = handleListCGIValue(self.form[name])
2345 break
2346 self.show = support.TruthDict(self.columns)
2348 # sorting and grouping
2349 self.sort = []
2350 self.group = []
2351 self._parse_sort(self.sort, 'sort')
2352 self._parse_sort(self.group, 'group')
2354 # filtering
2355 self.filter = []
2356 for name in ':filter @filter'.split():
2357 if self.form.has_key(name):
2358 self.special_char = name[0]
2359 self.filter = handleListCGIValue(self.form[name])
2361 self.filterspec = {}
2362 db = self.client.db
2363 if self.classname is not None:
2364 cls = db.getclass (self.classname)
2365 for name in self.filter:
2366 if not self.form.has_key(name):
2367 continue
2368 prop = cls.get_transitive_prop (name)
2369 fv = self.form[name]
2370 if (isinstance(prop, hyperdb.Link) or
2371 isinstance(prop, hyperdb.Multilink)):
2372 self.filterspec[name] = lookupIds(db, prop,
2373 handleListCGIValue(fv))
2374 else:
2375 if isinstance(fv, type([])):
2376 self.filterspec[name] = [v.value for v in fv]
2377 elif name == 'id':
2378 # special case "id" property
2379 self.filterspec[name] = handleListCGIValue(fv)
2380 else:
2381 self.filterspec[name] = fv.value
2383 # full-text search argument
2384 self.search_text = None
2385 for name in ':search_text @search_text'.split():
2386 if self.form.has_key(name):
2387 self.special_char = name[0]
2388 self.search_text = self.form.getfirst(name)
2390 # pagination - size and start index
2391 # figure batch args
2392 self.pagesize = 50
2393 for name in ':pagesize @pagesize'.split():
2394 if self.form.has_key(name):
2395 self.special_char = name[0]
2396 self.pagesize = int(self.form.getfirst(name))
2398 self.startwith = 0
2399 for name in ':startwith @startwith'.split():
2400 if self.form.has_key(name):
2401 self.special_char = name[0]
2402 self.startwith = int(self.form.getfirst(name))
2404 # dispname
2405 if self.form.has_key('@dispname'):
2406 self.dispname = self.form.getfirst('@dispname')
2407 else:
2408 self.dispname = None
2410 def updateFromURL(self, url):
2411 """ Parse the URL for query args, and update my attributes using the
2412 values.
2413 """
2414 env = {'QUERY_STRING': url}
2415 self.form = cgi.FieldStorage(environ=env)
2417 self._post_init()
2419 def update(self, kwargs):
2420 """ Update my attributes using the keyword args
2421 """
2422 self.__dict__.update(kwargs)
2423 if kwargs.has_key('columns'):
2424 self.show = support.TruthDict(self.columns)
2426 def description(self):
2427 """ Return a description of the request - handle for the page title.
2428 """
2429 s = [self.client.db.config.TRACKER_NAME]
2430 if self.classname:
2431 if self.client.nodeid:
2432 s.append('- %s%s'%(self.classname, self.client.nodeid))
2433 else:
2434 if self.template == 'item':
2435 s.append('- new %s'%self.classname)
2436 elif self.template == 'index':
2437 s.append('- %s index'%self.classname)
2438 else:
2439 s.append('- %s %s'%(self.classname, self.template))
2440 else:
2441 s.append('- home')
2442 return ' '.join(s)
2444 def __str__(self):
2445 d = {}
2446 d.update(self.__dict__)
2447 f = ''
2448 for k in self.form.keys():
2449 f += '\n %r=%r'%(k,handleListCGIValue(self.form[k]))
2450 d['form'] = f
2451 e = ''
2452 for k,v in self.env.items():
2453 e += '\n %r=%r'%(k, v)
2454 d['env'] = e
2455 return """
2456 form: %(form)s
2457 base: %(base)r
2458 classname: %(classname)r
2459 template: %(template)r
2460 columns: %(columns)r
2461 sort: %(sort)r
2462 group: %(group)r
2463 filter: %(filter)r
2464 search_text: %(search_text)r
2465 pagesize: %(pagesize)r
2466 startwith: %(startwith)r
2467 env: %(env)s
2468 """%d
2470 def indexargs_form(self, columns=1, sort=1, group=1, filter=1,
2471 filterspec=1, search_text=1):
2472 """ return the current index args as form elements """
2473 l = []
2474 sc = self.special_char
2475 def add(k, v):
2476 l.append(self.input(type="hidden", name=k, value=v))
2477 if columns and self.columns:
2478 add(sc+'columns', ','.join(self.columns))
2479 if sort:
2480 val = []
2481 for dir, attr in self.sort:
2482 if dir == '-':
2483 val.append('-'+attr)
2484 else:
2485 val.append(attr)
2486 add(sc+'sort', ','.join (val))
2487 if group:
2488 val = []
2489 for dir, attr in self.group:
2490 if dir == '-':
2491 val.append('-'+attr)
2492 else:
2493 val.append(attr)
2494 add(sc+'group', ','.join (val))
2495 if filter and self.filter:
2496 add(sc+'filter', ','.join(self.filter))
2497 if self.classname and filterspec:
2498 cls = self.client.db.getclass(self.classname)
2499 for k,v in self.filterspec.items():
2500 if type(v) == type([]):
2501 if isinstance(cls.get_transitive_prop(k), hyperdb.String):
2502 add(k, ' '.join(v))
2503 else:
2504 add(k, ','.join(v))
2505 else:
2506 add(k, v)
2507 if search_text and self.search_text:
2508 add(sc+'search_text', self.search_text)
2509 add(sc+'pagesize', self.pagesize)
2510 add(sc+'startwith', self.startwith)
2511 return '\n'.join(l)
2513 def indexargs_url(self, url, args):
2514 """ Embed the current index args in a URL
2515 """
2516 q = urllib.quote
2517 sc = self.special_char
2518 l = ['%s=%s'%(k,v) for k,v in args.items()]
2520 # pull out the special values (prefixed by @ or :)
2521 specials = {}
2522 for key in args.keys():
2523 if key[0] in '@:':
2524 specials[key[1:]] = args[key]
2526 # ok, now handle the specials we received in the request
2527 if self.columns and not specials.has_key('columns'):
2528 l.append(sc+'columns=%s'%(','.join(self.columns)))
2529 if self.sort and not specials.has_key('sort'):
2530 val = []
2531 for dir, attr in self.sort:
2532 if dir == '-':
2533 val.append('-'+attr)
2534 else:
2535 val.append(attr)
2536 l.append(sc+'sort=%s'%(','.join(val)))
2537 if self.group and not specials.has_key('group'):
2538 val = []
2539 for dir, attr in self.group:
2540 if dir == '-':
2541 val.append('-'+attr)
2542 else:
2543 val.append(attr)
2544 l.append(sc+'group=%s'%(','.join(val)))
2545 if self.filter and not specials.has_key('filter'):
2546 l.append(sc+'filter=%s'%(','.join(self.filter)))
2547 if self.search_text and not specials.has_key('search_text'):
2548 l.append(sc+'search_text=%s'%q(self.search_text))
2549 if not specials.has_key('pagesize'):
2550 l.append(sc+'pagesize=%s'%self.pagesize)
2551 if not specials.has_key('startwith'):
2552 l.append(sc+'startwith=%s'%self.startwith)
2554 # finally, the remainder of the filter args in the request
2555 if self.classname and self.filterspec:
2556 cls = self.client.db.getclass(self.classname)
2557 for k,v in self.filterspec.items():
2558 if not args.has_key(k):
2559 if type(v) == type([]):
2560 prop = cls.get_transitive_prop(k)
2561 if k != 'id' and isinstance(prop, hyperdb.String):
2562 l.append('%s=%s'%(k, '%20'.join([q(i) for i in v])))
2563 else:
2564 l.append('%s=%s'%(k, ','.join([q(i) for i in v])))
2565 else:
2566 l.append('%s=%s'%(k, q(v)))
2567 return '%s?%s'%(url, '&'.join(l))
2568 indexargs_href = indexargs_url
2570 def base_javascript(self):
2571 return """
2572 <script type="text/javascript">
2573 submitted = false;
2574 function submit_once() {
2575 if (submitted) {
2576 alert("Your request is being processed.\\nPlease be patient.");
2577 event.returnValue = 0; // work-around for IE
2578 return 0;
2579 }
2580 submitted = true;
2581 return 1;
2582 }
2584 function help_window(helpurl, width, height) {
2585 HelpWin = window.open('%s' + helpurl, 'RoundupHelpWindow', 'scrollbars=yes,resizable=yes,toolbar=no,height='+height+',width='+width);
2586 }
2587 </script>
2588 """%self.base
2590 def batch(self):
2591 """ Return a batch object for results from the "current search"
2592 """
2593 filterspec = self.filterspec
2594 sort = self.sort
2595 group = self.group
2597 # get the list of ids we're batching over
2598 klass = self.client.db.getclass(self.classname)
2599 if self.search_text:
2600 matches = self.client.db.indexer.search(
2601 [w.upper().encode("utf-8", "replace") for w in re.findall(
2602 r'(?u)\b\w{2,25}\b',
2603 unicode(self.search_text, "utf-8", "replace")
2604 )], klass)
2605 else:
2606 matches = None
2608 # filter for visibility
2609 check = self._client.db.security.hasPermission
2610 userid = self._client.userid
2611 l = [id for id in klass.filter(matches, filterspec, sort, group)
2612 if check('View', userid, self.classname, itemid=id)]
2614 # return the batch object, using IDs only
2615 return Batch(self.client, l, self.pagesize, self.startwith,
2616 classname=self.classname)
2618 # extend the standard ZTUtils Batch object to remove dependency on
2619 # Acquisition and add a couple of useful methods
2620 class Batch(ZTUtils.Batch):
2621 """ Use me to turn a list of items, or item ids of a given class, into a
2622 series of batches.
2624 ========= ========================================================
2625 Parameter Usage
2626 ========= ========================================================
2627 sequence a list of HTMLItems or item ids
2628 classname if sequence is a list of ids, this is the class of item
2629 size how big to make the sequence.
2630 start where to start (0-indexed) in the sequence.
2631 end where to end (0-indexed) in the sequence.
2632 orphan if the next batch would contain less items than this
2633 value, then it is combined with this batch
2634 overlap the number of items shared between adjacent batches
2635 ========= ========================================================
2637 Attributes: Note that the "start" attribute, unlike the
2638 argument, is a 1-based index (I know, lame). "first" is the
2639 0-based index. "length" is the actual number of elements in
2640 the batch.
2642 "sequence_length" is the length of the original, unbatched, sequence.
2643 """
2644 def __init__(self, client, sequence, size, start, end=0, orphan=0,
2645 overlap=0, classname=None):
2646 self.client = client
2647 self.last_index = self.last_item = None
2648 self.current_item = None
2649 self.classname = classname
2650 self.sequence_length = len(sequence)
2651 ZTUtils.Batch.__init__(self, sequence, size, start, end, orphan,
2652 overlap)
2654 # overwrite so we can late-instantiate the HTMLItem instance
2655 def __getitem__(self, index):
2656 if index < 0:
2657 if index + self.end < self.first: raise IndexError, index
2658 return self._sequence[index + self.end]
2660 if index >= self.length:
2661 raise IndexError, index
2663 # move the last_item along - but only if the fetched index changes
2664 # (for some reason, index 0 is fetched twice)
2665 if index != self.last_index:
2666 self.last_item = self.current_item
2667 self.last_index = index
2669 item = self._sequence[index + self.first]
2670 if self.classname:
2671 # map the item ids to instances
2672 item = HTMLItem(self.client, self.classname, item)
2673 self.current_item = item
2674 return item
2676 def propchanged(self, *properties):
2677 """ Detect if one of the properties marked as being a group
2678 property changed in the last iteration fetch
2679 """
2680 # we poke directly at the _value here since MissingValue can screw
2681 # us up and cause Nones to compare strangely
2682 if self.last_item is None:
2683 return 1
2684 for property in properties:
2685 if property == 'id' or isinstance (self.last_item[property], list):
2686 if (str(self.last_item[property]) !=
2687 str(self.current_item[property])):
2688 return 1
2689 else:
2690 if (self.last_item[property]._value !=
2691 self.current_item[property]._value):
2692 return 1
2693 return 0
2695 # override these 'cos we don't have access to acquisition
2696 def previous(self):
2697 if self.start == 1:
2698 return None
2699 return Batch(self.client, self._sequence, self._size,
2700 self.first - self._size + self.overlap, 0, self.orphan,
2701 self.overlap)
2703 def next(self):
2704 try:
2705 self._sequence[self.end]
2706 except IndexError:
2707 return None
2708 return Batch(self.client, self._sequence, self._size,
2709 self.end - self.overlap, 0, self.orphan, self.overlap)
2711 class TemplatingUtils:
2712 """ Utilities for templating
2713 """
2714 def __init__(self, client):
2715 self.client = client
2716 def Batch(self, sequence, size, start, end=0, orphan=0, overlap=0):
2717 return Batch(self.client, sequence, size, start, end, orphan,
2718 overlap)
2720 def url_quote(self, url):
2721 """URL-quote the supplied text."""
2722 return urllib.quote(url)
2724 def html_quote(self, html):
2725 """HTML-quote the supplied text."""
2726 return cgi.escape(html)
2728 def __getattr__(self, name):
2729 """Try the tracker's templating_utils."""
2730 if not hasattr(self.client.instance, 'templating_utils'):
2731 # backwards-compatibility
2732 raise AttributeError, name
2733 if not self.client.instance.templating_utils.has_key(name):
2734 raise AttributeError, name
2735 return self.client.instance.templating_utils[name]
2737 def html_calendar(self, request):
2738 """Generate a HTML calendar.
2740 `request` the roundup.request object
2741 - @template : name of the template
2742 - form : name of the form to store back the date
2743 - property : name of the property of the form to store
2744 back the date
2745 - date : current date
2746 - display : when browsing, specifies year and month
2748 html will simply be a table.
2749 """
2750 date_str = request.form.getfirst("date", ".")
2751 display = request.form.getfirst("display", date_str)
2752 template = request.form.getfirst("@template", "calendar")
2753 form = request.form.getfirst("form")
2754 property = request.form.getfirst("property")
2755 curr_date = date.Date(date_str) # to highlight
2756 display = date.Date(display) # to show
2757 day = display.day
2759 # for navigation
2760 date_prev_month = display + date.Interval("-1m")
2761 date_next_month = display + date.Interval("+1m")
2762 date_prev_year = display + date.Interval("-1y")
2763 date_next_year = display + date.Interval("+1y")
2765 res = []
2767 base_link = "%s?@template=%s&property=%s&form=%s&date=%s" % \
2768 (request.classname, template, property, form, curr_date)
2770 # navigation
2771 # month
2772 res.append('<table class="calendar"><tr><td>')
2773 res.append(' <table width="100%" class="calendar_nav"><tr>')
2774 link = "&display=%s"%date_prev_month
2775 res.append(' <td><a href="%s&display=%s"><</a></td>'%(base_link,
2776 date_prev_month))
2777 res.append(' <td>%s</td>'%calendar.month_name[display.month])
2778 res.append(' <td><a href="%s&display=%s">></a></td>'%(base_link,
2779 date_next_month))
2780 # spacer
2781 res.append(' <td width="100%"></td>')
2782 # year
2783 res.append(' <td><a href="%s&display=%s"><</a></td>'%(base_link,
2784 date_prev_year))
2785 res.append(' <td>%s</td>'%display.year)
2786 res.append(' <td><a href="%s&display=%s">></a></td>'%(base_link,
2787 date_next_year))
2788 res.append(' </tr></table>')
2789 res.append(' </td></tr>')
2791 # the calendar
2792 res.append(' <tr><td><table class="calendar_display">')
2793 res.append(' <tr class="weekdays">')
2794 for day in calendar.weekheader(3).split():
2795 res.append(' <td>%s</td>'%day)
2796 res.append(' </tr>')
2797 for week in calendar.monthcalendar(display.year, display.month):
2798 res.append(' <tr>')
2799 for day in week:
2800 link = "javascript:form[field].value = '%d-%02d-%02d'; " \
2801 "window.close ();"%(display.year, display.month, day)
2802 if (day == curr_date.day and display.month == curr_date.month
2803 and display.year == curr_date.year):
2804 # highlight
2805 style = "today"
2806 else :
2807 style = ""
2808 if day:
2809 res.append(' <td class="%s"><a href="%s">%s</a></td>'%(
2810 style, link, day))
2811 else :
2812 res.append(' <td></td>')
2813 res.append(' </tr>')
2814 res.append('</table></td></tr></table>')
2815 return "\n".join(res)
2817 class MissingValue:
2818 def __init__(self, description, **kwargs):
2819 self.__description = description
2820 for key, value in kwargs.items():
2821 self.__dict__[key] = value
2823 def __call__(self, *args, **kwargs): return MissingValue(self.__description)
2824 def __getattr__(self, name):
2825 # This allows assignments which assume all intermediate steps are Null
2826 # objects if they don't exist yet.
2827 #
2828 # For example (with just 'client' defined):
2829 #
2830 # client.db.config.TRACKER_WEB = 'BASE/'
2831 self.__dict__[name] = MissingValue(self.__description)
2832 return getattr(self, name)
2834 def __getitem__(self, key): return self
2835 def __nonzero__(self): return 0
2836 def __str__(self): return '[%s]'%self.__description
2837 def __repr__(self): return '<MissingValue 0x%x "%s">'%(id(self),
2838 self.__description)
2839 def gettext(self, str): return str
2840 _ = gettext
2842 # vim: set et sts=4 sw=4 :