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