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 form.has_key(item):
534 if isinstance(prop, hyperdb.Multilink):
535 value = lookupIds(self._db, prop,
536 handleListCGIValue(form[item]), fail_ok=1)
537 elif isinstance(prop, hyperdb.Link):
538 value = form.getfirst(item).strip()
539 if value:
540 value = lookupIds(self._db, prop, [value],
541 fail_ok=1)[0]
542 else:
543 value = None
544 else:
545 value = form.getfirst(item).strip() or None
546 else:
547 if isinstance(prop, hyperdb.Multilink):
548 value = []
549 else:
550 value = None
551 return htmlklass(self._client, self._classname, None, prop, item,
552 value, self._anonymous)
554 # no good
555 raise KeyError, item
557 def __getattr__(self, attr):
558 """ convenience access """
559 try:
560 return self[attr]
561 except KeyError:
562 raise AttributeError, attr
564 def designator(self):
565 """ Return this class' designator (classname) """
566 return self._classname
568 def getItem(self, itemid, num_re=num_re):
569 """ Get an item of this class by its item id.
570 """
571 # make sure we're looking at an itemid
572 if not isinstance(itemid, type(1)) and not num_re.match(itemid):
573 itemid = self._klass.lookup(itemid)
575 return HTMLItem(self._client, self.classname, itemid)
577 def properties(self, sort=1):
578 """ Return HTMLProperty for all of this class' properties.
579 """
580 l = []
581 for name, prop in self._props.items():
582 for klass, htmlklass in propclasses:
583 if isinstance(prop, hyperdb.Multilink):
584 value = []
585 else:
586 value = None
587 if isinstance(prop, klass):
588 l.append(htmlklass(self._client, self._classname, '',
589 prop, name, value, self._anonymous))
590 if sort:
591 l.sort(lambda a,b:cmp(a._name, b._name))
592 return l
594 def list(self, sort_on=None):
595 """ List all items in this class.
596 """
597 # get the list and sort it nicely
598 l = self._klass.list()
599 sortfunc = make_sort_function(self._db, self._classname, sort_on)
600 l.sort(sortfunc)
602 # check perms
603 check = self._client.db.security.hasPermission
604 userid = self._client.userid
606 l = [HTMLItem(self._client, self._classname, id) for id in l
607 if check('View', userid, self._classname, itemid=id)]
609 return l
611 def csv(self):
612 """ Return the items of this class as a chunk of CSV text.
613 """
614 props = self.propnames()
615 s = StringIO.StringIO()
616 writer = csv.writer(s)
617 writer.writerow(props)
618 for nodeid in self._klass.list():
619 l = []
620 for name in props:
621 value = self._klass.get(nodeid, name)
622 if value is None:
623 l.append('')
624 elif isinstance(value, type([])):
625 l.append(':'.join(map(str, value)))
626 else:
627 l.append(str(self._klass.get(nodeid, name)))
628 writer.writerow(l)
629 return s.getvalue()
631 def propnames(self):
632 """ Return the list of the names of the properties of this class.
633 """
634 idlessprops = self._klass.getprops(protected=0).keys()
635 idlessprops.sort()
636 return ['id'] + idlessprops
638 def filter(self, request=None, filterspec={}, sort=[], group=[]):
639 """ Return a list of items from this class, filtered and sorted
640 by the current requested filterspec/filter/sort/group args
642 "request" takes precedence over the other three arguments.
643 """
644 if request is not None:
645 filterspec = request.filterspec
646 sort = request.sort
647 group = request.group
649 check = self._db.security.hasPermission
650 userid = self._client.userid
652 l = [HTMLItem(self._client, self.classname, id)
653 for id in self._klass.filter(None, filterspec, sort, group)
654 if check('View', userid, self.classname, itemid=id)]
655 return l
657 def classhelp(self, properties=None, label=''"(list)", width='500',
658 height='400', property='', form='itemSynopsis',
659 pagesize=50, inputtype="checkbox", sort=None, filter=None):
660 """Pop up a javascript window with class help
662 This generates a link to a popup window which displays the
663 properties indicated by "properties" of the class named by
664 "classname". The "properties" should be a comma-separated list
665 (eg. 'id,name,description'). Properties defaults to all the
666 properties of a class (excluding id, creator, created and
667 activity).
669 You may optionally override the label displayed, the width,
670 the height, the number of items per page and the field on which
671 the list is sorted (defaults to username if in the displayed
672 properties).
674 With the "filter" arg it is possible to specify a filter for
675 which items are supposed to be displayed. It has to be of
676 the format "<field>=<values>;<field>=<values>;...".
678 The popup window will be resizable and scrollable.
680 If the "property" arg is given, it's passed through to the
681 javascript help_window function.
683 You can use inputtype="radio" to display a radio box instead
684 of the default checkbox (useful for entering Link-properties)
686 If the "form" arg is given, it's passed through to the
687 javascript help_window function. - it's the name of the form
688 the "property" belongs to.
689 """
690 if properties is None:
691 properties = self._klass.getprops(protected=0).keys()
692 properties.sort()
693 properties = ','.join(properties)
694 if sort is None:
695 if 'username' in properties.split( ',' ):
696 sort = 'username'
697 else:
698 sort = self._klass.orderprop()
699 sort = '&@sort=' + sort
700 if property:
701 property = '&property=%s'%property
702 if form:
703 form = '&form=%s'%form
704 if inputtype:
705 type= '&type=%s'%inputtype
706 if filter:
707 filterprops = filter.split(';')
708 filtervalues = []
709 names = []
710 for x in filterprops:
711 (name, values) = x.split('=')
712 names.append(name)
713 filtervalues.append('&%s=%s' % (name, urllib.quote(values)))
714 filter = '&@filter=%s%s' % (','.join(names), ''.join(filtervalues))
715 else:
716 filter = ''
717 help_url = "%s?@startwith=0&@template=help&"\
718 "properties=%s%s%s%s%s&@pagesize=%s%s" % \
719 (self.classname, properties, property, form, type,
720 sort, pagesize, filter)
721 onclick = "javascript:help_window('%s', '%s', '%s');return false;" % \
722 (help_url, width, height)
723 return '<a class="classhelp" href="%s" onclick="%s">%s</a>' % \
724 (help_url, onclick, self._(label))
726 def submit(self, label=''"Submit New Entry", action="new"):
727 """ Generate a submit button (and action hidden element)
729 Generate nothing if we're not editable.
730 """
731 if not self.is_edit_ok():
732 return ''
734 return self.input(type="hidden", name="@action", value=action) + \
735 '\n' + \
736 self.input(type="submit", name="submit_button", value=self._(label))
738 def history(self):
739 if not self.is_view_ok():
740 return self._('[hidden]')
741 return self._('New node - no history')
743 def renderWith(self, name, **kwargs):
744 """ Render this class with the given template.
745 """
746 # create a new request and override the specified args
747 req = HTMLRequest(self._client)
748 req.classname = self.classname
749 req.update(kwargs)
751 # new template, using the specified classname and request
752 pt = self._client.instance.templates.get(self.classname, name)
754 # use our fabricated request
755 args = {
756 'ok_message': self._client.ok_message,
757 'error_message': self._client.error_message
758 }
759 return pt.render(self._client, self.classname, req, **args)
761 class _HTMLItem(HTMLInputMixin, HTMLPermissions):
762 """ Accesses through an *item*
763 """
764 def __init__(self, client, classname, nodeid, anonymous=0):
765 self._client = client
766 self._db = client.db
767 self._classname = classname
768 self._nodeid = nodeid
769 self._klass = self._db.getclass(classname)
770 self._props = self._klass.getprops()
772 # do we prefix the form items with the item's identification?
773 self._anonymous = anonymous
775 HTMLInputMixin.__init__(self)
777 def is_edit_ok(self):
778 """ Is the user allowed to Edit the current class?
779 """
780 return self._db.security.hasPermission('Edit', self._client.userid,
781 self._classname, itemid=self._nodeid)
783 def is_view_ok(self):
784 """ Is the user allowed to View the current class?
785 """
786 if self._db.security.hasPermission('View', self._client.userid,
787 self._classname, itemid=self._nodeid):
788 return 1
789 return self.is_edit_ok()
791 def is_only_view_ok(self):
792 """ Is the user only allowed to View (ie. not Edit) the current class?
793 """
794 return self.is_view_ok() and not self.is_edit_ok()
796 def __repr__(self):
797 return '<HTMLItem(0x%x) %s %s>'%(id(self), self._classname,
798 self._nodeid)
800 def __getitem__(self, item):
801 """ return an HTMLProperty instance
802 this now can handle transitive lookups where item is of the
803 form x.y.z
804 """
805 if item == 'id':
806 return self._nodeid
808 items = item.split('.', 1)
809 has_rest = len(items) > 1
811 # get the property
812 prop = self._props[items[0]]
814 if has_rest and not isinstance(prop, (hyperdb.Link, hyperdb.Multilink)):
815 raise KeyError, item
817 # get the value, handling missing values
818 value = None
819 if int(self._nodeid) > 0:
820 value = self._klass.get(self._nodeid, items[0], None)
821 if value is None:
822 if isinstance(prop, hyperdb.Multilink):
823 value = []
825 # look up the correct HTMLProperty class
826 htmlprop = None
827 for klass, htmlklass in propclasses:
828 if isinstance(prop, klass):
829 htmlprop = htmlklass(self._client, self._classname,
830 self._nodeid, prop, items[0], value, self._anonymous)
831 if htmlprop is not None:
832 if has_rest:
833 if isinstance(htmlprop, MultilinkHTMLProperty):
834 return [h[items[1]] for h in htmlprop]
835 return htmlprop[items[1]]
836 return htmlprop
838 raise KeyError, item
840 def __getattr__(self, attr):
841 """ convenience access to properties """
842 try:
843 return self[attr]
844 except KeyError:
845 raise AttributeError, attr
847 def designator(self):
848 """Return this item's designator (classname + id)."""
849 return '%s%s'%(self._classname, self._nodeid)
851 def is_retired(self):
852 """Is this item retired?"""
853 return self._klass.is_retired(self._nodeid)
855 def submit(self, label=''"Submit Changes", action="edit"):
856 """Generate a submit button.
858 Also sneak in the lastactivity and action hidden elements.
859 """
860 return self.input(type="hidden", name="@lastactivity",
861 value=self.activity.local(0)) + '\n' + \
862 self.input(type="hidden", name="@action", value=action) + '\n' + \
863 self.input(type="submit", name="submit_button", value=self._(label))
865 def journal(self, direction='descending'):
866 """ Return a list of HTMLJournalEntry instances.
867 """
868 # XXX do this
869 return []
871 def history(self, direction='descending', dre=re.compile('^\d+$')):
872 if not self.is_view_ok():
873 return self._('[hidden]')
875 # pre-load the history with the current state
876 current = {}
877 for prop_n in self._props.keys():
878 prop = self[prop_n]
879 if not isinstance(prop, HTMLProperty):
880 continue
881 current[prop_n] = prop.plain(escape=1)
882 # make link if hrefable
883 if (self._props.has_key(prop_n) and
884 isinstance(self._props[prop_n], hyperdb.Link)):
885 classname = self._props[prop_n].classname
886 try:
887 template = find_template(self._db.config.TEMPLATES,
888 classname, 'item')
889 if template[1].startswith('_generic'):
890 raise NoTemplate, 'not really...'
891 except NoTemplate:
892 pass
893 else:
894 id = self._klass.get(self._nodeid, prop_n, None)
895 current[prop_n] = '<a href="%s%s">%s</a>'%(
896 classname, id, current[prop_n])
898 # get the journal, sort and reverse
899 history = self._klass.history(self._nodeid)
900 history.sort()
901 history.reverse()
903 timezone = self._db.getUserTimezone()
904 l = []
905 comments = {}
906 for id, evt_date, user, action, args in history:
907 date_s = str(evt_date.local(timezone)).replace("."," ")
908 arg_s = ''
909 if action == 'link' and type(args) == type(()):
910 if len(args) == 3:
911 linkcl, linkid, key = args
912 arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
913 linkcl, linkid, key)
914 else:
915 arg_s = str(args)
917 elif action == 'unlink' and type(args) == type(()):
918 if len(args) == 3:
919 linkcl, linkid, key = args
920 arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
921 linkcl, linkid, key)
922 else:
923 arg_s = str(args)
925 elif type(args) == type({}):
926 cell = []
927 for k in args.keys():
928 # try to get the relevant property and treat it
929 # specially
930 try:
931 prop = self._props[k]
932 except KeyError:
933 prop = None
934 if prop is None:
935 # property no longer exists
936 comments['no_exist'] = self._(
937 "<em>The indicated property no longer exists</em>")
938 cell.append(self._('<em>%s: %s</em>\n')
939 % (self._(k), str(args[k])))
940 continue
942 if args[k] and (isinstance(prop, hyperdb.Multilink) or
943 isinstance(prop, hyperdb.Link)):
944 # figure what the link class is
945 classname = prop.classname
946 try:
947 linkcl = self._db.getclass(classname)
948 except KeyError:
949 labelprop = None
950 comments[classname] = self._(
951 "The linked class %(classname)s no longer exists"
952 ) % locals()
953 labelprop = linkcl.labelprop(1)
954 try:
955 template = find_template(self._db.config.TEMPLATES,
956 classname, 'item')
957 if template[1].startswith('_generic'):
958 raise NoTemplate, 'not really...'
959 hrefable = 1
960 except NoTemplate:
961 hrefable = 0
963 if isinstance(prop, hyperdb.Multilink) and args[k]:
964 ml = []
965 for linkid in args[k]:
966 if isinstance(linkid, type(())):
967 sublabel = linkid[0] + ' '
968 linkids = linkid[1]
969 else:
970 sublabel = ''
971 linkids = [linkid]
972 subml = []
973 for linkid in linkids:
974 label = classname + linkid
975 # if we have a label property, try to use it
976 # TODO: test for node existence even when
977 # there's no labelprop!
978 try:
979 if labelprop is not None and \
980 labelprop != 'id':
981 label = linkcl.get(linkid, labelprop)
982 label = cgi.escape(label)
983 except IndexError:
984 comments['no_link'] = self._(
985 "<strike>The linked node"
986 " no longer exists</strike>")
987 subml.append('<strike>%s</strike>'%label)
988 else:
989 if hrefable:
990 subml.append('<a href="%s%s">%s</a>'%(
991 classname, linkid, label))
992 elif label is None:
993 subml.append('%s%s'%(classname,
994 linkid))
995 else:
996 subml.append(label)
997 ml.append(sublabel + ', '.join(subml))
998 cell.append('%s:\n %s'%(self._(k), ', '.join(ml)))
999 elif isinstance(prop, hyperdb.Link) and args[k]:
1000 label = classname + args[k]
1001 # if we have a label property, try to use it
1002 # TODO: test for node existence even when
1003 # there's no labelprop!
1004 if labelprop is not None and labelprop != 'id':
1005 try:
1006 label = cgi.escape(linkcl.get(args[k],
1007 labelprop))
1008 except IndexError:
1009 comments['no_link'] = self._(
1010 "<strike>The linked node"
1011 " no longer exists</strike>")
1012 cell.append(' <strike>%s</strike>,\n'%label)
1013 # "flag" this is done .... euwww
1014 label = None
1015 if label is not None:
1016 if hrefable:
1017 old = '<a href="%s%s">%s</a>'%(classname,
1018 args[k], label)
1019 else:
1020 old = label;
1021 cell.append('%s: %s' % (self._(k), old))
1022 if current.has_key(k):
1023 cell[-1] += ' -> %s'%current[k]
1024 current[k] = old
1026 elif isinstance(prop, hyperdb.Date) and args[k]:
1027 if args[k] is None:
1028 d = ''
1029 else:
1030 d = date.Date(args[k],
1031 translator=self._client).local(timezone)
1032 cell.append('%s: %s'%(self._(k), str(d)))
1033 if current.has_key(k):
1034 cell[-1] += ' -> %s' % current[k]
1035 current[k] = str(d)
1037 elif isinstance(prop, hyperdb.Interval) and args[k]:
1038 val = str(date.Interval(args[k],
1039 translator=self._client))
1040 cell.append('%s: %s'%(self._(k), val))
1041 if current.has_key(k):
1042 cell[-1] += ' -> %s'%current[k]
1043 current[k] = val
1045 elif isinstance(prop, hyperdb.String) and args[k]:
1046 val = cgi.escape(args[k])
1047 cell.append('%s: %s'%(self._(k), val))
1048 if current.has_key(k):
1049 cell[-1] += ' -> %s'%current[k]
1050 current[k] = val
1052 elif isinstance(prop, hyperdb.Boolean) and args[k] is not None:
1053 val = args[k] and ''"Yes" or ''"No"
1054 cell.append('%s: %s'%(self._(k), val))
1055 if current.has_key(k):
1056 cell[-1] += ' -> %s'%current[k]
1057 current[k] = val
1059 elif not args[k]:
1060 if current.has_key(k):
1061 cell.append('%s: %s'%(self._(k), current[k]))
1062 current[k] = '(no value)'
1063 else:
1064 cell.append(self._('%s: (no value)')%self._(k))
1066 else:
1067 cell.append('%s: %s'%(self._(k), str(args[k])))
1068 if current.has_key(k):
1069 cell[-1] += ' -> %s'%current[k]
1070 current[k] = str(args[k])
1072 arg_s = '<br />'.join(cell)
1073 else:
1074 # unkown event!!
1075 comments['unknown'] = self._(
1076 "<strong><em>This event is not handled"
1077 " by the history display!</em></strong>")
1078 arg_s = '<strong><em>' + str(args) + '</em></strong>'
1079 date_s = date_s.replace(' ', ' ')
1080 # if the user's an itemid, figure the username (older journals
1081 # have the username)
1082 if dre.match(user):
1083 user = self._db.user.get(user, 'username')
1084 l.append('<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>'%(
1085 date_s, user, self._(action), arg_s))
1086 if comments:
1087 l.append(self._(
1088 '<tr><td colspan=4><strong>Note:</strong></td></tr>'))
1089 for entry in comments.values():
1090 l.append('<tr><td colspan=4>%s</td></tr>'%entry)
1092 if direction == 'ascending':
1093 l.reverse()
1095 l[0:0] = ['<table class="history">'
1096 '<tr><th colspan="4" class="header">',
1097 self._('History'),
1098 '</th></tr><tr>',
1099 self._('<th>Date</th>'),
1100 self._('<th>User</th>'),
1101 self._('<th>Action</th>'),
1102 self._('<th>Args</th>'),
1103 '</tr>']
1104 l.append('</table>')
1105 return '\n'.join(l)
1107 def renderQueryForm(self):
1108 """ Render this item, which is a query, as a search form.
1109 """
1110 # create a new request and override the specified args
1111 req = HTMLRequest(self._client)
1112 req.classname = self._klass.get(self._nodeid, 'klass')
1113 name = self._klass.get(self._nodeid, 'name')
1114 req.updateFromURL(self._klass.get(self._nodeid, 'url') +
1115 '&@queryname=%s'%urllib.quote(name))
1117 # new template, using the specified classname and request
1118 pt = self._client.instance.templates.get(req.classname, 'search')
1120 # use our fabricated request
1121 return pt.render(self._client, req.classname, req)
1123 def download_url(self):
1124 """ Assume that this item is a FileClass and that it has a name
1125 and content. Construct a URL for the download of the content.
1126 """
1127 name = self._klass.get(self._nodeid, 'name')
1128 url = '%s%s/%s'%(self._classname, self._nodeid, name)
1129 return urllib.quote(url)
1131 def copy_url(self, exclude=("messages", "files")):
1132 """Construct a URL for creating a copy of this item
1134 "exclude" is an optional list of properties that should
1135 not be copied to the new object. By default, this list
1136 includes "messages" and "files" properties. Note that
1137 "id" property cannot be copied.
1139 """
1140 exclude = ("id", "activity", "actor", "creation", "creator") \
1141 + tuple(exclude)
1142 query = {
1143 "@template": "item",
1144 "@note": self._("Copy of %(class)s %(id)s") % {
1145 "class": self._(self._classname), "id": self._nodeid},
1146 }
1147 for name in self._props.keys():
1148 if name not in exclude:
1149 query[name] = self[name].plain()
1150 return self._classname + "?" + "&".join(
1151 ["%s=%s" % (key, urllib.quote(value))
1152 for key, value in query.items()])
1154 class _HTMLUser(_HTMLItem):
1155 """Add ability to check for permissions on users.
1156 """
1157 _marker = []
1158 def hasPermission(self, permission, classname=_marker,
1159 property=None, itemid=None):
1160 """Determine if the user has the Permission.
1162 The class being tested defaults to the template's class, but may
1163 be overidden for this test by suppling an alternate classname.
1164 """
1165 if classname is self._marker:
1166 classname = self._client.classname
1167 return self._db.security.hasPermission(permission,
1168 self._nodeid, classname, property, itemid)
1170 def hasRole(self, rolename):
1171 """Determine whether the user has the Role."""
1172 roles = self._db.user.get(self._nodeid, 'roles').split(',')
1173 for role in roles:
1174 if role.strip() == rolename: return True
1175 return False
1177 def HTMLItem(client, classname, nodeid, anonymous=0):
1178 if classname == 'user':
1179 return _HTMLUser(client, classname, nodeid, anonymous)
1180 else:
1181 return _HTMLItem(client, classname, nodeid, anonymous)
1183 class HTMLProperty(HTMLInputMixin, HTMLPermissions):
1184 """ String, Number, Date, Interval HTMLProperty
1186 Has useful attributes:
1188 _name the name of the property
1189 _value the value of the property if any
1191 A wrapper object which may be stringified for the plain() behaviour.
1192 """
1193 def __init__(self, client, classname, nodeid, prop, name, value,
1194 anonymous=0):
1195 self._client = client
1196 self._db = client.db
1197 self._ = client._
1198 self._classname = classname
1199 self._nodeid = nodeid
1200 self._prop = prop
1201 self._value = value
1202 self._anonymous = anonymous
1203 self._name = name
1204 if not anonymous:
1205 self._formname = '%s%s@%s'%(classname, nodeid, name)
1206 else:
1207 self._formname = name
1209 HTMLInputMixin.__init__(self)
1211 def __repr__(self):
1212 return '<HTMLProperty(0x%x) %s %r %r>'%(id(self), self._formname,
1213 self._prop, self._value)
1214 def __str__(self):
1215 return self.plain()
1216 def __cmp__(self, other):
1217 if isinstance(other, HTMLProperty):
1218 return cmp(self._value, other._value)
1219 return cmp(self._value, other)
1221 def __nonzero__(self):
1222 return not not self._value
1224 def isset(self):
1225 """Is my _value not None?"""
1226 return self._value is not None
1228 def is_edit_ok(self):
1229 """Should the user be allowed to use an edit form field for this
1230 property. Check "Create" for new items, or "Edit" for existing
1231 ones.
1232 """
1233 if self._nodeid:
1234 return self._db.security.hasPermission('Edit', self._client.userid,
1235 self._classname, self._name, self._nodeid)
1236 return self._db.security.hasPermission('Create', self._client.userid,
1237 self._classname, self._name)
1239 def is_view_ok(self):
1240 """ Is the user allowed to View the current class?
1241 """
1242 if self._db.security.hasPermission('View', self._client.userid,
1243 self._classname, self._name, self._nodeid):
1244 return 1
1245 return self.is_edit_ok()
1247 class StringHTMLProperty(HTMLProperty):
1248 hyper_re = re.compile(r'''(
1249 (?P<url>
1250 (
1251 (ht|f)tp(s?):// # protocol
1252 ([\w]+(:\w+)?@)? # username/password
1253 ([\w\-]+) # hostname
1254 ((\.[\w-]+)+)? # .domain.etc
1255 | # ... or ...
1256 ([\w]+(:\w+)?@)? # username/password
1257 www\. # "www."
1258 ([\w\-]+\.)+ # hostname
1259 [\w]{2,5} # TLD
1260 )
1261 (:[\d]{1,5})? # port
1262 (/[\w\-$.+!*(),;:@&=?/~\\#%]*)? # path etc.
1263 )|
1264 (?P<email>[-+=%/\w\.]+@[\w\.\-]+)|
1265 (?P<item>(?P<class>[A-Za-z_]+)(\s*)(?P<id>\d+))
1266 )''', re.X | re.I)
1267 protocol_re = re.compile('^(ht|f)tp(s?)://', re.I)
1269 def _hyper_repl_item(self,match,replacement):
1270 item = match.group('item')
1271 cls = match.group('class').lower()
1272 id = match.group('id')
1273 try:
1274 # make sure cls is a valid tracker classname
1275 cl = self._db.getclass(cls)
1276 if not cl.hasnode(id):
1277 return item
1278 return replacement % locals()
1279 except KeyError:
1280 return item
1282 def _hyper_repl(self, match):
1283 if match.group('url'):
1284 u = s = match.group('url')
1285 if not self.protocol_re.search(s):
1286 u = 'http://' + s
1287 # catch an escaped ">" at the end of the URL
1288 if s.endswith('>'):
1289 u = s = s[:-4]
1290 e = '>'
1291 else:
1292 e = ''
1293 return '<a href="%s">%s</a>%s'%(u, s, e)
1294 elif match.group('email'):
1295 s = match.group('email')
1296 return '<a href="mailto:%s">%s</a>'%(s, s)
1297 else:
1298 return self._hyper_repl_item(match,
1299 '<a href="%(cls)s%(id)s">%(item)s</a>')
1301 def _hyper_repl_rst(self, match):
1302 if match.group('url'):
1303 s = match.group('url')
1304 return '`%s <%s>`_'%(s, s)
1305 elif match.group('email'):
1306 s = match.group('email')
1307 return '`%s <mailto:%s>`_'%(s, s)
1308 else:
1309 return self._hyper_repl_item(match,'`%(item)s <%(cls)s%(id)s>`_')
1311 def hyperlinked(self):
1312 """ Render a "hyperlinked" version of the text """
1313 return self.plain(hyperlink=1)
1315 def plain(self, escape=0, hyperlink=0):
1316 """Render a "plain" representation of the property
1318 - "escape" turns on/off HTML quoting
1319 - "hyperlink" turns on/off in-text hyperlinking of URLs, email
1320 addresses and designators
1321 """
1322 if not self.is_view_ok():
1323 return self._('[hidden]')
1325 if self._value is None:
1326 return ''
1327 if escape:
1328 s = cgi.escape(str(self._value))
1329 else:
1330 s = str(self._value)
1331 if hyperlink:
1332 # no, we *must* escape this text
1333 if not escape:
1334 s = cgi.escape(s)
1335 s = self.hyper_re.sub(self._hyper_repl, s)
1336 return s
1338 def wrapped(self, escape=1, hyperlink=1):
1339 """Render a "wrapped" representation of the property.
1341 We wrap long lines at 80 columns on the nearest whitespace. Lines
1342 with no whitespace are not broken to force wrapping.
1344 Note that unlike plain() we default wrapped() to have the escaping
1345 and hyperlinking turned on since that's the most common usage.
1347 - "escape" turns on/off HTML quoting
1348 - "hyperlink" turns on/off in-text hyperlinking of URLs, email
1349 addresses and designators
1350 """
1351 if not self.is_view_ok():
1352 return self._('[hidden]')
1354 if self._value is None:
1355 return ''
1356 s = support.wrap(str(self._value), width=80)
1357 if escape:
1358 s = cgi.escape(s)
1359 if hyperlink:
1360 # no, we *must* escape this text
1361 if not escape:
1362 s = cgi.escape(s)
1363 s = self.hyper_re.sub(self._hyper_repl, s)
1364 return s
1366 def stext(self, escape=0, hyperlink=1):
1367 """ Render the value of the property as StructuredText.
1369 This requires the StructureText module to be installed separately.
1370 """
1371 if not self.is_view_ok():
1372 return self._('[hidden]')
1374 s = self.plain(escape=escape, hyperlink=hyperlink)
1375 if not StructuredText:
1376 return s
1377 return StructuredText(s,level=1,header=0)
1379 def rst(self, hyperlink=1):
1380 """ Render the value of the property as ReStructuredText.
1382 This requires docutils to be installed separately.
1383 """
1384 if not self.is_view_ok():
1385 return self._('[hidden]')
1387 if not ReStructuredText:
1388 return self.plain(escape=0, hyperlink=hyperlink)
1389 s = self.plain(escape=0, hyperlink=0)
1390 if hyperlink:
1391 s = self.hyper_re.sub(self._hyper_repl_rst, s)
1392 return ReStructuredText(s, writer_name="html")["body"].encode("utf-8",
1393 "replace")
1395 def field(self, **kwargs):
1396 """ Render the property as a field in HTML.
1398 If not editable, just display the value via plain().
1399 """
1400 if not self.is_edit_ok():
1401 return self.plain(escape=1)
1403 value = self._value
1404 if value is None:
1405 value = ''
1407 kwargs.setdefault("size", 30)
1408 kwargs.update({"name": self._formname, "value": value})
1409 return self.input(**kwargs)
1411 def multiline(self, escape=0, rows=5, cols=40, **kwargs):
1412 """ Render a multiline form edit field for the property.
1414 If not editable, just display the plain() value in a <pre> tag.
1415 """
1416 if not self.is_edit_ok():
1417 return '<pre>%s</pre>'%self.plain()
1419 if self._value is None:
1420 value = ''
1421 else:
1422 value = cgi.escape(str(self._value))
1424 value = '"'.join(value.split('"'))
1425 name = self._formname
1426 passthrough_args = ' '.join(['%s="%s"' % (k, cgi.escape(str(v), True))
1427 for k,v in kwargs.items()])
1428 return ('<textarea %(passthrough_args)s name="%(name)s" id="%(name)s"'
1429 ' rows="%(rows)s" cols="%(cols)s">'
1430 '%(value)s</textarea>') % locals()
1432 def email(self, escape=1):
1433 """ Render the value of the property as an obscured email address
1434 """
1435 if not self.is_view_ok():
1436 return self._('[hidden]')
1438 if self._value is None:
1439 value = ''
1440 else:
1441 value = str(self._value)
1442 split = value.split('@')
1443 if len(split) == 2:
1444 name, domain = split
1445 domain = ' '.join(domain.split('.')[:-1])
1446 name = name.replace('.', ' ')
1447 value = '%s at %s ...'%(name, domain)
1448 else:
1449 value = value.replace('.', ' ')
1450 if escape:
1451 value = cgi.escape(value)
1452 return value
1454 class PasswordHTMLProperty(HTMLProperty):
1455 def plain(self, escape=0):
1456 """ Render a "plain" representation of the property
1457 """
1458 if not self.is_view_ok():
1459 return self._('[hidden]')
1461 if self._value is None:
1462 return ''
1463 return self._('*encrypted*')
1465 def field(self, size=30):
1466 """ Render a form edit field for the property.
1468 If not editable, just display the value via plain().
1469 """
1470 if not self.is_edit_ok():
1471 return self.plain(escape=1)
1473 return self.input(type="password", name=self._formname, size=size)
1475 def confirm(self, size=30):
1476 """ Render a second form edit field for the property, used for
1477 confirmation that the user typed the password correctly. Generates
1478 a field with name "@confirm@name".
1480 If not editable, display nothing.
1481 """
1482 if not self.is_edit_ok():
1483 return ''
1485 return self.input(type="password",
1486 name="@confirm@%s"%self._formname,
1487 id="%s-confirm"%self._formname,
1488 size=size)
1490 class NumberHTMLProperty(HTMLProperty):
1491 def plain(self, escape=0):
1492 """ Render a "plain" representation of the property
1493 """
1494 if not self.is_view_ok():
1495 return self._('[hidden]')
1497 if self._value is None:
1498 return ''
1500 return str(self._value)
1502 def field(self, size=30):
1503 """ Render a form edit field for the property.
1505 If not editable, just display the value via plain().
1506 """
1507 if not self.is_edit_ok():
1508 return self.plain(escape=1)
1510 value = self._value
1511 if value is None:
1512 value = ''
1514 return self.input(name=self._formname, value=value, size=size)
1516 def __int__(self):
1517 """ Return an int of me
1518 """
1519 return int(self._value)
1521 def __float__(self):
1522 """ Return a float of me
1523 """
1524 return float(self._value)
1527 class BooleanHTMLProperty(HTMLProperty):
1528 def plain(self, escape=0):
1529 """ Render a "plain" representation of the property
1530 """
1531 if not self.is_view_ok():
1532 return self._('[hidden]')
1534 if self._value is None:
1535 return ''
1536 return self._value and self._("Yes") or self._("No")
1538 def field(self):
1539 """ Render a form edit field for the property
1541 If not editable, just display the value via plain().
1542 """
1543 if not self.is_edit_ok():
1544 return self.plain(escape=1)
1546 value = self._value
1547 if isinstance(value, str) or isinstance(value, unicode):
1548 value = value.strip().lower() in ('checked', 'yes', 'true',
1549 'on', '1')
1551 checked = value and "checked" or ""
1552 if value:
1553 s = self.input(type="radio", name=self._formname, value="yes",
1554 checked="checked")
1555 s += self._('Yes')
1556 s +=self.input(type="radio", name=self._formname, value="no")
1557 s += self._('No')
1558 else:
1559 s = self.input(type="radio", name=self._formname, value="yes")
1560 s += self._('Yes')
1561 s +=self.input(type="radio", name=self._formname, value="no",
1562 checked="checked")
1563 s += self._('No')
1564 return s
1566 class DateHTMLProperty(HTMLProperty):
1568 _marker = []
1570 def __init__(self, client, classname, nodeid, prop, name, value,
1571 anonymous=0, offset=None):
1572 HTMLProperty.__init__(self, client, classname, nodeid, prop, name,
1573 value, anonymous=anonymous)
1574 if self._value and not (isinstance(self._value, str) or
1575 isinstance(self._value, unicode)):
1576 self._value.setTranslator(self._client.translator)
1577 self._offset = offset
1578 if self._offset is None :
1579 self._offset = self._prop.offset (self._db)
1581 def plain(self, escape=0):
1582 """ Render a "plain" representation of the property
1583 """
1584 if not self.is_view_ok():
1585 return self._('[hidden]')
1587 if self._value is None:
1588 return ''
1589 if self._offset is None:
1590 offset = self._db.getUserTimezone()
1591 else:
1592 offset = self._offset
1593 return str(self._value.local(offset))
1595 def now(self, str_interval=None):
1596 """ Return the current time.
1598 This is useful for defaulting a new value. Returns a
1599 DateHTMLProperty.
1600 """
1601 if not self.is_view_ok():
1602 return self._('[hidden]')
1604 ret = date.Date('.', translator=self._client)
1606 if isinstance(str_interval, basestring):
1607 sign = 1
1608 if str_interval[0] == '-':
1609 sign = -1
1610 str_interval = str_interval[1:]
1611 interval = date.Interval(str_interval, translator=self._client)
1612 if sign > 0:
1613 ret = ret + interval
1614 else:
1615 ret = ret - interval
1617 return DateHTMLProperty(self._client, self._classname, self._nodeid,
1618 self._prop, self._formname, ret)
1620 def field(self, size=30, default=None, format=_marker, popcal=True):
1621 """Render a form edit field for the property
1623 If not editable, just display the value via plain().
1625 If "popcal" then include the Javascript calendar editor.
1626 Default=yes.
1628 The format string is a standard python strftime format string.
1629 """
1630 if not self.is_edit_ok():
1631 if format is self._marker:
1632 return self.plain(escape=1)
1633 else:
1634 return self.pretty(format)
1636 value = self._value
1638 if value is None:
1639 if default is None:
1640 raw_value = None
1641 else:
1642 if isinstance(default, basestring):
1643 raw_value = date.Date(default, translator=self._client)
1644 elif isinstance(default, date.Date):
1645 raw_value = default
1646 elif isinstance(default, DateHTMLProperty):
1647 raw_value = default._value
1648 else:
1649 raise ValueError, self._('default value for '
1650 'DateHTMLProperty must be either DateHTMLProperty '
1651 'or string date representation.')
1652 elif isinstance(value, str) or isinstance(value, unicode):
1653 # most likely erroneous input to be passed back to user
1654 if isinstance(value, unicode): value = value.encode('utf8')
1655 return self.input(name=self._formname, value=value, size=size)
1656 else:
1657 raw_value = value
1659 if raw_value is None:
1660 value = ''
1661 elif isinstance(raw_value, str) or isinstance(raw_value, unicode):
1662 if format is self._marker:
1663 value = raw_value
1664 else:
1665 value = date.Date(raw_value).pretty(format)
1666 else:
1667 if self._offset is None :
1668 offset = self._db.getUserTimezone()
1669 else :
1670 offset = self._offset
1671 value = raw_value.local(offset)
1672 if format is not self._marker:
1673 value = value.pretty(format)
1675 s = self.input(name=self._formname, value=value, size=size)
1676 if popcal:
1677 s += self.popcal()
1678 return s
1680 def reldate(self, pretty=1):
1681 """ Render the interval between the date and now.
1683 If the "pretty" flag is true, then make the display pretty.
1684 """
1685 if not self.is_view_ok():
1686 return self._('[hidden]')
1688 if not self._value:
1689 return ''
1691 # figure the interval
1692 interval = self._value - date.Date('.', translator=self._client)
1693 if pretty:
1694 return interval.pretty()
1695 return str(interval)
1697 def pretty(self, format=_marker):
1698 """ Render the date in a pretty format (eg. month names, spaces).
1700 The format string is a standard python strftime format string.
1701 Note that if the day is zero, and appears at the start of the
1702 string, then it'll be stripped from the output. This is handy
1703 for the situation when a date only specifies a month and a year.
1704 """
1705 if not self.is_view_ok():
1706 return self._('[hidden]')
1708 if self._offset is None:
1709 offset = self._db.getUserTimezone()
1710 else:
1711 offset = self._offset
1713 if not self._value:
1714 return ''
1715 elif format is not self._marker:
1716 return self._value.local(offset).pretty(format)
1717 else:
1718 return self._value.local(offset).pretty()
1720 def local(self, offset):
1721 """ Return the date/time as a local (timezone offset) date/time.
1722 """
1723 if not self.is_view_ok():
1724 return self._('[hidden]')
1726 return DateHTMLProperty(self._client, self._classname, self._nodeid,
1727 self._prop, self._formname, self._value, offset=offset)
1729 def popcal(self, width=300, height=200, label="(cal)",
1730 form="itemSynopsis"):
1731 """Generate a link to a calendar pop-up window.
1733 item: HTMLProperty e.g.: context.deadline
1734 """
1735 if self.isset():
1736 date = "&date=%s"%self._value
1737 else :
1738 date = ""
1739 return ('<a class="classhelp" href="javascript:help_window('
1740 "'%s?@template=calendar&property=%s&form=%s%s', %d, %d)"
1741 '">%s</a>'%(self._classname, self._name, form, date, width,
1742 height, label))
1744 class IntervalHTMLProperty(HTMLProperty):
1745 def __init__(self, client, classname, nodeid, prop, name, value,
1746 anonymous=0):
1747 HTMLProperty.__init__(self, client, classname, nodeid, prop,
1748 name, value, anonymous)
1749 if self._value and not isinstance(self._value, (str, unicode)):
1750 self._value.setTranslator(self._client.translator)
1752 def plain(self, escape=0):
1753 """ Render a "plain" representation of the property
1754 """
1755 if not self.is_view_ok():
1756 return self._('[hidden]')
1758 if self._value is None:
1759 return ''
1760 return str(self._value)
1762 def pretty(self):
1763 """ Render the interval in a pretty format (eg. "yesterday")
1764 """
1765 if not self.is_view_ok():
1766 return self._('[hidden]')
1768 return self._value.pretty()
1770 def field(self, size=30):
1771 """ Render a form edit field for the property
1773 If not editable, just display the value via plain().
1774 """
1775 if not self.is_edit_ok():
1776 return self.plain(escape=1)
1778 value = self._value
1779 if value is None:
1780 value = ''
1782 return self.input(name=self._formname, value=value, size=size)
1784 class LinkHTMLProperty(HTMLProperty):
1785 """ Link HTMLProperty
1786 Include the above as well as being able to access the class
1787 information. Stringifying the object itself results in the value
1788 from the item being displayed. Accessing attributes of this object
1789 result in the appropriate entry from the class being queried for the
1790 property accessed (so item/assignedto/name would look up the user
1791 entry identified by the assignedto property on item, and then the
1792 name property of that user)
1793 """
1794 def __init__(self, *args, **kw):
1795 HTMLProperty.__init__(self, *args, **kw)
1796 # if we're representing a form value, then the -1 from the form really
1797 # should be a None
1798 if str(self._value) == '-1':
1799 self._value = None
1801 def __getattr__(self, attr):
1802 """ return a new HTMLItem """
1803 if not self._value:
1804 # handle a special page templates lookup
1805 if attr == '__render_with_namespace__':
1806 def nothing(*args, **kw):
1807 return ''
1808 return nothing
1809 msg = self._('Attempt to look up %(attr)s on a missing value')
1810 return MissingValue(msg%locals())
1811 i = HTMLItem(self._client, self._prop.classname, self._value)
1812 return getattr(i, attr)
1814 def plain(self, escape=0):
1815 """ Render a "plain" representation of the property
1816 """
1817 if not self.is_view_ok():
1818 return self._('[hidden]')
1820 if self._value is None:
1821 return ''
1822 linkcl = self._db.classes[self._prop.classname]
1823 k = linkcl.labelprop(1)
1824 if num_re.match(self._value):
1825 value = str(linkcl.get(self._value, k))
1826 else :
1827 value = self._value
1828 if escape:
1829 value = cgi.escape(value)
1830 return value
1832 def field(self, showid=0, size=None):
1833 """ Render a form edit field for the property
1835 If not editable, just display the value via plain().
1836 """
1837 if not self.is_edit_ok():
1838 return self.plain(escape=1)
1840 # edit field
1841 linkcl = self._db.getclass(self._prop.classname)
1842 if self._value is None:
1843 value = ''
1844 else:
1845 k = linkcl.getkey()
1846 if k and num_re.match(self._value):
1847 value = linkcl.get(self._value, k)
1848 else:
1849 value = self._value
1850 return self.input(name=self._formname, value=value, size=size)
1852 def menu(self, size=None, height=None, showid=0, additional=[], value=None,
1853 sort_on=None, **conditions):
1854 """ Render a form select list for this property
1856 "size" is used to limit the length of the list labels
1857 "height" is used to set the <select> tag's "size" attribute
1858 "showid" includes the item ids in the list labels
1859 "value" specifies which item is pre-selected
1860 "additional" lists properties which should be included in the
1861 label
1862 "sort_on" indicates the property to sort the list on as
1863 (direction, property) where direction is '+' or '-'. A
1864 single string with the direction prepended may be used.
1865 For example: ('-', 'order'), '+name'.
1867 The remaining keyword arguments are used as conditions for
1868 filtering the items in the list - they're passed as the
1869 "filterspec" argument to a Class.filter() call.
1871 If not editable, just display the value via plain().
1872 """
1873 if not self.is_edit_ok():
1874 return self.plain(escape=1)
1876 # Since None indicates the default, we need another way to
1877 # indicate "no selection". We use -1 for this purpose, as
1878 # that is the value we use when submitting a form without the
1879 # value set.
1880 if value is None:
1881 value = self._value
1882 elif value == '-1':
1883 value = None
1885 linkcl = self._db.getclass(self._prop.classname)
1886 l = ['<select name="%s">'%self._formname]
1887 k = linkcl.labelprop(1)
1888 s = ''
1889 if value is None:
1890 s = 'selected="selected" '
1891 l.append(self._('<option %svalue="-1">- no selection -</option>')%s)
1893 if sort_on is not None:
1894 if not isinstance(sort_on, tuple):
1895 if sort_on[0] in '+-':
1896 sort_on = (sort_on[0], sort_on[1:])
1897 else:
1898 sort_on = ('+', sort_on)
1899 else:
1900 sort_on = ('+', linkcl.orderprop())
1902 options = [opt
1903 for opt in linkcl.filter(None, conditions, sort_on, (None, None))
1904 if self._db.security.hasPermission("View", self._client.userid,
1905 linkcl.classname, itemid=opt)]
1907 # make sure we list the current value if it's retired
1908 if value and value not in options:
1909 options.insert(0, value)
1911 if additional:
1912 additional_fns = []
1913 props = linkcl.getprops()
1914 for propname in additional:
1915 prop = props[propname]
1916 if isinstance(prop, hyperdb.Link):
1917 cl = self._db.getclass(prop.classname)
1918 labelprop = cl.labelprop()
1919 fn = lambda optionid: cl.get(linkcl.get(optionid,
1920 propname),
1921 labelprop)
1922 else:
1923 fn = lambda optionid: linkcl.get(optionid, propname)
1924 additional_fns.append(fn)
1926 for optionid in options:
1927 # get the option value, and if it's None use an empty string
1928 option = linkcl.get(optionid, k) or ''
1930 # figure if this option is selected
1931 s = ''
1932 if value in [optionid, option]:
1933 s = 'selected="selected" '
1935 # figure the label
1936 if showid:
1937 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1938 elif not option:
1939 lab = '%s%s'%(self._prop.classname, optionid)
1940 else:
1941 lab = option
1943 # truncate if it's too long
1944 if size is not None and len(lab) > size:
1945 lab = lab[:size-3] + '...'
1946 if additional:
1947 m = []
1948 for fn in additional_fns:
1949 m.append(str(fn(optionid)))
1950 lab = lab + ' (%s)'%', '.join(m)
1952 # and generate
1953 lab = cgi.escape(self._(lab))
1954 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
1955 l.append('</select>')
1956 return '\n'.join(l)
1957 # def checklist(self, ...)
1961 class MultilinkHTMLProperty(HTMLProperty):
1962 """ Multilink HTMLProperty
1964 Also be iterable, returning a wrapper object like the Link case for
1965 each entry in the multilink.
1966 """
1967 def __init__(self, *args, **kwargs):
1968 HTMLProperty.__init__(self, *args, **kwargs)
1969 if self._value:
1970 display_value = lookupIds(self._db, self._prop, self._value,
1971 fail_ok=1, do_lookup=False)
1972 sortfun = make_sort_function(self._db, self._prop.classname)
1973 # sorting fails if the value contains
1974 # items not yet stored in the database
1975 # ignore these errors to preserve user input
1976 try:
1977 display_value.sort(sortfun)
1978 except:
1979 pass
1980 self._value = display_value
1982 def __len__(self):
1983 """ length of the multilink """
1984 return len(self._value)
1986 def __getattr__(self, attr):
1987 """ no extended attribute accesses make sense here """
1988 raise AttributeError, attr
1990 def viewableGenerator(self, values):
1991 """Used to iterate over only the View'able items in a class."""
1992 check = self._db.security.hasPermission
1993 userid = self._client.userid
1994 classname = self._prop.classname
1995 for value in values:
1996 if check('View', userid, classname, itemid=value):
1997 yield HTMLItem(self._client, classname, value)
1999 def __iter__(self):
2000 """ iterate and return a new HTMLItem
2001 """
2002 return self.viewableGenerator(self._value)
2004 def reverse(self):
2005 """ return the list in reverse order
2006 """
2007 l = self._value[:]
2008 l.reverse()
2009 return self.viewableGenerator(l)
2011 def sorted(self, property):
2012 """ Return this multilink sorted by the given property """
2013 value = list(self.__iter__())
2014 value.sort(lambda a,b:cmp(a[property], b[property]))
2015 return value
2017 def __contains__(self, value):
2018 """ Support the "in" operator. We have to make sure the passed-in
2019 value is a string first, not a HTMLProperty.
2020 """
2021 return str(value) in self._value
2023 def isset(self):
2024 """Is my _value not []?"""
2025 return self._value != []
2027 def plain(self, escape=0):
2028 """ Render a "plain" representation of the property
2029 """
2030 if not self.is_view_ok():
2031 return self._('[hidden]')
2033 linkcl = self._db.classes[self._prop.classname]
2034 k = linkcl.labelprop(1)
2035 labels = []
2036 for v in self._value:
2037 label = linkcl.get(v, k)
2038 # fall back to designator if label is None
2039 if label is None: label = '%s%s'%(self._prop.classname, k)
2040 labels.append(label)
2041 value = ', '.join(labels)
2042 if escape:
2043 value = cgi.escape(value)
2044 return value
2046 def field(self, size=30, showid=0):
2047 """ Render a form edit field for the property
2049 If not editable, just display the value via plain().
2050 """
2051 if not self.is_edit_ok():
2052 return self.plain(escape=1)
2054 linkcl = self._db.getclass(self._prop.classname)
2055 value = self._value[:]
2056 # map the id to the label property
2057 if not linkcl.getkey():
2058 showid=1
2059 if not showid:
2060 k = linkcl.labelprop(1)
2061 value = lookupKeys(linkcl, k, value)
2062 value = ','.join(value)
2063 return self.input(name=self._formname, size=size, value=value)
2065 def menu(self, size=None, height=None, showid=0, additional=[],
2066 value=None, sort_on=None, **conditions):
2067 """ Render a form <select> list for this property.
2069 "size" is used to limit the length of the list labels
2070 "height" is used to set the <select> tag's "size" attribute
2071 "showid" includes the item ids in the list labels
2072 "additional" lists properties which should be included in the
2073 label
2074 "value" specifies which item is pre-selected
2075 "sort_on" indicates the property to sort the list on as
2076 (direction, property) where direction is '+' or '-'. A
2077 single string with the direction prepended may be used.
2078 For example: ('-', 'order'), '+name'.
2080 The remaining keyword arguments are used as conditions for
2081 filtering the items in the list - they're passed as the
2082 "filterspec" argument to a Class.filter() call.
2084 If not editable, just display the value via plain().
2085 """
2086 if not self.is_edit_ok():
2087 return self.plain(escape=1)
2089 if value is None:
2090 value = self._value
2092 linkcl = self._db.getclass(self._prop.classname)
2094 if sort_on is not None:
2095 if not isinstance(sort_on, tuple):
2096 if sort_on[0] in '+-':
2097 sort_on = (sort_on[0], sort_on[1:])
2098 else:
2099 sort_on = ('+', sort_on)
2100 else:
2101 sort_on = ('+', linkcl.orderprop())
2103 options = [opt
2104 for opt in linkcl.filter(None, conditions, sort_on)
2105 if self._db.security.hasPermission("View", self._client.userid,
2106 linkcl.classname, itemid=opt)]
2108 # make sure we list the current values if they're retired
2109 for val in value:
2110 if val not in options:
2111 options.insert(0, val)
2113 if not height:
2114 height = len(options)
2115 if value:
2116 # The "no selection" option.
2117 height += 1
2118 height = min(height, 7)
2119 l = ['<select multiple name="%s" size="%s">'%(self._formname, height)]
2120 k = linkcl.labelprop(1)
2122 if value:
2123 l.append('<option value="%s">- no selection -</option>'
2124 % ','.join(['-' + v for v in value]))
2126 if additional:
2127 additional_fns = []
2128 props = linkcl.getprops()
2129 for propname in additional:
2130 prop = props[propname]
2131 if isinstance(prop, hyperdb.Link):
2132 cl = self._db.getclass(prop.classname)
2133 labelprop = cl.labelprop()
2134 fn = lambda optionid: cl.get(linkcl.get(optionid,
2135 propname),
2136 labelprop)
2137 else:
2138 fn = lambda optionid: linkcl.get(optionid, propname)
2139 additional_fns.append(fn)
2141 for optionid in options:
2142 # get the option value, and if it's None use an empty string
2143 option = linkcl.get(optionid, k) or ''
2145 # figure if this option is selected
2146 s = ''
2147 if optionid in value or option in value:
2148 s = 'selected="selected" '
2150 # figure the label
2151 if showid:
2152 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
2153 else:
2154 lab = option
2155 # truncate if it's too long
2156 if size is not None and len(lab) > size:
2157 lab = lab[:size-3] + '...'
2158 if additional:
2159 m = []
2160 for fn in additional_fns:
2161 m.append(str(fn(optionid)))
2162 lab = lab + ' (%s)'%', '.join(m)
2164 # and generate
2165 lab = cgi.escape(self._(lab))
2166 l.append('<option %svalue="%s">%s</option>'%(s, optionid,
2167 lab))
2168 l.append('</select>')
2169 return '\n'.join(l)
2171 # set the propclasses for HTMLItem
2172 propclasses = (
2173 (hyperdb.String, StringHTMLProperty),
2174 (hyperdb.Number, NumberHTMLProperty),
2175 (hyperdb.Boolean, BooleanHTMLProperty),
2176 (hyperdb.Date, DateHTMLProperty),
2177 (hyperdb.Interval, IntervalHTMLProperty),
2178 (hyperdb.Password, PasswordHTMLProperty),
2179 (hyperdb.Link, LinkHTMLProperty),
2180 (hyperdb.Multilink, MultilinkHTMLProperty),
2181 )
2183 def make_sort_function(db, classname, sort_on=None):
2184 """Make a sort function for a given class
2185 """
2186 linkcl = db.getclass(classname)
2187 if sort_on is None:
2188 sort_on = linkcl.orderprop()
2189 def sortfunc(a, b):
2190 return cmp(linkcl.get(a, sort_on), linkcl.get(b, sort_on))
2191 return sortfunc
2193 def handleListCGIValue(value):
2194 """ Value is either a single item or a list of items. Each item has a
2195 .value that we're actually interested in.
2196 """
2197 if isinstance(value, type([])):
2198 return [value.value for value in value]
2199 else:
2200 value = value.value.strip()
2201 if not value:
2202 return []
2203 return [v.strip() for v in value.split(',')]
2205 class HTMLRequest(HTMLInputMixin):
2206 """The *request*, holding the CGI form and environment.
2208 - "form" the CGI form as a cgi.FieldStorage
2209 - "env" the CGI environment variables
2210 - "base" the base URL for this instance
2211 - "user" a HTMLItem instance for this user
2212 - "language" as determined by the browser or config
2213 - "classname" the current classname (possibly None)
2214 - "template" the current template (suffix, also possibly None)
2216 Index args:
2218 - "columns" dictionary of the columns to display in an index page
2219 - "show" a convenience access to columns - request/show/colname will
2220 be true if the columns should be displayed, false otherwise
2221 - "sort" index sort column (direction, column name)
2222 - "group" index grouping property (direction, column name)
2223 - "filter" properties to filter the index on
2224 - "filterspec" values to filter the index on
2225 - "search_text" text to perform a full-text search on for an index
2226 """
2227 def __repr__(self):
2228 return '<HTMLRequest %r>'%self.__dict__
2230 def __init__(self, client):
2231 # _client is needed by HTMLInputMixin
2232 self._client = self.client = client
2234 # easier access vars
2235 self.form = client.form
2236 self.env = client.env
2237 self.base = client.base
2238 self.user = HTMLItem(client, 'user', client.userid)
2239 self.language = client.language
2241 # store the current class name and action
2242 self.classname = client.classname
2243 self.nodeid = client.nodeid
2244 self.template = client.template
2246 # the special char to use for special vars
2247 self.special_char = '@'
2249 HTMLInputMixin.__init__(self)
2251 self._post_init()
2253 def current_url(self):
2254 url = self.base
2255 if self.classname:
2256 url += self.classname
2257 if self.nodeid:
2258 url += self.nodeid
2259 args = {}
2260 if self.template:
2261 args['@template'] = self.template
2262 return self.indexargs_url(url, args)
2264 def _parse_sort(self, var, name):
2265 """ Parse sort/group options. Append to var
2266 """
2267 fields = []
2268 dirs = []
2269 for special in '@:':
2270 idx = 0
2271 key = '%s%s%d'%(special, name, idx)
2272 while key in self.form:
2273 self.special_char = special
2274 fields.append(self.form.getfirst(key))
2275 dirkey = '%s%sdir%d'%(special, name, idx)
2276 if dirkey in self.form:
2277 dirs.append(self.form.getfirst(dirkey))
2278 else:
2279 dirs.append(None)
2280 idx += 1
2281 key = '%s%s%d'%(special, name, idx)
2282 # backward compatible (and query) URL format
2283 key = special + name
2284 dirkey = key + 'dir'
2285 if key in self.form and not fields:
2286 fields = handleListCGIValue(self.form[key])
2287 if dirkey in self.form:
2288 dirs.append(self.form.getfirst(dirkey))
2289 if fields: # only try other special char if nothing found
2290 break
2291 for f, d in map(None, fields, dirs):
2292 if f.startswith('-'):
2293 var.append(('-', f[1:]))
2294 elif d:
2295 var.append(('-', f))
2296 else:
2297 var.append(('+', f))
2299 def _post_init(self):
2300 """ Set attributes based on self.form
2301 """
2302 # extract the index display information from the form
2303 self.columns = []
2304 for name in ':columns @columns'.split():
2305 if self.form.has_key(name):
2306 self.special_char = name[0]
2307 self.columns = handleListCGIValue(self.form[name])
2308 break
2309 self.show = support.TruthDict(self.columns)
2311 # sorting and grouping
2312 self.sort = []
2313 self.group = []
2314 self._parse_sort(self.sort, 'sort')
2315 self._parse_sort(self.group, 'group')
2317 # filtering
2318 self.filter = []
2319 for name in ':filter @filter'.split():
2320 if self.form.has_key(name):
2321 self.special_char = name[0]
2322 self.filter = handleListCGIValue(self.form[name])
2324 self.filterspec = {}
2325 db = self.client.db
2326 if self.classname is not None:
2327 cls = db.getclass (self.classname)
2328 for name in self.filter:
2329 if not self.form.has_key(name):
2330 continue
2331 prop = cls.get_transitive_prop (name)
2332 fv = self.form[name]
2333 if (isinstance(prop, hyperdb.Link) or
2334 isinstance(prop, hyperdb.Multilink)):
2335 self.filterspec[name] = lookupIds(db, prop,
2336 handleListCGIValue(fv))
2337 else:
2338 if isinstance(fv, type([])):
2339 self.filterspec[name] = [v.value for v in fv]
2340 elif name == 'id':
2341 # special case "id" property
2342 self.filterspec[name] = handleListCGIValue(fv)
2343 else:
2344 self.filterspec[name] = fv.value
2346 # full-text search argument
2347 self.search_text = None
2348 for name in ':search_text @search_text'.split():
2349 if self.form.has_key(name):
2350 self.special_char = name[0]
2351 self.search_text = self.form.getfirst(name)
2353 # pagination - size and start index
2354 # figure batch args
2355 self.pagesize = 50
2356 for name in ':pagesize @pagesize'.split():
2357 if self.form.has_key(name):
2358 self.special_char = name[0]
2359 self.pagesize = int(self.form.getfirst(name))
2361 self.startwith = 0
2362 for name in ':startwith @startwith'.split():
2363 if self.form.has_key(name):
2364 self.special_char = name[0]
2365 self.startwith = int(self.form.getfirst(name))
2367 # dispname
2368 if self.form.has_key('@dispname'):
2369 self.dispname = self.form.getfirst('@dispname')
2370 else:
2371 self.dispname = None
2373 def updateFromURL(self, url):
2374 """ Parse the URL for query args, and update my attributes using the
2375 values.
2376 """
2377 env = {'QUERY_STRING': url}
2378 self.form = cgi.FieldStorage(environ=env)
2380 self._post_init()
2382 def update(self, kwargs):
2383 """ Update my attributes using the keyword args
2384 """
2385 self.__dict__.update(kwargs)
2386 if kwargs.has_key('columns'):
2387 self.show = support.TruthDict(self.columns)
2389 def description(self):
2390 """ Return a description of the request - handle for the page title.
2391 """
2392 s = [self.client.db.config.TRACKER_NAME]
2393 if self.classname:
2394 if self.client.nodeid:
2395 s.append('- %s%s'%(self.classname, self.client.nodeid))
2396 else:
2397 if self.template == 'item':
2398 s.append('- new %s'%self.classname)
2399 elif self.template == 'index':
2400 s.append('- %s index'%self.classname)
2401 else:
2402 s.append('- %s %s'%(self.classname, self.template))
2403 else:
2404 s.append('- home')
2405 return ' '.join(s)
2407 def __str__(self):
2408 d = {}
2409 d.update(self.__dict__)
2410 f = ''
2411 for k in self.form.keys():
2412 f += '\n %r=%r'%(k,handleListCGIValue(self.form[k]))
2413 d['form'] = f
2414 e = ''
2415 for k,v in self.env.items():
2416 e += '\n %r=%r'%(k, v)
2417 d['env'] = e
2418 return """
2419 form: %(form)s
2420 base: %(base)r
2421 classname: %(classname)r
2422 template: %(template)r
2423 columns: %(columns)r
2424 sort: %(sort)r
2425 group: %(group)r
2426 filter: %(filter)r
2427 search_text: %(search_text)r
2428 pagesize: %(pagesize)r
2429 startwith: %(startwith)r
2430 env: %(env)s
2431 """%d
2433 def indexargs_form(self, columns=1, sort=1, group=1, filter=1,
2434 filterspec=1, search_text=1):
2435 """ return the current index args as form elements """
2436 l = []
2437 sc = self.special_char
2438 def add(k, v):
2439 l.append(self.input(type="hidden", name=k, value=v))
2440 if columns and self.columns:
2441 add(sc+'columns', ','.join(self.columns))
2442 if sort:
2443 val = []
2444 for dir, attr in self.sort:
2445 if dir == '-':
2446 val.append('-'+attr)
2447 else:
2448 val.append(attr)
2449 add(sc+'sort', ','.join (val))
2450 if group:
2451 val = []
2452 for dir, attr in self.group:
2453 if dir == '-':
2454 val.append('-'+attr)
2455 else:
2456 val.append(attr)
2457 add(sc+'group', ','.join (val))
2458 if filter and self.filter:
2459 add(sc+'filter', ','.join(self.filter))
2460 if self.classname and filterspec:
2461 cls = self.client.db.getclass(self.classname)
2462 for k,v in self.filterspec.items():
2463 if type(v) == type([]):
2464 if isinstance(cls.get_transitive_prop(k), hyperdb.String):
2465 add(k, ' '.join(v))
2466 else:
2467 add(k, ','.join(v))
2468 else:
2469 add(k, v)
2470 if search_text and self.search_text:
2471 add(sc+'search_text', self.search_text)
2472 add(sc+'pagesize', self.pagesize)
2473 add(sc+'startwith', self.startwith)
2474 return '\n'.join(l)
2476 def indexargs_url(self, url, args):
2477 """ Embed the current index args in a URL
2478 """
2479 q = urllib.quote
2480 sc = self.special_char
2481 l = ['%s=%s'%(k,v) for k,v in args.items()]
2483 # pull out the special values (prefixed by @ or :)
2484 specials = {}
2485 for key in args.keys():
2486 if key[0] in '@:':
2487 specials[key[1:]] = args[key]
2489 # ok, now handle the specials we received in the request
2490 if self.columns and not specials.has_key('columns'):
2491 l.append(sc+'columns=%s'%(','.join(self.columns)))
2492 if self.sort and not specials.has_key('sort'):
2493 val = []
2494 for dir, attr in self.sort:
2495 if dir == '-':
2496 val.append('-'+attr)
2497 else:
2498 val.append(attr)
2499 l.append(sc+'sort=%s'%(','.join(val)))
2500 if self.group and not specials.has_key('group'):
2501 val = []
2502 for dir, attr in self.group:
2503 if dir == '-':
2504 val.append('-'+attr)
2505 else:
2506 val.append(attr)
2507 l.append(sc+'group=%s'%(','.join(val)))
2508 if self.filter and not specials.has_key('filter'):
2509 l.append(sc+'filter=%s'%(','.join(self.filter)))
2510 if self.search_text and not specials.has_key('search_text'):
2511 l.append(sc+'search_text=%s'%q(self.search_text))
2512 if not specials.has_key('pagesize'):
2513 l.append(sc+'pagesize=%s'%self.pagesize)
2514 if not specials.has_key('startwith'):
2515 l.append(sc+'startwith=%s'%self.startwith)
2517 # finally, the remainder of the filter args in the request
2518 if self.classname and self.filterspec:
2519 cls = self.client.db.getclass(self.classname)
2520 for k,v in self.filterspec.items():
2521 if not args.has_key(k):
2522 if type(v) == type([]):
2523 prop = cls.get_transitive_prop(k)
2524 if k != 'id' and isinstance(prop, hyperdb.String):
2525 l.append('%s=%s'%(k, '%20'.join([q(i) for i in v])))
2526 else:
2527 l.append('%s=%s'%(k, ','.join([q(i) for i in v])))
2528 else:
2529 l.append('%s=%s'%(k, q(v)))
2530 return '%s?%s'%(url, '&'.join(l))
2531 indexargs_href = indexargs_url
2533 def base_javascript(self):
2534 return """
2535 <script type="text/javascript">
2536 submitted = false;
2537 function submit_once() {
2538 if (submitted) {
2539 alert("Your request is being processed.\\nPlease be patient.");
2540 event.returnValue = 0; // work-around for IE
2541 return 0;
2542 }
2543 submitted = true;
2544 return 1;
2545 }
2547 function help_window(helpurl, width, height) {
2548 HelpWin = window.open('%s' + helpurl, 'RoundupHelpWindow', 'scrollbars=yes,resizable=yes,toolbar=no,height='+height+',width='+width);
2549 }
2550 </script>
2551 """%self.base
2553 def batch(self):
2554 """ Return a batch object for results from the "current search"
2555 """
2556 filterspec = self.filterspec
2557 sort = self.sort
2558 group = self.group
2560 # get the list of ids we're batching over
2561 klass = self.client.db.getclass(self.classname)
2562 if self.search_text:
2563 matches = self.client.db.indexer.search(
2564 [w.upper().encode("utf-8", "replace") for w in re.findall(
2565 r'(?u)\b\w{2,25}\b',
2566 unicode(self.search_text, "utf-8", "replace")
2567 )], klass)
2568 else:
2569 matches = None
2571 # filter for visibility
2572 check = self._client.db.security.hasPermission
2573 userid = self._client.userid
2574 l = [id for id in klass.filter(matches, filterspec, sort, group)
2575 if check('View', userid, self.classname, itemid=id)]
2577 # return the batch object, using IDs only
2578 return Batch(self.client, l, self.pagesize, self.startwith,
2579 classname=self.classname)
2581 # extend the standard ZTUtils Batch object to remove dependency on
2582 # Acquisition and add a couple of useful methods
2583 class Batch(ZTUtils.Batch):
2584 """ Use me to turn a list of items, or item ids of a given class, into a
2585 series of batches.
2587 ========= ========================================================
2588 Parameter Usage
2589 ========= ========================================================
2590 sequence a list of HTMLItems or item ids
2591 classname if sequence is a list of ids, this is the class of item
2592 size how big to make the sequence.
2593 start where to start (0-indexed) in the sequence.
2594 end where to end (0-indexed) in the sequence.
2595 orphan if the next batch would contain less items than this
2596 value, then it is combined with this batch
2597 overlap the number of items shared between adjacent batches
2598 ========= ========================================================
2600 Attributes: Note that the "start" attribute, unlike the
2601 argument, is a 1-based index (I know, lame). "first" is the
2602 0-based index. "length" is the actual number of elements in
2603 the batch.
2605 "sequence_length" is the length of the original, unbatched, sequence.
2606 """
2607 def __init__(self, client, sequence, size, start, end=0, orphan=0,
2608 overlap=0, classname=None):
2609 self.client = client
2610 self.last_index = self.last_item = None
2611 self.current_item = None
2612 self.classname = classname
2613 self.sequence_length = len(sequence)
2614 ZTUtils.Batch.__init__(self, sequence, size, start, end, orphan,
2615 overlap)
2617 # overwrite so we can late-instantiate the HTMLItem instance
2618 def __getitem__(self, index):
2619 if index < 0:
2620 if index + self.end < self.first: raise IndexError, index
2621 return self._sequence[index + self.end]
2623 if index >= self.length:
2624 raise IndexError, index
2626 # move the last_item along - but only if the fetched index changes
2627 # (for some reason, index 0 is fetched twice)
2628 if index != self.last_index:
2629 self.last_item = self.current_item
2630 self.last_index = index
2632 item = self._sequence[index + self.first]
2633 if self.classname:
2634 # map the item ids to instances
2635 item = HTMLItem(self.client, self.classname, item)
2636 self.current_item = item
2637 return item
2639 def propchanged(self, *properties):
2640 """ Detect if one of the properties marked as being a group
2641 property changed in the last iteration fetch
2642 """
2643 # we poke directly at the _value here since MissingValue can screw
2644 # us up and cause Nones to compare strangely
2645 if self.last_item is None:
2646 return 1
2647 for property in properties:
2648 if property == 'id' or isinstance (self.last_item[property], list):
2649 if (str(self.last_item[property]) !=
2650 str(self.current_item[property])):
2651 return 1
2652 else:
2653 if (self.last_item[property]._value !=
2654 self.current_item[property]._value):
2655 return 1
2656 return 0
2658 # override these 'cos we don't have access to acquisition
2659 def previous(self):
2660 if self.start == 1:
2661 return None
2662 return Batch(self.client, self._sequence, self._size,
2663 self.first - self._size + self.overlap, 0, self.orphan,
2664 self.overlap)
2666 def next(self):
2667 try:
2668 self._sequence[self.end]
2669 except IndexError:
2670 return None
2671 return Batch(self.client, self._sequence, self._size,
2672 self.end - self.overlap, 0, self.orphan, self.overlap)
2674 class TemplatingUtils:
2675 """ Utilities for templating
2676 """
2677 def __init__(self, client):
2678 self.client = client
2679 def Batch(self, sequence, size, start, end=0, orphan=0, overlap=0):
2680 return Batch(self.client, sequence, size, start, end, orphan,
2681 overlap)
2683 def url_quote(self, url):
2684 """URL-quote the supplied text."""
2685 return urllib.quote(url)
2687 def html_quote(self, html):
2688 """HTML-quote the supplied text."""
2689 return cgi.escape(html)
2691 def __getattr__(self, name):
2692 """Try the tracker's templating_utils."""
2693 if not hasattr(self.client.instance, 'templating_utils'):
2694 # backwards-compatibility
2695 raise AttributeError, name
2696 if not self.client.instance.templating_utils.has_key(name):
2697 raise AttributeError, name
2698 return self.client.instance.templating_utils[name]
2700 def html_calendar(self, request):
2701 """Generate a HTML calendar.
2703 `request` the roundup.request object
2704 - @template : name of the template
2705 - form : name of the form to store back the date
2706 - property : name of the property of the form to store
2707 back the date
2708 - date : current date
2709 - display : when browsing, specifies year and month
2711 html will simply be a table.
2712 """
2713 date_str = request.form.getfirst("date", ".")
2714 display = request.form.getfirst("display", date_str)
2715 template = request.form.getfirst("@template", "calendar")
2716 form = request.form.getfirst("form")
2717 property = request.form.getfirst("property")
2718 curr_date = date.Date(date_str) # to highlight
2719 display = date.Date(display) # to show
2720 day = display.day
2722 # for navigation
2723 date_prev_month = display + date.Interval("-1m")
2724 date_next_month = display + date.Interval("+1m")
2725 date_prev_year = display + date.Interval("-1y")
2726 date_next_year = display + date.Interval("+1y")
2728 res = []
2730 base_link = "%s?@template=%s&property=%s&form=%s&date=%s" % \
2731 (request.classname, template, property, form, curr_date)
2733 # navigation
2734 # month
2735 res.append('<table class="calendar"><tr><td>')
2736 res.append(' <table width="100%" class="calendar_nav"><tr>')
2737 link = "&display=%s"%date_prev_month
2738 res.append(' <td><a href="%s&display=%s"><</a></td>'%(base_link,
2739 date_prev_month))
2740 res.append(' <td>%s</td>'%calendar.month_name[display.month])
2741 res.append(' <td><a href="%s&display=%s">></a></td>'%(base_link,
2742 date_next_month))
2743 # spacer
2744 res.append(' <td width="100%"></td>')
2745 # year
2746 res.append(' <td><a href="%s&display=%s"><</a></td>'%(base_link,
2747 date_prev_year))
2748 res.append(' <td>%s</td>'%display.year)
2749 res.append(' <td><a href="%s&display=%s">></a></td>'%(base_link,
2750 date_next_year))
2751 res.append(' </tr></table>')
2752 res.append(' </td></tr>')
2754 # the calendar
2755 res.append(' <tr><td><table class="calendar_display">')
2756 res.append(' <tr class="weekdays">')
2757 for day in calendar.weekheader(3).split():
2758 res.append(' <td>%s</td>'%day)
2759 res.append(' </tr>')
2760 for week in calendar.monthcalendar(display.year, display.month):
2761 res.append(' <tr>')
2762 for day in week:
2763 link = "javascript:form[field].value = '%d-%02d-%02d'; " \
2764 "window.close ();"%(display.year, display.month, day)
2765 if (day == curr_date.day and display.month == curr_date.month
2766 and display.year == curr_date.year):
2767 # highlight
2768 style = "today"
2769 else :
2770 style = ""
2771 if day:
2772 res.append(' <td class="%s"><a href="%s">%s</a></td>'%(
2773 style, link, day))
2774 else :
2775 res.append(' <td></td>')
2776 res.append(' </tr>')
2777 res.append('</table></td></tr></table>')
2778 return "\n".join(res)
2780 class MissingValue:
2781 def __init__(self, description, **kwargs):
2782 self.__description = description
2783 for key, value in kwargs.items():
2784 self.__dict__[key] = value
2786 def __call__(self, *args, **kwargs): return MissingValue(self.__description)
2787 def __getattr__(self, name):
2788 # This allows assignments which assume all intermediate steps are Null
2789 # objects if they don't exist yet.
2790 #
2791 # For example (with just 'client' defined):
2792 #
2793 # client.db.config.TRACKER_WEB = 'BASE/'
2794 self.__dict__[name] = MissingValue(self.__description)
2795 return getattr(self, name)
2797 def __getitem__(self, key): return self
2798 def __nonzero__(self): return 0
2799 def __str__(self): return '[%s]'%self.__description
2800 def __repr__(self): return '<MissingValue 0x%x "%s">'%(id(self),
2801 self.__description)
2802 def gettext(self, str): return str
2803 _ = gettext
2805 # vim: set et sts=4 sw=4 :