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