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