f91d65a6721f441aac459876629f908e9402123d
1 """Implements the API used in the HTML templating for the web interface.
2 """
3 __docformat__ = 'restructuredtext'
5 from __future__ import nested_scopes
7 import sys, cgi, urllib, os, re, os.path, time, errno, mimetypes
9 from roundup import hyperdb, date, rcsv
10 from roundup.i18n import _
12 try:
13 import cPickle as pickle
14 except ImportError:
15 import pickle
16 try:
17 import cStringIO as StringIO
18 except ImportError:
19 import StringIO
20 try:
21 import StructuredText
22 except ImportError:
23 StructuredText = None
25 # bring in the templating support
26 from roundup.cgi.PageTemplates import PageTemplate
27 from roundup.cgi.PageTemplates.Expressions import getEngine
28 from roundup.cgi.TAL.TALInterpreter import TALInterpreter
29 from roundup.cgi import ZTUtils
31 class NoTemplate(Exception):
32 pass
34 class Unauthorised(Exception):
35 def __init__(self, action, klass):
36 self.action = action
37 self.klass = klass
38 def __str__(self):
39 return 'You are not allowed to %s items of class %s'%(self.action,
40 self.klass)
42 def find_template(dir, name, extension):
43 ''' Find a template in the nominated dir
44 '''
45 # find the source
46 if extension:
47 filename = '%s.%s'%(name, extension)
48 else:
49 filename = name
51 # try old-style
52 src = os.path.join(dir, filename)
53 if os.path.exists(src):
54 return (src, filename)
56 # try with a .html extension (new-style)
57 filename = filename + '.html'
58 src = os.path.join(dir, filename)
59 if os.path.exists(src):
60 return (src, filename)
62 # no extension == no generic template is possible
63 if not extension:
64 raise NoTemplate, 'Template file "%s" doesn\'t exist'%name
66 # try for a _generic template
67 generic = '_generic.%s'%extension
68 src = os.path.join(dir, generic)
69 if os.path.exists(src):
70 return (src, generic)
72 # finally, try _generic.html
73 generic = generic + '.html'
74 src = os.path.join(dir, generic)
75 if os.path.exists(src):
76 return (src, generic)
78 raise NoTemplate, 'No template file exists for templating "%s" '\
79 'with template "%s" (neither "%s" nor "%s")'%(name, extension,
80 filename, generic)
82 class Templates:
83 templates = {}
85 def __init__(self, dir):
86 self.dir = dir
88 def precompileTemplates(self):
89 ''' Go through a directory and precompile all the templates therein
90 '''
91 for filename in os.listdir(self.dir):
92 if os.path.isdir(filename): continue
93 if '.' in filename:
94 name, extension = filename.split('.')
95 self.get(name, extension)
96 else:
97 self.get(filename, None)
99 def get(self, name, extension=None):
100 ''' Interface to get a template, possibly loading a compiled template.
102 "name" and "extension" indicate the template we're after, which in
103 most cases will be "name.extension". If "extension" is None, then
104 we look for a template just called "name" with no extension.
106 If the file "name.extension" doesn't exist, we look for
107 "_generic.extension" as a fallback.
108 '''
109 # default the name to "home"
110 if name is None:
111 name = 'home'
112 elif extension is None and '.' in name:
113 # split name
114 name, extension = name.split('.')
116 # find the source
117 src, filename = find_template(self.dir, name, extension)
119 # has it changed?
120 try:
121 stime = os.stat(src)[os.path.stat.ST_MTIME]
122 except os.error, error:
123 if error.errno != errno.ENOENT:
124 raise
126 if self.templates.has_key(src) and \
127 stime < self.templates[src].mtime:
128 # compiled template is up to date
129 return self.templates[src]
131 # compile the template
132 self.templates[src] = pt = RoundupPageTemplate()
133 # use pt_edit so we can pass the content_type guess too
134 content_type = mimetypes.guess_type(filename)[0] or 'text/html'
135 pt.pt_edit(open(src).read(), content_type)
136 pt.id = filename
137 pt.mtime = time.time()
138 return pt
140 def __getitem__(self, name):
141 name, extension = os.path.splitext(name)
142 if extension:
143 extension = extension[1:]
144 try:
145 return self.get(name, extension)
146 except NoTemplate, message:
147 raise KeyError, message
149 class RoundupPageTemplate(PageTemplate.PageTemplate):
150 '''A Roundup-specific PageTemplate.
152 Interrogate the client to set up the various template variables to
153 be available:
155 *context*
156 this is one of three things:
158 1. None - we're viewing a "home" page
159 2. The current class of item being displayed. This is an HTMLClass
160 instance.
161 3. The current item from the database, if we're viewing a specific
162 item, as an HTMLItem instance.
163 *request*
164 Includes information about the current request, including:
166 - the url
167 - the current index information (``filterspec``, ``filter`` args,
168 ``properties``, etc) parsed out of the form.
169 - methods for easy filterspec link generation
170 - *user*, the current user node as an HTMLItem instance
171 - *form*, the current CGI form information as a FieldStorage
172 *config*
173 The current tracker config.
174 *db*
175 The current database, used to access arbitrary database items.
176 *utils*
177 This is a special class that has its base in the TemplatingUtils
178 class in this file. If the tracker interfaces module defines a
179 TemplatingUtils class then it is mixed in, overriding the methods
180 in the base class.
181 '''
182 def getContext(self, client, classname, request):
183 # construct the TemplatingUtils class
184 utils = TemplatingUtils
185 if hasattr(client.instance.interfaces, 'TemplatingUtils'):
186 class utils(client.instance.interfaces.TemplatingUtils, utils):
187 pass
189 c = {
190 'options': {},
191 'nothing': None,
192 'request': request,
193 'db': HTMLDatabase(client),
194 'config': client.instance.config,
195 'tracker': client.instance,
196 'utils': utils(client),
197 'templates': Templates(client.instance.config.TEMPLATES),
198 }
199 # add in the item if there is one
200 if client.nodeid:
201 if classname == 'user':
202 c['context'] = HTMLUser(client, classname, client.nodeid,
203 anonymous=1)
204 else:
205 c['context'] = HTMLItem(client, classname, client.nodeid,
206 anonymous=1)
207 elif client.db.classes.has_key(classname):
208 c['context'] = HTMLClass(client, classname, anonymous=1)
209 return c
211 def render(self, client, classname, request, **options):
212 """Render this Page Template"""
214 if not self._v_cooked:
215 self._cook()
217 __traceback_supplement__ = (PageTemplate.PageTemplateTracebackSupplement, self)
219 if self._v_errors:
220 raise PageTemplate.PTRuntimeError, \
221 'Page Template %s has errors.'%self.id
223 # figure the context
224 classname = classname or client.classname
225 request = request or HTMLRequest(client)
226 c = self.getContext(client, classname, request)
227 c.update({'options': options})
229 # and go
230 output = StringIO.StringIO()
231 TALInterpreter(self._v_program, self.macros,
232 getEngine().getContext(c), output, tal=1, strictinsert=0)()
233 return output.getvalue()
235 def __repr__(self):
236 return '<Roundup PageTemplate %r>'%self.id
238 class HTMLDatabase:
239 ''' Return HTMLClasses for valid class fetches
240 '''
241 def __init__(self, client):
242 self._client = client
243 self._db = client.db
245 # we want config to be exposed
246 self.config = client.db.config
248 def __getitem__(self, item, desre=re.compile(r'(?P<cl>\w+)(?P<id>[-\d]+)')):
249 # check to see if we're actually accessing an item
250 m = desre.match(item)
251 if m:
252 self._client.db.getclass(m.group('cl'))
253 return HTMLItem(self._client, m.group('cl'), m.group('id'))
254 else:
255 self._client.db.getclass(item)
256 return HTMLClass(self._client, item)
258 def __getattr__(self, attr):
259 try:
260 return self[attr]
261 except KeyError:
262 raise AttributeError, attr
264 def classes(self):
265 l = self._client.db.classes.keys()
266 l.sort()
267 return [HTMLClass(self._client, cn) for cn in l]
269 def lookupIds(db, prop, ids, num_re=re.compile('-?\d+')):
270 cl = db.getclass(prop.classname)
271 l = []
272 for entry in ids:
273 if num_re.match(entry):
274 l.append(entry)
275 else:
276 try:
277 l.append(cl.lookup(entry))
278 except KeyError:
279 # ignore invalid keys
280 pass
281 return l
283 class HTMLPermissions:
284 ''' Helpers that provide answers to commonly asked Permission questions.
285 '''
286 def is_edit_ok(self):
287 ''' Is the user allowed to Edit the current class?
288 '''
289 return self._db.security.hasPermission('Edit', self._client.userid,
290 self._classname)
292 def is_view_ok(self):
293 ''' Is the user allowed to View the current class?
294 '''
295 return self._db.security.hasPermission('View', self._client.userid,
296 self._classname)
298 def is_only_view_ok(self):
299 ''' Is the user only allowed to View (ie. not Edit) the current class?
300 '''
301 return self.is_view_ok() and not self.is_edit_ok()
303 def view_check(self):
304 ''' Raise the Unauthorised exception if the user's not permitted to
305 view this class.
306 '''
307 if not self.is_view_ok():
308 raise Unauthorised("view", self._classname)
310 def edit_check(self):
311 ''' Raise the Unauthorised exception if the user's not permitted to
312 edit this class.
313 '''
314 if not self.is_edit_ok():
315 raise Unauthorised("edit", self._classname)
317 def input_html4(**attrs):
318 """Generate an 'input' (html4) element with given attributes"""
319 return '<input %s>'%' '.join(['%s="%s"'%item for item in attrs.items()])
321 def input_xhtml(**attrs):
322 """Generate an 'input' (xhtml) element with given attributes"""
323 return '<input %s/>'%' '.join(['%s="%s"'%item for item in attrs.items()])
325 class HTMLInputMixin:
326 ''' requires a _client property '''
327 def __init__(self):
328 html_version = 'html4'
329 if hasattr(self._client.instance.config, 'HTML_VERSION'):
330 html_version = self._client.instance.config.HTML_VERSION
331 if html_version == 'xhtml':
332 self.input = input_xhtml
333 else:
334 self.input = input_html4
336 class HTMLClass(HTMLInputMixin, HTMLPermissions):
337 ''' Accesses through a class (either through *class* or *db.<classname>*)
338 '''
339 def __init__(self, client, classname, anonymous=0):
340 self._client = client
341 self._db = client.db
342 self._anonymous = anonymous
344 # we want classname to be exposed, but _classname gives a
345 # consistent API for extending Class/Item
346 self._classname = self.classname = classname
347 self._klass = self._db.getclass(self.classname)
348 self._props = self._klass.getprops()
350 HTMLInputMixin.__init__(self)
352 def __repr__(self):
353 return '<HTMLClass(0x%x) %s>'%(id(self), self.classname)
355 def __getitem__(self, item):
356 ''' return an HTMLProperty instance
357 '''
358 #print 'HTMLClass.getitem', (self, item)
360 # we don't exist
361 if item == 'id':
362 return None
364 # get the property
365 prop = self._props[item]
367 # look up the correct HTMLProperty class
368 form = self._client.form
369 for klass, htmlklass in propclasses:
370 if not isinstance(prop, klass):
371 continue
372 if form.has_key(item):
373 if isinstance(prop, hyperdb.Multilink):
374 value = lookupIds(self._db, prop,
375 handleListCGIValue(form[item]))
376 elif isinstance(prop, hyperdb.Link):
377 value = form[item].value.strip()
378 if value:
379 value = lookupIds(self._db, prop, [value])[0]
380 else:
381 value = None
382 else:
383 value = form[item].value.strip() or None
384 else:
385 if isinstance(prop, hyperdb.Multilink):
386 value = []
387 else:
388 value = None
389 return htmlklass(self._client, self._classname, '', prop, item,
390 value, self._anonymous)
392 # no good
393 raise KeyError, item
395 def __getattr__(self, attr):
396 ''' convenience access '''
397 try:
398 return self[attr]
399 except KeyError:
400 raise AttributeError, attr
402 def designator(self):
403 ''' Return this class' designator (classname) '''
404 return self._classname
406 def getItem(self, itemid, num_re=re.compile('-?\d+')):
407 ''' Get an item of this class by its item id.
408 '''
409 # make sure we're looking at an itemid
410 if not isinstance(itemid, type(1)) and not num_re.match(itemid):
411 itemid = self._klass.lookup(itemid)
413 if self.classname == 'user':
414 klass = HTMLUser
415 else:
416 klass = HTMLItem
418 return klass(self._client, self.classname, itemid)
420 def properties(self, sort=1):
421 ''' Return HTMLProperty for all of this class' properties.
422 '''
423 l = []
424 for name, prop in self._props.items():
425 for klass, htmlklass in propclasses:
426 if isinstance(prop, hyperdb.Multilink):
427 value = []
428 else:
429 value = None
430 if isinstance(prop, klass):
431 l.append(htmlklass(self._client, self._classname, '',
432 prop, name, value, self._anonymous))
433 if sort:
434 l.sort(lambda a,b:cmp(a._name, b._name))
435 return l
437 def list(self, sort_on=None):
438 ''' List all items in this class.
439 '''
440 if self.classname == 'user':
441 klass = HTMLUser
442 else:
443 klass = HTMLItem
445 # get the list and sort it nicely
446 l = self._klass.list()
447 sortfunc = make_sort_function(self._db, self.classname, sort_on)
448 l.sort(sortfunc)
450 l = [klass(self._client, self.classname, x) for x in l]
451 return l
453 def csv(self):
454 ''' Return the items of this class as a chunk of CSV text.
455 '''
456 if rcsv.error:
457 return rcsv.error
459 props = self.propnames()
460 s = StringIO.StringIO()
461 writer = rcsv.writer(s, rcsv.comma_separated)
462 writer.writerow(props)
463 for nodeid in self._klass.list():
464 l = []
465 for name in props:
466 value = self._klass.get(nodeid, name)
467 if value is None:
468 l.append('')
469 elif isinstance(value, type([])):
470 l.append(':'.join(map(str, value)))
471 else:
472 l.append(str(self._klass.get(nodeid, name)))
473 writer.writerow(l)
474 return s.getvalue()
476 def propnames(self):
477 ''' Return the list of the names of the properties of this class.
478 '''
479 idlessprops = self._klass.getprops(protected=0).keys()
480 idlessprops.sort()
481 return ['id'] + idlessprops
483 def filter(self, request=None, filterspec={}, sort=(None,None),
484 group=(None,None)):
485 ''' Return a list of items from this class, filtered and sorted
486 by the current requested filterspec/filter/sort/group args
488 "request" takes precedence over the other three arguments.
489 '''
490 if request is not None:
491 filterspec = request.filterspec
492 sort = request.sort
493 group = request.group
494 if self.classname == 'user':
495 klass = HTMLUser
496 else:
497 klass = HTMLItem
498 l = [klass(self._client, self.classname, x)
499 for x in self._klass.filter(None, filterspec, sort, group)]
500 return l
502 def classhelp(self, properties=None, label='(list)', width='500',
503 height='400', property=''):
504 ''' Pop up a javascript window with class help
506 This generates a link to a popup window which displays the
507 properties indicated by "properties" of the class named by
508 "classname". The "properties" should be a comma-separated list
509 (eg. 'id,name,description'). Properties defaults to all the
510 properties of a class (excluding id, creator, created and
511 activity).
513 You may optionally override the label displayed, the width and
514 height. The popup window will be resizable and scrollable.
516 If the "property" arg is given, it's passed through to the
517 javascript help_window function.
518 '''
519 if properties is None:
520 properties = self._klass.getprops(protected=0).keys()
521 properties.sort()
522 properties = ','.join(properties)
523 if property:
524 property = '&property=%s'%property
525 return '<a class="classhelp" href="javascript:help_window(\'%s?'\
526 '@startwith=0&@template=help&properties=%s%s\', \'%s\', \
527 \'%s\')">%s</a>'%(self.classname, properties, property, width,
528 height, label)
530 def submit(self, label="Submit New Entry"):
531 ''' Generate a submit button (and action hidden element)
532 '''
533 self.view_check()
534 if self.is_edit_ok():
535 return self.input(type="hidden",name="@action",value="new") + \
536 '\n' + self.input(type="submit",name="submit",value=label)
537 return ''
539 def history(self):
540 self.view_check()
541 return 'New node - no history'
543 def renderWith(self, name, **kwargs):
544 ''' Render this class with the given template.
545 '''
546 # create a new request and override the specified args
547 req = HTMLRequest(self._client)
548 req.classname = self.classname
549 req.update(kwargs)
551 # new template, using the specified classname and request
552 pt = Templates(self._db.config.TEMPLATES).get(self.classname, name)
554 # use our fabricated request
555 args = {
556 'ok_message': self._client.ok_message,
557 'error_message': self._client.error_message
558 }
559 return pt.render(self._client, self.classname, req, **args)
561 class HTMLItem(HTMLInputMixin, HTMLPermissions):
562 ''' Accesses through an *item*
563 '''
564 def __init__(self, client, classname, nodeid, anonymous=0):
565 self._client = client
566 self._db = client.db
567 self._classname = classname
568 self._nodeid = nodeid
569 self._klass = self._db.getclass(classname)
570 self._props = self._klass.getprops()
572 # do we prefix the form items with the item's identification?
573 self._anonymous = anonymous
575 HTMLInputMixin.__init__(self)
577 def __repr__(self):
578 return '<HTMLItem(0x%x) %s %s>'%(id(self), self._classname,
579 self._nodeid)
581 def __getitem__(self, item):
582 ''' return an HTMLProperty instance
583 '''
584 #print 'HTMLItem.getitem', (self, item)
585 if item == 'id':
586 return self._nodeid
588 # get the property
589 prop = self._props[item]
591 # get the value, handling missing values
592 value = None
593 if int(self._nodeid) > 0:
594 value = self._klass.get(self._nodeid, item, None)
595 if value is None:
596 if isinstance(self._props[item], hyperdb.Multilink):
597 value = []
599 # look up the correct HTMLProperty class
600 for klass, htmlklass in propclasses:
601 if isinstance(prop, klass):
602 return htmlklass(self._client, self._classname,
603 self._nodeid, prop, item, value, self._anonymous)
605 raise KeyError, item
607 def __getattr__(self, attr):
608 ''' convenience access to properties '''
609 try:
610 return self[attr]
611 except KeyError:
612 raise AttributeError, attr
614 def designator(self):
615 """Return this item's designator (classname + id)."""
616 return '%s%s'%(self._classname, self._nodeid)
618 def submit(self, label="Submit Changes"):
619 """Generate a submit button.
621 Also sneak in the lastactivity and action hidden elements.
622 """
623 return self.input(type="hidden", name="@lastactivity", value=date.Date('.')) + '\n' + \
624 self.input(type="hidden", name="@action", value="edit") + '\n' + \
625 self.input(type="submit", name="submit", value=label)
627 def journal(self, direction='descending'):
628 ''' Return a list of HTMLJournalEntry instances.
629 '''
630 # XXX do this
631 return []
633 def history(self, direction='descending', dre=re.compile('\d+')):
634 self.view_check()
636 l = ['<table class="history">'
637 '<tr><th colspan="4" class="header">',
638 _('History'),
639 '</th></tr><tr>',
640 _('<th>Date</th>'),
641 _('<th>User</th>'),
642 _('<th>Action</th>'),
643 _('<th>Args</th>'),
644 '</tr>']
645 current = {}
646 comments = {}
647 history = self._klass.history(self._nodeid)
648 history.sort()
649 timezone = self._db.getUserTimezone()
650 if direction == 'descending':
651 history.reverse()
652 for prop_n in self._props.keys():
653 prop = self[prop_n]
654 if isinstance(prop, HTMLProperty):
655 current[prop_n] = prop.plain()
656 # make link if hrefable
657 if (self._props.has_key(prop_n) and
658 isinstance(self._props[prop_n], hyperdb.Link)):
659 classname = self._props[prop_n].classname
660 try:
661 template = find_template(self._db.config.TEMPLATES,
662 classname, 'item')
663 if template[1].startswith('_generic'):
664 raise NoTemplate, 'not really...'
665 except NoTemplate:
666 pass
667 else:
668 id = self._klass.get(self._nodeid, prop_n, None)
669 current[prop_n] = '<a href="%s%s">%s</a>'%(
670 classname, id, current[prop_n])
672 for id, evt_date, user, action, args in history:
673 date_s = str(evt_date.local(timezone)).replace("."," ")
674 arg_s = ''
675 if action == 'link' and type(args) == type(()):
676 if len(args) == 3:
677 linkcl, linkid, key = args
678 arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
679 linkcl, linkid, key)
680 else:
681 arg_s = str(args)
683 elif action == 'unlink' and type(args) == type(()):
684 if len(args) == 3:
685 linkcl, linkid, key = args
686 arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
687 linkcl, linkid, key)
688 else:
689 arg_s = str(args)
691 elif type(args) == type({}):
692 cell = []
693 for k in args.keys():
694 # try to get the relevant property and treat it
695 # specially
696 try:
697 prop = self._props[k]
698 except KeyError:
699 prop = None
700 if prop is None:
701 # property no longer exists
702 comments['no_exist'] = _('''<em>The indicated property
703 no longer exists</em>''')
704 cell.append('<em>%s: %s</em>\n'%(k, str(args[k])))
705 continue
707 if args[k] and (isinstance(prop, hyperdb.Multilink) or
708 isinstance(prop, hyperdb.Link)):
709 # figure what the link class is
710 classname = prop.classname
711 try:
712 linkcl = self._db.getclass(classname)
713 except KeyError:
714 labelprop = None
715 comments[classname] = _('''The linked class
716 %(classname)s no longer exists''')%locals()
717 labelprop = linkcl.labelprop(1)
718 try:
719 template = find_template(self._db.config.TEMPLATES,
720 classname, 'item')
721 if template[1].startswith('_generic'):
722 raise NoTemplate, 'not really...'
723 hrefable = 1
724 except NoTemplate:
725 hrefable = 0
727 if isinstance(prop, hyperdb.Multilink) and args[k]:
728 ml = []
729 for linkid in args[k]:
730 if isinstance(linkid, type(())):
731 sublabel = linkid[0] + ' '
732 linkids = linkid[1]
733 else:
734 sublabel = ''
735 linkids = [linkid]
736 subml = []
737 for linkid in linkids:
738 label = classname + linkid
739 # if we have a label property, try to use it
740 # TODO: test for node existence even when
741 # there's no labelprop!
742 try:
743 if labelprop is not None and \
744 labelprop != 'id':
745 label = linkcl.get(linkid, labelprop)
746 except IndexError:
747 comments['no_link'] = _('''<strike>The
748 linked node no longer
749 exists</strike>''')
750 subml.append('<strike>%s</strike>'%label)
751 else:
752 if hrefable:
753 subml.append('<a href="%s%s">%s</a>'%(
754 classname, linkid, label))
755 else:
756 subml.append(label)
757 ml.append(sublabel + ', '.join(subml))
758 cell.append('%s:\n %s'%(k, ', '.join(ml)))
759 elif isinstance(prop, hyperdb.Link) and args[k]:
760 label = classname + args[k]
761 # if we have a label property, try to use it
762 # TODO: test for node existence even when
763 # there's no labelprop!
764 if labelprop is not None and labelprop != 'id':
765 try:
766 label = linkcl.get(args[k], labelprop)
767 except IndexError:
768 comments['no_link'] = _('''<strike>The
769 linked node no longer
770 exists</strike>''')
771 cell.append(' <strike>%s</strike>,\n'%label)
772 # "flag" this is done .... euwww
773 label = None
774 if label is not None:
775 if hrefable:
776 old = '<a href="%s%s">%s</a>'%(classname, args[k], label)
777 else:
778 old = label;
779 cell.append('%s: %s' % (k,old))
780 if current.has_key(k):
781 cell[-1] += ' -> %s'%current[k]
782 current[k] = old
784 elif isinstance(prop, hyperdb.Date) and args[k]:
785 d = date.Date(args[k]).local(timezone)
786 cell.append('%s: %s'%(k, str(d)))
787 if current.has_key(k):
788 cell[-1] += ' -> %s' % current[k]
789 current[k] = str(d)
791 elif isinstance(prop, hyperdb.Interval) and args[k]:
792 d = date.Interval(args[k])
793 cell.append('%s: %s'%(k, str(d)))
794 if current.has_key(k):
795 cell[-1] += ' -> %s'%current[k]
796 current[k] = str(d)
798 elif isinstance(prop, hyperdb.String) and args[k]:
799 cell.append('%s: %s'%(k, cgi.escape(args[k])))
800 if current.has_key(k):
801 cell[-1] += ' -> %s'%current[k]
802 current[k] = cgi.escape(args[k])
804 elif not args[k]:
805 if current.has_key(k):
806 cell.append('%s: %s'%(k, current[k]))
807 current[k] = '(no value)'
808 else:
809 cell.append('%s: (no value)'%k)
811 else:
812 cell.append('%s: %s'%(k, str(args[k])))
813 if current.has_key(k):
814 cell[-1] += ' -> %s'%current[k]
815 current[k] = str(args[k])
817 arg_s = '<br />'.join(cell)
818 else:
819 # unkown event!!
820 comments['unknown'] = _('''<strong><em>This event is not
821 handled by the history display!</em></strong>''')
822 arg_s = '<strong><em>' + str(args) + '</em></strong>'
823 date_s = date_s.replace(' ', ' ')
824 # if the user's an itemid, figure the username (older journals
825 # have the username)
826 if dre.match(user):
827 user = self._db.user.get(user, 'username')
828 l.append('<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>'%(
829 date_s, user, action, arg_s))
830 if comments:
831 l.append(_('<tr><td colspan=4><strong>Note:</strong></td></tr>'))
832 for entry in comments.values():
833 l.append('<tr><td colspan=4>%s</td></tr>'%entry)
834 l.append('</table>')
835 return '\n'.join(l)
837 def renderQueryForm(self):
838 ''' Render this item, which is a query, as a search form.
839 '''
840 # create a new request and override the specified args
841 req = HTMLRequest(self._client)
842 req.classname = self._klass.get(self._nodeid, 'klass')
843 name = self._klass.get(self._nodeid, 'name')
844 req.updateFromURL(self._klass.get(self._nodeid, 'url') +
845 '&@queryname=%s'%urllib.quote(name))
847 # new template, using the specified classname and request
848 pt = Templates(self._db.config.TEMPLATES).get(req.classname, 'search')
850 # use our fabricated request
851 return pt.render(self._client, req.classname, req)
853 class HTMLUser(HTMLItem):
854 ''' Accesses through the *user* (a special case of item)
855 '''
856 def __init__(self, client, classname, nodeid, anonymous=0):
857 HTMLItem.__init__(self, client, 'user', nodeid, anonymous)
858 self._default_classname = client.classname
860 # used for security checks
861 self._security = client.db.security
863 _marker = []
864 def hasPermission(self, permission, classname=_marker):
865 ''' Determine if the user has the Permission.
867 The class being tested defaults to the template's class, but may
868 be overidden for this test by suppling an alternate classname.
869 '''
870 if classname is self._marker:
871 classname = self._default_classname
872 return self._security.hasPermission(permission, self._nodeid, classname)
874 def is_edit_ok(self):
875 ''' Is the user allowed to Edit the current class?
876 Also check whether this is the current user's info.
877 '''
878 return self._db.security.hasPermission('Edit', self._client.userid,
879 self._classname) or (self._nodeid == self._client.userid and
880 self._db.user.get(self._client.userid, 'username') != 'anonymous')
882 def is_view_ok(self):
883 ''' Is the user allowed to View the current class?
884 Also check whether this is the current user's info.
885 '''
886 return self._db.security.hasPermission('View', self._client.userid,
887 self._classname) or (self._nodeid == self._client.userid and
888 self._db.user.get(self._client.userid, 'username') != 'anonymous')
890 class HTMLProperty(HTMLInputMixin, HTMLPermissions):
891 ''' String, Number, Date, Interval HTMLProperty
893 Has useful attributes:
895 _name the name of the property
896 _value the value of the property if any
898 A wrapper object which may be stringified for the plain() behaviour.
899 '''
900 def __init__(self, client, classname, nodeid, prop, name, value,
901 anonymous=0):
902 self._client = client
903 self._db = client.db
904 self._classname = classname
905 self._nodeid = nodeid
906 self._prop = prop
907 self._value = value
908 self._anonymous = anonymous
909 self._name = name
910 if not anonymous:
911 self._formname = '%s%s@%s'%(classname, nodeid, name)
912 else:
913 self._formname = name
915 HTMLInputMixin.__init__(self)
917 def __repr__(self):
918 return '<HTMLProperty(0x%x) %s %r %r>'%(id(self), self._formname,
919 self._prop, self._value)
920 def __str__(self):
921 return self.plain()
922 def __cmp__(self, other):
923 if isinstance(other, HTMLProperty):
924 return cmp(self._value, other._value)
925 return cmp(self._value, other)
927 def is_edit_ok(self):
928 ''' Is the user allowed to Edit the current class?
929 '''
930 thing = HTMLDatabase(self._client)[self._classname]
931 if self._nodeid:
932 # this is a special-case for the User class where permission's
933 # on a per-item basis :(
934 thing = thing.getItem(self._nodeid)
935 return thing.is_edit_ok()
937 def is_view_ok(self):
938 ''' Is the user allowed to View the current class?
939 '''
940 thing = HTMLDatabase(self._client)[self._classname]
941 if self._nodeid:
942 # this is a special-case for the User class where permission's
943 # on a per-item basis :(
944 thing = thing.getItem(self._nodeid)
945 return thing.is_view_ok()
947 class StringHTMLProperty(HTMLProperty):
948 hyper_re = re.compile(r'((?P<url>\w{3,6}://\S+)|'
949 r'(?P<email>[-+=%/\w\.]+@[\w\.\-]+)|'
950 r'(?P<item>(?P<class>[a-z_]+)(?P<id>\d+)))')
951 def _hyper_repl(self, match):
952 if match.group('url'):
953 s = match.group('url')
954 return '<a href="%s">%s</a>'%(s, s)
955 elif match.group('email'):
956 s = match.group('email')
957 return '<a href="mailto:%s">%s</a>'%(s, s)
958 else:
959 s = match.group('item')
960 s1 = match.group('class')
961 s2 = match.group('id')
962 try:
963 # make sure s1 is a valid tracker classname
964 cl = self._db.getclass(s1)
965 if not cl.hasnode(s2):
966 raise KeyError, 'oops'
967 return '<a href="%s">%s%s</a>'%(s, s1, s2)
968 except KeyError:
969 return '%s%s'%(s1, s2)
971 def hyperlinked(self):
972 ''' Render a "hyperlinked" version of the text '''
973 return self.plain(hyperlink=1)
975 def plain(self, escape=0, hyperlink=0):
976 '''Render a "plain" representation of the property
978 - "escape" turns on/off HTML quoting
979 - "hyperlink" turns on/off in-text hyperlinking of URLs, email
980 addresses and designators
981 '''
982 self.view_check()
984 if self._value is None:
985 return ''
986 if escape:
987 s = cgi.escape(str(self._value))
988 else:
989 s = str(self._value)
990 if hyperlink:
991 # no, we *must* escape this text
992 if not escape:
993 s = cgi.escape(s)
994 s = self.hyper_re.sub(self._hyper_repl, s)
995 return s
997 def stext(self, escape=0):
998 ''' Render the value of the property as StructuredText.
1000 This requires the StructureText module to be installed separately.
1001 '''
1002 self.view_check()
1004 s = self.plain(escape=escape)
1005 if not StructuredText:
1006 return s
1007 return StructuredText(s,level=1,header=0)
1009 def field(self, size = 30):
1010 ''' Render the property as a field in HTML.
1012 If not editable, just display the value via plain().
1013 '''
1014 self.view_check()
1016 if self._value is None:
1017 value = ''
1018 else:
1019 value = cgi.escape(str(self._value))
1021 if self.is_edit_ok():
1022 value = '"'.join(value.split('"'))
1023 return self.input(name=self._formname,value=value,size=size)
1025 return self.plain()
1027 def multiline(self, escape=0, rows=5, cols=40):
1028 ''' Render a multiline form edit field for the property.
1030 If not editable, just display the plain() value in a <pre> tag.
1031 '''
1032 self.view_check()
1034 if self._value is None:
1035 value = ''
1036 else:
1037 value = cgi.escape(str(self._value))
1039 if self.is_edit_ok():
1040 value = '"'.join(value.split('"'))
1041 return '<textarea name="%s" rows="%s" cols="%s">%s</textarea>'%(
1042 self._formname, rows, cols, value)
1044 return '<pre>%s</pre>'%self.plain()
1046 def email(self, escape=1):
1047 ''' Render the value of the property as an obscured email address
1048 '''
1049 self.view_check()
1051 if self._value is None:
1052 value = ''
1053 else:
1054 value = str(self._value)
1055 if value.find('@') != -1:
1056 name, domain = value.split('@')
1057 domain = ' '.join(domain.split('.')[:-1])
1058 name = name.replace('.', ' ')
1059 value = '%s at %s ...'%(name, domain)
1060 else:
1061 value = value.replace('.', ' ')
1062 if escape:
1063 value = cgi.escape(value)
1064 return value
1066 class PasswordHTMLProperty(HTMLProperty):
1067 def plain(self):
1068 ''' Render a "plain" representation of the property
1069 '''
1070 self.view_check()
1072 if self._value is None:
1073 return ''
1074 return _('*encrypted*')
1076 def field(self, size = 30):
1077 ''' Render a form edit field for the property.
1079 If not editable, just display the value via plain().
1080 '''
1081 self.view_check()
1083 if self.is_edit_ok():
1084 return self.input(type="password", name=self._formname, size=size)
1086 return self.plain()
1088 def confirm(self, size = 30):
1089 ''' Render a second form edit field for the property, used for
1090 confirmation that the user typed the password correctly. Generates
1091 a field with name "@confirm@name".
1093 If not editable, display nothing.
1094 '''
1095 self.view_check()
1097 if self.is_edit_ok():
1098 return self.input(type="password",
1099 name="@confirm@%s"%self._formname, size=size)
1101 return ''
1103 class NumberHTMLProperty(HTMLProperty):
1104 def plain(self):
1105 ''' Render a "plain" representation of the property
1106 '''
1107 self.view_check()
1109 return str(self._value)
1111 def field(self, size = 30):
1112 ''' Render a form edit field for the property.
1114 If not editable, just display the value via plain().
1115 '''
1116 self.view_check()
1118 if self._value is None:
1119 value = ''
1120 else:
1121 value = cgi.escape(str(self._value))
1123 if self.is_edit_ok():
1124 value = '"'.join(value.split('"'))
1125 return self.input(name=self._formname,value=value,size=size)
1127 return self.plain()
1129 def __int__(self):
1130 ''' Return an int of me
1131 '''
1132 return int(self._value)
1134 def __float__(self):
1135 ''' Return a float of me
1136 '''
1137 return float(self._value)
1140 class BooleanHTMLProperty(HTMLProperty):
1141 def plain(self):
1142 ''' Render a "plain" representation of the property
1143 '''
1144 self.view_check()
1146 if self._value is None:
1147 return ''
1148 return self._value and "Yes" or "No"
1150 def field(self):
1151 ''' Render a form edit field for the property
1153 If not editable, just display the value via plain().
1154 '''
1155 self.view_check()
1157 if not is_edit_ok():
1158 return self.plain()
1160 checked = self._value and "checked" or ""
1161 if self._value:
1162 s = self.input(type="radio", name=self._formname, value="yes",
1163 checked="checked")
1164 s += 'Yes'
1165 s +=self.input(type="radio", name=self._formname, value="no")
1166 s += 'No'
1167 else:
1168 s = self.input(type="radio", name=self._formname, value="yes")
1169 s += 'Yes'
1170 s +=self.input(type="radio", name=self._formname, value="no",
1171 checked="checked")
1172 s += 'No'
1173 return s
1175 class DateHTMLProperty(HTMLProperty):
1176 def plain(self):
1177 ''' Render a "plain" representation of the property
1178 '''
1179 self.view_check()
1181 if self._value is None:
1182 return ''
1183 return str(self._value.local(self._db.getUserTimezone()))
1185 def now(self):
1186 ''' Return the current time.
1188 This is useful for defaulting a new value. Returns a
1189 DateHTMLProperty.
1190 '''
1191 self.view_check()
1193 return DateHTMLProperty(self._client, self._nodeid, self._prop,
1194 self._formname, date.Date('.'))
1196 def field(self, size = 30):
1197 ''' Render a form edit field for the property
1199 If not editable, just display the value via plain().
1200 '''
1201 self.view_check()
1203 if self._value is None:
1204 value = ''
1205 else:
1206 tz = self._db.getUserTimezone()
1207 value = cgi.escape(str(self._value.local(tz)))
1209 if is_edit_ok():
1210 value = '"'.join(value.split('"'))
1211 return self.input(name=self._formname,value=value,size=size)
1213 return self.plain()
1215 def reldate(self, pretty=1):
1216 ''' Render the interval between the date and now.
1218 If the "pretty" flag is true, then make the display pretty.
1219 '''
1220 self.view_check()
1222 if not self._value:
1223 return ''
1225 # figure the interval
1226 interval = self._value - date.Date('.')
1227 if pretty:
1228 return interval.pretty()
1229 return str(interval)
1231 _marker = []
1232 def pretty(self, format=_marker):
1233 ''' Render the date in a pretty format (eg. month names, spaces).
1235 The format string is a standard python strftime format string.
1236 Note that if the day is zero, and appears at the start of the
1237 string, then it'll be stripped from the output. This is handy
1238 for the situatin when a date only specifies a month and a year.
1239 '''
1240 self.view_check()
1242 if format is not self._marker:
1243 return self._value.pretty(format)
1244 else:
1245 return self._value.pretty()
1247 def local(self, offset):
1248 ''' Return the date/time as a local (timezone offset) date/time.
1249 '''
1250 self.view_check()
1252 return DateHTMLProperty(self._client, self._nodeid, self._prop,
1253 self._formname, self._value.local(offset))
1255 class IntervalHTMLProperty(HTMLProperty):
1256 def plain(self):
1257 ''' Render a "plain" representation of the property
1258 '''
1259 self.view_check()
1261 if self._value is None:
1262 return ''
1263 return str(self._value)
1265 def pretty(self):
1266 ''' Render the interval in a pretty format (eg. "yesterday")
1267 '''
1268 self.view_check()
1270 return self._value.pretty()
1272 def field(self, size = 30):
1273 ''' Render a form edit field for the property
1275 If not editable, just display the value via plain().
1276 '''
1277 self.view_check()
1279 if self._value is None:
1280 value = ''
1281 else:
1282 value = cgi.escape(str(self._value))
1284 if is_edit_ok():
1285 value = '"'.join(value.split('"'))
1286 return self.input(name=self._formname,value=value,size=size)
1288 return self.plain()
1290 class LinkHTMLProperty(HTMLProperty):
1291 ''' Link HTMLProperty
1292 Include the above as well as being able to access the class
1293 information. Stringifying the object itself results in the value
1294 from the item being displayed. Accessing attributes of this object
1295 result in the appropriate entry from the class being queried for the
1296 property accessed (so item/assignedto/name would look up the user
1297 entry identified by the assignedto property on item, and then the
1298 name property of that user)
1299 '''
1300 def __init__(self, *args, **kw):
1301 HTMLProperty.__init__(self, *args, **kw)
1302 # if we're representing a form value, then the -1 from the form really
1303 # should be a None
1304 if str(self._value) == '-1':
1305 self._value = None
1307 def __getattr__(self, attr):
1308 ''' return a new HTMLItem '''
1309 #print 'Link.getattr', (self, attr, self._value)
1310 if not self._value:
1311 raise AttributeError, "Can't access missing value"
1312 if self._prop.classname == 'user':
1313 klass = HTMLUser
1314 else:
1315 klass = HTMLItem
1316 i = klass(self._client, self._prop.classname, self._value)
1317 return getattr(i, attr)
1319 def plain(self, escape=0):
1320 ''' Render a "plain" representation of the property
1321 '''
1322 self.view_check()
1324 if self._value is None:
1325 return ''
1326 linkcl = self._db.classes[self._prop.classname]
1327 k = linkcl.labelprop(1)
1328 value = str(linkcl.get(self._value, k))
1329 if escape:
1330 value = cgi.escape(value)
1331 return value
1333 def field(self, showid=0, size=None):
1334 ''' Render a form edit field for the property
1336 If not editable, just display the value via plain().
1337 '''
1338 self.view_check()
1340 if not self.is_edit_ok():
1341 return self.plain()
1343 # edit field
1344 linkcl = self._db.getclass(self._prop.classname)
1345 if self._value is None:
1346 value = ''
1347 else:
1348 k = linkcl.getkey()
1349 if k:
1350 label = linkcl.get(self._value, k)
1351 else:
1352 label = self._value
1353 value = cgi.escape(str(self._value))
1354 value = '"'.join(value.split('"'))
1355 return '<input name="%s" value="%s" size="%s">'%(self._formname,
1356 label, size)
1358 def menu(self, size=None, height=None, showid=0, additional=[],
1359 sort_on=None, **conditions):
1360 ''' Render a form select list for this property
1362 If not editable, just display the value via plain().
1363 '''
1364 self.view_check()
1366 if not self.is_edit_ok():
1367 return self.plain()
1369 value = self._value
1371 linkcl = self._db.getclass(self._prop.classname)
1372 l = ['<select name="%s">'%self._formname]
1373 k = linkcl.labelprop(1)
1374 s = ''
1375 if value is None:
1376 s = 'selected="selected" '
1377 l.append(_('<option %svalue="-1">- no selection -</option>')%s)
1378 if linkcl.getprops().has_key('order'):
1379 sort_on = ('+', 'order')
1380 else:
1381 if sort_on is None:
1382 sort_on = ('+', linkcl.labelprop())
1383 else:
1384 sort_on = ('+', sort_on)
1385 options = linkcl.filter(None, conditions, sort_on, (None, None))
1387 # make sure we list the current value if it's retired
1388 if self._value and self._value not in options:
1389 options.insert(0, self._value)
1391 for optionid in options:
1392 # get the option value, and if it's None use an empty string
1393 option = linkcl.get(optionid, k) or ''
1395 # figure if this option is selected
1396 s = ''
1397 if value in [optionid, option]:
1398 s = 'selected="selected" '
1400 # figure the label
1401 if showid:
1402 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1403 else:
1404 lab = option
1406 # truncate if it's too long
1407 if size is not None and len(lab) > size:
1408 lab = lab[:size-3] + '...'
1409 if additional:
1410 m = []
1411 for propname in additional:
1412 m.append(linkcl.get(optionid, propname))
1413 lab = lab + ' (%s)'%', '.join(map(str, m))
1415 # and generate
1416 lab = cgi.escape(lab)
1417 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
1418 l.append('</select>')
1419 return '\n'.join(l)
1420 # def checklist(self, ...)
1422 class MultilinkHTMLProperty(HTMLProperty):
1423 ''' Multilink HTMLProperty
1425 Also be iterable, returning a wrapper object like the Link case for
1426 each entry in the multilink.
1427 '''
1428 def __init__(self, *args, **kwargs):
1429 HTMLProperty.__init__(self, *args, **kwargs)
1430 if self._value:
1431 sortfun = make_sort_function(self._db, self._prop.classname)
1432 self._value.sort(sortfun)
1434 def __len__(self):
1435 ''' length of the multilink '''
1436 return len(self._value)
1438 def __getattr__(self, attr):
1439 ''' no extended attribute accesses make sense here '''
1440 raise AttributeError, attr
1442 def __getitem__(self, num):
1443 ''' iterate and return a new HTMLItem
1444 '''
1445 #print 'Multi.getitem', (self, num)
1446 value = self._value[num]
1447 if self._prop.classname == 'user':
1448 klass = HTMLUser
1449 else:
1450 klass = HTMLItem
1451 return klass(self._client, self._prop.classname, value)
1453 def __contains__(self, value):
1454 ''' Support the "in" operator. We have to make sure the passed-in
1455 value is a string first, not a HTMLProperty.
1456 '''
1457 return str(value) in self._value
1459 def reverse(self):
1460 ''' return the list in reverse order
1461 '''
1462 l = self._value[:]
1463 l.reverse()
1464 if self._prop.classname == 'user':
1465 klass = HTMLUser
1466 else:
1467 klass = HTMLItem
1468 return [klass(self._client, self._prop.classname, value) for value in l]
1470 def plain(self, escape=0):
1471 ''' Render a "plain" representation of the property
1472 '''
1473 self.view_check()
1475 linkcl = self._db.classes[self._prop.classname]
1476 k = linkcl.labelprop(1)
1477 labels = []
1478 for v in self._value:
1479 labels.append(linkcl.get(v, k))
1480 value = ', '.join(labels)
1481 if escape:
1482 value = cgi.escape(value)
1483 return value
1485 def field(self, size=30, showid=0):
1486 ''' Render a form edit field for the property
1488 If not editable, just display the value via plain().
1489 '''
1490 self.view_check()
1492 if not self.is_edit_ok():
1493 return self.plain()
1495 linkcl = self._db.getclass(self._prop.classname)
1496 value = self._value[:]
1497 # map the id to the label property
1498 if not linkcl.getkey():
1499 showid=1
1500 if not showid:
1501 k = linkcl.labelprop(1)
1502 value = [linkcl.get(v, k) for v in value]
1503 value = cgi.escape(','.join(value))
1504 return self.input(name=self._formname,size=size,value=value)
1506 def menu(self, size=None, height=None, showid=0, additional=[],
1507 sort_on=None, **conditions):
1508 ''' Render a form select list for this property
1510 If not editable, just display the value via plain().
1511 '''
1512 self.view_check()
1514 if not self.is_edit_ok():
1515 return self.plain()
1517 value = self._value
1519 linkcl = self._db.getclass(self._prop.classname)
1520 if sort_on is None:
1521 sort_on = ('+', find_sort_key(linkcl))
1522 else:
1523 sort_on = ('+', sort_on)
1524 options = linkcl.filter(None, conditions, sort_on)
1525 height = height or min(len(options), 7)
1526 l = ['<select multiple name="%s" size="%s">'%(self._formname, height)]
1527 k = linkcl.labelprop(1)
1529 # make sure we list the current values if they're retired
1530 for val in value:
1531 if val not in options:
1532 options.insert(0, val)
1534 for optionid in options:
1535 # get the option value, and if it's None use an empty string
1536 option = linkcl.get(optionid, k) or ''
1538 # figure if this option is selected
1539 s = ''
1540 if optionid in value or option in value:
1541 s = 'selected="selected" '
1543 # figure the label
1544 if showid:
1545 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1546 else:
1547 lab = option
1548 # truncate if it's too long
1549 if size is not None and len(lab) > size:
1550 lab = lab[:size-3] + '...'
1551 if additional:
1552 m = []
1553 for propname in additional:
1554 m.append(linkcl.get(optionid, propname))
1555 lab = lab + ' (%s)'%', '.join(m)
1557 # and generate
1558 lab = cgi.escape(lab)
1559 l.append('<option %svalue="%s">%s</option>'%(s, optionid,
1560 lab))
1561 l.append('</select>')
1562 return '\n'.join(l)
1564 # set the propclasses for HTMLItem
1565 propclasses = (
1566 (hyperdb.String, StringHTMLProperty),
1567 (hyperdb.Number, NumberHTMLProperty),
1568 (hyperdb.Boolean, BooleanHTMLProperty),
1569 (hyperdb.Date, DateHTMLProperty),
1570 (hyperdb.Interval, IntervalHTMLProperty),
1571 (hyperdb.Password, PasswordHTMLProperty),
1572 (hyperdb.Link, LinkHTMLProperty),
1573 (hyperdb.Multilink, MultilinkHTMLProperty),
1574 )
1576 def make_sort_function(db, classname, sort_on=None):
1577 '''Make a sort function for a given class
1578 '''
1579 linkcl = db.getclass(classname)
1580 if sort_on is None:
1581 sort_on = find_sort_key(linkcl)
1582 def sortfunc(a, b):
1583 return cmp(linkcl.get(a, sort_on), linkcl.get(b, sort_on))
1584 return sortfunc
1586 def find_sort_key(linkcl):
1587 if linkcl.getprops().has_key('order'):
1588 return 'order'
1589 else:
1590 return linkcl.labelprop()
1592 def handleListCGIValue(value):
1593 ''' Value is either a single item or a list of items. Each item has a
1594 .value that we're actually interested in.
1595 '''
1596 if isinstance(value, type([])):
1597 return [value.value for value in value]
1598 else:
1599 value = value.value.strip()
1600 if not value:
1601 return []
1602 return value.split(',')
1604 class ShowDict:
1605 ''' A convenience access to the :columns index parameters
1606 '''
1607 def __init__(self, columns):
1608 self.columns = {}
1609 for col in columns:
1610 self.columns[col] = 1
1611 def __getitem__(self, name):
1612 return self.columns.has_key(name)
1614 class HTMLRequest(HTMLInputMixin):
1615 '''The *request*, holding the CGI form and environment.
1617 - "form" the CGI form as a cgi.FieldStorage
1618 - "env" the CGI environment variables
1619 - "base" the base URL for this instance
1620 - "user" a HTMLUser instance for this user
1621 - "classname" the current classname (possibly None)
1622 - "template" the current template (suffix, also possibly None)
1624 Index args:
1626 - "columns" dictionary of the columns to display in an index page
1627 - "show" a convenience access to columns - request/show/colname will
1628 be true if the columns should be displayed, false otherwise
1629 - "sort" index sort column (direction, column name)
1630 - "group" index grouping property (direction, column name)
1631 - "filter" properties to filter the index on
1632 - "filterspec" values to filter the index on
1633 - "search_text" text to perform a full-text search on for an index
1634 '''
1635 def __init__(self, client):
1636 # _client is needed by HTMLInputMixin
1637 self._client = self.client = client
1639 # easier access vars
1640 self.form = client.form
1641 self.env = client.env
1642 self.base = client.base
1643 self.user = HTMLUser(client, 'user', client.userid)
1645 # store the current class name and action
1646 self.classname = client.classname
1647 self.template = client.template
1649 # the special char to use for special vars
1650 self.special_char = '@'
1652 HTMLInputMixin.__init__(self)
1654 self._post_init()
1656 def _post_init(self):
1657 ''' Set attributes based on self.form
1658 '''
1659 # extract the index display information from the form
1660 self.columns = []
1661 for name in ':columns @columns'.split():
1662 if self.form.has_key(name):
1663 self.special_char = name[0]
1664 self.columns = handleListCGIValue(self.form[name])
1665 break
1666 self.show = ShowDict(self.columns)
1668 # sorting
1669 self.sort = (None, None)
1670 for name in ':sort @sort'.split():
1671 if self.form.has_key(name):
1672 self.special_char = name[0]
1673 sort = self.form[name].value
1674 if sort.startswith('-'):
1675 self.sort = ('-', sort[1:])
1676 else:
1677 self.sort = ('+', sort)
1678 if self.form.has_key(self.special_char+'sortdir'):
1679 self.sort = ('-', self.sort[1])
1681 # grouping
1682 self.group = (None, None)
1683 for name in ':group @group'.split():
1684 if self.form.has_key(name):
1685 self.special_char = name[0]
1686 group = self.form[name].value
1687 if group.startswith('-'):
1688 self.group = ('-', group[1:])
1689 else:
1690 self.group = ('+', group)
1691 if self.form.has_key(self.special_char+'groupdir'):
1692 self.group = ('-', self.group[1])
1694 # filtering
1695 self.filter = []
1696 for name in ':filter @filter'.split():
1697 if self.form.has_key(name):
1698 self.special_char = name[0]
1699 self.filter = handleListCGIValue(self.form[name])
1701 self.filterspec = {}
1702 db = self.client.db
1703 if self.classname is not None:
1704 props = db.getclass(self.classname).getprops()
1705 for name in self.filter:
1706 if not self.form.has_key(name):
1707 continue
1708 prop = props[name]
1709 fv = self.form[name]
1710 if (isinstance(prop, hyperdb.Link) or
1711 isinstance(prop, hyperdb.Multilink)):
1712 self.filterspec[name] = lookupIds(db, prop,
1713 handleListCGIValue(fv))
1714 else:
1715 if isinstance(fv, type([])):
1716 self.filterspec[name] = [v.value for v in fv]
1717 else:
1718 self.filterspec[name] = fv.value
1720 # full-text search argument
1721 self.search_text = None
1722 for name in ':search_text @search_text'.split():
1723 if self.form.has_key(name):
1724 self.special_char = name[0]
1725 self.search_text = self.form[name].value
1727 # pagination - size and start index
1728 # figure batch args
1729 self.pagesize = 50
1730 for name in ':pagesize @pagesize'.split():
1731 if self.form.has_key(name):
1732 self.special_char = name[0]
1733 self.pagesize = int(self.form[name].value)
1735 self.startwith = 0
1736 for name in ':startwith @startwith'.split():
1737 if self.form.has_key(name):
1738 self.special_char = name[0]
1739 self.startwith = int(self.form[name].value)
1741 def updateFromURL(self, url):
1742 ''' Parse the URL for query args, and update my attributes using the
1743 values.
1744 '''
1745 env = {'QUERY_STRING': url}
1746 self.form = cgi.FieldStorage(environ=env)
1748 self._post_init()
1750 def update(self, kwargs):
1751 ''' Update my attributes using the keyword args
1752 '''
1753 self.__dict__.update(kwargs)
1754 if kwargs.has_key('columns'):
1755 self.show = ShowDict(self.columns)
1757 def description(self):
1758 ''' Return a description of the request - handle for the page title.
1759 '''
1760 s = [self.client.db.config.TRACKER_NAME]
1761 if self.classname:
1762 if self.client.nodeid:
1763 s.append('- %s%s'%(self.classname, self.client.nodeid))
1764 else:
1765 if self.template == 'item':
1766 s.append('- new %s'%self.classname)
1767 elif self.template == 'index':
1768 s.append('- %s index'%self.classname)
1769 else:
1770 s.append('- %s %s'%(self.classname, self.template))
1771 else:
1772 s.append('- home')
1773 return ' '.join(s)
1775 def __str__(self):
1776 d = {}
1777 d.update(self.__dict__)
1778 f = ''
1779 for k in self.form.keys():
1780 f += '\n %r=%r'%(k,handleListCGIValue(self.form[k]))
1781 d['form'] = f
1782 e = ''
1783 for k,v in self.env.items():
1784 e += '\n %r=%r'%(k, v)
1785 d['env'] = e
1786 return '''
1787 form: %(form)s
1788 base: %(base)r
1789 classname: %(classname)r
1790 template: %(template)r
1791 columns: %(columns)r
1792 sort: %(sort)r
1793 group: %(group)r
1794 filter: %(filter)r
1795 search_text: %(search_text)r
1796 pagesize: %(pagesize)r
1797 startwith: %(startwith)r
1798 env: %(env)s
1799 '''%d
1801 def indexargs_form(self, columns=1, sort=1, group=1, filter=1,
1802 filterspec=1):
1803 ''' return the current index args as form elements '''
1804 l = []
1805 sc = self.special_char
1806 s = self.input(type="hidden",name="%s",value="%s")
1807 if columns and self.columns:
1808 l.append(s%(sc+'columns', ','.join(self.columns)))
1809 if sort and self.sort[1] is not None:
1810 if self.sort[0] == '-':
1811 val = '-'+self.sort[1]
1812 else:
1813 val = self.sort[1]
1814 l.append(s%(sc+'sort', val))
1815 if group and self.group[1] is not None:
1816 if self.group[0] == '-':
1817 val = '-'+self.group[1]
1818 else:
1819 val = self.group[1]
1820 l.append(s%(sc+'group', val))
1821 if filter and self.filter:
1822 l.append(s%(sc+'filter', ','.join(self.filter)))
1823 if filterspec:
1824 for k,v in self.filterspec.items():
1825 if type(v) == type([]):
1826 l.append(s%(k, ','.join(v)))
1827 else:
1828 l.append(s%(k, v))
1829 if self.search_text:
1830 l.append(s%(sc+'search_text', self.search_text))
1831 l.append(s%(sc+'pagesize', self.pagesize))
1832 l.append(s%(sc+'startwith', self.startwith))
1833 return '\n'.join(l)
1835 def indexargs_url(self, url, args):
1836 ''' Embed the current index args in a URL
1837 '''
1838 sc = self.special_char
1839 l = ['%s=%s'%(k,v) for k,v in args.items()]
1841 # pull out the special values (prefixed by @ or :)
1842 specials = {}
1843 for key in args.keys():
1844 if key[0] in '@:':
1845 specials[key[1:]] = args[key]
1847 # ok, now handle the specials we received in the request
1848 if self.columns and not specials.has_key('columns'):
1849 l.append(sc+'columns=%s'%(','.join(self.columns)))
1850 if self.sort[1] is not None and not specials.has_key('sort'):
1851 if self.sort[0] == '-':
1852 val = '-'+self.sort[1]
1853 else:
1854 val = self.sort[1]
1855 l.append(sc+'sort=%s'%val)
1856 if self.group[1] is not None and not specials.has_key('group'):
1857 if self.group[0] == '-':
1858 val = '-'+self.group[1]
1859 else:
1860 val = self.group[1]
1861 l.append(sc+'group=%s'%val)
1862 if self.filter and not specials.has_key('filter'):
1863 l.append(sc+'filter=%s'%(','.join(self.filter)))
1864 if self.search_text and not specials.has_key('search_text'):
1865 l.append(sc+'search_text=%s'%self.search_text)
1866 if not specials.has_key('pagesize'):
1867 l.append(sc+'pagesize=%s'%self.pagesize)
1868 if not specials.has_key('startwith'):
1869 l.append(sc+'startwith=%s'%self.startwith)
1871 # finally, the remainder of the filter args in the request
1872 for k,v in self.filterspec.items():
1873 if not args.has_key(k):
1874 if type(v) == type([]):
1875 l.append('%s=%s'%(k, ','.join(v)))
1876 else:
1877 l.append('%s=%s'%(k, v))
1878 return '%s?%s'%(url, '&'.join(l))
1879 indexargs_href = indexargs_url
1881 def base_javascript(self):
1882 return '''
1883 <script type="text/javascript">
1884 submitted = false;
1885 function submit_once() {
1886 if (submitted) {
1887 alert("Your request is being processed.\\nPlease be patient.");
1888 event.returnValue = 0; // work-around for IE
1889 return 0;
1890 }
1891 submitted = true;
1892 return 1;
1893 }
1895 function help_window(helpurl, width, height) {
1896 HelpWin = window.open('%s' + helpurl, 'RoundupHelpWindow', 'scrollbars=yes,resizable=yes,toolbar=no,height='+height+',width='+width);
1897 }
1898 </script>
1899 '''%self.base
1901 def batch(self):
1902 ''' Return a batch object for results from the "current search"
1903 '''
1904 filterspec = self.filterspec
1905 sort = self.sort
1906 group = self.group
1908 # get the list of ids we're batching over
1909 klass = self.client.db.getclass(self.classname)
1910 if self.search_text:
1911 matches = self.client.db.indexer.search(
1912 re.findall(r'\b\w{2,25}\b', self.search_text), klass)
1913 else:
1914 matches = None
1915 l = klass.filter(matches, filterspec, sort, group)
1917 # return the batch object, using IDs only
1918 return Batch(self.client, l, self.pagesize, self.startwith,
1919 classname=self.classname)
1921 # extend the standard ZTUtils Batch object to remove dependency on
1922 # Acquisition and add a couple of useful methods
1923 class Batch(ZTUtils.Batch):
1924 ''' Use me to turn a list of items, or item ids of a given class, into a
1925 series of batches.
1927 ========= ========================================================
1928 Parameter Usage
1929 ========= ========================================================
1930 sequence a list of HTMLItems or item ids
1931 classname if sequence is a list of ids, this is the class of item
1932 size how big to make the sequence.
1933 start where to start (0-indexed) in the sequence.
1934 end where to end (0-indexed) in the sequence.
1935 orphan if the next batch would contain less items than this
1936 value, then it is combined with this batch
1937 overlap the number of items shared between adjacent batches
1938 ========= ========================================================
1940 Attributes: Note that the "start" attribute, unlike the
1941 argument, is a 1-based index (I know, lame). "first" is the
1942 0-based index. "length" is the actual number of elements in
1943 the batch.
1945 "sequence_length" is the length of the original, unbatched, sequence.
1946 '''
1947 def __init__(self, client, sequence, size, start, end=0, orphan=0,
1948 overlap=0, classname=None):
1949 self.client = client
1950 self.last_index = self.last_item = None
1951 self.current_item = None
1952 self.classname = classname
1953 self.sequence_length = len(sequence)
1954 ZTUtils.Batch.__init__(self, sequence, size, start, end, orphan,
1955 overlap)
1957 # overwrite so we can late-instantiate the HTMLItem instance
1958 def __getitem__(self, index):
1959 if index < 0:
1960 if index + self.end < self.first: raise IndexError, index
1961 return self._sequence[index + self.end]
1963 if index >= self.length:
1964 raise IndexError, index
1966 # move the last_item along - but only if the fetched index changes
1967 # (for some reason, index 0 is fetched twice)
1968 if index != self.last_index:
1969 self.last_item = self.current_item
1970 self.last_index = index
1972 item = self._sequence[index + self.first]
1973 if self.classname:
1974 # map the item ids to instances
1975 if self.classname == 'user':
1976 item = HTMLUser(self.client, self.classname, item)
1977 else:
1978 item = HTMLItem(self.client, self.classname, item)
1979 self.current_item = item
1980 return item
1982 def propchanged(self, property):
1983 ''' Detect if the property marked as being the group property
1984 changed in the last iteration fetch
1985 '''
1986 if (self.last_item is None or
1987 self.last_item[property] != self.current_item[property]):
1988 return 1
1989 return 0
1991 # override these 'cos we don't have access to acquisition
1992 def previous(self):
1993 if self.start == 1:
1994 return None
1995 return Batch(self.client, self._sequence, self._size,
1996 self.first - self._size + self.overlap, 0, self.orphan,
1997 self.overlap)
1999 def next(self):
2000 try:
2001 self._sequence[self.end]
2002 except IndexError:
2003 return None
2004 return Batch(self.client, self._sequence, self._size,
2005 self.end - self.overlap, 0, self.orphan, self.overlap)
2007 class TemplatingUtils:
2008 ''' Utilities for templating
2009 '''
2010 def __init__(self, client):
2011 self.client = client
2012 def Batch(self, sequence, size, start, end=0, orphan=0, overlap=0):
2013 return Batch(self.client, sequence, size, start, end, orphan,
2014 overlap)