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