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')
1105 # The context for a search page should be the class, not any
1106 # node.
1107 self._client.nodeid = None
1109 # use our fabricated request
1110 return pt.render(self._client, req.classname, req)
1112 def download_url(self):
1113 """ Assume that this item is a FileClass and that it has a name
1114 and content. Construct a URL for the download of the content.
1115 """
1116 name = self._klass.get(self._nodeid, 'name')
1117 url = '%s%s/%s'%(self._classname, self._nodeid, name)
1118 return urllib.quote(url)
1120 def copy_url(self, exclude=("messages", "files")):
1121 """Construct a URL for creating a copy of this item
1123 "exclude" is an optional list of properties that should
1124 not be copied to the new object. By default, this list
1125 includes "messages" and "files" properties. Note that
1126 "id" property cannot be copied.
1128 """
1129 exclude = ("id", "activity", "actor", "creation", "creator") \
1130 + tuple(exclude)
1131 query = {
1132 "@template": "item",
1133 "@note": self._("Copy of %(class)s %(id)s") % {
1134 "class": self._(self._classname), "id": self._nodeid},
1135 }
1136 for name in self._props.keys():
1137 if name not in exclude:
1138 query[name] = self[name].plain()
1139 return self._classname + "?" + "&".join(
1140 ["%s=%s" % (key, urllib.quote(value))
1141 for key, value in query.items()])
1143 class _HTMLUser(_HTMLItem):
1144 """Add ability to check for permissions on users.
1145 """
1146 _marker = []
1147 def hasPermission(self, permission, classname=_marker,
1148 property=None, itemid=None):
1149 """Determine if the user has the Permission.
1151 The class being tested defaults to the template's class, but may
1152 be overidden for this test by suppling an alternate classname.
1153 """
1154 if classname is self._marker:
1155 classname = self._client.classname
1156 return self._db.security.hasPermission(permission,
1157 self._nodeid, classname, property, itemid)
1159 def hasRole(self, rolename):
1160 """Determine whether the user has the Role."""
1161 roles = self._db.user.get(self._nodeid, 'roles').split(',')
1162 for role in roles:
1163 if role.strip() == rolename: return True
1164 return False
1166 def HTMLItem(client, classname, nodeid, anonymous=0):
1167 if classname == 'user':
1168 return _HTMLUser(client, classname, nodeid, anonymous)
1169 else:
1170 return _HTMLItem(client, classname, nodeid, anonymous)
1172 class HTMLProperty(HTMLInputMixin, HTMLPermissions):
1173 """ String, Number, Date, Interval HTMLProperty
1175 Has useful attributes:
1177 _name the name of the property
1178 _value the value of the property if any
1180 A wrapper object which may be stringified for the plain() behaviour.
1181 """
1182 def __init__(self, client, classname, nodeid, prop, name, value,
1183 anonymous=0):
1184 self._client = client
1185 self._db = client.db
1186 self._ = client._
1187 self._classname = classname
1188 self._nodeid = nodeid
1189 self._prop = prop
1190 self._value = value
1191 self._anonymous = anonymous
1192 self._name = name
1193 if not anonymous:
1194 self._formname = '%s%s@%s'%(classname, nodeid, name)
1195 else:
1196 self._formname = name
1198 # If no value is already present for this property, see if one
1199 # is specified in the current form.
1200 form = self._client.form
1201 if not self._value and form.has_key(self._formname):
1202 if isinstance(prop, hyperdb.Multilink):
1203 value = lookupIds(self._db, prop,
1204 handleListCGIValue(form[self._formname]),
1205 fail_ok=1)
1206 elif isinstance(prop, hyperdb.Link):
1207 value = form.getfirst(self._formname).strip()
1208 if value:
1209 value = lookupIds(self._db, prop, [value],
1210 fail_ok=1)[0]
1211 else:
1212 value = None
1213 else:
1214 value = form.getfirst(self._formname).strip() or None
1215 self._value = value
1217 HTMLInputMixin.__init__(self)
1219 def __repr__(self):
1220 return '<HTMLProperty(0x%x) %s %r %r>'%(id(self), self._formname,
1221 self._prop, self._value)
1222 def __str__(self):
1223 return self.plain()
1224 def __cmp__(self, other):
1225 if isinstance(other, HTMLProperty):
1226 return cmp(self._value, other._value)
1227 return cmp(self._value, other)
1229 def __nonzero__(self):
1230 return not not self._value
1232 def isset(self):
1233 """Is my _value not None?"""
1234 return self._value is not None
1236 def is_edit_ok(self):
1237 """Should the user be allowed to use an edit form field for this
1238 property. Check "Create" for new items, or "Edit" for existing
1239 ones.
1240 """
1241 if self._nodeid:
1242 return self._db.security.hasPermission('Edit', self._client.userid,
1243 self._classname, self._name, self._nodeid)
1244 return self._db.security.hasPermission('Create', self._client.userid,
1245 self._classname, self._name)
1247 def is_view_ok(self):
1248 """ Is the user allowed to View the current class?
1249 """
1250 if self._db.security.hasPermission('View', self._client.userid,
1251 self._classname, self._name, self._nodeid):
1252 return 1
1253 return self.is_edit_ok()
1255 class StringHTMLProperty(HTMLProperty):
1256 hyper_re = re.compile(r'''(
1257 (?P<url>
1258 (
1259 (ht|f)tp(s?):// # protocol
1260 ([\w]+(:\w+)?@)? # username/password
1261 ([\w\-]+) # hostname
1262 ((\.[\w-]+)+)? # .domain.etc
1263 | # ... or ...
1264 ([\w]+(:\w+)?@)? # username/password
1265 www\. # "www."
1266 ([\w\-]+\.)+ # hostname
1267 [\w]{2,5} # TLD
1268 )
1269 (:[\d]{1,5})? # port
1270 (/[\w\-$.+!*(),;:@&=?/~\\#%]*)? # path etc.
1271 )|
1272 (?P<email>[-+=%/\w\.]+@[\w\.\-]+)|
1273 (?P<item>(?P<class>[A-Za-z_]+)(\s*)(?P<id>\d+))
1274 )''', re.X | re.I)
1275 protocol_re = re.compile('^(ht|f)tp(s?)://', re.I)
1277 def _hyper_repl_item(self,match,replacement):
1278 item = match.group('item')
1279 cls = match.group('class').lower()
1280 id = match.group('id')
1281 try:
1282 # make sure cls is a valid tracker classname
1283 cl = self._db.getclass(cls)
1284 if not cl.hasnode(id):
1285 return item
1286 return replacement % locals()
1287 except KeyError:
1288 return item
1290 def _hyper_repl(self, match):
1291 if match.group('url'):
1292 u = s = match.group('url')
1293 if not self.protocol_re.search(s):
1294 u = 'http://' + s
1295 # catch an escaped ">" at the end of the URL
1296 if s.endswith('>'):
1297 u = s = s[:-4]
1298 e = '>'
1299 else:
1300 e = ''
1301 return '<a href="%s">%s</a>%s'%(u, s, e)
1302 elif match.group('email'):
1303 s = match.group('email')
1304 return '<a href="mailto:%s">%s</a>'%(s, s)
1305 else:
1306 return self._hyper_repl_item(match,
1307 '<a href="%(cls)s%(id)s">%(item)s</a>')
1309 def _hyper_repl_rst(self, match):
1310 if match.group('url'):
1311 s = match.group('url')
1312 return '`%s <%s>`_'%(s, s)
1313 elif match.group('email'):
1314 s = match.group('email')
1315 return '`%s <mailto:%s>`_'%(s, s)
1316 else:
1317 return self._hyper_repl_item(match,'`%(item)s <%(cls)s%(id)s>`_')
1319 def hyperlinked(self):
1320 """ Render a "hyperlinked" version of the text """
1321 return self.plain(hyperlink=1)
1323 def plain(self, escape=0, hyperlink=0):
1324 """Render a "plain" representation of the property
1326 - "escape" turns on/off HTML quoting
1327 - "hyperlink" turns on/off in-text hyperlinking of URLs, email
1328 addresses and designators
1329 """
1330 if not self.is_view_ok():
1331 return self._('[hidden]')
1333 if self._value is None:
1334 return ''
1335 if escape:
1336 s = cgi.escape(str(self._value))
1337 else:
1338 s = str(self._value)
1339 if hyperlink:
1340 # no, we *must* escape this text
1341 if not escape:
1342 s = cgi.escape(s)
1343 s = self.hyper_re.sub(self._hyper_repl, s)
1344 return s
1346 def wrapped(self, escape=1, hyperlink=1):
1347 """Render a "wrapped" representation of the property.
1349 We wrap long lines at 80 columns on the nearest whitespace. Lines
1350 with no whitespace are not broken to force wrapping.
1352 Note that unlike plain() we default wrapped() to have the escaping
1353 and hyperlinking turned on since that's the most common usage.
1355 - "escape" turns on/off HTML quoting
1356 - "hyperlink" turns on/off in-text hyperlinking of URLs, email
1357 addresses and designators
1358 """
1359 if not self.is_view_ok():
1360 return self._('[hidden]')
1362 if self._value is None:
1363 return ''
1364 s = support.wrap(str(self._value), width=80)
1365 if escape:
1366 s = cgi.escape(s)
1367 if hyperlink:
1368 # no, we *must* escape this text
1369 if not escape:
1370 s = cgi.escape(s)
1371 s = self.hyper_re.sub(self._hyper_repl, s)
1372 return s
1374 def stext(self, escape=0, hyperlink=1):
1375 """ Render the value of the property as StructuredText.
1377 This requires the StructureText module to be installed separately.
1378 """
1379 if not self.is_view_ok():
1380 return self._('[hidden]')
1382 s = self.plain(escape=escape, hyperlink=hyperlink)
1383 if not StructuredText:
1384 return s
1385 return StructuredText(s,level=1,header=0)
1387 def rst(self, hyperlink=1):
1388 """ Render the value of the property as ReStructuredText.
1390 This requires docutils to be installed separately.
1391 """
1392 if not self.is_view_ok():
1393 return self._('[hidden]')
1395 if not ReStructuredText:
1396 return self.plain(escape=0, hyperlink=hyperlink)
1397 s = self.plain(escape=0, hyperlink=0)
1398 if hyperlink:
1399 s = self.hyper_re.sub(self._hyper_repl_rst, s)
1400 return ReStructuredText(s, writer_name="html")["body"].encode("utf-8",
1401 "replace")
1403 def field(self, **kwargs):
1404 """ Render the property as a field in HTML.
1406 If not editable, just display the value via plain().
1407 """
1408 if not self.is_edit_ok():
1409 return self.plain(escape=1)
1411 value = self._value
1412 if value is None:
1413 value = ''
1415 kwargs.setdefault("size", 30)
1416 kwargs.update({"name": self._formname, "value": value})
1417 return self.input(**kwargs)
1419 def multiline(self, escape=0, rows=5, cols=40, **kwargs):
1420 """ Render a multiline form edit field for the property.
1422 If not editable, just display the plain() value in a <pre> tag.
1423 """
1424 if not self.is_edit_ok():
1425 return '<pre>%s</pre>'%self.plain()
1427 if self._value is None:
1428 value = ''
1429 else:
1430 value = cgi.escape(str(self._value))
1432 value = '"'.join(value.split('"'))
1433 name = self._formname
1434 passthrough_args = ' '.join(['%s="%s"' % (k, cgi.escape(str(v), True))
1435 for k,v in kwargs.items()])
1436 return ('<textarea %(passthrough_args)s name="%(name)s" id="%(name)s"'
1437 ' rows="%(rows)s" cols="%(cols)s">'
1438 '%(value)s</textarea>') % locals()
1440 def email(self, escape=1):
1441 """ Render the value of the property as an obscured email address
1442 """
1443 if not self.is_view_ok():
1444 return self._('[hidden]')
1446 if self._value is None:
1447 value = ''
1448 else:
1449 value = str(self._value)
1450 split = value.split('@')
1451 if len(split) == 2:
1452 name, domain = split
1453 domain = ' '.join(domain.split('.')[:-1])
1454 name = name.replace('.', ' ')
1455 value = '%s at %s ...'%(name, domain)
1456 else:
1457 value = value.replace('.', ' ')
1458 if escape:
1459 value = cgi.escape(value)
1460 return value
1462 class PasswordHTMLProperty(HTMLProperty):
1463 def plain(self, escape=0):
1464 """ Render a "plain" representation of the property
1465 """
1466 if not self.is_view_ok():
1467 return self._('[hidden]')
1469 if self._value is None:
1470 return ''
1471 return self._('*encrypted*')
1473 def field(self, size=30):
1474 """ Render a form edit field for the property.
1476 If not editable, just display the value via plain().
1477 """
1478 if not self.is_edit_ok():
1479 return self.plain(escape=1)
1481 return self.input(type="password", name=self._formname, size=size)
1483 def confirm(self, size=30):
1484 """ Render a second form edit field for the property, used for
1485 confirmation that the user typed the password correctly. Generates
1486 a field with name "@confirm@name".
1488 If not editable, display nothing.
1489 """
1490 if not self.is_edit_ok():
1491 return ''
1493 return self.input(type="password",
1494 name="@confirm@%s"%self._formname,
1495 id="%s-confirm"%self._formname,
1496 size=size)
1498 class NumberHTMLProperty(HTMLProperty):
1499 def plain(self, escape=0):
1500 """ Render a "plain" representation of the property
1501 """
1502 if not self.is_view_ok():
1503 return self._('[hidden]')
1505 if self._value is None:
1506 return ''
1508 return str(self._value)
1510 def field(self, size=30):
1511 """ Render a form edit field for the property.
1513 If not editable, just display the value via plain().
1514 """
1515 if not self.is_edit_ok():
1516 return self.plain(escape=1)
1518 value = self._value
1519 if value is None:
1520 value = ''
1522 return self.input(name=self._formname, value=value, size=size)
1524 def __int__(self):
1525 """ Return an int of me
1526 """
1527 return int(self._value)
1529 def __float__(self):
1530 """ Return a float of me
1531 """
1532 return float(self._value)
1535 class BooleanHTMLProperty(HTMLProperty):
1536 def plain(self, escape=0):
1537 """ Render a "plain" representation of the property
1538 """
1539 if not self.is_view_ok():
1540 return self._('[hidden]')
1542 if self._value is None:
1543 return ''
1544 return self._value and self._("Yes") or self._("No")
1546 def field(self):
1547 """ Render a form edit field for the property
1549 If not editable, just display the value via plain().
1550 """
1551 if not self.is_edit_ok():
1552 return self.plain(escape=1)
1554 value = self._value
1555 if isinstance(value, str) or isinstance(value, unicode):
1556 value = value.strip().lower() in ('checked', 'yes', 'true',
1557 'on', '1')
1559 checked = value and "checked" or ""
1560 if value:
1561 s = self.input(type="radio", name=self._formname, value="yes",
1562 checked="checked")
1563 s += self._('Yes')
1564 s +=self.input(type="radio", name=self._formname, value="no")
1565 s += self._('No')
1566 else:
1567 s = self.input(type="radio", name=self._formname, value="yes")
1568 s += self._('Yes')
1569 s +=self.input(type="radio", name=self._formname, value="no",
1570 checked="checked")
1571 s += self._('No')
1572 return s
1574 class DateHTMLProperty(HTMLProperty):
1576 _marker = []
1578 def __init__(self, client, classname, nodeid, prop, name, value,
1579 anonymous=0, offset=None):
1580 HTMLProperty.__init__(self, client, classname, nodeid, prop, name,
1581 value, anonymous=anonymous)
1582 if self._value and not (isinstance(self._value, str) or
1583 isinstance(self._value, unicode)):
1584 self._value.setTranslator(self._client.translator)
1585 self._offset = offset
1586 if self._offset is None :
1587 self._offset = self._prop.offset (self._db)
1589 def plain(self, escape=0):
1590 """ Render a "plain" representation of the property
1591 """
1592 if not self.is_view_ok():
1593 return self._('[hidden]')
1595 if self._value is None:
1596 return ''
1597 if self._offset is None:
1598 offset = self._db.getUserTimezone()
1599 else:
1600 offset = self._offset
1601 return str(self._value.local(offset))
1603 def now(self, str_interval=None):
1604 """ Return the current time.
1606 This is useful for defaulting a new value. Returns a
1607 DateHTMLProperty.
1608 """
1609 if not self.is_view_ok():
1610 return self._('[hidden]')
1612 ret = date.Date('.', translator=self._client)
1614 if isinstance(str_interval, basestring):
1615 sign = 1
1616 if str_interval[0] == '-':
1617 sign = -1
1618 str_interval = str_interval[1:]
1619 interval = date.Interval(str_interval, translator=self._client)
1620 if sign > 0:
1621 ret = ret + interval
1622 else:
1623 ret = ret - interval
1625 return DateHTMLProperty(self._client, self._classname, self._nodeid,
1626 self._prop, self._formname, ret)
1628 def field(self, size=30, default=None, format=_marker, popcal=True):
1629 """Render a form edit field for the property
1631 If not editable, just display the value via plain().
1633 If "popcal" then include the Javascript calendar editor.
1634 Default=yes.
1636 The format string is a standard python strftime format string.
1637 """
1638 if not self.is_edit_ok():
1639 if format is self._marker:
1640 return self.plain(escape=1)
1641 else:
1642 return self.pretty(format)
1644 value = self._value
1646 if value is None:
1647 if default is None:
1648 raw_value = None
1649 else:
1650 if isinstance(default, basestring):
1651 raw_value = date.Date(default, translator=self._client)
1652 elif isinstance(default, date.Date):
1653 raw_value = default
1654 elif isinstance(default, DateHTMLProperty):
1655 raw_value = default._value
1656 else:
1657 raise ValueError, self._('default value for '
1658 'DateHTMLProperty must be either DateHTMLProperty '
1659 'or string date representation.')
1660 elif isinstance(value, str) or isinstance(value, unicode):
1661 # most likely erroneous input to be passed back to user
1662 if isinstance(value, unicode): value = value.encode('utf8')
1663 return self.input(name=self._formname, value=value, size=size)
1664 else:
1665 raw_value = value
1667 if raw_value is None:
1668 value = ''
1669 elif isinstance(raw_value, str) or isinstance(raw_value, unicode):
1670 if format is self._marker:
1671 value = raw_value
1672 else:
1673 value = date.Date(raw_value).pretty(format)
1674 else:
1675 if self._offset is None :
1676 offset = self._db.getUserTimezone()
1677 else :
1678 offset = self._offset
1679 value = raw_value.local(offset)
1680 if format is not self._marker:
1681 value = value.pretty(format)
1683 s = self.input(name=self._formname, value=value, size=size)
1684 if popcal:
1685 s += self.popcal()
1686 return s
1688 def reldate(self, pretty=1):
1689 """ Render the interval between the date and now.
1691 If the "pretty" flag is true, then make the display pretty.
1692 """
1693 if not self.is_view_ok():
1694 return self._('[hidden]')
1696 if not self._value:
1697 return ''
1699 # figure the interval
1700 interval = self._value - date.Date('.', translator=self._client)
1701 if pretty:
1702 return interval.pretty()
1703 return str(interval)
1705 def pretty(self, format=_marker):
1706 """ Render the date in a pretty format (eg. month names, spaces).
1708 The format string is a standard python strftime format string.
1709 Note that if the day is zero, and appears at the start of the
1710 string, then it'll be stripped from the output. This is handy
1711 for the situation when a date only specifies a month and a year.
1712 """
1713 if not self.is_view_ok():
1714 return self._('[hidden]')
1716 if self._offset is None:
1717 offset = self._db.getUserTimezone()
1718 else:
1719 offset = self._offset
1721 if not self._value:
1722 return ''
1723 elif format is not self._marker:
1724 return self._value.local(offset).pretty(format)
1725 else:
1726 return self._value.local(offset).pretty()
1728 def local(self, offset):
1729 """ Return the date/time as a local (timezone offset) date/time.
1730 """
1731 if not self.is_view_ok():
1732 return self._('[hidden]')
1734 return DateHTMLProperty(self._client, self._classname, self._nodeid,
1735 self._prop, self._formname, self._value, offset=offset)
1737 def popcal(self, width=300, height=200, label="(cal)",
1738 form="itemSynopsis"):
1739 """Generate a link to a calendar pop-up window.
1741 item: HTMLProperty e.g.: context.deadline
1742 """
1743 if self.isset():
1744 date = "&date=%s"%self._value
1745 else :
1746 date = ""
1747 return ('<a class="classhelp" href="javascript:help_window('
1748 "'%s?@template=calendar&property=%s&form=%s%s', %d, %d)"
1749 '">%s</a>'%(self._classname, self._name, form, date, width,
1750 height, label))
1752 class IntervalHTMLProperty(HTMLProperty):
1753 def __init__(self, client, classname, nodeid, prop, name, value,
1754 anonymous=0):
1755 HTMLProperty.__init__(self, client, classname, nodeid, prop,
1756 name, value, anonymous)
1757 if self._value and not isinstance(self._value, (str, unicode)):
1758 self._value.setTranslator(self._client.translator)
1760 def plain(self, escape=0):
1761 """ Render a "plain" representation of the property
1762 """
1763 if not self.is_view_ok():
1764 return self._('[hidden]')
1766 if self._value is None:
1767 return ''
1768 return str(self._value)
1770 def pretty(self):
1771 """ Render the interval in a pretty format (eg. "yesterday")
1772 """
1773 if not self.is_view_ok():
1774 return self._('[hidden]')
1776 return self._value.pretty()
1778 def field(self, size=30):
1779 """ Render a form edit field for the property
1781 If not editable, just display the value via plain().
1782 """
1783 if not self.is_edit_ok():
1784 return self.plain(escape=1)
1786 value = self._value
1787 if value is None:
1788 value = ''
1790 return self.input(name=self._formname, value=value, size=size)
1792 class LinkHTMLProperty(HTMLProperty):
1793 """ Link HTMLProperty
1794 Include the above as well as being able to access the class
1795 information. Stringifying the object itself results in the value
1796 from the item being displayed. Accessing attributes of this object
1797 result in the appropriate entry from the class being queried for the
1798 property accessed (so item/assignedto/name would look up the user
1799 entry identified by the assignedto property on item, and then the
1800 name property of that user)
1801 """
1802 def __init__(self, *args, **kw):
1803 HTMLProperty.__init__(self, *args, **kw)
1804 # if we're representing a form value, then the -1 from the form really
1805 # should be a None
1806 if str(self._value) == '-1':
1807 self._value = None
1809 def __getattr__(self, attr):
1810 """ return a new HTMLItem """
1811 if not self._value:
1812 # handle a special page templates lookup
1813 if attr == '__render_with_namespace__':
1814 def nothing(*args, **kw):
1815 return ''
1816 return nothing
1817 msg = self._('Attempt to look up %(attr)s on a missing value')
1818 return MissingValue(msg%locals())
1819 i = HTMLItem(self._client, self._prop.classname, self._value)
1820 return getattr(i, attr)
1822 def plain(self, escape=0):
1823 """ Render a "plain" representation of the property
1824 """
1825 if not self.is_view_ok():
1826 return self._('[hidden]')
1828 if self._value is None:
1829 return ''
1830 linkcl = self._db.classes[self._prop.classname]
1831 k = linkcl.labelprop(1)
1832 if num_re.match(self._value):
1833 try:
1834 value = str(linkcl.get(self._value, k))
1835 except IndexError:
1836 value = self._value
1837 else :
1838 value = self._value
1839 if escape:
1840 value = cgi.escape(value)
1841 return value
1843 def field(self, showid=0, size=None):
1844 """ Render a form edit field for the property
1846 If not editable, just display the value via plain().
1847 """
1848 if not self.is_edit_ok():
1849 return self.plain(escape=1)
1851 # edit field
1852 linkcl = self._db.getclass(self._prop.classname)
1853 if self._value is None:
1854 value = ''
1855 else:
1856 k = linkcl.getkey()
1857 if k and num_re.match(self._value):
1858 value = linkcl.get(self._value, k)
1859 else:
1860 value = self._value
1861 return self.input(name=self._formname, value=value, size=size)
1863 def menu(self, size=None, height=None, showid=0, additional=[], value=None,
1864 sort_on=None, **conditions):
1865 """ Render a form select list for this property
1867 "size" is used to limit the length of the list labels
1868 "height" is used to set the <select> tag's "size" attribute
1869 "showid" includes the item ids in the list labels
1870 "value" specifies which item is pre-selected
1871 "additional" lists properties which should be included in the
1872 label
1873 "sort_on" indicates the property to sort the list on as
1874 (direction, property) where direction is '+' or '-'. A
1875 single string with the direction prepended may be used.
1876 For example: ('-', 'order'), '+name'.
1878 The remaining keyword arguments are used as conditions for
1879 filtering the items in the list - they're passed as the
1880 "filterspec" argument to a Class.filter() call.
1882 If not editable, just display the value via plain().
1883 """
1884 if not self.is_edit_ok():
1885 return self.plain(escape=1)
1887 # Since None indicates the default, we need another way to
1888 # indicate "no selection". We use -1 for this purpose, as
1889 # that is the value we use when submitting a form without the
1890 # value set.
1891 if value is None:
1892 value = self._value
1893 elif value == '-1':
1894 value = None
1896 linkcl = self._db.getclass(self._prop.classname)
1897 l = ['<select name="%s">'%self._formname]
1898 k = linkcl.labelprop(1)
1899 s = ''
1900 if value is None:
1901 s = 'selected="selected" '
1902 l.append(self._('<option %svalue="-1">- no selection -</option>')%s)
1904 if sort_on is not None:
1905 if not isinstance(sort_on, tuple):
1906 if sort_on[0] in '+-':
1907 sort_on = (sort_on[0], sort_on[1:])
1908 else:
1909 sort_on = ('+', sort_on)
1910 else:
1911 sort_on = ('+', linkcl.orderprop())
1913 options = [opt
1914 for opt in linkcl.filter(None, conditions, sort_on, (None, None))
1915 if self._db.security.hasPermission("View", self._client.userid,
1916 linkcl.classname, itemid=opt)]
1918 # make sure we list the current value if it's retired
1919 if value and value not in options:
1920 options.insert(0, value)
1922 if additional:
1923 additional_fns = []
1924 props = linkcl.getprops()
1925 for propname in additional:
1926 prop = props[propname]
1927 if isinstance(prop, hyperdb.Link):
1928 cl = self._db.getclass(prop.classname)
1929 labelprop = cl.labelprop()
1930 fn = lambda optionid: cl.get(linkcl.get(optionid,
1931 propname),
1932 labelprop)
1933 else:
1934 fn = lambda optionid: linkcl.get(optionid, propname)
1935 additional_fns.append(fn)
1937 for optionid in options:
1938 # get the option value, and if it's None use an empty string
1939 option = linkcl.get(optionid, k) or ''
1941 # figure if this option is selected
1942 s = ''
1943 if value in [optionid, option]:
1944 s = 'selected="selected" '
1946 # figure the label
1947 if showid:
1948 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1949 elif not option:
1950 lab = '%s%s'%(self._prop.classname, optionid)
1951 else:
1952 lab = option
1954 # truncate if it's too long
1955 if size is not None and len(lab) > size:
1956 lab = lab[:size-3] + '...'
1957 if additional:
1958 m = []
1959 for fn in additional_fns:
1960 m.append(str(fn(optionid)))
1961 lab = lab + ' (%s)'%', '.join(m)
1963 # and generate
1964 lab = cgi.escape(self._(lab))
1965 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
1966 l.append('</select>')
1967 return '\n'.join(l)
1968 # def checklist(self, ...)
1972 class MultilinkHTMLProperty(HTMLProperty):
1973 """ Multilink HTMLProperty
1975 Also be iterable, returning a wrapper object like the Link case for
1976 each entry in the multilink.
1977 """
1978 def __init__(self, *args, **kwargs):
1979 HTMLProperty.__init__(self, *args, **kwargs)
1980 if self._value:
1981 display_value = lookupIds(self._db, self._prop, self._value,
1982 fail_ok=1, do_lookup=False)
1983 sortfun = make_sort_function(self._db, self._prop.classname)
1984 # sorting fails if the value contains
1985 # items not yet stored in the database
1986 # ignore these errors to preserve user input
1987 try:
1988 display_value.sort(sortfun)
1989 except:
1990 pass
1991 self._value = display_value
1993 def __len__(self):
1994 """ length of the multilink """
1995 return len(self._value)
1997 def __getattr__(self, attr):
1998 """ no extended attribute accesses make sense here """
1999 raise AttributeError, attr
2001 def viewableGenerator(self, values):
2002 """Used to iterate over only the View'able items in a class."""
2003 check = self._db.security.hasPermission
2004 userid = self._client.userid
2005 classname = self._prop.classname
2006 for value in values:
2007 if check('View', userid, classname, itemid=value):
2008 yield HTMLItem(self._client, classname, value)
2010 def __iter__(self):
2011 """ iterate and return a new HTMLItem
2012 """
2013 return self.viewableGenerator(self._value)
2015 def reverse(self):
2016 """ return the list in reverse order
2017 """
2018 l = self._value[:]
2019 l.reverse()
2020 return self.viewableGenerator(l)
2022 def sorted(self, property):
2023 """ Return this multilink sorted by the given property """
2024 value = list(self.__iter__())
2025 value.sort(lambda a,b:cmp(a[property], b[property]))
2026 return value
2028 def __contains__(self, value):
2029 """ Support the "in" operator. We have to make sure the passed-in
2030 value is a string first, not a HTMLProperty.
2031 """
2032 return str(value) in self._value
2034 def isset(self):
2035 """Is my _value not []?"""
2036 return self._value != []
2038 def plain(self, escape=0):
2039 """ Render a "plain" representation of the property
2040 """
2041 if not self.is_view_ok():
2042 return self._('[hidden]')
2044 linkcl = self._db.classes[self._prop.classname]
2045 k = linkcl.labelprop(1)
2046 labels = []
2047 for v in self._value:
2048 if num_re.match(v):
2049 try:
2050 label = linkcl.get(v, k)
2051 except IndexError:
2052 label = None
2053 # fall back to designator if label is None
2054 if label is None: label = '%s%s'%(self._prop.classname, k)
2055 else:
2056 label = v
2057 labels.append(label)
2058 value = ', '.join(labels)
2059 if escape:
2060 value = cgi.escape(value)
2061 return value
2063 def field(self, size=30, showid=0):
2064 """ Render a form edit field for the property
2066 If not editable, just display the value via plain().
2067 """
2068 if not self.is_edit_ok():
2069 return self.plain(escape=1)
2071 linkcl = self._db.getclass(self._prop.classname)
2072 value = self._value[:]
2073 # map the id to the label property
2074 if not linkcl.getkey():
2075 showid=1
2076 if not showid:
2077 k = linkcl.labelprop(1)
2078 value = lookupKeys(linkcl, k, value)
2079 value = ','.join(value)
2080 return self.input(name=self._formname, size=size, value=value)
2082 def menu(self, size=None, height=None, showid=0, additional=[],
2083 value=None, sort_on=None, **conditions):
2084 """ Render a form <select> list for this property.
2086 "size" is used to limit the length of the list labels
2087 "height" is used to set the <select> tag's "size" attribute
2088 "showid" includes the item ids in the list labels
2089 "additional" lists properties which should be included in the
2090 label
2091 "value" specifies which item is pre-selected
2092 "sort_on" indicates the property to sort the list on as
2093 (direction, property) where direction is '+' or '-'. A
2094 single string with the direction prepended may be used.
2095 For example: ('-', 'order'), '+name'.
2097 The remaining keyword arguments are used as conditions for
2098 filtering the items in the list - they're passed as the
2099 "filterspec" argument to a Class.filter() call.
2101 If not editable, just display the value via plain().
2102 """
2103 if not self.is_edit_ok():
2104 return self.plain(escape=1)
2106 if value is None:
2107 value = self._value
2109 linkcl = self._db.getclass(self._prop.classname)
2111 if sort_on is not None:
2112 if not isinstance(sort_on, tuple):
2113 if sort_on[0] in '+-':
2114 sort_on = (sort_on[0], sort_on[1:])
2115 else:
2116 sort_on = ('+', sort_on)
2117 else:
2118 sort_on = ('+', linkcl.orderprop())
2120 options = [opt
2121 for opt in linkcl.filter(None, conditions, sort_on)
2122 if self._db.security.hasPermission("View", self._client.userid,
2123 linkcl.classname, itemid=opt)]
2125 # make sure we list the current values if they're retired
2126 for val in value:
2127 if val not in options:
2128 options.insert(0, val)
2130 if not height:
2131 height = len(options)
2132 if value:
2133 # The "no selection" option.
2134 height += 1
2135 height = min(height, 7)
2136 l = ['<select multiple name="%s" size="%s">'%(self._formname, height)]
2137 k = linkcl.labelprop(1)
2139 if value:
2140 l.append('<option value="%s">- no selection -</option>'
2141 % ','.join(['-' + v for v in value]))
2143 if additional:
2144 additional_fns = []
2145 props = linkcl.getprops()
2146 for propname in additional:
2147 prop = props[propname]
2148 if isinstance(prop, hyperdb.Link):
2149 cl = self._db.getclass(prop.classname)
2150 labelprop = cl.labelprop()
2151 fn = lambda optionid: cl.get(linkcl.get(optionid,
2152 propname),
2153 labelprop)
2154 else:
2155 fn = lambda optionid: linkcl.get(optionid, propname)
2156 additional_fns.append(fn)
2158 for optionid in options:
2159 # get the option value, and if it's None use an empty string
2160 option = linkcl.get(optionid, k) or ''
2162 # figure if this option is selected
2163 s = ''
2164 if optionid in value or option in value:
2165 s = 'selected="selected" '
2167 # figure the label
2168 if showid:
2169 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
2170 else:
2171 lab = option
2172 # truncate if it's too long
2173 if size is not None and len(lab) > size:
2174 lab = lab[:size-3] + '...'
2175 if additional:
2176 m = []
2177 for fn in additional_fns:
2178 m.append(str(fn(optionid)))
2179 lab = lab + ' (%s)'%', '.join(m)
2181 # and generate
2182 lab = cgi.escape(self._(lab))
2183 l.append('<option %svalue="%s">%s</option>'%(s, optionid,
2184 lab))
2185 l.append('</select>')
2186 return '\n'.join(l)
2188 # set the propclasses for HTMLItem
2189 propclasses = (
2190 (hyperdb.String, StringHTMLProperty),
2191 (hyperdb.Number, NumberHTMLProperty),
2192 (hyperdb.Boolean, BooleanHTMLProperty),
2193 (hyperdb.Date, DateHTMLProperty),
2194 (hyperdb.Interval, IntervalHTMLProperty),
2195 (hyperdb.Password, PasswordHTMLProperty),
2196 (hyperdb.Link, LinkHTMLProperty),
2197 (hyperdb.Multilink, MultilinkHTMLProperty),
2198 )
2200 def make_sort_function(db, classname, sort_on=None):
2201 """Make a sort function for a given class
2202 """
2203 linkcl = db.getclass(classname)
2204 if sort_on is None:
2205 sort_on = linkcl.orderprop()
2206 def sortfunc(a, b):
2207 return cmp(linkcl.get(a, sort_on), linkcl.get(b, sort_on))
2208 return sortfunc
2210 def handleListCGIValue(value):
2211 """ Value is either a single item or a list of items. Each item has a
2212 .value that we're actually interested in.
2213 """
2214 if isinstance(value, type([])):
2215 return [value.value for value in value]
2216 else:
2217 value = value.value.strip()
2218 if not value:
2219 return []
2220 return [v.strip() for v in value.split(',')]
2222 class HTMLRequest(HTMLInputMixin):
2223 """The *request*, holding the CGI form and environment.
2225 - "form" the CGI form as a cgi.FieldStorage
2226 - "env" the CGI environment variables
2227 - "base" the base URL for this instance
2228 - "user" a HTMLItem instance for this user
2229 - "language" as determined by the browser or config
2230 - "classname" the current classname (possibly None)
2231 - "template" the current template (suffix, also possibly None)
2233 Index args:
2235 - "columns" dictionary of the columns to display in an index page
2236 - "show" a convenience access to columns - request/show/colname will
2237 be true if the columns should be displayed, false otherwise
2238 - "sort" index sort column (direction, column name)
2239 - "group" index grouping property (direction, column name)
2240 - "filter" properties to filter the index on
2241 - "filterspec" values to filter the index on
2242 - "search_text" text to perform a full-text search on for an index
2243 """
2244 def __repr__(self):
2245 return '<HTMLRequest %r>'%self.__dict__
2247 def __init__(self, client):
2248 # _client is needed by HTMLInputMixin
2249 self._client = self.client = client
2251 # easier access vars
2252 self.form = client.form
2253 self.env = client.env
2254 self.base = client.base
2255 self.user = HTMLItem(client, 'user', client.userid)
2256 self.language = client.language
2258 # store the current class name and action
2259 self.classname = client.classname
2260 self.nodeid = client.nodeid
2261 self.template = client.template
2263 # the special char to use for special vars
2264 self.special_char = '@'
2266 HTMLInputMixin.__init__(self)
2268 self._post_init()
2270 def current_url(self):
2271 url = self.base
2272 if self.classname:
2273 url += self.classname
2274 if self.nodeid:
2275 url += self.nodeid
2276 args = {}
2277 if self.template:
2278 args['@template'] = self.template
2279 return self.indexargs_url(url, args)
2281 def _parse_sort(self, var, name):
2282 """ Parse sort/group options. Append to var
2283 """
2284 fields = []
2285 dirs = []
2286 for special in '@:':
2287 idx = 0
2288 key = '%s%s%d'%(special, name, idx)
2289 while key in self.form:
2290 self.special_char = special
2291 fields.append(self.form.getfirst(key))
2292 dirkey = '%s%sdir%d'%(special, name, idx)
2293 if dirkey in self.form:
2294 dirs.append(self.form.getfirst(dirkey))
2295 else:
2296 dirs.append(None)
2297 idx += 1
2298 key = '%s%s%d'%(special, name, idx)
2299 # backward compatible (and query) URL format
2300 key = special + name
2301 dirkey = key + 'dir'
2302 if key in self.form and not fields:
2303 fields = handleListCGIValue(self.form[key])
2304 if dirkey in self.form:
2305 dirs.append(self.form.getfirst(dirkey))
2306 if fields: # only try other special char if nothing found
2307 break
2308 for f, d in map(None, fields, dirs):
2309 if f.startswith('-'):
2310 var.append(('-', f[1:]))
2311 elif d:
2312 var.append(('-', f))
2313 else:
2314 var.append(('+', f))
2316 def _post_init(self):
2317 """ Set attributes based on self.form
2318 """
2319 # extract the index display information from the form
2320 self.columns = []
2321 for name in ':columns @columns'.split():
2322 if self.form.has_key(name):
2323 self.special_char = name[0]
2324 self.columns = handleListCGIValue(self.form[name])
2325 break
2326 self.show = support.TruthDict(self.columns)
2328 # sorting and grouping
2329 self.sort = []
2330 self.group = []
2331 self._parse_sort(self.sort, 'sort')
2332 self._parse_sort(self.group, 'group')
2334 # filtering
2335 self.filter = []
2336 for name in ':filter @filter'.split():
2337 if self.form.has_key(name):
2338 self.special_char = name[0]
2339 self.filter = handleListCGIValue(self.form[name])
2341 self.filterspec = {}
2342 db = self.client.db
2343 if self.classname is not None:
2344 cls = db.getclass (self.classname)
2345 for name in self.filter:
2346 if not self.form.has_key(name):
2347 continue
2348 prop = cls.get_transitive_prop (name)
2349 fv = self.form[name]
2350 if (isinstance(prop, hyperdb.Link) or
2351 isinstance(prop, hyperdb.Multilink)):
2352 self.filterspec[name] = lookupIds(db, prop,
2353 handleListCGIValue(fv))
2354 else:
2355 if isinstance(fv, type([])):
2356 self.filterspec[name] = [v.value for v in fv]
2357 elif name == 'id':
2358 # special case "id" property
2359 self.filterspec[name] = handleListCGIValue(fv)
2360 else:
2361 self.filterspec[name] = fv.value
2363 # full-text search argument
2364 self.search_text = None
2365 for name in ':search_text @search_text'.split():
2366 if self.form.has_key(name):
2367 self.special_char = name[0]
2368 self.search_text = self.form.getfirst(name)
2370 # pagination - size and start index
2371 # figure batch args
2372 self.pagesize = 50
2373 for name in ':pagesize @pagesize'.split():
2374 if self.form.has_key(name):
2375 self.special_char = name[0]
2376 self.pagesize = int(self.form.getfirst(name))
2378 self.startwith = 0
2379 for name in ':startwith @startwith'.split():
2380 if self.form.has_key(name):
2381 self.special_char = name[0]
2382 self.startwith = int(self.form.getfirst(name))
2384 # dispname
2385 if self.form.has_key('@dispname'):
2386 self.dispname = self.form.getfirst('@dispname')
2387 else:
2388 self.dispname = None
2390 def updateFromURL(self, url):
2391 """ Parse the URL for query args, and update my attributes using the
2392 values.
2393 """
2394 env = {'QUERY_STRING': url}
2395 self.form = cgi.FieldStorage(environ=env)
2397 self._post_init()
2399 def update(self, kwargs):
2400 """ Update my attributes using the keyword args
2401 """
2402 self.__dict__.update(kwargs)
2403 if kwargs.has_key('columns'):
2404 self.show = support.TruthDict(self.columns)
2406 def description(self):
2407 """ Return a description of the request - handle for the page title.
2408 """
2409 s = [self.client.db.config.TRACKER_NAME]
2410 if self.classname:
2411 if self.client.nodeid:
2412 s.append('- %s%s'%(self.classname, self.client.nodeid))
2413 else:
2414 if self.template == 'item':
2415 s.append('- new %s'%self.classname)
2416 elif self.template == 'index':
2417 s.append('- %s index'%self.classname)
2418 else:
2419 s.append('- %s %s'%(self.classname, self.template))
2420 else:
2421 s.append('- home')
2422 return ' '.join(s)
2424 def __str__(self):
2425 d = {}
2426 d.update(self.__dict__)
2427 f = ''
2428 for k in self.form.keys():
2429 f += '\n %r=%r'%(k,handleListCGIValue(self.form[k]))
2430 d['form'] = f
2431 e = ''
2432 for k,v in self.env.items():
2433 e += '\n %r=%r'%(k, v)
2434 d['env'] = e
2435 return """
2436 form: %(form)s
2437 base: %(base)r
2438 classname: %(classname)r
2439 template: %(template)r
2440 columns: %(columns)r
2441 sort: %(sort)r
2442 group: %(group)r
2443 filter: %(filter)r
2444 search_text: %(search_text)r
2445 pagesize: %(pagesize)r
2446 startwith: %(startwith)r
2447 env: %(env)s
2448 """%d
2450 def indexargs_form(self, columns=1, sort=1, group=1, filter=1,
2451 filterspec=1, search_text=1):
2452 """ return the current index args as form elements """
2453 l = []
2454 sc = self.special_char
2455 def add(k, v):
2456 l.append(self.input(type="hidden", name=k, value=v))
2457 if columns and self.columns:
2458 add(sc+'columns', ','.join(self.columns))
2459 if sort:
2460 val = []
2461 for dir, attr in self.sort:
2462 if dir == '-':
2463 val.append('-'+attr)
2464 else:
2465 val.append(attr)
2466 add(sc+'sort', ','.join (val))
2467 if group:
2468 val = []
2469 for dir, attr in self.group:
2470 if dir == '-':
2471 val.append('-'+attr)
2472 else:
2473 val.append(attr)
2474 add(sc+'group', ','.join (val))
2475 if filter and self.filter:
2476 add(sc+'filter', ','.join(self.filter))
2477 if self.classname and filterspec:
2478 cls = self.client.db.getclass(self.classname)
2479 for k,v in self.filterspec.items():
2480 if type(v) == type([]):
2481 if isinstance(cls.get_transitive_prop(k), hyperdb.String):
2482 add(k, ' '.join(v))
2483 else:
2484 add(k, ','.join(v))
2485 else:
2486 add(k, v)
2487 if search_text and self.search_text:
2488 add(sc+'search_text', self.search_text)
2489 add(sc+'pagesize', self.pagesize)
2490 add(sc+'startwith', self.startwith)
2491 return '\n'.join(l)
2493 def indexargs_url(self, url, args):
2494 """ Embed the current index args in a URL
2495 """
2496 q = urllib.quote
2497 sc = self.special_char
2498 l = ['%s=%s'%(k,v) for k,v in args.items()]
2500 # pull out the special values (prefixed by @ or :)
2501 specials = {}
2502 for key in args.keys():
2503 if key[0] in '@:':
2504 specials[key[1:]] = args[key]
2506 # ok, now handle the specials we received in the request
2507 if self.columns and not specials.has_key('columns'):
2508 l.append(sc+'columns=%s'%(','.join(self.columns)))
2509 if self.sort and not specials.has_key('sort'):
2510 val = []
2511 for dir, attr in self.sort:
2512 if dir == '-':
2513 val.append('-'+attr)
2514 else:
2515 val.append(attr)
2516 l.append(sc+'sort=%s'%(','.join(val)))
2517 if self.group and not specials.has_key('group'):
2518 val = []
2519 for dir, attr in self.group:
2520 if dir == '-':
2521 val.append('-'+attr)
2522 else:
2523 val.append(attr)
2524 l.append(sc+'group=%s'%(','.join(val)))
2525 if self.filter and not specials.has_key('filter'):
2526 l.append(sc+'filter=%s'%(','.join(self.filter)))
2527 if self.search_text and not specials.has_key('search_text'):
2528 l.append(sc+'search_text=%s'%q(self.search_text))
2529 if not specials.has_key('pagesize'):
2530 l.append(sc+'pagesize=%s'%self.pagesize)
2531 if not specials.has_key('startwith'):
2532 l.append(sc+'startwith=%s'%self.startwith)
2534 # finally, the remainder of the filter args in the request
2535 if self.classname and self.filterspec:
2536 cls = self.client.db.getclass(self.classname)
2537 for k,v in self.filterspec.items():
2538 if not args.has_key(k):
2539 if type(v) == type([]):
2540 prop = cls.get_transitive_prop(k)
2541 if k != 'id' and isinstance(prop, hyperdb.String):
2542 l.append('%s=%s'%(k, '%20'.join([q(i) for i in v])))
2543 else:
2544 l.append('%s=%s'%(k, ','.join([q(i) for i in v])))
2545 else:
2546 l.append('%s=%s'%(k, q(v)))
2547 return '%s?%s'%(url, '&'.join(l))
2548 indexargs_href = indexargs_url
2550 def base_javascript(self):
2551 return """
2552 <script type="text/javascript">
2553 submitted = false;
2554 function submit_once() {
2555 if (submitted) {
2556 alert("Your request is being processed.\\nPlease be patient.");
2557 event.returnValue = 0; // work-around for IE
2558 return 0;
2559 }
2560 submitted = true;
2561 return 1;
2562 }
2564 function help_window(helpurl, width, height) {
2565 HelpWin = window.open('%s' + helpurl, 'RoundupHelpWindow', 'scrollbars=yes,resizable=yes,toolbar=no,height='+height+',width='+width);
2566 }
2567 </script>
2568 """%self.base
2570 def batch(self):
2571 """ Return a batch object for results from the "current search"
2572 """
2573 filterspec = self.filterspec
2574 sort = self.sort
2575 group = self.group
2577 # get the list of ids we're batching over
2578 klass = self.client.db.getclass(self.classname)
2579 if self.search_text:
2580 matches = self.client.db.indexer.search(
2581 [w.upper().encode("utf-8", "replace") for w in re.findall(
2582 r'(?u)\b\w{2,25}\b',
2583 unicode(self.search_text, "utf-8", "replace")
2584 )], klass)
2585 else:
2586 matches = None
2588 # filter for visibility
2589 check = self._client.db.security.hasPermission
2590 userid = self._client.userid
2591 l = [id for id in klass.filter(matches, filterspec, sort, group)
2592 if check('View', userid, self.classname, itemid=id)]
2594 # return the batch object, using IDs only
2595 return Batch(self.client, l, self.pagesize, self.startwith,
2596 classname=self.classname)
2598 # extend the standard ZTUtils Batch object to remove dependency on
2599 # Acquisition and add a couple of useful methods
2600 class Batch(ZTUtils.Batch):
2601 """ Use me to turn a list of items, or item ids of a given class, into a
2602 series of batches.
2604 ========= ========================================================
2605 Parameter Usage
2606 ========= ========================================================
2607 sequence a list of HTMLItems or item ids
2608 classname if sequence is a list of ids, this is the class of item
2609 size how big to make the sequence.
2610 start where to start (0-indexed) in the sequence.
2611 end where to end (0-indexed) in the sequence.
2612 orphan if the next batch would contain less items than this
2613 value, then it is combined with this batch
2614 overlap the number of items shared between adjacent batches
2615 ========= ========================================================
2617 Attributes: Note that the "start" attribute, unlike the
2618 argument, is a 1-based index (I know, lame). "first" is the
2619 0-based index. "length" is the actual number of elements in
2620 the batch.
2622 "sequence_length" is the length of the original, unbatched, sequence.
2623 """
2624 def __init__(self, client, sequence, size, start, end=0, orphan=0,
2625 overlap=0, classname=None):
2626 self.client = client
2627 self.last_index = self.last_item = None
2628 self.current_item = None
2629 self.classname = classname
2630 self.sequence_length = len(sequence)
2631 ZTUtils.Batch.__init__(self, sequence, size, start, end, orphan,
2632 overlap)
2634 # overwrite so we can late-instantiate the HTMLItem instance
2635 def __getitem__(self, index):
2636 if index < 0:
2637 if index + self.end < self.first: raise IndexError, index
2638 return self._sequence[index + self.end]
2640 if index >= self.length:
2641 raise IndexError, index
2643 # move the last_item along - but only if the fetched index changes
2644 # (for some reason, index 0 is fetched twice)
2645 if index != self.last_index:
2646 self.last_item = self.current_item
2647 self.last_index = index
2649 item = self._sequence[index + self.first]
2650 if self.classname:
2651 # map the item ids to instances
2652 item = HTMLItem(self.client, self.classname, item)
2653 self.current_item = item
2654 return item
2656 def propchanged(self, *properties):
2657 """ Detect if one of the properties marked as being a group
2658 property changed in the last iteration fetch
2659 """
2660 # we poke directly at the _value here since MissingValue can screw
2661 # us up and cause Nones to compare strangely
2662 if self.last_item is None:
2663 return 1
2664 for property in properties:
2665 if property == 'id' or isinstance (self.last_item[property], list):
2666 if (str(self.last_item[property]) !=
2667 str(self.current_item[property])):
2668 return 1
2669 else:
2670 if (self.last_item[property]._value !=
2671 self.current_item[property]._value):
2672 return 1
2673 return 0
2675 # override these 'cos we don't have access to acquisition
2676 def previous(self):
2677 if self.start == 1:
2678 return None
2679 return Batch(self.client, self._sequence, self._size,
2680 self.first - self._size + self.overlap, 0, self.orphan,
2681 self.overlap)
2683 def next(self):
2684 try:
2685 self._sequence[self.end]
2686 except IndexError:
2687 return None
2688 return Batch(self.client, self._sequence, self._size,
2689 self.end - self.overlap, 0, self.orphan, self.overlap)
2691 class TemplatingUtils:
2692 """ Utilities for templating
2693 """
2694 def __init__(self, client):
2695 self.client = client
2696 def Batch(self, sequence, size, start, end=0, orphan=0, overlap=0):
2697 return Batch(self.client, sequence, size, start, end, orphan,
2698 overlap)
2700 def url_quote(self, url):
2701 """URL-quote the supplied text."""
2702 return urllib.quote(url)
2704 def html_quote(self, html):
2705 """HTML-quote the supplied text."""
2706 return cgi.escape(html)
2708 def __getattr__(self, name):
2709 """Try the tracker's templating_utils."""
2710 if not hasattr(self.client.instance, 'templating_utils'):
2711 # backwards-compatibility
2712 raise AttributeError, name
2713 if not self.client.instance.templating_utils.has_key(name):
2714 raise AttributeError, name
2715 return self.client.instance.templating_utils[name]
2717 def html_calendar(self, request):
2718 """Generate a HTML calendar.
2720 `request` the roundup.request object
2721 - @template : name of the template
2722 - form : name of the form to store back the date
2723 - property : name of the property of the form to store
2724 back the date
2725 - date : current date
2726 - display : when browsing, specifies year and month
2728 html will simply be a table.
2729 """
2730 date_str = request.form.getfirst("date", ".")
2731 display = request.form.getfirst("display", date_str)
2732 template = request.form.getfirst("@template", "calendar")
2733 form = request.form.getfirst("form")
2734 property = request.form.getfirst("property")
2735 curr_date = date.Date(date_str) # to highlight
2736 display = date.Date(display) # to show
2737 day = display.day
2739 # for navigation
2740 date_prev_month = display + date.Interval("-1m")
2741 date_next_month = display + date.Interval("+1m")
2742 date_prev_year = display + date.Interval("-1y")
2743 date_next_year = display + date.Interval("+1y")
2745 res = []
2747 base_link = "%s?@template=%s&property=%s&form=%s&date=%s" % \
2748 (request.classname, template, property, form, curr_date)
2750 # navigation
2751 # month
2752 res.append('<table class="calendar"><tr><td>')
2753 res.append(' <table width="100%" class="calendar_nav"><tr>')
2754 link = "&display=%s"%date_prev_month
2755 res.append(' <td><a href="%s&display=%s"><</a></td>'%(base_link,
2756 date_prev_month))
2757 res.append(' <td>%s</td>'%calendar.month_name[display.month])
2758 res.append(' <td><a href="%s&display=%s">></a></td>'%(base_link,
2759 date_next_month))
2760 # spacer
2761 res.append(' <td width="100%"></td>')
2762 # year
2763 res.append(' <td><a href="%s&display=%s"><</a></td>'%(base_link,
2764 date_prev_year))
2765 res.append(' <td>%s</td>'%display.year)
2766 res.append(' <td><a href="%s&display=%s">></a></td>'%(base_link,
2767 date_next_year))
2768 res.append(' </tr></table>')
2769 res.append(' </td></tr>')
2771 # the calendar
2772 res.append(' <tr><td><table class="calendar_display">')
2773 res.append(' <tr class="weekdays">')
2774 for day in calendar.weekheader(3).split():
2775 res.append(' <td>%s</td>'%day)
2776 res.append(' </tr>')
2777 for week in calendar.monthcalendar(display.year, display.month):
2778 res.append(' <tr>')
2779 for day in week:
2780 link = "javascript:form[field].value = '%d-%02d-%02d'; " \
2781 "window.close ();"%(display.year, display.month, day)
2782 if (day == curr_date.day and display.month == curr_date.month
2783 and display.year == curr_date.year):
2784 # highlight
2785 style = "today"
2786 else :
2787 style = ""
2788 if day:
2789 res.append(' <td class="%s"><a href="%s">%s</a></td>'%(
2790 style, link, day))
2791 else :
2792 res.append(' <td></td>')
2793 res.append(' </tr>')
2794 res.append('</table></td></tr></table>')
2795 return "\n".join(res)
2797 class MissingValue:
2798 def __init__(self, description, **kwargs):
2799 self.__description = description
2800 for key, value in kwargs.items():
2801 self.__dict__[key] = value
2803 def __call__(self, *args, **kwargs): return MissingValue(self.__description)
2804 def __getattr__(self, name):
2805 # This allows assignments which assume all intermediate steps are Null
2806 # objects if they don't exist yet.
2807 #
2808 # For example (with just 'client' defined):
2809 #
2810 # client.db.config.TRACKER_WEB = 'BASE/'
2811 self.__dict__[name] = MissingValue(self.__description)
2812 return getattr(self, name)
2814 def __getitem__(self, key): return self
2815 def __nonzero__(self): return 0
2816 def __str__(self): return '[%s]'%self.__description
2817 def __repr__(self): return '<MissingValue 0x%x "%s">'%(id(self),
2818 self.__description)
2819 def gettext(self, str): return str
2820 _ = gettext
2822 # vim: set et sts=4 sw=4 :