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[item].value.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[item].value.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 = find_sort_key(self._klass)
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 = ('+', find_sort_key(linkcl))
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 = ('+', find_sort_key(linkcl))
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 = find_sort_key(linkcl)
2143 def sortfunc(a, b):
2144 return cmp(linkcl.get(a, sort_on), linkcl.get(b, sort_on))
2145 return sortfunc
2147 def find_sort_key(linkcl):
2148 if linkcl.getprops().has_key('order'):
2149 return 'order'
2150 else:
2151 return linkcl.labelprop()
2153 def handleListCGIValue(value):
2154 """ Value is either a single item or a list of items. Each item has a
2155 .value that we're actually interested in.
2156 """
2157 if isinstance(value, type([])):
2158 return [value.value for value in value]
2159 else:
2160 value = value.value.strip()
2161 if not value:
2162 return []
2163 return [v.strip() for v in value.split(',')]
2165 class HTMLRequest(HTMLInputMixin):
2166 """The *request*, holding the CGI form and environment.
2168 - "form" the CGI form as a cgi.FieldStorage
2169 - "env" the CGI environment variables
2170 - "base" the base URL for this instance
2171 - "user" a HTMLItem instance for this user
2172 - "language" as determined by the browser or config
2173 - "classname" the current classname (possibly None)
2174 - "template" the current template (suffix, also possibly None)
2176 Index args:
2178 - "columns" dictionary of the columns to display in an index page
2179 - "show" a convenience access to columns - request/show/colname will
2180 be true if the columns should be displayed, false otherwise
2181 - "sort" index sort column (direction, column name)
2182 - "group" index grouping property (direction, column name)
2183 - "filter" properties to filter the index on
2184 - "filterspec" values to filter the index on
2185 - "search_text" text to perform a full-text search on for an index
2186 """
2187 def __repr__(self):
2188 return '<HTMLRequest %r>'%self.__dict__
2190 def __init__(self, client):
2191 # _client is needed by HTMLInputMixin
2192 self._client = self.client = client
2194 # easier access vars
2195 self.form = client.form
2196 self.env = client.env
2197 self.base = client.base
2198 self.user = HTMLItem(client, 'user', client.userid)
2199 self.language = client.language
2201 # store the current class name and action
2202 self.classname = client.classname
2203 self.nodeid = client.nodeid
2204 self.template = client.template
2206 # the special char to use for special vars
2207 self.special_char = '@'
2209 HTMLInputMixin.__init__(self)
2211 self._post_init()
2213 def current_url(self):
2214 url = self.base
2215 if self.classname:
2216 url += self.classname
2217 if self.nodeid:
2218 url += self.nodeid
2219 args = {}
2220 if self.template:
2221 args['@template'] = self.template
2222 return self.indexargs_url(url, args)
2224 def _parse_sort(self, var, name):
2225 """ Parse sort/group options. Append to var
2226 """
2227 fields = []
2228 dirs = []
2229 for special in '@:':
2230 idx = 0
2231 key = '%s%s%d'%(special, name, idx)
2232 while key in self.form:
2233 self.special_char = special
2234 fields.append (self.form[key].value)
2235 dirkey = '%s%sdir%d'%(special, name, idx)
2236 if dirkey in self.form:
2237 dirs.append(self.form[dirkey].value)
2238 else:
2239 dirs.append(None)
2240 idx += 1
2241 key = '%s%s%d'%(special, name, idx)
2242 # backward compatible (and query) URL format
2243 key = special + name
2244 dirkey = key + 'dir'
2245 if key in self.form and not fields:
2246 fields = handleListCGIValue(self.form[key])
2247 if dirkey in self.form:
2248 dirs.append(self.form[dirkey].value)
2249 if fields: # only try other special char if nothing found
2250 break
2251 for f, d in map(None, fields, dirs):
2252 if f.startswith('-'):
2253 var.append(('-', f[1:]))
2254 elif d:
2255 var.append(('-', f))
2256 else:
2257 var.append(('+', f))
2259 def _post_init(self):
2260 """ Set attributes based on self.form
2261 """
2262 # extract the index display information from the form
2263 self.columns = []
2264 for name in ':columns @columns'.split():
2265 if self.form.has_key(name):
2266 self.special_char = name[0]
2267 self.columns = handleListCGIValue(self.form[name])
2268 break
2269 self.show = support.TruthDict(self.columns)
2271 # sorting and grouping
2272 self.sort = []
2273 self.group = []
2274 self._parse_sort(self.sort, 'sort')
2275 self._parse_sort(self.group, 'group')
2277 # filtering
2278 self.filter = []
2279 for name in ':filter @filter'.split():
2280 if self.form.has_key(name):
2281 self.special_char = name[0]
2282 self.filter = handleListCGIValue(self.form[name])
2284 self.filterspec = {}
2285 db = self.client.db
2286 if self.classname is not None:
2287 cls = db.getclass (self.classname)
2288 for name in self.filter:
2289 if not self.form.has_key(name):
2290 continue
2291 prop = cls.get_transitive_prop (name)
2292 fv = self.form[name]
2293 if (isinstance(prop, hyperdb.Link) or
2294 isinstance(prop, hyperdb.Multilink)):
2295 self.filterspec[name] = lookupIds(db, prop,
2296 handleListCGIValue(fv))
2297 else:
2298 if isinstance(fv, type([])):
2299 self.filterspec[name] = [v.value for v in fv]
2300 elif name == 'id':
2301 # special case "id" property
2302 self.filterspec[name] = handleListCGIValue(fv)
2303 else:
2304 self.filterspec[name] = fv.value
2306 # full-text search argument
2307 self.search_text = None
2308 for name in ':search_text @search_text'.split():
2309 if self.form.has_key(name):
2310 self.special_char = name[0]
2311 try:
2312 self.search_text = self.form[name].value
2313 except AttributeError:
2314 # http://psf.upfronthosting.co.za/roundup/meta/issue111
2315 # Multiple search_text, probably some kind of spambot.
2316 # Use first value.
2317 self.search_text = self.form[name][0].value
2319 # pagination - size and start index
2320 # figure batch args
2321 self.pagesize = 50
2322 for name in ':pagesize @pagesize'.split():
2323 if self.form.has_key(name):
2324 self.special_char = name[0]
2325 self.pagesize = int(self.form[name].value)
2327 self.startwith = 0
2328 for name in ':startwith @startwith'.split():
2329 if self.form.has_key(name):
2330 self.special_char = name[0]
2331 self.startwith = int(self.form[name].value)
2333 # dispname
2334 if self.form.has_key('@dispname'):
2335 self.dispname = self.form['@dispname'].value
2336 else:
2337 self.dispname = None
2339 def updateFromURL(self, url):
2340 """ Parse the URL for query args, and update my attributes using the
2341 values.
2342 """
2343 env = {'QUERY_STRING': url}
2344 self.form = cgi.FieldStorage(environ=env)
2346 self._post_init()
2348 def update(self, kwargs):
2349 """ Update my attributes using the keyword args
2350 """
2351 self.__dict__.update(kwargs)
2352 if kwargs.has_key('columns'):
2353 self.show = support.TruthDict(self.columns)
2355 def description(self):
2356 """ Return a description of the request - handle for the page title.
2357 """
2358 s = [self.client.db.config.TRACKER_NAME]
2359 if self.classname:
2360 if self.client.nodeid:
2361 s.append('- %s%s'%(self.classname, self.client.nodeid))
2362 else:
2363 if self.template == 'item':
2364 s.append('- new %s'%self.classname)
2365 elif self.template == 'index':
2366 s.append('- %s index'%self.classname)
2367 else:
2368 s.append('- %s %s'%(self.classname, self.template))
2369 else:
2370 s.append('- home')
2371 return ' '.join(s)
2373 def __str__(self):
2374 d = {}
2375 d.update(self.__dict__)
2376 f = ''
2377 for k in self.form.keys():
2378 f += '\n %r=%r'%(k,handleListCGIValue(self.form[k]))
2379 d['form'] = f
2380 e = ''
2381 for k,v in self.env.items():
2382 e += '\n %r=%r'%(k, v)
2383 d['env'] = e
2384 return """
2385 form: %(form)s
2386 base: %(base)r
2387 classname: %(classname)r
2388 template: %(template)r
2389 columns: %(columns)r
2390 sort: %(sort)r
2391 group: %(group)r
2392 filter: %(filter)r
2393 search_text: %(search_text)r
2394 pagesize: %(pagesize)r
2395 startwith: %(startwith)r
2396 env: %(env)s
2397 """%d
2399 def indexargs_form(self, columns=1, sort=1, group=1, filter=1,
2400 filterspec=1, search_text=1):
2401 """ return the current index args as form elements """
2402 l = []
2403 sc = self.special_char
2404 def add(k, v):
2405 l.append(self.input(type="hidden", name=k, value=v))
2406 if columns and self.columns:
2407 add(sc+'columns', ','.join(self.columns))
2408 if sort:
2409 val = []
2410 for dir, attr in self.sort:
2411 if dir == '-':
2412 val.append('-'+attr)
2413 else:
2414 val.append(attr)
2415 add(sc+'sort', ','.join (val))
2416 if group:
2417 val = []
2418 for dir, attr in self.group:
2419 if dir == '-':
2420 val.append('-'+attr)
2421 else:
2422 val.append(attr)
2423 add(sc+'group', ','.join (val))
2424 if filter and self.filter:
2425 add(sc+'filter', ','.join(self.filter))
2426 if self.classname and filterspec:
2427 props = self.client.db.getclass(self.classname).getprops()
2428 for k,v in self.filterspec.items():
2429 if type(v) == type([]):
2430 if isinstance(props[k], hyperdb.String):
2431 add(k, ' '.join(v))
2432 else:
2433 add(k, ','.join(v))
2434 else:
2435 add(k, v)
2436 if search_text and self.search_text:
2437 add(sc+'search_text', self.search_text)
2438 add(sc+'pagesize', self.pagesize)
2439 add(sc+'startwith', self.startwith)
2440 return '\n'.join(l)
2442 def indexargs_url(self, url, args):
2443 """ Embed the current index args in a URL
2444 """
2445 q = urllib.quote
2446 sc = self.special_char
2447 l = ['%s=%s'%(k,v) for k,v in args.items()]
2449 # pull out the special values (prefixed by @ or :)
2450 specials = {}
2451 for key in args.keys():
2452 if key[0] in '@:':
2453 specials[key[1:]] = args[key]
2455 # ok, now handle the specials we received in the request
2456 if self.columns and not specials.has_key('columns'):
2457 l.append(sc+'columns=%s'%(','.join(self.columns)))
2458 if self.sort and not specials.has_key('sort'):
2459 val = []
2460 for dir, attr in self.sort:
2461 if dir == '-':
2462 val.append('-'+attr)
2463 else:
2464 val.append(attr)
2465 l.append(sc+'sort=%s'%(','.join(val)))
2466 if self.group and not specials.has_key('group'):
2467 val = []
2468 for dir, attr in self.group:
2469 if dir == '-':
2470 val.append('-'+attr)
2471 else:
2472 val.append(attr)
2473 l.append(sc+'group=%s'%(','.join(val)))
2474 if self.filter and not specials.has_key('filter'):
2475 l.append(sc+'filter=%s'%(','.join(self.filter)))
2476 if self.search_text and not specials.has_key('search_text'):
2477 l.append(sc+'search_text=%s'%q(self.search_text))
2478 if not specials.has_key('pagesize'):
2479 l.append(sc+'pagesize=%s'%self.pagesize)
2480 if not specials.has_key('startwith'):
2481 l.append(sc+'startwith=%s'%self.startwith)
2483 # finally, the remainder of the filter args in the request
2484 if self.classname and self.filterspec:
2485 cls = self.client.db.getclass(self.classname)
2486 for k,v in self.filterspec.items():
2487 if not args.has_key(k):
2488 if type(v) == type([]):
2489 prop = cls.get_transitive_prop(k)
2490 if k != 'id' and isinstance(prop, hyperdb.String):
2491 l.append('%s=%s'%(k, '%20'.join([q(i) for i in v])))
2492 else:
2493 l.append('%s=%s'%(k, ','.join([q(i) for i in v])))
2494 else:
2495 l.append('%s=%s'%(k, q(v)))
2496 return '%s?%s'%(url, '&'.join(l))
2497 indexargs_href = indexargs_url
2499 def base_javascript(self):
2500 return """
2501 <script type="text/javascript">
2502 submitted = false;
2503 function submit_once() {
2504 if (submitted) {
2505 alert("Your request is being processed.\\nPlease be patient.");
2506 event.returnValue = 0; // work-around for IE
2507 return 0;
2508 }
2509 submitted = true;
2510 return 1;
2511 }
2513 function help_window(helpurl, width, height) {
2514 HelpWin = window.open('%s' + helpurl, 'RoundupHelpWindow', 'scrollbars=yes,resizable=yes,toolbar=no,height='+height+',width='+width);
2515 }
2516 </script>
2517 """%self.base
2519 def batch(self):
2520 """ Return a batch object for results from the "current search"
2521 """
2522 filterspec = self.filterspec
2523 sort = self.sort
2524 group = self.group
2526 # get the list of ids we're batching over
2527 klass = self.client.db.getclass(self.classname)
2528 if self.search_text:
2529 matches = self.client.db.indexer.search(
2530 [w.upper().encode("utf-8", "replace") for w in re.findall(
2531 r'(?u)\b\w{2,25}\b',
2532 unicode(self.search_text, "utf-8", "replace")
2533 )], klass)
2534 else:
2535 matches = None
2537 # filter for visibility
2538 check = self._client.db.security.hasPermission
2539 userid = self._client.userid
2540 l = [id for id in klass.filter(matches, filterspec, sort, group)
2541 if check('View', userid, self.classname, itemid=id)]
2543 # return the batch object, using IDs only
2544 return Batch(self.client, l, self.pagesize, self.startwith,
2545 classname=self.classname)
2547 # extend the standard ZTUtils Batch object to remove dependency on
2548 # Acquisition and add a couple of useful methods
2549 class Batch(ZTUtils.Batch):
2550 """ Use me to turn a list of items, or item ids of a given class, into a
2551 series of batches.
2553 ========= ========================================================
2554 Parameter Usage
2555 ========= ========================================================
2556 sequence a list of HTMLItems or item ids
2557 classname if sequence is a list of ids, this is the class of item
2558 size how big to make the sequence.
2559 start where to start (0-indexed) in the sequence.
2560 end where to end (0-indexed) in the sequence.
2561 orphan if the next batch would contain less items than this
2562 value, then it is combined with this batch
2563 overlap the number of items shared between adjacent batches
2564 ========= ========================================================
2566 Attributes: Note that the "start" attribute, unlike the
2567 argument, is a 1-based index (I know, lame). "first" is the
2568 0-based index. "length" is the actual number of elements in
2569 the batch.
2571 "sequence_length" is the length of the original, unbatched, sequence.
2572 """
2573 def __init__(self, client, sequence, size, start, end=0, orphan=0,
2574 overlap=0, classname=None):
2575 self.client = client
2576 self.last_index = self.last_item = None
2577 self.current_item = None
2578 self.classname = classname
2579 self.sequence_length = len(sequence)
2580 ZTUtils.Batch.__init__(self, sequence, size, start, end, orphan,
2581 overlap)
2583 # overwrite so we can late-instantiate the HTMLItem instance
2584 def __getitem__(self, index):
2585 if index < 0:
2586 if index + self.end < self.first: raise IndexError, index
2587 return self._sequence[index + self.end]
2589 if index >= self.length:
2590 raise IndexError, index
2592 # move the last_item along - but only if the fetched index changes
2593 # (for some reason, index 0 is fetched twice)
2594 if index != self.last_index:
2595 self.last_item = self.current_item
2596 self.last_index = index
2598 item = self._sequence[index + self.first]
2599 if self.classname:
2600 # map the item ids to instances
2601 item = HTMLItem(self.client, self.classname, item)
2602 self.current_item = item
2603 return item
2605 def propchanged(self, *properties):
2606 """ Detect if one of the properties marked as being a group
2607 property changed in the last iteration fetch
2608 """
2609 # we poke directly at the _value here since MissingValue can screw
2610 # us up and cause Nones to compare strangely
2611 if self.last_item is None:
2612 return 1
2613 for property in properties:
2614 if property == 'id' or isinstance (self.last_item[property], list):
2615 if (str(self.last_item[property]) !=
2616 str(self.current_item[property])):
2617 return 1
2618 else:
2619 if (self.last_item[property]._value !=
2620 self.current_item[property]._value):
2621 return 1
2622 return 0
2624 # override these 'cos we don't have access to acquisition
2625 def previous(self):
2626 if self.start == 1:
2627 return None
2628 return Batch(self.client, self._sequence, self._size,
2629 self.first - self._size + self.overlap, 0, self.orphan,
2630 self.overlap)
2632 def next(self):
2633 try:
2634 self._sequence[self.end]
2635 except IndexError:
2636 return None
2637 return Batch(self.client, self._sequence, self._size,
2638 self.end - self.overlap, 0, self.orphan, self.overlap)
2640 class TemplatingUtils:
2641 """ Utilities for templating
2642 """
2643 def __init__(self, client):
2644 self.client = client
2645 def Batch(self, sequence, size, start, end=0, orphan=0, overlap=0):
2646 return Batch(self.client, sequence, size, start, end, orphan,
2647 overlap)
2649 def url_quote(self, url):
2650 """URL-quote the supplied text."""
2651 return urllib.quote(url)
2653 def html_quote(self, html):
2654 """HTML-quote the supplied text."""
2655 return cgi.escape(html)
2657 def __getattr__(self, name):
2658 """Try the tracker's templating_utils."""
2659 if not hasattr(self.client.instance, 'templating_utils'):
2660 # backwards-compatibility
2661 raise AttributeError, name
2662 if not self.client.instance.templating_utils.has_key(name):
2663 raise AttributeError, name
2664 return self.client.instance.templating_utils[name]
2666 def html_calendar(self, request):
2667 """Generate a HTML calendar.
2669 `request` the roundup.request object
2670 - @template : name of the template
2671 - form : name of the form to store back the date
2672 - property : name of the property of the form to store
2673 back the date
2674 - date : current date
2675 - display : when browsing, specifies year and month
2677 html will simply be a table.
2678 """
2679 date_str = request.form.getfirst("date", ".")
2680 display = request.form.getfirst("display", date_str)
2681 template = request.form.getfirst("@template", "calendar")
2682 form = request.form.getfirst("form")
2683 property = request.form.getfirst("property")
2684 curr_date = date.Date(date_str) # to highlight
2685 display = date.Date(display) # to show
2686 day = display.day
2688 # for navigation
2689 date_prev_month = display + date.Interval("-1m")
2690 date_next_month = display + date.Interval("+1m")
2691 date_prev_year = display + date.Interval("-1y")
2692 date_next_year = display + date.Interval("+1y")
2694 res = []
2696 base_link = "%s?@template=%s&property=%s&form=%s&date=%s" % \
2697 (request.classname, template, property, form, curr_date)
2699 # navigation
2700 # month
2701 res.append('<table class="calendar"><tr><td>')
2702 res.append(' <table width="100%" class="calendar_nav"><tr>')
2703 link = "&display=%s"%date_prev_month
2704 res.append(' <td><a href="%s&display=%s"><</a></td>'%(base_link,
2705 date_prev_month))
2706 res.append(' <td>%s</td>'%calendar.month_name[display.month])
2707 res.append(' <td><a href="%s&display=%s">></a></td>'%(base_link,
2708 date_next_month))
2709 # spacer
2710 res.append(' <td width="100%"></td>')
2711 # year
2712 res.append(' <td><a href="%s&display=%s"><</a></td>'%(base_link,
2713 date_prev_year))
2714 res.append(' <td>%s</td>'%display.year)
2715 res.append(' <td><a href="%s&display=%s">></a></td>'%(base_link,
2716 date_next_year))
2717 res.append(' </tr></table>')
2718 res.append(' </td></tr>')
2720 # the calendar
2721 res.append(' <tr><td><table class="calendar_display">')
2722 res.append(' <tr class="weekdays">')
2723 for day in calendar.weekheader(3).split():
2724 res.append(' <td>%s</td>'%day)
2725 res.append(' </tr>')
2726 for week in calendar.monthcalendar(display.year, display.month):
2727 res.append(' <tr>')
2728 for day in week:
2729 link = "javascript:form[field].value = '%d-%02d-%02d'; " \
2730 "window.close ();"%(display.year, display.month, day)
2731 if (day == curr_date.day and display.month == curr_date.month
2732 and display.year == curr_date.year):
2733 # highlight
2734 style = "today"
2735 else :
2736 style = ""
2737 if day:
2738 res.append(' <td class="%s"><a href="%s">%s</a></td>'%(
2739 style, link, day))
2740 else :
2741 res.append(' <td></td>')
2742 res.append(' </tr>')
2743 res.append('</table></td></tr></table>')
2744 return "\n".join(res)
2746 class MissingValue:
2747 def __init__(self, description, **kwargs):
2748 self.__description = description
2749 for key, value in kwargs.items():
2750 self.__dict__[key] = value
2752 def __call__(self, *args, **kwargs): return MissingValue(self.__description)
2753 def __getattr__(self, name):
2754 # This allows assignments which assume all intermediate steps are Null
2755 # objects if they don't exist yet.
2756 #
2757 # For example (with just 'client' defined):
2758 #
2759 # client.db.config.TRACKER_WEB = 'BASE/'
2760 self.__dict__[name] = MissingValue(self.__description)
2761 return getattr(self, name)
2763 def __getitem__(self, key): return self
2764 def __nonzero__(self): return 0
2765 def __str__(self): return '[%s]'%self.__description
2766 def __repr__(self): return '<MissingValue 0x%x "%s">'%(id(self),
2767 self.__description)
2768 def gettext(self, str): return str
2769 _ = gettext
2771 # vim: set et sts=4 sw=4 :