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 # Since None indicates the default, we need another way to
1877 # indicate "no selection". We use -1 for this purpose, as
1878 # that is the value we use when submitting a form without the
1879 # value set.
1880 if value is None:
1881 value = self._value
1882 elif value == '-1':
1883 value = None
1885 linkcl = self._db.getclass(self._prop.classname)
1886 l = ['<select name="%s">'%self._formname]
1887 k = linkcl.labelprop(1)
1888 s = ''
1889 if value is None:
1890 s = 'selected="selected" '
1891 l.append(self._('<option %svalue="-1">- no selection -</option>')%s)
1893 if sort_on is not None:
1894 if not isinstance(sort_on, tuple):
1895 if sort_on[0] in '+-':
1896 sort_on = (sort_on[0], sort_on[1:])
1897 else:
1898 sort_on = ('+', sort_on)
1899 else:
1900 sort_on = ('+', linkcl.orderprop())
1902 options = [opt
1903 for opt in linkcl.filter(None, conditions, sort_on, (None, None))
1904 if self._db.security.hasPermission("View", self._client.userid,
1905 linkcl.classname, itemid=opt)]
1907 # make sure we list the current value if it's retired
1908 if value and value not in options:
1909 options.insert(0, value)
1911 for optionid in options:
1912 # get the option value, and if it's None use an empty string
1913 option = linkcl.get(optionid, k) or ''
1915 # figure if this option is selected
1916 s = ''
1917 if value in [optionid, option]:
1918 s = 'selected="selected" '
1920 # figure the label
1921 if showid:
1922 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1923 elif not option:
1924 lab = '%s%s'%(self._prop.classname, optionid)
1925 else:
1926 lab = option
1928 # truncate if it's too long
1929 if size is not None and len(lab) > size:
1930 lab = lab[:size-3] + '...'
1931 if additional:
1932 m = []
1933 for propname in additional:
1934 m.append(linkcl.get(optionid, propname))
1935 lab = lab + ' (%s)'%', '.join(map(str, m))
1937 # and generate
1938 lab = cgi.escape(self._(lab))
1939 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
1940 l.append('</select>')
1941 return '\n'.join(l)
1942 # def checklist(self, ...)
1946 class MultilinkHTMLProperty(HTMLProperty):
1947 """ Multilink HTMLProperty
1949 Also be iterable, returning a wrapper object like the Link case for
1950 each entry in the multilink.
1951 """
1952 def __init__(self, *args, **kwargs):
1953 HTMLProperty.__init__(self, *args, **kwargs)
1954 if self._value:
1955 display_value = lookupIds(self._db, self._prop, self._value,
1956 fail_ok=1, do_lookup=False)
1957 sortfun = make_sort_function(self._db, self._prop.classname)
1958 # sorting fails if the value contains
1959 # items not yet stored in the database
1960 # ignore these errors to preserve user input
1961 try:
1962 display_value.sort(sortfun)
1963 except:
1964 pass
1965 self._value = display_value
1967 def __len__(self):
1968 """ length of the multilink """
1969 return len(self._value)
1971 def __getattr__(self, attr):
1972 """ no extended attribute accesses make sense here """
1973 raise AttributeError, attr
1975 def viewableGenerator(self, values):
1976 """Used to iterate over only the View'able items in a class."""
1977 check = self._db.security.hasPermission
1978 userid = self._client.userid
1979 classname = self._prop.classname
1980 for value in values:
1981 if check('View', userid, classname, itemid=value):
1982 yield HTMLItem(self._client, classname, value)
1984 def __iter__(self):
1985 """ iterate and return a new HTMLItem
1986 """
1987 return self.viewableGenerator(self._value)
1989 def reverse(self):
1990 """ return the list in reverse order
1991 """
1992 l = self._value[:]
1993 l.reverse()
1994 return self.viewableGenerator(l)
1996 def sorted(self, property):
1997 """ Return this multilink sorted by the given property """
1998 value = list(self.__iter__())
1999 value.sort(lambda a,b:cmp(a[property], b[property]))
2000 return value
2002 def __contains__(self, value):
2003 """ Support the "in" operator. We have to make sure the passed-in
2004 value is a string first, not a HTMLProperty.
2005 """
2006 return str(value) in self._value
2008 def isset(self):
2009 """Is my _value not []?"""
2010 return self._value != []
2012 def plain(self, escape=0):
2013 """ Render a "plain" representation of the property
2014 """
2015 if not self.is_view_ok():
2016 return self._('[hidden]')
2018 linkcl = self._db.classes[self._prop.classname]
2019 k = linkcl.labelprop(1)
2020 labels = []
2021 for v in self._value:
2022 label = linkcl.get(v, k)
2023 # fall back to designator if label is None
2024 if label is None: label = '%s%s'%(self._prop.classname, k)
2025 labels.append(label)
2026 value = ', '.join(labels)
2027 if escape:
2028 value = cgi.escape(value)
2029 return value
2031 def field(self, size=30, showid=0):
2032 """ Render a form edit field for the property
2034 If not editable, just display the value via plain().
2035 """
2036 if not self.is_edit_ok():
2037 return self.plain(escape=1)
2039 linkcl = self._db.getclass(self._prop.classname)
2040 value = self._value[:]
2041 # map the id to the label property
2042 if not linkcl.getkey():
2043 showid=1
2044 if not showid:
2045 k = linkcl.labelprop(1)
2046 value = lookupKeys(linkcl, k, value)
2047 value = ','.join(value)
2048 return self.input(name=self._formname, size=size, value=value)
2050 def menu(self, size=None, height=None, showid=0, additional=[],
2051 value=None, sort_on=None, **conditions):
2052 """ Render a form <select> list for this property.
2054 "size" is used to limit the length of the list labels
2055 "height" is used to set the <select> tag's "size" attribute
2056 "showid" includes the item ids in the list labels
2057 "additional" lists properties which should be included in the
2058 label
2059 "value" specifies which item is pre-selected
2060 "sort_on" indicates the property to sort the list on as
2061 (direction, property) where direction is '+' or '-'. A
2062 single string with the direction prepended may be used.
2063 For example: ('-', 'order'), '+name'.
2065 The remaining keyword arguments are used as conditions for
2066 filtering the items in the list - they're passed as the
2067 "filterspec" argument to a Class.filter() call.
2069 If not editable, just display the value via plain().
2070 """
2071 if not self.is_edit_ok():
2072 return self.plain(escape=1)
2074 if value is None:
2075 value = self._value
2077 linkcl = self._db.getclass(self._prop.classname)
2079 if sort_on is not None:
2080 if not isinstance(sort_on, tuple):
2081 if sort_on[0] in '+-':
2082 sort_on = (sort_on[0], sort_on[1:])
2083 else:
2084 sort_on = ('+', sort_on)
2085 else:
2086 sort_on = ('+', linkcl.orderprop())
2088 options = [opt
2089 for opt in linkcl.filter(None, conditions, sort_on)
2090 if self._db.security.hasPermission("View", self._client.userid,
2091 linkcl.classname, itemid=opt)]
2093 # make sure we list the current values if they're retired
2094 for val in value:
2095 if val not in options:
2096 options.insert(0, val)
2098 if not height:
2099 height = len(options)
2100 if value:
2101 # The "no selection" option.
2102 height += 1
2103 height = min(height, 7)
2104 l = ['<select multiple name="%s" size="%s">'%(self._formname, height)]
2105 k = linkcl.labelprop(1)
2107 # make sure we list the current values if they're retired
2108 for val in value:
2109 if val not in options:
2110 options.insert(0, val)
2112 for optionid in options:
2113 # get the option value, and if it's None use an empty string
2114 option = linkcl.get(optionid, k) or ''
2116 # figure if this option is selected
2117 s = ''
2118 if optionid in value or option in value:
2119 s = 'selected="selected" '
2121 # figure the label
2122 if showid:
2123 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
2124 else:
2125 lab = option
2126 # truncate if it's too long
2127 if size is not None and len(lab) > size:
2128 lab = lab[:size-3] + '...'
2129 if additional:
2130 m = []
2131 for propname in additional:
2132 m.append(linkcl.get(optionid, propname))
2133 lab = lab + ' (%s)'%', '.join(m)
2135 # and generate
2136 lab = cgi.escape(self._(lab))
2137 l.append('<option %svalue="%s">%s</option>'%(s, optionid,
2138 lab))
2139 l.append('</select>')
2140 return '\n'.join(l)
2142 # set the propclasses for HTMLItem
2143 propclasses = (
2144 (hyperdb.String, StringHTMLProperty),
2145 (hyperdb.Number, NumberHTMLProperty),
2146 (hyperdb.Boolean, BooleanHTMLProperty),
2147 (hyperdb.Date, DateHTMLProperty),
2148 (hyperdb.Interval, IntervalHTMLProperty),
2149 (hyperdb.Password, PasswordHTMLProperty),
2150 (hyperdb.Link, LinkHTMLProperty),
2151 (hyperdb.Multilink, MultilinkHTMLProperty),
2152 )
2154 def make_sort_function(db, classname, sort_on=None):
2155 """Make a sort function for a given class
2156 """
2157 linkcl = db.getclass(classname)
2158 if sort_on is None:
2159 sort_on = linkcl.orderprop()
2160 def sortfunc(a, b):
2161 return cmp(linkcl.get(a, sort_on), linkcl.get(b, sort_on))
2162 return sortfunc
2164 def handleListCGIValue(value):
2165 """ Value is either a single item or a list of items. Each item has a
2166 .value that we're actually interested in.
2167 """
2168 if isinstance(value, type([])):
2169 return [value.value for value in value]
2170 else:
2171 value = value.value.strip()
2172 if not value:
2173 return []
2174 return [v.strip() for v in value.split(',')]
2176 class HTMLRequest(HTMLInputMixin):
2177 """The *request*, holding the CGI form and environment.
2179 - "form" the CGI form as a cgi.FieldStorage
2180 - "env" the CGI environment variables
2181 - "base" the base URL for this instance
2182 - "user" a HTMLItem instance for this user
2183 - "language" as determined by the browser or config
2184 - "classname" the current classname (possibly None)
2185 - "template" the current template (suffix, also possibly None)
2187 Index args:
2189 - "columns" dictionary of the columns to display in an index page
2190 - "show" a convenience access to columns - request/show/colname will
2191 be true if the columns should be displayed, false otherwise
2192 - "sort" index sort column (direction, column name)
2193 - "group" index grouping property (direction, column name)
2194 - "filter" properties to filter the index on
2195 - "filterspec" values to filter the index on
2196 - "search_text" text to perform a full-text search on for an index
2197 """
2198 def __repr__(self):
2199 return '<HTMLRequest %r>'%self.__dict__
2201 def __init__(self, client):
2202 # _client is needed by HTMLInputMixin
2203 self._client = self.client = client
2205 # easier access vars
2206 self.form = client.form
2207 self.env = client.env
2208 self.base = client.base
2209 self.user = HTMLItem(client, 'user', client.userid)
2210 self.language = client.language
2212 # store the current class name and action
2213 self.classname = client.classname
2214 self.nodeid = client.nodeid
2215 self.template = client.template
2217 # the special char to use for special vars
2218 self.special_char = '@'
2220 HTMLInputMixin.__init__(self)
2222 self._post_init()
2224 def current_url(self):
2225 url = self.base
2226 if self.classname:
2227 url += self.classname
2228 if self.nodeid:
2229 url += self.nodeid
2230 args = {}
2231 if self.template:
2232 args['@template'] = self.template
2233 return self.indexargs_url(url, args)
2235 def _parse_sort(self, var, name):
2236 """ Parse sort/group options. Append to var
2237 """
2238 fields = []
2239 dirs = []
2240 for special in '@:':
2241 idx = 0
2242 key = '%s%s%d'%(special, name, idx)
2243 while key in self.form:
2244 self.special_char = special
2245 fields.append(self.form.getfirst(key))
2246 dirkey = '%s%sdir%d'%(special, name, idx)
2247 if dirkey in self.form:
2248 dirs.append(self.form.getfirst(dirkey))
2249 else:
2250 dirs.append(None)
2251 idx += 1
2252 key = '%s%s%d'%(special, name, idx)
2253 # backward compatible (and query) URL format
2254 key = special + name
2255 dirkey = key + 'dir'
2256 if key in self.form and not fields:
2257 fields = handleListCGIValue(self.form[key])
2258 if dirkey in self.form:
2259 dirs.append(self.form.getfirst(dirkey))
2260 if fields: # only try other special char if nothing found
2261 break
2262 for f, d in map(None, fields, dirs):
2263 if f.startswith('-'):
2264 var.append(('-', f[1:]))
2265 elif d:
2266 var.append(('-', f))
2267 else:
2268 var.append(('+', f))
2270 def _post_init(self):
2271 """ Set attributes based on self.form
2272 """
2273 # extract the index display information from the form
2274 self.columns = []
2275 for name in ':columns @columns'.split():
2276 if self.form.has_key(name):
2277 self.special_char = name[0]
2278 self.columns = handleListCGIValue(self.form[name])
2279 break
2280 self.show = support.TruthDict(self.columns)
2282 # sorting and grouping
2283 self.sort = []
2284 self.group = []
2285 self._parse_sort(self.sort, 'sort')
2286 self._parse_sort(self.group, 'group')
2288 # filtering
2289 self.filter = []
2290 for name in ':filter @filter'.split():
2291 if self.form.has_key(name):
2292 self.special_char = name[0]
2293 self.filter = handleListCGIValue(self.form[name])
2295 self.filterspec = {}
2296 db = self.client.db
2297 if self.classname is not None:
2298 cls = db.getclass (self.classname)
2299 for name in self.filter:
2300 if not self.form.has_key(name):
2301 continue
2302 prop = cls.get_transitive_prop (name)
2303 fv = self.form[name]
2304 if (isinstance(prop, hyperdb.Link) or
2305 isinstance(prop, hyperdb.Multilink)):
2306 self.filterspec[name] = lookupIds(db, prop,
2307 handleListCGIValue(fv))
2308 else:
2309 if isinstance(fv, type([])):
2310 self.filterspec[name] = [v.value for v in fv]
2311 elif name == 'id':
2312 # special case "id" property
2313 self.filterspec[name] = handleListCGIValue(fv)
2314 else:
2315 self.filterspec[name] = fv.value
2317 # full-text search argument
2318 self.search_text = None
2319 for name in ':search_text @search_text'.split():
2320 if self.form.has_key(name):
2321 self.special_char = name[0]
2322 self.search_text = self.form.getfirst(name)
2324 # pagination - size and start index
2325 # figure batch args
2326 self.pagesize = 50
2327 for name in ':pagesize @pagesize'.split():
2328 if self.form.has_key(name):
2329 self.special_char = name[0]
2330 self.pagesize = int(self.form.getfirst(name))
2332 self.startwith = 0
2333 for name in ':startwith @startwith'.split():
2334 if self.form.has_key(name):
2335 self.special_char = name[0]
2336 self.startwith = int(self.form.getfirst(name))
2338 # dispname
2339 if self.form.has_key('@dispname'):
2340 self.dispname = self.form.getfirst('@dispname')
2341 else:
2342 self.dispname = None
2344 def updateFromURL(self, url):
2345 """ Parse the URL for query args, and update my attributes using the
2346 values.
2347 """
2348 env = {'QUERY_STRING': url}
2349 self.form = cgi.FieldStorage(environ=env)
2351 self._post_init()
2353 def update(self, kwargs):
2354 """ Update my attributes using the keyword args
2355 """
2356 self.__dict__.update(kwargs)
2357 if kwargs.has_key('columns'):
2358 self.show = support.TruthDict(self.columns)
2360 def description(self):
2361 """ Return a description of the request - handle for the page title.
2362 """
2363 s = [self.client.db.config.TRACKER_NAME]
2364 if self.classname:
2365 if self.client.nodeid:
2366 s.append('- %s%s'%(self.classname, self.client.nodeid))
2367 else:
2368 if self.template == 'item':
2369 s.append('- new %s'%self.classname)
2370 elif self.template == 'index':
2371 s.append('- %s index'%self.classname)
2372 else:
2373 s.append('- %s %s'%(self.classname, self.template))
2374 else:
2375 s.append('- home')
2376 return ' '.join(s)
2378 def __str__(self):
2379 d = {}
2380 d.update(self.__dict__)
2381 f = ''
2382 for k in self.form.keys():
2383 f += '\n %r=%r'%(k,handleListCGIValue(self.form[k]))
2384 d['form'] = f
2385 e = ''
2386 for k,v in self.env.items():
2387 e += '\n %r=%r'%(k, v)
2388 d['env'] = e
2389 return """
2390 form: %(form)s
2391 base: %(base)r
2392 classname: %(classname)r
2393 template: %(template)r
2394 columns: %(columns)r
2395 sort: %(sort)r
2396 group: %(group)r
2397 filter: %(filter)r
2398 search_text: %(search_text)r
2399 pagesize: %(pagesize)r
2400 startwith: %(startwith)r
2401 env: %(env)s
2402 """%d
2404 def indexargs_form(self, columns=1, sort=1, group=1, filter=1,
2405 filterspec=1, search_text=1):
2406 """ return the current index args as form elements """
2407 l = []
2408 sc = self.special_char
2409 def add(k, v):
2410 l.append(self.input(type="hidden", name=k, value=v))
2411 if columns and self.columns:
2412 add(sc+'columns', ','.join(self.columns))
2413 if sort:
2414 val = []
2415 for dir, attr in self.sort:
2416 if dir == '-':
2417 val.append('-'+attr)
2418 else:
2419 val.append(attr)
2420 add(sc+'sort', ','.join (val))
2421 if group:
2422 val = []
2423 for dir, attr in self.group:
2424 if dir == '-':
2425 val.append('-'+attr)
2426 else:
2427 val.append(attr)
2428 add(sc+'group', ','.join (val))
2429 if filter and self.filter:
2430 add(sc+'filter', ','.join(self.filter))
2431 if self.classname and filterspec:
2432 props = self.client.db.getclass(self.classname).getprops()
2433 for k,v in self.filterspec.items():
2434 if type(v) == type([]):
2435 if isinstance(props[k], hyperdb.String):
2436 add(k, ' '.join(v))
2437 else:
2438 add(k, ','.join(v))
2439 else:
2440 add(k, v)
2441 if search_text and self.search_text:
2442 add(sc+'search_text', self.search_text)
2443 add(sc+'pagesize', self.pagesize)
2444 add(sc+'startwith', self.startwith)
2445 return '\n'.join(l)
2447 def indexargs_url(self, url, args):
2448 """ Embed the current index args in a URL
2449 """
2450 q = urllib.quote
2451 sc = self.special_char
2452 l = ['%s=%s'%(k,v) for k,v in args.items()]
2454 # pull out the special values (prefixed by @ or :)
2455 specials = {}
2456 for key in args.keys():
2457 if key[0] in '@:':
2458 specials[key[1:]] = args[key]
2460 # ok, now handle the specials we received in the request
2461 if self.columns and not specials.has_key('columns'):
2462 l.append(sc+'columns=%s'%(','.join(self.columns)))
2463 if self.sort and not specials.has_key('sort'):
2464 val = []
2465 for dir, attr in self.sort:
2466 if dir == '-':
2467 val.append('-'+attr)
2468 else:
2469 val.append(attr)
2470 l.append(sc+'sort=%s'%(','.join(val)))
2471 if self.group and not specials.has_key('group'):
2472 val = []
2473 for dir, attr in self.group:
2474 if dir == '-':
2475 val.append('-'+attr)
2476 else:
2477 val.append(attr)
2478 l.append(sc+'group=%s'%(','.join(val)))
2479 if self.filter and not specials.has_key('filter'):
2480 l.append(sc+'filter=%s'%(','.join(self.filter)))
2481 if self.search_text and not specials.has_key('search_text'):
2482 l.append(sc+'search_text=%s'%q(self.search_text))
2483 if not specials.has_key('pagesize'):
2484 l.append(sc+'pagesize=%s'%self.pagesize)
2485 if not specials.has_key('startwith'):
2486 l.append(sc+'startwith=%s'%self.startwith)
2488 # finally, the remainder of the filter args in the request
2489 if self.classname and self.filterspec:
2490 cls = self.client.db.getclass(self.classname)
2491 for k,v in self.filterspec.items():
2492 if not args.has_key(k):
2493 if type(v) == type([]):
2494 prop = cls.get_transitive_prop(k)
2495 if k != 'id' and isinstance(prop, hyperdb.String):
2496 l.append('%s=%s'%(k, '%20'.join([q(i) for i in v])))
2497 else:
2498 l.append('%s=%s'%(k, ','.join([q(i) for i in v])))
2499 else:
2500 l.append('%s=%s'%(k, q(v)))
2501 return '%s?%s'%(url, '&'.join(l))
2502 indexargs_href = indexargs_url
2504 def base_javascript(self):
2505 return """
2506 <script type="text/javascript">
2507 submitted = false;
2508 function submit_once() {
2509 if (submitted) {
2510 alert("Your request is being processed.\\nPlease be patient.");
2511 event.returnValue = 0; // work-around for IE
2512 return 0;
2513 }
2514 submitted = true;
2515 return 1;
2516 }
2518 function help_window(helpurl, width, height) {
2519 HelpWin = window.open('%s' + helpurl, 'RoundupHelpWindow', 'scrollbars=yes,resizable=yes,toolbar=no,height='+height+',width='+width);
2520 }
2521 </script>
2522 """%self.base
2524 def batch(self):
2525 """ Return a batch object for results from the "current search"
2526 """
2527 filterspec = self.filterspec
2528 sort = self.sort
2529 group = self.group
2531 # get the list of ids we're batching over
2532 klass = self.client.db.getclass(self.classname)
2533 if self.search_text:
2534 matches = self.client.db.indexer.search(
2535 [w.upper().encode("utf-8", "replace") for w in re.findall(
2536 r'(?u)\b\w{2,25}\b',
2537 unicode(self.search_text, "utf-8", "replace")
2538 )], klass)
2539 else:
2540 matches = None
2542 # filter for visibility
2543 check = self._client.db.security.hasPermission
2544 userid = self._client.userid
2545 l = [id for id in klass.filter(matches, filterspec, sort, group)
2546 if check('View', userid, self.classname, itemid=id)]
2548 # return the batch object, using IDs only
2549 return Batch(self.client, l, self.pagesize, self.startwith,
2550 classname=self.classname)
2552 # extend the standard ZTUtils Batch object to remove dependency on
2553 # Acquisition and add a couple of useful methods
2554 class Batch(ZTUtils.Batch):
2555 """ Use me to turn a list of items, or item ids of a given class, into a
2556 series of batches.
2558 ========= ========================================================
2559 Parameter Usage
2560 ========= ========================================================
2561 sequence a list of HTMLItems or item ids
2562 classname if sequence is a list of ids, this is the class of item
2563 size how big to make the sequence.
2564 start where to start (0-indexed) in the sequence.
2565 end where to end (0-indexed) in the sequence.
2566 orphan if the next batch would contain less items than this
2567 value, then it is combined with this batch
2568 overlap the number of items shared between adjacent batches
2569 ========= ========================================================
2571 Attributes: Note that the "start" attribute, unlike the
2572 argument, is a 1-based index (I know, lame). "first" is the
2573 0-based index. "length" is the actual number of elements in
2574 the batch.
2576 "sequence_length" is the length of the original, unbatched, sequence.
2577 """
2578 def __init__(self, client, sequence, size, start, end=0, orphan=0,
2579 overlap=0, classname=None):
2580 self.client = client
2581 self.last_index = self.last_item = None
2582 self.current_item = None
2583 self.classname = classname
2584 self.sequence_length = len(sequence)
2585 ZTUtils.Batch.__init__(self, sequence, size, start, end, orphan,
2586 overlap)
2588 # overwrite so we can late-instantiate the HTMLItem instance
2589 def __getitem__(self, index):
2590 if index < 0:
2591 if index + self.end < self.first: raise IndexError, index
2592 return self._sequence[index + self.end]
2594 if index >= self.length:
2595 raise IndexError, index
2597 # move the last_item along - but only if the fetched index changes
2598 # (for some reason, index 0 is fetched twice)
2599 if index != self.last_index:
2600 self.last_item = self.current_item
2601 self.last_index = index
2603 item = self._sequence[index + self.first]
2604 if self.classname:
2605 # map the item ids to instances
2606 item = HTMLItem(self.client, self.classname, item)
2607 self.current_item = item
2608 return item
2610 def propchanged(self, *properties):
2611 """ Detect if one of the properties marked as being a group
2612 property changed in the last iteration fetch
2613 """
2614 # we poke directly at the _value here since MissingValue can screw
2615 # us up and cause Nones to compare strangely
2616 if self.last_item is None:
2617 return 1
2618 for property in properties:
2619 if property == 'id' or isinstance (self.last_item[property], list):
2620 if (str(self.last_item[property]) !=
2621 str(self.current_item[property])):
2622 return 1
2623 else:
2624 if (self.last_item[property]._value !=
2625 self.current_item[property]._value):
2626 return 1
2627 return 0
2629 # override these 'cos we don't have access to acquisition
2630 def previous(self):
2631 if self.start == 1:
2632 return None
2633 return Batch(self.client, self._sequence, self._size,
2634 self.first - self._size + self.overlap, 0, self.orphan,
2635 self.overlap)
2637 def next(self):
2638 try:
2639 self._sequence[self.end]
2640 except IndexError:
2641 return None
2642 return Batch(self.client, self._sequence, self._size,
2643 self.end - self.overlap, 0, self.orphan, self.overlap)
2645 class TemplatingUtils:
2646 """ Utilities for templating
2647 """
2648 def __init__(self, client):
2649 self.client = client
2650 def Batch(self, sequence, size, start, end=0, orphan=0, overlap=0):
2651 return Batch(self.client, sequence, size, start, end, orphan,
2652 overlap)
2654 def url_quote(self, url):
2655 """URL-quote the supplied text."""
2656 return urllib.quote(url)
2658 def html_quote(self, html):
2659 """HTML-quote the supplied text."""
2660 return cgi.escape(html)
2662 def __getattr__(self, name):
2663 """Try the tracker's templating_utils."""
2664 if not hasattr(self.client.instance, 'templating_utils'):
2665 # backwards-compatibility
2666 raise AttributeError, name
2667 if not self.client.instance.templating_utils.has_key(name):
2668 raise AttributeError, name
2669 return self.client.instance.templating_utils[name]
2671 def html_calendar(self, request):
2672 """Generate a HTML calendar.
2674 `request` the roundup.request object
2675 - @template : name of the template
2676 - form : name of the form to store back the date
2677 - property : name of the property of the form to store
2678 back the date
2679 - date : current date
2680 - display : when browsing, specifies year and month
2682 html will simply be a table.
2683 """
2684 date_str = request.form.getfirst("date", ".")
2685 display = request.form.getfirst("display", date_str)
2686 template = request.form.getfirst("@template", "calendar")
2687 form = request.form.getfirst("form")
2688 property = request.form.getfirst("property")
2689 curr_date = date.Date(date_str) # to highlight
2690 display = date.Date(display) # to show
2691 day = display.day
2693 # for navigation
2694 date_prev_month = display + date.Interval("-1m")
2695 date_next_month = display + date.Interval("+1m")
2696 date_prev_year = display + date.Interval("-1y")
2697 date_next_year = display + date.Interval("+1y")
2699 res = []
2701 base_link = "%s?@template=%s&property=%s&form=%s&date=%s" % \
2702 (request.classname, template, property, form, curr_date)
2704 # navigation
2705 # month
2706 res.append('<table class="calendar"><tr><td>')
2707 res.append(' <table width="100%" class="calendar_nav"><tr>')
2708 link = "&display=%s"%date_prev_month
2709 res.append(' <td><a href="%s&display=%s"><</a></td>'%(base_link,
2710 date_prev_month))
2711 res.append(' <td>%s</td>'%calendar.month_name[display.month])
2712 res.append(' <td><a href="%s&display=%s">></a></td>'%(base_link,
2713 date_next_month))
2714 # spacer
2715 res.append(' <td width="100%"></td>')
2716 # year
2717 res.append(' <td><a href="%s&display=%s"><</a></td>'%(base_link,
2718 date_prev_year))
2719 res.append(' <td>%s</td>'%display.year)
2720 res.append(' <td><a href="%s&display=%s">></a></td>'%(base_link,
2721 date_next_year))
2722 res.append(' </tr></table>')
2723 res.append(' </td></tr>')
2725 # the calendar
2726 res.append(' <tr><td><table class="calendar_display">')
2727 res.append(' <tr class="weekdays">')
2728 for day in calendar.weekheader(3).split():
2729 res.append(' <td>%s</td>'%day)
2730 res.append(' </tr>')
2731 for week in calendar.monthcalendar(display.year, display.month):
2732 res.append(' <tr>')
2733 for day in week:
2734 link = "javascript:form[field].value = '%d-%02d-%02d'; " \
2735 "window.close ();"%(display.year, display.month, day)
2736 if (day == curr_date.day and display.month == curr_date.month
2737 and display.year == curr_date.year):
2738 # highlight
2739 style = "today"
2740 else :
2741 style = ""
2742 if day:
2743 res.append(' <td class="%s"><a href="%s">%s</a></td>'%(
2744 style, link, day))
2745 else :
2746 res.append(' <td></td>')
2747 res.append(' </tr>')
2748 res.append('</table></td></tr></table>')
2749 return "\n".join(res)
2751 class MissingValue:
2752 def __init__(self, description, **kwargs):
2753 self.__description = description
2754 for key, value in kwargs.items():
2755 self.__dict__[key] = value
2757 def __call__(self, *args, **kwargs): return MissingValue(self.__description)
2758 def __getattr__(self, name):
2759 # This allows assignments which assume all intermediate steps are Null
2760 # objects if they don't exist yet.
2761 #
2762 # For example (with just 'client' defined):
2763 #
2764 # client.db.config.TRACKER_WEB = 'BASE/'
2765 self.__dict__[name] = MissingValue(self.__description)
2766 return getattr(self, name)
2768 def __getitem__(self, key): return self
2769 def __nonzero__(self): return 0
2770 def __str__(self): return '[%s]'%self.__description
2771 def __repr__(self): return '<MissingValue 0x%x "%s">'%(id(self),
2772 self.__description)
2773 def gettext(self, str): return str
2774 _ = gettext
2776 # vim: set et sts=4 sw=4 :