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)
1273 def is_view_ok(self):
1274 """ Is the user allowed to View the current class?
1275 """
1276 if self._db.security.hasPermission('View', self._client.userid,
1277 self._classname, self._name, self._nodeid):
1278 return 1
1279 return self.is_edit_ok()
1281 class StringHTMLProperty(HTMLProperty):
1282 hyper_re = re.compile(r'''(
1283 (?P<url>
1284 (
1285 (ht|f)tp(s?):// # protocol
1286 ([\w]+(:\w+)?@)? # username/password
1287 ([\w\-]+) # hostname
1288 ((\.[\w-]+)+)? # .domain.etc
1289 | # ... or ...
1290 ([\w]+(:\w+)?@)? # username/password
1291 www\. # "www."
1292 ([\w\-]+\.)+ # hostname
1293 [\w]{2,5} # TLD
1294 )
1295 (:[\d]{1,5})? # port
1296 (/[\w\-$.+!*(),;:@&=?/~\\#%]*)? # path etc.
1297 )|
1298 (?P<email>[-+=%/\w\.]+@[\w\.\-]+)|
1299 (?P<item>(?P<class>[A-Za-z_]+)(\s*)(?P<id>\d+))
1300 )''', re.X | re.I)
1301 protocol_re = re.compile('^(ht|f)tp(s?)://', re.I)
1303 def _hyper_repl_item(self,match,replacement):
1304 item = match.group('item')
1305 cls = match.group('class').lower()
1306 id = match.group('id')
1307 try:
1308 # make sure cls is a valid tracker classname
1309 cl = self._db.getclass(cls)
1310 if not cl.hasnode(id):
1311 return item
1312 return replacement % locals()
1313 except KeyError:
1314 return item
1316 def _hyper_repl(self, match):
1317 if match.group('url'):
1318 u = s = match.group('url')
1319 if not self.protocol_re.search(s):
1320 u = 'http://' + s
1321 # catch an escaped ">" at the end of the URL
1322 if s.endswith('>'):
1323 u = s = s[:-4]
1324 e = '>'
1325 else:
1326 e = ''
1327 return '<a href="%s">%s</a>%s'%(u, s, e)
1328 elif match.group('email'):
1329 s = match.group('email')
1330 return '<a href="mailto:%s">%s</a>'%(s, s)
1331 else:
1332 return self._hyper_repl_item(match,
1333 '<a href="%(cls)s%(id)s">%(item)s</a>')
1335 def _hyper_repl_rst(self, match):
1336 if match.group('url'):
1337 s = match.group('url')
1338 return '`%s <%s>`_'%(s, s)
1339 elif match.group('email'):
1340 s = match.group('email')
1341 return '`%s <mailto:%s>`_'%(s, s)
1342 else:
1343 return self._hyper_repl_item(match,'`%(item)s <%(cls)s%(id)s>`_')
1345 def hyperlinked(self):
1346 """ Render a "hyperlinked" version of the text """
1347 return self.plain(hyperlink=1)
1349 def plain(self, escape=0, hyperlink=0):
1350 """Render a "plain" representation of the property
1352 - "escape" turns on/off HTML quoting
1353 - "hyperlink" turns on/off in-text hyperlinking of URLs, email
1354 addresses and designators
1355 """
1356 if not self.is_view_ok():
1357 return self._('[hidden]')
1359 if self._value is None:
1360 return ''
1361 if escape:
1362 s = cgi.escape(str(self._value))
1363 else:
1364 s = str(self._value)
1365 if hyperlink:
1366 # no, we *must* escape this text
1367 if not escape:
1368 s = cgi.escape(s)
1369 s = self.hyper_re.sub(self._hyper_repl, s)
1370 return s
1372 def wrapped(self, escape=1, hyperlink=1):
1373 """Render a "wrapped" representation of the property.
1375 We wrap long lines at 80 columns on the nearest whitespace. Lines
1376 with no whitespace are not broken to force wrapping.
1378 Note that unlike plain() we default wrapped() to have the escaping
1379 and hyperlinking turned on since that's the most common usage.
1381 - "escape" turns on/off HTML quoting
1382 - "hyperlink" turns on/off in-text hyperlinking of URLs, email
1383 addresses and designators
1384 """
1385 if not self.is_view_ok():
1386 return self._('[hidden]')
1388 if self._value is None:
1389 return ''
1390 s = support.wrap(str(self._value), width=80)
1391 if escape:
1392 s = cgi.escape(s)
1393 if hyperlink:
1394 # no, we *must* escape this text
1395 if not escape:
1396 s = cgi.escape(s)
1397 s = self.hyper_re.sub(self._hyper_repl, s)
1398 return s
1400 def stext(self, escape=0, hyperlink=1):
1401 """ Render the value of the property as StructuredText.
1403 This requires the StructureText module to be installed separately.
1404 """
1405 if not self.is_view_ok():
1406 return self._('[hidden]')
1408 s = self.plain(escape=escape, hyperlink=hyperlink)
1409 if not StructuredText:
1410 return s
1411 return StructuredText(s,level=1,header=0)
1413 def rst(self, hyperlink=1):
1414 """ Render the value of the property as ReStructuredText.
1416 This requires docutils to be installed separately.
1417 """
1418 if not self.is_view_ok():
1419 return self._('[hidden]')
1421 if not ReStructuredText:
1422 return self.plain(escape=0, hyperlink=hyperlink)
1423 s = self.plain(escape=0, hyperlink=0)
1424 if hyperlink:
1425 s = self.hyper_re.sub(self._hyper_repl_rst, s)
1426 return ReStructuredText(s, writer_name="html")["html_body"].encode("utf-8",
1427 "replace")
1429 def field(self, **kwargs):
1430 """ Render the property as a field in HTML.
1432 If not editable, just display the value via plain().
1433 """
1434 if not self.is_edit_ok():
1435 return self.plain(escape=1)
1437 value = self._value
1438 if value is None:
1439 value = ''
1441 kwargs.setdefault("size", 30)
1442 kwargs.update({"name": self._formname, "value": value})
1443 return self.input(**kwargs)
1445 def multiline(self, escape=0, rows=5, cols=40, **kwargs):
1446 """ Render a multiline form edit field for the property.
1448 If not editable, just display the plain() value in a <pre> tag.
1449 """
1450 if not self.is_edit_ok():
1451 return '<pre>%s</pre>'%self.plain()
1453 if self._value is None:
1454 value = ''
1455 else:
1456 value = cgi.escape(str(self._value))
1458 value = '"'.join(value.split('"'))
1459 name = self._formname
1460 passthrough_args = ' '.join(['%s="%s"' % (k, cgi.escape(str(v), True))
1461 for k,v in kwargs.items()])
1462 return ('<textarea %(passthrough_args)s name="%(name)s" id="%(name)s"'
1463 ' rows="%(rows)s" cols="%(cols)s">'
1464 '%(value)s</textarea>') % locals()
1466 def email(self, escape=1):
1467 """ Render the value of the property as an obscured email address
1468 """
1469 if not self.is_view_ok():
1470 return self._('[hidden]')
1472 if self._value is None:
1473 value = ''
1474 else:
1475 value = str(self._value)
1476 split = value.split('@')
1477 if len(split) == 2:
1478 name, domain = split
1479 domain = ' '.join(domain.split('.')[:-1])
1480 name = name.replace('.', ' ')
1481 value = '%s at %s ...'%(name, domain)
1482 else:
1483 value = value.replace('.', ' ')
1484 if escape:
1485 value = cgi.escape(value)
1486 return value
1488 class PasswordHTMLProperty(HTMLProperty):
1489 def plain(self, escape=0):
1490 """ Render a "plain" representation of the property
1491 """
1492 if not self.is_view_ok():
1493 return self._('[hidden]')
1495 if self._value is None:
1496 return ''
1497 return self._('*encrypted*')
1499 def field(self, size=30):
1500 """ Render a form edit field for the property.
1502 If not editable, just display the value via plain().
1503 """
1504 if not self.is_edit_ok():
1505 return self.plain(escape=1)
1507 return self.input(type="password", name=self._formname, size=size)
1509 def confirm(self, size=30):
1510 """ Render a second form edit field for the property, used for
1511 confirmation that the user typed the password correctly. Generates
1512 a field with name "@confirm@name".
1514 If not editable, display nothing.
1515 """
1516 if not self.is_edit_ok():
1517 return ''
1519 return self.input(type="password",
1520 name="@confirm@%s"%self._formname,
1521 id="%s-confirm"%self._formname,
1522 size=size)
1524 class NumberHTMLProperty(HTMLProperty):
1525 def plain(self, escape=0):
1526 """ Render a "plain" representation of the property
1527 """
1528 if not self.is_view_ok():
1529 return self._('[hidden]')
1531 if self._value is None:
1532 return ''
1534 return str(self._value)
1536 def field(self, size=30):
1537 """ Render a form edit field for the property.
1539 If not editable, just display the value via plain().
1540 """
1541 if not self.is_edit_ok():
1542 return self.plain(escape=1)
1544 value = self._value
1545 if value is None:
1546 value = ''
1548 return self.input(name=self._formname, value=value, size=size)
1550 def __int__(self):
1551 """ Return an int of me
1552 """
1553 return int(self._value)
1555 def __float__(self):
1556 """ Return a float of me
1557 """
1558 return float(self._value)
1561 class BooleanHTMLProperty(HTMLProperty):
1562 def plain(self, escape=0):
1563 """ Render a "plain" representation of the property
1564 """
1565 if not self.is_view_ok():
1566 return self._('[hidden]')
1568 if self._value is None:
1569 return ''
1570 return self._value and self._("Yes") or self._("No")
1572 def field(self):
1573 """ Render a form edit field for the property
1575 If not editable, just display the value via plain().
1576 """
1577 if not self.is_edit_ok():
1578 return self.plain(escape=1)
1580 value = self._value
1581 if isinstance(value, str) or isinstance(value, unicode):
1582 value = value.strip().lower() in ('checked', 'yes', 'true',
1583 'on', '1')
1585 checked = value and "checked" or ""
1586 if value:
1587 s = self.input(type="radio", name=self._formname, value="yes",
1588 checked="checked")
1589 s += self._('Yes')
1590 s +=self.input(type="radio", name=self._formname, value="no")
1591 s += self._('No')
1592 else:
1593 s = self.input(type="radio", name=self._formname, value="yes")
1594 s += self._('Yes')
1595 s +=self.input(type="radio", name=self._formname, value="no",
1596 checked="checked")
1597 s += self._('No')
1598 return s
1600 class DateHTMLProperty(HTMLProperty):
1602 _marker = []
1604 def __init__(self, client, classname, nodeid, prop, name, value,
1605 anonymous=0, offset=None):
1606 HTMLProperty.__init__(self, client, classname, nodeid, prop, name,
1607 value, anonymous=anonymous)
1608 if self._value and not (isinstance(self._value, str) or
1609 isinstance(self._value, unicode)):
1610 self._value.setTranslator(self._client.translator)
1611 self._offset = offset
1612 if self._offset is None :
1613 self._offset = self._prop.offset (self._db)
1615 def plain(self, escape=0):
1616 """ Render a "plain" representation of the property
1617 """
1618 if not self.is_view_ok():
1619 return self._('[hidden]')
1621 if self._value is None:
1622 return ''
1623 if self._offset is None:
1624 offset = self._db.getUserTimezone()
1625 else:
1626 offset = self._offset
1627 return str(self._value.local(offset))
1629 def now(self, str_interval=None):
1630 """ Return the current time.
1632 This is useful for defaulting a new value. Returns a
1633 DateHTMLProperty.
1634 """
1635 if not self.is_view_ok():
1636 return self._('[hidden]')
1638 ret = date.Date('.', translator=self._client)
1640 if isinstance(str_interval, basestring):
1641 sign = 1
1642 if str_interval[0] == '-':
1643 sign = -1
1644 str_interval = str_interval[1:]
1645 interval = date.Interval(str_interval, translator=self._client)
1646 if sign > 0:
1647 ret = ret + interval
1648 else:
1649 ret = ret - interval
1651 return DateHTMLProperty(self._client, self._classname, self._nodeid,
1652 self._prop, self._formname, ret)
1654 def field(self, size=30, default=None, format=_marker, popcal=True):
1655 """Render a form edit field for the property
1657 If not editable, just display the value via plain().
1659 If "popcal" then include the Javascript calendar editor.
1660 Default=yes.
1662 The format string is a standard python strftime format string.
1663 """
1664 if not self.is_edit_ok():
1665 if format is self._marker:
1666 return self.plain(escape=1)
1667 else:
1668 return self.pretty(format)
1670 value = self._value
1672 if value is None:
1673 if default is None:
1674 raw_value = None
1675 else:
1676 if isinstance(default, basestring):
1677 raw_value = date.Date(default, translator=self._client)
1678 elif isinstance(default, date.Date):
1679 raw_value = default
1680 elif isinstance(default, DateHTMLProperty):
1681 raw_value = default._value
1682 else:
1683 raise ValueError, self._('default value for '
1684 'DateHTMLProperty must be either DateHTMLProperty '
1685 'or string date representation.')
1686 elif isinstance(value, str) or isinstance(value, unicode):
1687 # most likely erroneous input to be passed back to user
1688 if isinstance(value, unicode): value = value.encode('utf8')
1689 return self.input(name=self._formname, value=value, size=size)
1690 else:
1691 raw_value = value
1693 if raw_value is None:
1694 value = ''
1695 elif isinstance(raw_value, str) or isinstance(raw_value, unicode):
1696 if format is self._marker:
1697 value = raw_value
1698 else:
1699 value = date.Date(raw_value).pretty(format)
1700 else:
1701 if self._offset is None :
1702 offset = self._db.getUserTimezone()
1703 else :
1704 offset = self._offset
1705 value = raw_value.local(offset)
1706 if format is not self._marker:
1707 value = value.pretty(format)
1709 s = self.input(name=self._formname, value=value, size=size)
1710 if popcal:
1711 s += self.popcal()
1712 return s
1714 def reldate(self, pretty=1):
1715 """ Render the interval between the date and now.
1717 If the "pretty" flag is true, then make the display pretty.
1718 """
1719 if not self.is_view_ok():
1720 return self._('[hidden]')
1722 if not self._value:
1723 return ''
1725 # figure the interval
1726 interval = self._value - date.Date('.', translator=self._client)
1727 if pretty:
1728 return interval.pretty()
1729 return str(interval)
1731 def pretty(self, format=_marker):
1732 """ Render the date in a pretty format (eg. month names, spaces).
1734 The format string is a standard python strftime format string.
1735 Note that if the day is zero, and appears at the start of the
1736 string, then it'll be stripped from the output. This is handy
1737 for the situation when a date only specifies a month and a year.
1738 """
1739 if not self.is_view_ok():
1740 return self._('[hidden]')
1742 if self._offset is None:
1743 offset = self._db.getUserTimezone()
1744 else:
1745 offset = self._offset
1747 if not self._value:
1748 return ''
1749 elif format is not self._marker:
1750 return self._value.local(offset).pretty(format)
1751 else:
1752 return self._value.local(offset).pretty()
1754 def local(self, offset):
1755 """ Return the date/time as a local (timezone offset) date/time.
1756 """
1757 if not self.is_view_ok():
1758 return self._('[hidden]')
1760 return DateHTMLProperty(self._client, self._classname, self._nodeid,
1761 self._prop, self._formname, self._value, offset=offset)
1763 def popcal(self, width=300, height=200, label="(cal)",
1764 form="itemSynopsis"):
1765 """Generate a link to a calendar pop-up window.
1767 item: HTMLProperty e.g.: context.deadline
1768 """
1769 if self.isset():
1770 date = "&date=%s"%self._value
1771 else :
1772 date = ""
1773 return ('<a class="classhelp" href="javascript:help_window('
1774 "'%s?@template=calendar&property=%s&form=%s%s', %d, %d)"
1775 '">%s</a>'%(self._classname, self._name, form, date, width,
1776 height, label))
1778 class IntervalHTMLProperty(HTMLProperty):
1779 def __init__(self, client, classname, nodeid, prop, name, value,
1780 anonymous=0):
1781 HTMLProperty.__init__(self, client, classname, nodeid, prop,
1782 name, value, anonymous)
1783 if self._value and not isinstance(self._value, (str, unicode)):
1784 self._value.setTranslator(self._client.translator)
1786 def plain(self, escape=0):
1787 """ Render a "plain" representation of the property
1788 """
1789 if not self.is_view_ok():
1790 return self._('[hidden]')
1792 if self._value is None:
1793 return ''
1794 return str(self._value)
1796 def pretty(self):
1797 """ Render the interval in a pretty format (eg. "yesterday")
1798 """
1799 if not self.is_view_ok():
1800 return self._('[hidden]')
1802 return self._value.pretty()
1804 def field(self, size=30):
1805 """ Render a form edit field for the property
1807 If not editable, just display the value via plain().
1808 """
1809 if not self.is_edit_ok():
1810 return self.plain(escape=1)
1812 value = self._value
1813 if value is None:
1814 value = ''
1816 return self.input(name=self._formname, value=value, size=size)
1818 class LinkHTMLProperty(HTMLProperty):
1819 """ Link HTMLProperty
1820 Include the above as well as being able to access the class
1821 information. Stringifying the object itself results in the value
1822 from the item being displayed. Accessing attributes of this object
1823 result in the appropriate entry from the class being queried for the
1824 property accessed (so item/assignedto/name would look up the user
1825 entry identified by the assignedto property on item, and then the
1826 name property of that user)
1827 """
1828 def __init__(self, *args, **kw):
1829 HTMLProperty.__init__(self, *args, **kw)
1830 # if we're representing a form value, then the -1 from the form really
1831 # should be a None
1832 if str(self._value) == '-1':
1833 self._value = None
1835 def __getattr__(self, attr):
1836 """ return a new HTMLItem """
1837 if not self._value:
1838 # handle a special page templates lookup
1839 if attr == '__render_with_namespace__':
1840 def nothing(*args, **kw):
1841 return ''
1842 return nothing
1843 msg = self._('Attempt to look up %(attr)s on a missing value')
1844 return MissingValue(msg%locals())
1845 i = HTMLItem(self._client, self._prop.classname, self._value)
1846 return getattr(i, attr)
1848 def plain(self, escape=0):
1849 """ Render a "plain" representation of the property
1850 """
1851 if not self.is_view_ok():
1852 return self._('[hidden]')
1854 if self._value is None:
1855 return ''
1856 linkcl = self._db.classes[self._prop.classname]
1857 k = linkcl.labelprop(1)
1858 if num_re.match(self._value):
1859 try:
1860 value = str(linkcl.get(self._value, k))
1861 except IndexError:
1862 value = self._value
1863 else :
1864 value = self._value
1865 if escape:
1866 value = cgi.escape(value)
1867 return value
1869 def field(self, showid=0, size=None):
1870 """ Render a form edit field for the property
1872 If not editable, just display the value via plain().
1873 """
1874 if not self.is_edit_ok():
1875 return self.plain(escape=1)
1877 # edit field
1878 linkcl = self._db.getclass(self._prop.classname)
1879 if self._value is None:
1880 value = ''
1881 else:
1882 k = linkcl.getkey()
1883 if k and num_re.match(self._value):
1884 value = linkcl.get(self._value, k)
1885 else:
1886 value = self._value
1887 return self.input(name=self._formname, value=value, size=size)
1889 def menu(self, size=None, height=None, showid=0, additional=[], value=None,
1890 sort_on=None, **conditions):
1891 """ Render a form select list for this property
1893 "size" is used to limit the length of the list labels
1894 "height" is used to set the <select> tag's "size" attribute
1895 "showid" includes the item ids in the list labels
1896 "value" specifies which item is pre-selected
1897 "additional" lists properties which should be included in the
1898 label
1899 "sort_on" indicates the property to sort the list on as
1900 (direction, property) where direction is '+' or '-'. A
1901 single string with the direction prepended may be used.
1902 For example: ('-', 'order'), '+name'.
1904 The remaining keyword arguments are used as conditions for
1905 filtering the items in the list - they're passed as the
1906 "filterspec" argument to a Class.filter() call.
1908 If not editable, just display the value via plain().
1909 """
1910 if not self.is_edit_ok():
1911 return self.plain(escape=1)
1913 # Since None indicates the default, we need another way to
1914 # indicate "no selection". We use -1 for this purpose, as
1915 # that is the value we use when submitting a form without the
1916 # value set.
1917 if value is None:
1918 value = self._value
1919 elif value == '-1':
1920 value = None
1922 linkcl = self._db.getclass(self._prop.classname)
1923 l = ['<select name="%s">'%self._formname]
1924 k = linkcl.labelprop(1)
1925 s = ''
1926 if value is None:
1927 s = 'selected="selected" '
1928 l.append(self._('<option %svalue="-1">- no selection -</option>')%s)
1930 if sort_on is not None:
1931 if not isinstance(sort_on, tuple):
1932 if sort_on[0] in '+-':
1933 sort_on = (sort_on[0], sort_on[1:])
1934 else:
1935 sort_on = ('+', sort_on)
1936 else:
1937 sort_on = ('+', linkcl.orderprop())
1939 options = [opt
1940 for opt in linkcl.filter(None, conditions, sort_on, (None, None))
1941 if self._db.security.hasPermission("View", self._client.userid,
1942 linkcl.classname, itemid=opt)]
1944 # make sure we list the current value if it's retired
1945 if value and value not in options:
1946 options.insert(0, value)
1948 if additional:
1949 additional_fns = []
1950 props = linkcl.getprops()
1951 for propname in additional:
1952 prop = props[propname]
1953 if isinstance(prop, hyperdb.Link):
1954 cl = self._db.getclass(prop.classname)
1955 labelprop = cl.labelprop()
1956 fn = lambda optionid: cl.get(linkcl.get(optionid,
1957 propname),
1958 labelprop)
1959 else:
1960 fn = lambda optionid: linkcl.get(optionid, propname)
1961 additional_fns.append(fn)
1963 for optionid in options:
1964 # get the option value, and if it's None use an empty string
1965 option = linkcl.get(optionid, k) or ''
1967 # figure if this option is selected
1968 s = ''
1969 if value in [optionid, option]:
1970 s = 'selected="selected" '
1972 # figure the label
1973 if showid:
1974 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1975 elif not option:
1976 lab = '%s%s'%(self._prop.classname, optionid)
1977 else:
1978 lab = option
1980 # truncate if it's too long
1981 if size is not None and len(lab) > size:
1982 lab = lab[:size-3] + '...'
1983 if additional:
1984 m = []
1985 for fn in additional_fns:
1986 m.append(str(fn(optionid)))
1987 lab = lab + ' (%s)'%', '.join(m)
1989 # and generate
1990 lab = cgi.escape(self._(lab))
1991 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
1992 l.append('</select>')
1993 return '\n'.join(l)
1994 # def checklist(self, ...)
1998 class MultilinkHTMLProperty(HTMLProperty):
1999 """ Multilink HTMLProperty
2001 Also be iterable, returning a wrapper object like the Link case for
2002 each entry in the multilink.
2003 """
2004 def __init__(self, *args, **kwargs):
2005 HTMLProperty.__init__(self, *args, **kwargs)
2006 if self._value:
2007 display_value = lookupIds(self._db, self._prop, self._value,
2008 fail_ok=1, do_lookup=False)
2009 sortfun = make_sort_function(self._db, self._prop.classname)
2010 # sorting fails if the value contains
2011 # items not yet stored in the database
2012 # ignore these errors to preserve user input
2013 try:
2014 display_value.sort(sortfun)
2015 except:
2016 pass
2017 self._value = display_value
2019 def __len__(self):
2020 """ length of the multilink """
2021 return len(self._value)
2023 def __getattr__(self, attr):
2024 """ no extended attribute accesses make sense here """
2025 raise AttributeError, attr
2027 def viewableGenerator(self, values):
2028 """Used to iterate over only the View'able items in a class."""
2029 check = self._db.security.hasPermission
2030 userid = self._client.userid
2031 classname = self._prop.classname
2032 for value in values:
2033 if check('View', userid, classname, itemid=value):
2034 yield HTMLItem(self._client, classname, value)
2036 def __iter__(self):
2037 """ iterate and return a new HTMLItem
2038 """
2039 return self.viewableGenerator(self._value)
2041 def reverse(self):
2042 """ return the list in reverse order
2043 """
2044 l = self._value[:]
2045 l.reverse()
2046 return self.viewableGenerator(l)
2048 def sorted(self, property):
2049 """ Return this multilink sorted by the given property """
2050 value = list(self.__iter__())
2051 value.sort(lambda a,b:cmp(a[property], b[property]))
2052 return value
2054 def __contains__(self, value):
2055 """ Support the "in" operator. We have to make sure the passed-in
2056 value is a string first, not a HTMLProperty.
2057 """
2058 return str(value) in self._value
2060 def isset(self):
2061 """Is my _value not []?"""
2062 return self._value != []
2064 def plain(self, escape=0):
2065 """ Render a "plain" representation of the property
2066 """
2067 if not self.is_view_ok():
2068 return self._('[hidden]')
2070 linkcl = self._db.classes[self._prop.classname]
2071 k = linkcl.labelprop(1)
2072 labels = []
2073 for v in self._value:
2074 if num_re.match(v):
2075 try:
2076 label = linkcl.get(v, k)
2077 except IndexError:
2078 label = None
2079 # fall back to designator if label is None
2080 if label is None: label = '%s%s'%(self._prop.classname, k)
2081 else:
2082 label = v
2083 labels.append(label)
2084 value = ', '.join(labels)
2085 if escape:
2086 value = cgi.escape(value)
2087 return value
2089 def field(self, size=30, showid=0):
2090 """ Render a form edit field for the property
2092 If not editable, just display the value via plain().
2093 """
2094 if not self.is_edit_ok():
2095 return self.plain(escape=1)
2097 linkcl = self._db.getclass(self._prop.classname)
2098 value = self._value[:]
2099 # map the id to the label property
2100 if not linkcl.getkey():
2101 showid=1
2102 if not showid:
2103 k = linkcl.labelprop(1)
2104 value = lookupKeys(linkcl, k, value)
2105 value = ','.join(value)
2106 return self.input(name=self._formname, size=size, value=value)
2108 def menu(self, size=None, height=None, showid=0, additional=[],
2109 value=None, sort_on=None, **conditions):
2110 """ Render a form <select> list for this property.
2112 "size" is used to limit the length of the list labels
2113 "height" is used to set the <select> tag's "size" attribute
2114 "showid" includes the item ids in the list labels
2115 "additional" lists properties which should be included in the
2116 label
2117 "value" specifies which item is pre-selected
2118 "sort_on" indicates the property to sort the list on as
2119 (direction, property) where direction is '+' or '-'. A
2120 single string with the direction prepended may be used.
2121 For example: ('-', 'order'), '+name'.
2123 The remaining keyword arguments are used as conditions for
2124 filtering the items in the list - they're passed as the
2125 "filterspec" argument to a Class.filter() call.
2127 If not editable, just display the value via plain().
2128 """
2129 if not self.is_edit_ok():
2130 return self.plain(escape=1)
2132 if value is None:
2133 value = self._value
2135 linkcl = self._db.getclass(self._prop.classname)
2137 if sort_on is not None:
2138 if not isinstance(sort_on, tuple):
2139 if sort_on[0] in '+-':
2140 sort_on = (sort_on[0], sort_on[1:])
2141 else:
2142 sort_on = ('+', sort_on)
2143 else:
2144 sort_on = ('+', linkcl.orderprop())
2146 options = [opt
2147 for opt in linkcl.filter(None, conditions, sort_on)
2148 if self._db.security.hasPermission("View", self._client.userid,
2149 linkcl.classname, itemid=opt)]
2151 # make sure we list the current values if they're retired
2152 for val in value:
2153 if val not in options:
2154 options.insert(0, val)
2156 if not height:
2157 height = len(options)
2158 if value:
2159 # The "no selection" option.
2160 height += 1
2161 height = min(height, 7)
2162 l = ['<select multiple name="%s" size="%s">'%(self._formname, height)]
2163 k = linkcl.labelprop(1)
2165 if value:
2166 l.append('<option value="%s">- no selection -</option>'
2167 % ','.join(['-' + v for v in value]))
2169 if additional:
2170 additional_fns = []
2171 props = linkcl.getprops()
2172 for propname in additional:
2173 prop = props[propname]
2174 if isinstance(prop, hyperdb.Link):
2175 cl = self._db.getclass(prop.classname)
2176 labelprop = cl.labelprop()
2177 fn = lambda optionid: cl.get(linkcl.get(optionid,
2178 propname),
2179 labelprop)
2180 else:
2181 fn = lambda optionid: linkcl.get(optionid, propname)
2182 additional_fns.append(fn)
2184 for optionid in options:
2185 # get the option value, and if it's None use an empty string
2186 option = linkcl.get(optionid, k) or ''
2188 # figure if this option is selected
2189 s = ''
2190 if optionid in value or option in value:
2191 s = 'selected="selected" '
2193 # figure the label
2194 if showid:
2195 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
2196 else:
2197 lab = option
2198 # truncate if it's too long
2199 if size is not None and len(lab) > size:
2200 lab = lab[:size-3] + '...'
2201 if additional:
2202 m = []
2203 for fn in additional_fns:
2204 m.append(str(fn(optionid)))
2205 lab = lab + ' (%s)'%', '.join(m)
2207 # and generate
2208 lab = cgi.escape(self._(lab))
2209 l.append('<option %svalue="%s">%s</option>'%(s, optionid,
2210 lab))
2211 l.append('</select>')
2212 return '\n'.join(l)
2214 # set the propclasses for HTMLItem
2215 propclasses = (
2216 (hyperdb.String, StringHTMLProperty),
2217 (hyperdb.Number, NumberHTMLProperty),
2218 (hyperdb.Boolean, BooleanHTMLProperty),
2219 (hyperdb.Date, DateHTMLProperty),
2220 (hyperdb.Interval, IntervalHTMLProperty),
2221 (hyperdb.Password, PasswordHTMLProperty),
2222 (hyperdb.Link, LinkHTMLProperty),
2223 (hyperdb.Multilink, MultilinkHTMLProperty),
2224 )
2226 def make_sort_function(db, classname, sort_on=None):
2227 """Make a sort function for a given class
2228 """
2229 linkcl = db.getclass(classname)
2230 if sort_on is None:
2231 sort_on = linkcl.orderprop()
2232 def sortfunc(a, b):
2233 return cmp(linkcl.get(a, sort_on), linkcl.get(b, sort_on))
2234 return sortfunc
2236 def handleListCGIValue(value):
2237 """ Value is either a single item or a list of items. Each item has a
2238 .value that we're actually interested in.
2239 """
2240 if isinstance(value, type([])):
2241 return [value.value for value in value]
2242 else:
2243 value = value.value.strip()
2244 if not value:
2245 return []
2246 return [v.strip() for v in value.split(',')]
2248 class HTMLRequest(HTMLInputMixin):
2249 """The *request*, holding the CGI form and environment.
2251 - "form" the CGI form as a cgi.FieldStorage
2252 - "env" the CGI environment variables
2253 - "base" the base URL for this instance
2254 - "user" a HTMLItem instance for this user
2255 - "language" as determined by the browser or config
2256 - "classname" the current classname (possibly None)
2257 - "template" the current template (suffix, also possibly None)
2259 Index args:
2261 - "columns" dictionary of the columns to display in an index page
2262 - "show" a convenience access to columns - request/show/colname will
2263 be true if the columns should be displayed, false otherwise
2264 - "sort" index sort column (direction, column name)
2265 - "group" index grouping property (direction, column name)
2266 - "filter" properties to filter the index on
2267 - "filterspec" values to filter the index on
2268 - "search_text" text to perform a full-text search on for an index
2269 """
2270 def __repr__(self):
2271 return '<HTMLRequest %r>'%self.__dict__
2273 def __init__(self, client):
2274 # _client is needed by HTMLInputMixin
2275 self._client = self.client = client
2277 # easier access vars
2278 self.form = client.form
2279 self.env = client.env
2280 self.base = client.base
2281 self.user = HTMLItem(client, 'user', client.userid)
2282 self.language = client.language
2284 # store the current class name and action
2285 self.classname = client.classname
2286 self.nodeid = client.nodeid
2287 self.template = client.template
2289 # the special char to use for special vars
2290 self.special_char = '@'
2292 HTMLInputMixin.__init__(self)
2294 self._post_init()
2296 def current_url(self):
2297 url = self.base
2298 if self.classname:
2299 url += self.classname
2300 if self.nodeid:
2301 url += self.nodeid
2302 args = {}
2303 if self.template:
2304 args['@template'] = self.template
2305 return self.indexargs_url(url, args)
2307 def _parse_sort(self, var, name):
2308 """ Parse sort/group options. Append to var
2309 """
2310 fields = []
2311 dirs = []
2312 for special in '@:':
2313 idx = 0
2314 key = '%s%s%d'%(special, name, idx)
2315 while key in self.form:
2316 self.special_char = special
2317 fields.append(self.form.getfirst(key))
2318 dirkey = '%s%sdir%d'%(special, name, idx)
2319 if dirkey in self.form:
2320 dirs.append(self.form.getfirst(dirkey))
2321 else:
2322 dirs.append(None)
2323 idx += 1
2324 key = '%s%s%d'%(special, name, idx)
2325 # backward compatible (and query) URL format
2326 key = special + name
2327 dirkey = key + 'dir'
2328 if key in self.form and not fields:
2329 fields = handleListCGIValue(self.form[key])
2330 if dirkey in self.form:
2331 dirs.append(self.form.getfirst(dirkey))
2332 if fields: # only try other special char if nothing found
2333 break
2334 for f, d in map(None, fields, dirs):
2335 if f.startswith('-'):
2336 var.append(('-', f[1:]))
2337 elif d:
2338 var.append(('-', f))
2339 else:
2340 var.append(('+', f))
2342 def _post_init(self):
2343 """ Set attributes based on self.form
2344 """
2345 # extract the index display information from the form
2346 self.columns = []
2347 for name in ':columns @columns'.split():
2348 if self.form.has_key(name):
2349 self.special_char = name[0]
2350 self.columns = handleListCGIValue(self.form[name])
2351 break
2352 self.show = support.TruthDict(self.columns)
2354 # sorting and grouping
2355 self.sort = []
2356 self.group = []
2357 self._parse_sort(self.sort, 'sort')
2358 self._parse_sort(self.group, 'group')
2360 # filtering
2361 self.filter = []
2362 for name in ':filter @filter'.split():
2363 if self.form.has_key(name):
2364 self.special_char = name[0]
2365 self.filter = handleListCGIValue(self.form[name])
2367 self.filterspec = {}
2368 db = self.client.db
2369 if self.classname is not None:
2370 cls = db.getclass (self.classname)
2371 for name in self.filter:
2372 if not self.form.has_key(name):
2373 continue
2374 prop = cls.get_transitive_prop (name)
2375 fv = self.form[name]
2376 if (isinstance(prop, hyperdb.Link) or
2377 isinstance(prop, hyperdb.Multilink)):
2378 self.filterspec[name] = lookupIds(db, prop,
2379 handleListCGIValue(fv))
2380 else:
2381 if isinstance(fv, type([])):
2382 self.filterspec[name] = [v.value for v in fv]
2383 elif name == 'id':
2384 # special case "id" property
2385 self.filterspec[name] = handleListCGIValue(fv)
2386 else:
2387 self.filterspec[name] = fv.value
2389 # full-text search argument
2390 self.search_text = None
2391 for name in ':search_text @search_text'.split():
2392 if self.form.has_key(name):
2393 self.special_char = name[0]
2394 self.search_text = self.form.getfirst(name)
2396 # pagination - size and start index
2397 # figure batch args
2398 self.pagesize = 50
2399 for name in ':pagesize @pagesize'.split():
2400 if self.form.has_key(name):
2401 self.special_char = name[0]
2402 try:
2403 self.pagesize = int(self.form.getfirst(name))
2404 except ValueError:
2405 # not an integer - ignore
2406 pass
2408 self.startwith = 0
2409 for name in ':startwith @startwith'.split():
2410 if self.form.has_key(name):
2411 self.special_char = name[0]
2412 try:
2413 self.startwith = int(self.form.getfirst(name))
2414 except ValueError:
2415 # not an integer - ignore
2416 pass
2418 # dispname
2419 if self.form.has_key('@dispname'):
2420 self.dispname = self.form.getfirst('@dispname')
2421 else:
2422 self.dispname = None
2424 def updateFromURL(self, url):
2425 """ Parse the URL for query args, and update my attributes using the
2426 values.
2427 """
2428 env = {'QUERY_STRING': url}
2429 self.form = cgi.FieldStorage(environ=env)
2431 self._post_init()
2433 def update(self, kwargs):
2434 """ Update my attributes using the keyword args
2435 """
2436 self.__dict__.update(kwargs)
2437 if kwargs.has_key('columns'):
2438 self.show = support.TruthDict(self.columns)
2440 def description(self):
2441 """ Return a description of the request - handle for the page title.
2442 """
2443 s = [self.client.db.config.TRACKER_NAME]
2444 if self.classname:
2445 if self.client.nodeid:
2446 s.append('- %s%s'%(self.classname, self.client.nodeid))
2447 else:
2448 if self.template == 'item':
2449 s.append('- new %s'%self.classname)
2450 elif self.template == 'index':
2451 s.append('- %s index'%self.classname)
2452 else:
2453 s.append('- %s %s'%(self.classname, self.template))
2454 else:
2455 s.append('- home')
2456 return ' '.join(s)
2458 def __str__(self):
2459 d = {}
2460 d.update(self.__dict__)
2461 f = ''
2462 for k in self.form.keys():
2463 f += '\n %r=%r'%(k,handleListCGIValue(self.form[k]))
2464 d['form'] = f
2465 e = ''
2466 for k,v in self.env.items():
2467 e += '\n %r=%r'%(k, v)
2468 d['env'] = e
2469 return """
2470 form: %(form)s
2471 base: %(base)r
2472 classname: %(classname)r
2473 template: %(template)r
2474 columns: %(columns)r
2475 sort: %(sort)r
2476 group: %(group)r
2477 filter: %(filter)r
2478 search_text: %(search_text)r
2479 pagesize: %(pagesize)r
2480 startwith: %(startwith)r
2481 env: %(env)s
2482 """%d
2484 def indexargs_form(self, columns=1, sort=1, group=1, filter=1,
2485 filterspec=1, search_text=1):
2486 """ return the current index args as form elements """
2487 l = []
2488 sc = self.special_char
2489 def add(k, v):
2490 l.append(self.input(type="hidden", name=k, value=v))
2491 if columns and self.columns:
2492 add(sc+'columns', ','.join(self.columns))
2493 if sort:
2494 val = []
2495 for dir, attr in self.sort:
2496 if dir == '-':
2497 val.append('-'+attr)
2498 else:
2499 val.append(attr)
2500 add(sc+'sort', ','.join (val))
2501 if group:
2502 val = []
2503 for dir, attr in self.group:
2504 if dir == '-':
2505 val.append('-'+attr)
2506 else:
2507 val.append(attr)
2508 add(sc+'group', ','.join (val))
2509 if filter and self.filter:
2510 add(sc+'filter', ','.join(self.filter))
2511 if self.classname and filterspec:
2512 cls = self.client.db.getclass(self.classname)
2513 for k,v in self.filterspec.items():
2514 if type(v) == type([]):
2515 if isinstance(cls.get_transitive_prop(k), hyperdb.String):
2516 add(k, ' '.join(v))
2517 else:
2518 add(k, ','.join(v))
2519 else:
2520 add(k, v)
2521 if search_text and self.search_text:
2522 add(sc+'search_text', self.search_text)
2523 add(sc+'pagesize', self.pagesize)
2524 add(sc+'startwith', self.startwith)
2525 return '\n'.join(l)
2527 def indexargs_url(self, url, args):
2528 """ Embed the current index args in a URL
2529 """
2530 q = urllib.quote
2531 sc = self.special_char
2532 l = ['%s=%s'%(k,v) for k,v in args.items()]
2534 # pull out the special values (prefixed by @ or :)
2535 specials = {}
2536 for key in args.keys():
2537 if key[0] in '@:':
2538 specials[key[1:]] = args[key]
2540 # ok, now handle the specials we received in the request
2541 if self.columns and not specials.has_key('columns'):
2542 l.append(sc+'columns=%s'%(','.join(self.columns)))
2543 if self.sort and not specials.has_key('sort'):
2544 val = []
2545 for dir, attr in self.sort:
2546 if dir == '-':
2547 val.append('-'+attr)
2548 else:
2549 val.append(attr)
2550 l.append(sc+'sort=%s'%(','.join(val)))
2551 if self.group and not specials.has_key('group'):
2552 val = []
2553 for dir, attr in self.group:
2554 if dir == '-':
2555 val.append('-'+attr)
2556 else:
2557 val.append(attr)
2558 l.append(sc+'group=%s'%(','.join(val)))
2559 if self.filter and not specials.has_key('filter'):
2560 l.append(sc+'filter=%s'%(','.join(self.filter)))
2561 if self.search_text and not specials.has_key('search_text'):
2562 l.append(sc+'search_text=%s'%q(self.search_text))
2563 if not specials.has_key('pagesize'):
2564 l.append(sc+'pagesize=%s'%self.pagesize)
2565 if not specials.has_key('startwith'):
2566 l.append(sc+'startwith=%s'%self.startwith)
2568 # finally, the remainder of the filter args in the request
2569 if self.classname and self.filterspec:
2570 cls = self.client.db.getclass(self.classname)
2571 for k,v in self.filterspec.items():
2572 if not args.has_key(k):
2573 if type(v) == type([]):
2574 prop = cls.get_transitive_prop(k)
2575 if k != 'id' and isinstance(prop, hyperdb.String):
2576 l.append('%s=%s'%(k, '%20'.join([q(i) for i in v])))
2577 else:
2578 l.append('%s=%s'%(k, ','.join([q(i) for i in v])))
2579 else:
2580 l.append('%s=%s'%(k, q(v)))
2581 return '%s?%s'%(url, '&'.join(l))
2582 indexargs_href = indexargs_url
2584 def base_javascript(self):
2585 return """
2586 <script type="text/javascript">
2587 submitted = false;
2588 function submit_once() {
2589 if (submitted) {
2590 alert("Your request is being processed.\\nPlease be patient.");
2591 event.returnValue = 0; // work-around for IE
2592 return 0;
2593 }
2594 submitted = true;
2595 return 1;
2596 }
2598 function help_window(helpurl, width, height) {
2599 HelpWin = window.open('%s' + helpurl, 'RoundupHelpWindow', 'scrollbars=yes,resizable=yes,toolbar=no,height='+height+',width='+width);
2600 }
2601 </script>
2602 """%self.base
2604 def batch(self):
2605 """ Return a batch object for results from the "current search"
2606 """
2607 filterspec = self.filterspec
2608 sort = self.sort
2609 group = self.group
2611 # get the list of ids we're batching over
2612 klass = self.client.db.getclass(self.classname)
2613 if self.search_text:
2614 matches = self.client.db.indexer.search(
2615 [w.upper().encode("utf-8", "replace") for w in re.findall(
2616 r'(?u)\b\w{2,25}\b',
2617 unicode(self.search_text, "utf-8", "replace")
2618 )], klass)
2619 else:
2620 matches = None
2622 # filter for visibility
2623 check = self._client.db.security.hasPermission
2624 userid = self._client.userid
2625 l = [id for id in klass.filter(matches, filterspec, sort, group)
2626 if check('View', userid, self.classname, itemid=id)]
2628 # return the batch object, using IDs only
2629 return Batch(self.client, l, self.pagesize, self.startwith,
2630 classname=self.classname)
2632 # extend the standard ZTUtils Batch object to remove dependency on
2633 # Acquisition and add a couple of useful methods
2634 class Batch(ZTUtils.Batch):
2635 """ Use me to turn a list of items, or item ids of a given class, into a
2636 series of batches.
2638 ========= ========================================================
2639 Parameter Usage
2640 ========= ========================================================
2641 sequence a list of HTMLItems or item ids
2642 classname if sequence is a list of ids, this is the class of item
2643 size how big to make the sequence.
2644 start where to start (0-indexed) in the sequence.
2645 end where to end (0-indexed) in the sequence.
2646 orphan if the next batch would contain less items than this
2647 value, then it is combined with this batch
2648 overlap the number of items shared between adjacent batches
2649 ========= ========================================================
2651 Attributes: Note that the "start" attribute, unlike the
2652 argument, is a 1-based index (I know, lame). "first" is the
2653 0-based index. "length" is the actual number of elements in
2654 the batch.
2656 "sequence_length" is the length of the original, unbatched, sequence.
2657 """
2658 def __init__(self, client, sequence, size, start, end=0, orphan=0,
2659 overlap=0, classname=None):
2660 self.client = client
2661 self.last_index = self.last_item = None
2662 self.current_item = None
2663 self.classname = classname
2664 self.sequence_length = len(sequence)
2665 ZTUtils.Batch.__init__(self, sequence, size, start, end, orphan,
2666 overlap)
2668 # overwrite so we can late-instantiate the HTMLItem instance
2669 def __getitem__(self, index):
2670 if index < 0:
2671 if index + self.end < self.first: raise IndexError, index
2672 return self._sequence[index + self.end]
2674 if index >= self.length:
2675 raise IndexError, index
2677 # move the last_item along - but only if the fetched index changes
2678 # (for some reason, index 0 is fetched twice)
2679 if index != self.last_index:
2680 self.last_item = self.current_item
2681 self.last_index = index
2683 item = self._sequence[index + self.first]
2684 if self.classname:
2685 # map the item ids to instances
2686 item = HTMLItem(self.client, self.classname, item)
2687 self.current_item = item
2688 return item
2690 def propchanged(self, *properties):
2691 """ Detect if one of the properties marked as being a group
2692 property changed in the last iteration fetch
2693 """
2694 # we poke directly at the _value here since MissingValue can screw
2695 # us up and cause Nones to compare strangely
2696 if self.last_item is None:
2697 return 1
2698 for property in properties:
2699 if property == 'id' or isinstance (self.last_item[property], list):
2700 if (str(self.last_item[property]) !=
2701 str(self.current_item[property])):
2702 return 1
2703 else:
2704 if (self.last_item[property]._value !=
2705 self.current_item[property]._value):
2706 return 1
2707 return 0
2709 # override these 'cos we don't have access to acquisition
2710 def previous(self):
2711 if self.start == 1:
2712 return None
2713 return Batch(self.client, self._sequence, self._size,
2714 self.first - self._size + self.overlap, 0, self.orphan,
2715 self.overlap)
2717 def next(self):
2718 try:
2719 self._sequence[self.end]
2720 except IndexError:
2721 return None
2722 return Batch(self.client, self._sequence, self._size,
2723 self.end - self.overlap, 0, self.orphan, self.overlap)
2725 class TemplatingUtils:
2726 """ Utilities for templating
2727 """
2728 def __init__(self, client):
2729 self.client = client
2730 def Batch(self, sequence, size, start, end=0, orphan=0, overlap=0):
2731 return Batch(self.client, sequence, size, start, end, orphan,
2732 overlap)
2734 def url_quote(self, url):
2735 """URL-quote the supplied text."""
2736 return urllib.quote(url)
2738 def html_quote(self, html):
2739 """HTML-quote the supplied text."""
2740 return cgi.escape(html)
2742 def __getattr__(self, name):
2743 """Try the tracker's templating_utils."""
2744 if not hasattr(self.client.instance, 'templating_utils'):
2745 # backwards-compatibility
2746 raise AttributeError, name
2747 if not self.client.instance.templating_utils.has_key(name):
2748 raise AttributeError, name
2749 return self.client.instance.templating_utils[name]
2751 def html_calendar(self, request):
2752 """Generate a HTML calendar.
2754 `request` the roundup.request object
2755 - @template : name of the template
2756 - form : name of the form to store back the date
2757 - property : name of the property of the form to store
2758 back the date
2759 - date : current date
2760 - display : when browsing, specifies year and month
2762 html will simply be a table.
2763 """
2764 date_str = request.form.getfirst("date", ".")
2765 display = request.form.getfirst("display", date_str)
2766 template = request.form.getfirst("@template", "calendar")
2767 form = request.form.getfirst("form")
2768 property = request.form.getfirst("property")
2769 curr_date = date.Date(date_str) # to highlight
2770 display = date.Date(display) # to show
2771 day = display.day
2773 # for navigation
2774 date_prev_month = display + date.Interval("-1m")
2775 date_next_month = display + date.Interval("+1m")
2776 date_prev_year = display + date.Interval("-1y")
2777 date_next_year = display + date.Interval("+1y")
2779 res = []
2781 base_link = "%s?@template=%s&property=%s&form=%s&date=%s" % \
2782 (request.classname, template, property, form, curr_date)
2784 # navigation
2785 # month
2786 res.append('<table class="calendar"><tr><td>')
2787 res.append(' <table width="100%" class="calendar_nav"><tr>')
2788 link = "&display=%s"%date_prev_month
2789 res.append(' <td><a href="%s&display=%s"><</a></td>'%(base_link,
2790 date_prev_month))
2791 res.append(' <td>%s</td>'%calendar.month_name[display.month])
2792 res.append(' <td><a href="%s&display=%s">></a></td>'%(base_link,
2793 date_next_month))
2794 # spacer
2795 res.append(' <td width="100%"></td>')
2796 # year
2797 res.append(' <td><a href="%s&display=%s"><</a></td>'%(base_link,
2798 date_prev_year))
2799 res.append(' <td>%s</td>'%display.year)
2800 res.append(' <td><a href="%s&display=%s">></a></td>'%(base_link,
2801 date_next_year))
2802 res.append(' </tr></table>')
2803 res.append(' </td></tr>')
2805 # the calendar
2806 res.append(' <tr><td><table class="calendar_display">')
2807 res.append(' <tr class="weekdays">')
2808 for day in calendar.weekheader(3).split():
2809 res.append(' <td>%s</td>'%day)
2810 res.append(' </tr>')
2811 for week in calendar.monthcalendar(display.year, display.month):
2812 res.append(' <tr>')
2813 for day in week:
2814 link = "javascript:form[field].value = '%d-%02d-%02d'; " \
2815 "window.close ();"%(display.year, display.month, day)
2816 if (day == curr_date.day and display.month == curr_date.month
2817 and display.year == curr_date.year):
2818 # highlight
2819 style = "today"
2820 else :
2821 style = ""
2822 if day:
2823 res.append(' <td class="%s"><a href="%s">%s</a></td>'%(
2824 style, link, day))
2825 else :
2826 res.append(' <td></td>')
2827 res.append(' </tr>')
2828 res.append('</table></td></tr></table>')
2829 return "\n".join(res)
2831 class MissingValue:
2832 def __init__(self, description, **kwargs):
2833 self.__description = description
2834 for key, value in kwargs.items():
2835 self.__dict__[key] = value
2837 def __call__(self, *args, **kwargs): return MissingValue(self.__description)
2838 def __getattr__(self, name):
2839 # This allows assignments which assume all intermediate steps are Null
2840 # objects if they don't exist yet.
2841 #
2842 # For example (with just 'client' defined):
2843 #
2844 # client.db.config.TRACKER_WEB = 'BASE/'
2845 self.__dict__[name] = MissingValue(self.__description)
2846 return getattr(self, name)
2848 def __getitem__(self, key): return self
2849 def __nonzero__(self): return 0
2850 def __str__(self): return '[%s]'%self.__description
2851 def __repr__(self): return '<MissingValue 0x%x "%s">'%(id(self),
2852 self.__description)
2853 def gettext(self, str): return str
2854 _ = gettext
2856 # vim: set et sts=4 sw=4 :