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 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 (and action hidden element)
620 '''
621 return self.input(type="hidden",name="@action",value="edit") + '\n' + \
622 self.input(type="submit",name="submit",value=label)
624 def journal(self, direction='descending'):
625 ''' Return a list of HTMLJournalEntry instances.
626 '''
627 # XXX do this
628 return []
630 def history(self, direction='descending', dre=re.compile('\d+')):
631 self.view_check()
633 l = ['<table class="history">'
634 '<tr><th colspan="4" class="header">',
635 _('History'),
636 '</th></tr><tr>',
637 _('<th>Date</th>'),
638 _('<th>User</th>'),
639 _('<th>Action</th>'),
640 _('<th>Args</th>'),
641 '</tr>']
642 current = {}
643 comments = {}
644 history = self._klass.history(self._nodeid)
645 history.sort()
646 timezone = self._db.getUserTimezone()
647 if direction == 'descending':
648 history.reverse()
649 for prop_n in self._props.keys():
650 prop = self[prop_n]
651 if isinstance(prop, HTMLProperty):
652 current[prop_n] = prop.plain()
653 # make link if hrefable
654 if (self._props.has_key(prop_n) and
655 isinstance(self._props[prop_n], hyperdb.Link)):
656 classname = self._props[prop_n].classname
657 try:
658 template = find_template(self._db.config.TEMPLATES,
659 classname, 'item')
660 if template[1].startswith('_generic'):
661 raise NoTemplate, 'not really...'
662 except NoTemplate:
663 pass
664 else:
665 id = self._klass.get(self._nodeid, prop_n, None)
666 current[prop_n] = '<a href="%s%s">%s</a>'%(
667 classname, id, current[prop_n])
669 for id, evt_date, user, action, args in history:
670 date_s = str(evt_date.local(timezone)).replace("."," ")
671 arg_s = ''
672 if action == 'link' and type(args) == type(()):
673 if len(args) == 3:
674 linkcl, linkid, key = args
675 arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
676 linkcl, linkid, key)
677 else:
678 arg_s = str(args)
680 elif action == 'unlink' and type(args) == type(()):
681 if len(args) == 3:
682 linkcl, linkid, key = args
683 arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
684 linkcl, linkid, key)
685 else:
686 arg_s = str(args)
688 elif type(args) == type({}):
689 cell = []
690 for k in args.keys():
691 # try to get the relevant property and treat it
692 # specially
693 try:
694 prop = self._props[k]
695 except KeyError:
696 prop = None
697 if prop is None:
698 # property no longer exists
699 comments['no_exist'] = _('''<em>The indicated property
700 no longer exists</em>''')
701 cell.append('<em>%s: %s</em>\n'%(k, str(args[k])))
702 continue
704 if args[k] and (isinstance(prop, hyperdb.Multilink) or
705 isinstance(prop, hyperdb.Link)):
706 # figure what the link class is
707 classname = prop.classname
708 try:
709 linkcl = self._db.getclass(classname)
710 except KeyError:
711 labelprop = None
712 comments[classname] = _('''The linked class
713 %(classname)s no longer exists''')%locals()
714 labelprop = linkcl.labelprop(1)
715 try:
716 template = find_template(self._db.config.TEMPLATES,
717 classname, 'item')
718 if template[1].startswith('_generic'):
719 raise NoTemplate, 'not really...'
720 hrefable = 1
721 except NoTemplate:
722 hrefable = 0
724 if isinstance(prop, hyperdb.Multilink) and args[k]:
725 ml = []
726 for linkid in args[k]:
727 if isinstance(linkid, type(())):
728 sublabel = linkid[0] + ' '
729 linkids = linkid[1]
730 else:
731 sublabel = ''
732 linkids = [linkid]
733 subml = []
734 for linkid in linkids:
735 label = classname + linkid
736 # if we have a label property, try to use it
737 # TODO: test for node existence even when
738 # there's no labelprop!
739 try:
740 if labelprop is not None and \
741 labelprop != 'id':
742 label = linkcl.get(linkid, labelprop)
743 except IndexError:
744 comments['no_link'] = _('''<strike>The
745 linked node no longer
746 exists</strike>''')
747 subml.append('<strike>%s</strike>'%label)
748 else:
749 if hrefable:
750 subml.append('<a href="%s%s">%s</a>'%(
751 classname, linkid, label))
752 else:
753 subml.append(label)
754 ml.append(sublabel + ', '.join(subml))
755 cell.append('%s:\n %s'%(k, ', '.join(ml)))
756 elif isinstance(prop, hyperdb.Link) and args[k]:
757 label = classname + args[k]
758 # if we have a label property, try to use it
759 # TODO: test for node existence even when
760 # there's no labelprop!
761 if labelprop is not None and labelprop != 'id':
762 try:
763 label = linkcl.get(args[k], labelprop)
764 except IndexError:
765 comments['no_link'] = _('''<strike>The
766 linked node no longer
767 exists</strike>''')
768 cell.append(' <strike>%s</strike>,\n'%label)
769 # "flag" this is done .... euwww
770 label = None
771 if label is not None:
772 if hrefable:
773 old = '<a href="%s%s">%s</a>'%(classname, args[k], label)
774 else:
775 old = label;
776 cell.append('%s: %s' % (k,old))
777 if current.has_key(k):
778 cell[-1] += ' -> %s'%current[k]
779 current[k] = old
781 elif isinstance(prop, hyperdb.Date) and args[k]:
782 d = date.Date(args[k]).local(timezone)
783 cell.append('%s: %s'%(k, str(d)))
784 if current.has_key(k):
785 cell[-1] += ' -> %s' % current[k]
786 current[k] = str(d)
788 elif isinstance(prop, hyperdb.Interval) and args[k]:
789 d = date.Interval(args[k])
790 cell.append('%s: %s'%(k, str(d)))
791 if current.has_key(k):
792 cell[-1] += ' -> %s'%current[k]
793 current[k] = str(d)
795 elif isinstance(prop, hyperdb.String) and args[k]:
796 cell.append('%s: %s'%(k, cgi.escape(args[k])))
797 if current.has_key(k):
798 cell[-1] += ' -> %s'%current[k]
799 current[k] = cgi.escape(args[k])
801 elif not args[k]:
802 if current.has_key(k):
803 cell.append('%s: %s'%(k, current[k]))
804 current[k] = '(no value)'
805 else:
806 cell.append('%s: (no value)'%k)
808 else:
809 cell.append('%s: %s'%(k, str(args[k])))
810 if current.has_key(k):
811 cell[-1] += ' -> %s'%current[k]
812 current[k] = str(args[k])
814 arg_s = '<br />'.join(cell)
815 else:
816 # unkown event!!
817 comments['unknown'] = _('''<strong><em>This event is not
818 handled by the history display!</em></strong>''')
819 arg_s = '<strong><em>' + str(args) + '</em></strong>'
820 date_s = date_s.replace(' ', ' ')
821 # if the user's an itemid, figure the username (older journals
822 # have the username)
823 if dre.match(user):
824 user = self._db.user.get(user, 'username')
825 l.append('<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>'%(
826 date_s, user, action, arg_s))
827 if comments:
828 l.append(_('<tr><td colspan=4><strong>Note:</strong></td></tr>'))
829 for entry in comments.values():
830 l.append('<tr><td colspan=4>%s</td></tr>'%entry)
831 l.append('</table>')
832 return '\n'.join(l)
834 def renderQueryForm(self):
835 ''' Render this item, which is a query, as a search form.
836 '''
837 # create a new request and override the specified args
838 req = HTMLRequest(self._client)
839 req.classname = self._klass.get(self._nodeid, 'klass')
840 name = self._klass.get(self._nodeid, 'name')
841 req.updateFromURL(self._klass.get(self._nodeid, 'url') +
842 '&@queryname=%s'%urllib.quote(name))
844 # new template, using the specified classname and request
845 pt = Templates(self._db.config.TEMPLATES).get(req.classname, 'search')
847 # use our fabricated request
848 return pt.render(self._client, req.classname, req)
850 class HTMLUser(HTMLItem):
851 ''' Accesses through the *user* (a special case of item)
852 '''
853 def __init__(self, client, classname, nodeid, anonymous=0):
854 HTMLItem.__init__(self, client, 'user', nodeid, anonymous)
855 self._default_classname = client.classname
857 # used for security checks
858 self._security = client.db.security
860 _marker = []
861 def hasPermission(self, permission, classname=_marker):
862 ''' Determine if the user has the Permission.
864 The class being tested defaults to the template's class, but may
865 be overidden for this test by suppling an alternate classname.
866 '''
867 if classname is self._marker:
868 classname = self._default_classname
869 return self._security.hasPermission(permission, self._nodeid, classname)
871 def is_edit_ok(self):
872 ''' Is the user allowed to Edit the current class?
873 Also check whether this is the current user's info.
874 '''
875 return self._db.security.hasPermission('Edit', self._client.userid,
876 self._classname) or (self._nodeid == self._client.userid and
877 self._db.user.get(self._client.userid, 'username') != 'anonymous')
879 def is_view_ok(self):
880 ''' Is the user allowed to View the current class?
881 Also check whether this is the current user's info.
882 '''
883 return self._db.security.hasPermission('View', self._client.userid,
884 self._classname) or (self._nodeid == self._client.userid and
885 self._db.user.get(self._client.userid, 'username') != 'anonymous')
887 class HTMLProperty(HTMLInputMixin, HTMLPermissions):
888 ''' String, Number, Date, Interval HTMLProperty
890 Has useful attributes:
892 _name the name of the property
893 _value the value of the property if any
895 A wrapper object which may be stringified for the plain() behaviour.
896 '''
897 def __init__(self, client, classname, nodeid, prop, name, value,
898 anonymous=0):
899 self._client = client
900 self._db = client.db
901 self._classname = classname
902 self._nodeid = nodeid
903 self._prop = prop
904 self._value = value
905 self._anonymous = anonymous
906 self._name = name
907 if not anonymous:
908 self._formname = '%s%s@%s'%(classname, nodeid, name)
909 else:
910 self._formname = name
912 HTMLInputMixin.__init__(self)
914 def __repr__(self):
915 return '<HTMLProperty(0x%x) %s %r %r>'%(id(self), self._formname,
916 self._prop, self._value)
917 def __str__(self):
918 return self.plain()
919 def __cmp__(self, other):
920 if isinstance(other, HTMLProperty):
921 return cmp(self._value, other._value)
922 return cmp(self._value, other)
924 def is_edit_ok(self):
925 ''' Is the user allowed to Edit the current class?
926 '''
927 thing = HTMLDatabase(self._client)[self._classname]
928 if self._nodeid:
929 # this is a special-case for the User class where permission's
930 # on a per-item basis :(
931 thing = thing.getItem(self._nodeid)
932 return thing.is_edit_ok()
934 def is_view_ok(self):
935 ''' Is the user allowed to View the current class?
936 '''
937 thing = HTMLDatabase(self._client)[self._classname]
938 if self._nodeid:
939 # this is a special-case for the User class where permission's
940 # on a per-item basis :(
941 thing = thing.getItem(self._nodeid)
942 return thing.is_view_ok()
944 class StringHTMLProperty(HTMLProperty):
945 hyper_re = re.compile(r'((?P<url>\w{3,6}://\S+)|'
946 r'(?P<email>[-+=%/\w\.]+@[\w\.\-]+)|'
947 r'(?P<item>(?P<class>[a-z_]+)(?P<id>\d+)))')
948 def _hyper_repl(self, match):
949 if match.group('url'):
950 s = match.group('url')
951 return '<a href="%s">%s</a>'%(s, s)
952 elif match.group('email'):
953 s = match.group('email')
954 return '<a href="mailto:%s">%s</a>'%(s, s)
955 else:
956 s = match.group('item')
957 s1 = match.group('class')
958 s2 = match.group('id')
959 try:
960 # make sure s1 is a valid tracker classname
961 self._db.getclass(s1)
962 return '<a href="%s">%s %s</a>'%(s, s1, s2)
963 except KeyError:
964 return '%s%s'%(s1, s2)
966 def hyperlinked(self):
967 ''' Render a "hyperlinked" version of the text '''
968 return self.plain(hyperlink=1)
970 def plain(self, escape=0, hyperlink=0):
971 '''Render a "plain" representation of the property
973 - "escape" turns on/off HTML quoting
974 - "hyperlink" turns on/off in-text hyperlinking of URLs, email
975 addresses and designators
976 '''
977 self.view_check()
979 if self._value is None:
980 return ''
981 if escape:
982 s = cgi.escape(str(self._value))
983 else:
984 s = str(self._value)
985 if hyperlink:
986 # no, we *must* escape this text
987 if not escape:
988 s = cgi.escape(s)
989 s = self.hyper_re.sub(self._hyper_repl, s)
990 return s
992 def stext(self, escape=0):
993 ''' Render the value of the property as StructuredText.
995 This requires the StructureText module to be installed separately.
996 '''
997 self.view_check()
999 s = self.plain(escape=escape)
1000 if not StructuredText:
1001 return s
1002 return StructuredText(s,level=1,header=0)
1004 def field(self, size = 30):
1005 ''' Render the property as a field in HTML.
1007 If not editable, just display the value via plain().
1008 '''
1009 self.view_check()
1011 if self._value is None:
1012 value = ''
1013 else:
1014 value = cgi.escape(str(self._value))
1016 if self.is_edit_ok():
1017 value = '"'.join(value.split('"'))
1018 return self.input(name=self._formname,value=value,size=size)
1020 return self.plain()
1022 def multiline(self, escape=0, rows=5, cols=40):
1023 ''' Render a multiline form edit field for the property.
1025 If not editable, just display the plain() value in a <pre> tag.
1026 '''
1027 self.view_check()
1029 if self._value is None:
1030 value = ''
1031 else:
1032 value = cgi.escape(str(self._value))
1034 if self.is_edit_ok():
1035 value = '"'.join(value.split('"'))
1036 return '<textarea name="%s" rows="%s" cols="%s">%s</textarea>'%(
1037 self._formname, rows, cols, value)
1039 return '<pre>%s</pre>'%self.plain()
1041 def email(self, escape=1):
1042 ''' Render the value of the property as an obscured email address
1043 '''
1044 self.view_check()
1046 if self._value is None:
1047 value = ''
1048 else:
1049 value = str(self._value)
1050 if value.find('@') != -1:
1051 name, domain = value.split('@')
1052 domain = ' '.join(domain.split('.')[:-1])
1053 name = name.replace('.', ' ')
1054 value = '%s at %s ...'%(name, domain)
1055 else:
1056 value = value.replace('.', ' ')
1057 if escape:
1058 value = cgi.escape(value)
1059 return value
1061 class PasswordHTMLProperty(HTMLProperty):
1062 def plain(self):
1063 ''' Render a "plain" representation of the property
1064 '''
1065 self.view_check()
1067 if self._value is None:
1068 return ''
1069 return _('*encrypted*')
1071 def field(self, size = 30):
1072 ''' Render a form edit field for the property.
1074 If not editable, just display the value via plain().
1075 '''
1076 self.view_check()
1078 if self.is_edit_ok():
1079 return self.input(type="password", name=self._formname, size=size)
1081 return self.plain()
1083 def confirm(self, size = 30):
1084 ''' Render a second form edit field for the property, used for
1085 confirmation that the user typed the password correctly. Generates
1086 a field with name "@confirm@name".
1088 If not editable, display nothing.
1089 '''
1090 self.view_check()
1092 if self.is_edit_ok():
1093 return self.input(type="password",
1094 name="@confirm@%s"%self._formname, size=size)
1096 return ''
1098 class NumberHTMLProperty(HTMLProperty):
1099 def plain(self):
1100 ''' Render a "plain" representation of the property
1101 '''
1102 self.view_check()
1104 return str(self._value)
1106 def field(self, size = 30):
1107 ''' Render a form edit field for the property.
1109 If not editable, just display the value via plain().
1110 '''
1111 self.view_check()
1113 if self._value is None:
1114 value = ''
1115 else:
1116 value = cgi.escape(str(self._value))
1118 if self.is_edit_ok():
1119 value = '"'.join(value.split('"'))
1120 return self.input(name=self._formname,value=value,size=size)
1122 return self.plain()
1124 def __int__(self):
1125 ''' Return an int of me
1126 '''
1127 return int(self._value)
1129 def __float__(self):
1130 ''' Return a float of me
1131 '''
1132 return float(self._value)
1135 class BooleanHTMLProperty(HTMLProperty):
1136 def plain(self):
1137 ''' Render a "plain" representation of the property
1138 '''
1139 self.view_check()
1141 if self._value is None:
1142 return ''
1143 return self._value and "Yes" or "No"
1145 def field(self):
1146 ''' Render a form edit field for the property
1148 If not editable, just display the value via plain().
1149 '''
1150 self.view_check()
1152 if not is_edit_ok():
1153 return self.plain()
1155 checked = self._value and "checked" or ""
1156 if self._value:
1157 s = self.input(type="radio", name=self._formname, value="yes",
1158 checked="checked")
1159 s += 'Yes'
1160 s +=self.input(type="radio", name=self._formname, value="no")
1161 s += 'No'
1162 else:
1163 s = self.input(type="radio", name=self._formname, value="yes")
1164 s += 'Yes'
1165 s +=self.input(type="radio", name=self._formname, value="no",
1166 checked="checked")
1167 s += 'No'
1168 return s
1170 class DateHTMLProperty(HTMLProperty):
1171 def plain(self):
1172 ''' Render a "plain" representation of the property
1173 '''
1174 self.view_check()
1176 if self._value is None:
1177 return ''
1178 return str(self._value.local(self._db.getUserTimezone()))
1180 def now(self):
1181 ''' Return the current time.
1183 This is useful for defaulting a new value. Returns a
1184 DateHTMLProperty.
1185 '''
1186 self.view_check()
1188 return DateHTMLProperty(self._client, self._nodeid, self._prop,
1189 self._formname, date.Date('.'))
1191 def field(self, size = 30):
1192 ''' Render a form edit field for the property
1194 If not editable, just display the value via plain().
1195 '''
1196 self.view_check()
1198 if self._value is None:
1199 value = ''
1200 else:
1201 tz = self._db.getUserTimezone()
1202 value = cgi.escape(str(self._value.local(tz)))
1204 if is_edit_ok():
1205 value = '"'.join(value.split('"'))
1206 return self.input(name=self._formname,value=value,size=size)
1208 return self.plain()
1210 def reldate(self, pretty=1):
1211 ''' Render the interval between the date and now.
1213 If the "pretty" flag is true, then make the display pretty.
1214 '''
1215 self.view_check()
1217 if not self._value:
1218 return ''
1220 # figure the interval
1221 interval = self._value - date.Date('.')
1222 if pretty:
1223 return interval.pretty()
1224 return str(interval)
1226 _marker = []
1227 def pretty(self, format=_marker):
1228 ''' Render the date in a pretty format (eg. month names, spaces).
1230 The format string is a standard python strftime format string.
1231 Note that if the day is zero, and appears at the start of the
1232 string, then it'll be stripped from the output. This is handy
1233 for the situatin when a date only specifies a month and a year.
1234 '''
1235 self.view_check()
1237 if format is not self._marker:
1238 return self._value.pretty(format)
1239 else:
1240 return self._value.pretty()
1242 def local(self, offset):
1243 ''' Return the date/time as a local (timezone offset) date/time.
1244 '''
1245 self.view_check()
1247 return DateHTMLProperty(self._client, self._nodeid, self._prop,
1248 self._formname, self._value.local(offset))
1250 class IntervalHTMLProperty(HTMLProperty):
1251 def plain(self):
1252 ''' Render a "plain" representation of the property
1253 '''
1254 self.view_check()
1256 if self._value is None:
1257 return ''
1258 return str(self._value)
1260 def pretty(self):
1261 ''' Render the interval in a pretty format (eg. "yesterday")
1262 '''
1263 self.view_check()
1265 return self._value.pretty()
1267 def field(self, size = 30):
1268 ''' Render a form edit field for the property
1270 If not editable, just display the value via plain().
1271 '''
1272 self.view_check()
1274 if self._value is None:
1275 value = ''
1276 else:
1277 value = cgi.escape(str(self._value))
1279 if is_edit_ok():
1280 value = '"'.join(value.split('"'))
1281 return self.input(name=self._formname,value=value,size=size)
1283 return self.plain()
1285 class LinkHTMLProperty(HTMLProperty):
1286 ''' Link HTMLProperty
1287 Include the above as well as being able to access the class
1288 information. Stringifying the object itself results in the value
1289 from the item being displayed. Accessing attributes of this object
1290 result in the appropriate entry from the class being queried for the
1291 property accessed (so item/assignedto/name would look up the user
1292 entry identified by the assignedto property on item, and then the
1293 name property of that user)
1294 '''
1295 def __init__(self, *args, **kw):
1296 HTMLProperty.__init__(self, *args, **kw)
1297 # if we're representing a form value, then the -1 from the form really
1298 # should be a None
1299 if str(self._value) == '-1':
1300 self._value = None
1302 def __getattr__(self, attr):
1303 ''' return a new HTMLItem '''
1304 #print 'Link.getattr', (self, attr, self._value)
1305 if not self._value:
1306 raise AttributeError, "Can't access missing value"
1307 if self._prop.classname == 'user':
1308 klass = HTMLUser
1309 else:
1310 klass = HTMLItem
1311 i = klass(self._client, self._prop.classname, self._value)
1312 return getattr(i, attr)
1314 def plain(self, escape=0):
1315 ''' Render a "plain" representation of the property
1316 '''
1317 self.view_check()
1319 if self._value is None:
1320 return ''
1321 linkcl = self._db.classes[self._prop.classname]
1322 k = linkcl.labelprop(1)
1323 value = str(linkcl.get(self._value, k))
1324 if escape:
1325 value = cgi.escape(value)
1326 return value
1328 def field(self, showid=0, size=None):
1329 ''' Render a form edit field for the property
1331 If not editable, just display the value via plain().
1332 '''
1333 self.view_check()
1335 if not self.is_edit_ok():
1336 return self.plain()
1338 # edit field
1339 linkcl = self._db.getclass(self._prop.classname)
1340 if self._value is None:
1341 value = ''
1342 else:
1343 k = linkcl.getkey()
1344 if k:
1345 label = linkcl.get(self._value, k)
1346 else:
1347 label = self._value
1348 value = cgi.escape(str(self._value))
1349 value = '"'.join(value.split('"'))
1350 return '<input name="%s" value="%s" size="%s">'%(self._formname,
1351 label, size)
1353 def menu(self, size=None, height=None, showid=0, additional=[],
1354 sort_on=None, **conditions):
1355 ''' Render a form select list for this property
1357 If not editable, just display the value via plain().
1358 '''
1359 self.view_check()
1361 if not self.is_edit_ok():
1362 return self.plain()
1364 value = self._value
1366 linkcl = self._db.getclass(self._prop.classname)
1367 l = ['<select name="%s">'%self._formname]
1368 k = linkcl.labelprop(1)
1369 s = ''
1370 if value is None:
1371 s = 'selected="selected" '
1372 l.append(_('<option %svalue="-1">- no selection -</option>')%s)
1373 if linkcl.getprops().has_key('order'):
1374 sort_on = ('+', 'order')
1375 else:
1376 if sort_on is None:
1377 sort_on = ('+', linkcl.labelprop())
1378 else:
1379 sort_on = ('+', sort_on)
1380 options = linkcl.filter(None, conditions, sort_on, (None, None))
1382 # make sure we list the current value if it's retired
1383 if self._value and self._value not in options:
1384 options.insert(0, self._value)
1386 for optionid in options:
1387 # get the option value, and if it's None use an empty string
1388 option = linkcl.get(optionid, k) or ''
1390 # figure if this option is selected
1391 s = ''
1392 if value in [optionid, option]:
1393 s = 'selected="selected" '
1395 # figure the label
1396 if showid:
1397 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1398 else:
1399 lab = option
1401 # truncate if it's too long
1402 if size is not None and len(lab) > size:
1403 lab = lab[:size-3] + '...'
1404 if additional:
1405 m = []
1406 for propname in additional:
1407 m.append(linkcl.get(optionid, propname))
1408 lab = lab + ' (%s)'%', '.join(map(str, m))
1410 # and generate
1411 lab = cgi.escape(lab)
1412 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
1413 l.append('</select>')
1414 return '\n'.join(l)
1415 # def checklist(self, ...)
1417 class MultilinkHTMLProperty(HTMLProperty):
1418 ''' Multilink HTMLProperty
1420 Also be iterable, returning a wrapper object like the Link case for
1421 each entry in the multilink.
1422 '''
1423 def __init__(self, *args, **kwargs):
1424 HTMLProperty.__init__(self, *args, **kwargs)
1425 if self._value:
1426 sortfun = make_sort_function(self._db, self._prop.classname)
1427 self._value.sort(sortfun)
1429 def __len__(self):
1430 ''' length of the multilink '''
1431 return len(self._value)
1433 def __getattr__(self, attr):
1434 ''' no extended attribute accesses make sense here '''
1435 raise AttributeError, attr
1437 def __getitem__(self, num):
1438 ''' iterate and return a new HTMLItem
1439 '''
1440 #print 'Multi.getitem', (self, num)
1441 value = self._value[num]
1442 if self._prop.classname == 'user':
1443 klass = HTMLUser
1444 else:
1445 klass = HTMLItem
1446 return klass(self._client, self._prop.classname, value)
1448 def __contains__(self, value):
1449 ''' Support the "in" operator. We have to make sure the passed-in
1450 value is a string first, not a HTMLProperty.
1451 '''
1452 return str(value) in self._value
1454 def reverse(self):
1455 ''' return the list in reverse order
1456 '''
1457 l = self._value[:]
1458 l.reverse()
1459 if self._prop.classname == 'user':
1460 klass = HTMLUser
1461 else:
1462 klass = HTMLItem
1463 return [klass(self._client, self._prop.classname, value) for value in l]
1465 def plain(self, escape=0):
1466 ''' Render a "plain" representation of the property
1467 '''
1468 self.view_check()
1470 linkcl = self._db.classes[self._prop.classname]
1471 k = linkcl.labelprop(1)
1472 labels = []
1473 for v in self._value:
1474 labels.append(linkcl.get(v, k))
1475 value = ', '.join(labels)
1476 if escape:
1477 value = cgi.escape(value)
1478 return value
1480 def field(self, size=30, showid=0):
1481 ''' Render a form edit field for the property
1483 If not editable, just display the value via plain().
1484 '''
1485 self.view_check()
1487 if not self.is_edit_ok():
1488 return self.plain()
1490 linkcl = self._db.getclass(self._prop.classname)
1491 value = self._value[:]
1492 # map the id to the label property
1493 if not linkcl.getkey():
1494 showid=1
1495 if not showid:
1496 k = linkcl.labelprop(1)
1497 value = [linkcl.get(v, k) for v in value]
1498 value = cgi.escape(','.join(value))
1499 return self.input(name=self._formname,size=size,value=value)
1501 def menu(self, size=None, height=None, showid=0, additional=[],
1502 sort_on=None, **conditions):
1503 ''' Render a form select list for this property
1505 If not editable, just display the value via plain().
1506 '''
1507 self.view_check()
1509 if not self.is_edit_ok():
1510 return self.plain()
1512 value = self._value
1514 linkcl = self._db.getclass(self._prop.classname)
1515 if sort_on is None:
1516 sort_on = ('+', find_sort_key(linkcl))
1517 else:
1518 sort_on = ('+', sort_on)
1519 options = linkcl.filter(None, conditions, sort_on)
1520 height = height or min(len(options), 7)
1521 l = ['<select multiple name="%s" size="%s">'%(self._formname, height)]
1522 k = linkcl.labelprop(1)
1524 # make sure we list the current values if they're retired
1525 for val in value:
1526 if val not in options:
1527 options.insert(0, val)
1529 for optionid in options:
1530 # get the option value, and if it's None use an empty string
1531 option = linkcl.get(optionid, k) or ''
1533 # figure if this option is selected
1534 s = ''
1535 if optionid in value or option in value:
1536 s = 'selected="selected" '
1538 # figure the label
1539 if showid:
1540 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1541 else:
1542 lab = option
1543 # truncate if it's too long
1544 if size is not None and len(lab) > size:
1545 lab = lab[:size-3] + '...'
1546 if additional:
1547 m = []
1548 for propname in additional:
1549 m.append(linkcl.get(optionid, propname))
1550 lab = lab + ' (%s)'%', '.join(m)
1552 # and generate
1553 lab = cgi.escape(lab)
1554 l.append('<option %svalue="%s">%s</option>'%(s, optionid,
1555 lab))
1556 l.append('</select>')
1557 return '\n'.join(l)
1559 # set the propclasses for HTMLItem
1560 propclasses = (
1561 (hyperdb.String, StringHTMLProperty),
1562 (hyperdb.Number, NumberHTMLProperty),
1563 (hyperdb.Boolean, BooleanHTMLProperty),
1564 (hyperdb.Date, DateHTMLProperty),
1565 (hyperdb.Interval, IntervalHTMLProperty),
1566 (hyperdb.Password, PasswordHTMLProperty),
1567 (hyperdb.Link, LinkHTMLProperty),
1568 (hyperdb.Multilink, MultilinkHTMLProperty),
1569 )
1571 def make_sort_function(db, classname, sort_on=None):
1572 '''Make a sort function for a given class
1573 '''
1574 linkcl = db.getclass(classname)
1575 if sort_on is None:
1576 sort_on = find_sort_key(linkcl)
1577 def sortfunc(a, b):
1578 return cmp(linkcl.get(a, sort_on), linkcl.get(b, sort_on))
1579 return sortfunc
1581 def find_sort_key(linkcl):
1582 if linkcl.getprops().has_key('order'):
1583 return 'order'
1584 else:
1585 return linkcl.labelprop()
1587 def handleListCGIValue(value):
1588 ''' Value is either a single item or a list of items. Each item has a
1589 .value that we're actually interested in.
1590 '''
1591 if isinstance(value, type([])):
1592 return [value.value for value in value]
1593 else:
1594 value = value.value.strip()
1595 if not value:
1596 return []
1597 return value.split(',')
1599 class ShowDict:
1600 ''' A convenience access to the :columns index parameters
1601 '''
1602 def __init__(self, columns):
1603 self.columns = {}
1604 for col in columns:
1605 self.columns[col] = 1
1606 def __getitem__(self, name):
1607 return self.columns.has_key(name)
1609 class HTMLRequest(HTMLInputMixin):
1610 '''The *request*, holding the CGI form and environment.
1612 - "form" the CGI form as a cgi.FieldStorage
1613 - "env" the CGI environment variables
1614 - "base" the base URL for this instance
1615 - "user" a HTMLUser instance for this user
1616 - "classname" the current classname (possibly None)
1617 - "template" the current template (suffix, also possibly None)
1619 Index args:
1621 - "columns" dictionary of the columns to display in an index page
1622 - "show" a convenience access to columns - request/show/colname will
1623 be true if the columns should be displayed, false otherwise
1624 - "sort" index sort column (direction, column name)
1625 - "group" index grouping property (direction, column name)
1626 - "filter" properties to filter the index on
1627 - "filterspec" values to filter the index on
1628 - "search_text" text to perform a full-text search on for an index
1629 '''
1630 def __init__(self, client):
1631 # _client is needed by HTMLInputMixin
1632 self._client = self.client = client
1634 # easier access vars
1635 self.form = client.form
1636 self.env = client.env
1637 self.base = client.base
1638 self.user = HTMLUser(client, 'user', client.userid)
1640 # store the current class name and action
1641 self.classname = client.classname
1642 self.template = client.template
1644 # the special char to use for special vars
1645 self.special_char = '@'
1647 HTMLInputMixin.__init__(self)
1649 self._post_init()
1651 def _post_init(self):
1652 ''' Set attributes based on self.form
1653 '''
1654 # extract the index display information from the form
1655 self.columns = []
1656 for name in ':columns @columns'.split():
1657 if self.form.has_key(name):
1658 self.special_char = name[0]
1659 self.columns = handleListCGIValue(self.form[name])
1660 break
1661 self.show = ShowDict(self.columns)
1663 # sorting
1664 self.sort = (None, None)
1665 for name in ':sort @sort'.split():
1666 if self.form.has_key(name):
1667 self.special_char = name[0]
1668 sort = self.form[name].value
1669 if sort.startswith('-'):
1670 self.sort = ('-', sort[1:])
1671 else:
1672 self.sort = ('+', sort)
1673 if self.form.has_key(self.special_char+'sortdir'):
1674 self.sort = ('-', self.sort[1])
1676 # grouping
1677 self.group = (None, None)
1678 for name in ':group @group'.split():
1679 if self.form.has_key(name):
1680 self.special_char = name[0]
1681 group = self.form[name].value
1682 if group.startswith('-'):
1683 self.group = ('-', group[1:])
1684 else:
1685 self.group = ('+', group)
1686 if self.form.has_key(self.special_char+'groupdir'):
1687 self.group = ('-', self.group[1])
1689 # filtering
1690 self.filter = []
1691 for name in ':filter @filter'.split():
1692 if self.form.has_key(name):
1693 self.special_char = name[0]
1694 self.filter = handleListCGIValue(self.form[name])
1696 self.filterspec = {}
1697 db = self.client.db
1698 if self.classname is not None:
1699 props = db.getclass(self.classname).getprops()
1700 for name in self.filter:
1701 if not self.form.has_key(name):
1702 continue
1703 prop = props[name]
1704 fv = self.form[name]
1705 if (isinstance(prop, hyperdb.Link) or
1706 isinstance(prop, hyperdb.Multilink)):
1707 self.filterspec[name] = lookupIds(db, prop,
1708 handleListCGIValue(fv))
1709 else:
1710 if isinstance(fv, type([])):
1711 self.filterspec[name] = [v.value for v in fv]
1712 else:
1713 self.filterspec[name] = fv.value
1715 # full-text search argument
1716 self.search_text = None
1717 for name in ':search_text @search_text'.split():
1718 if self.form.has_key(name):
1719 self.special_char = name[0]
1720 self.search_text = self.form[name].value
1722 # pagination - size and start index
1723 # figure batch args
1724 self.pagesize = 50
1725 for name in ':pagesize @pagesize'.split():
1726 if self.form.has_key(name):
1727 self.special_char = name[0]
1728 self.pagesize = int(self.form[name].value)
1730 self.startwith = 0
1731 for name in ':startwith @startwith'.split():
1732 if self.form.has_key(name):
1733 self.special_char = name[0]
1734 self.startwith = int(self.form[name].value)
1736 def updateFromURL(self, url):
1737 ''' Parse the URL for query args, and update my attributes using the
1738 values.
1739 '''
1740 env = {'QUERY_STRING': url}
1741 self.form = cgi.FieldStorage(environ=env)
1743 self._post_init()
1745 def update(self, kwargs):
1746 ''' Update my attributes using the keyword args
1747 '''
1748 self.__dict__.update(kwargs)
1749 if kwargs.has_key('columns'):
1750 self.show = ShowDict(self.columns)
1752 def description(self):
1753 ''' Return a description of the request - handle for the page title.
1754 '''
1755 s = [self.client.db.config.TRACKER_NAME]
1756 if self.classname:
1757 if self.client.nodeid:
1758 s.append('- %s%s'%(self.classname, self.client.nodeid))
1759 else:
1760 if self.template == 'item':
1761 s.append('- new %s'%self.classname)
1762 elif self.template == 'index':
1763 s.append('- %s index'%self.classname)
1764 else:
1765 s.append('- %s %s'%(self.classname, self.template))
1766 else:
1767 s.append('- home')
1768 return ' '.join(s)
1770 def __str__(self):
1771 d = {}
1772 d.update(self.__dict__)
1773 f = ''
1774 for k in self.form.keys():
1775 f += '\n %r=%r'%(k,handleListCGIValue(self.form[k]))
1776 d['form'] = f
1777 e = ''
1778 for k,v in self.env.items():
1779 e += '\n %r=%r'%(k, v)
1780 d['env'] = e
1781 return '''
1782 form: %(form)s
1783 base: %(base)r
1784 classname: %(classname)r
1785 template: %(template)r
1786 columns: %(columns)r
1787 sort: %(sort)r
1788 group: %(group)r
1789 filter: %(filter)r
1790 search_text: %(search_text)r
1791 pagesize: %(pagesize)r
1792 startwith: %(startwith)r
1793 env: %(env)s
1794 '''%d
1796 def indexargs_form(self, columns=1, sort=1, group=1, filter=1,
1797 filterspec=1):
1798 ''' return the current index args as form elements '''
1799 l = []
1800 sc = self.special_char
1801 s = self.input(type="hidden",name="%s",value="%s")
1802 if columns and self.columns:
1803 l.append(s%(sc+'columns', ','.join(self.columns)))
1804 if sort and self.sort[1] is not None:
1805 if self.sort[0] == '-':
1806 val = '-'+self.sort[1]
1807 else:
1808 val = self.sort[1]
1809 l.append(s%(sc+'sort', val))
1810 if group and self.group[1] is not None:
1811 if self.group[0] == '-':
1812 val = '-'+self.group[1]
1813 else:
1814 val = self.group[1]
1815 l.append(s%(sc+'group', val))
1816 if filter and self.filter:
1817 l.append(s%(sc+'filter', ','.join(self.filter)))
1818 if filterspec:
1819 for k,v in self.filterspec.items():
1820 if type(v) == type([]):
1821 l.append(s%(k, ','.join(v)))
1822 else:
1823 l.append(s%(k, v))
1824 if self.search_text:
1825 l.append(s%(sc+'search_text', self.search_text))
1826 l.append(s%(sc+'pagesize', self.pagesize))
1827 l.append(s%(sc+'startwith', self.startwith))
1828 return '\n'.join(l)
1830 def indexargs_url(self, url, args):
1831 ''' Embed the current index args in a URL
1832 '''
1833 sc = self.special_char
1834 l = ['%s=%s'%(k,v) for k,v in args.items()]
1836 # pull out the special values (prefixed by @ or :)
1837 specials = {}
1838 for key in args.keys():
1839 if key[0] in '@:':
1840 specials[key[1:]] = args[key]
1842 # ok, now handle the specials we received in the request
1843 if self.columns and not specials.has_key('columns'):
1844 l.append(sc+'columns=%s'%(','.join(self.columns)))
1845 if self.sort[1] is not None and not specials.has_key('sort'):
1846 if self.sort[0] == '-':
1847 val = '-'+self.sort[1]
1848 else:
1849 val = self.sort[1]
1850 l.append(sc+'sort=%s'%val)
1851 if self.group[1] is not None and not specials.has_key('group'):
1852 if self.group[0] == '-':
1853 val = '-'+self.group[1]
1854 else:
1855 val = self.group[1]
1856 l.append(sc+'group=%s'%val)
1857 if self.filter and not specials.has_key('filter'):
1858 l.append(sc+'filter=%s'%(','.join(self.filter)))
1859 if self.search_text and not specials.has_key('search_text'):
1860 l.append(sc+'search_text=%s'%self.search_text)
1861 if not specials.has_key('pagesize'):
1862 l.append(sc+'pagesize=%s'%self.pagesize)
1863 if not specials.has_key('startwith'):
1864 l.append(sc+'startwith=%s'%self.startwith)
1866 # finally, the remainder of the filter args in the request
1867 for k,v in self.filterspec.items():
1868 if not args.has_key(k):
1869 if type(v) == type([]):
1870 l.append('%s=%s'%(k, ','.join(v)))
1871 else:
1872 l.append('%s=%s'%(k, v))
1873 return '%s?%s'%(url, '&'.join(l))
1874 indexargs_href = indexargs_url
1876 def base_javascript(self):
1877 return '''
1878 <script type="text/javascript">
1879 submitted = false;
1880 function submit_once() {
1881 if (submitted) {
1882 alert("Your request is being processed.\\nPlease be patient.");
1883 event.returnValue = 0; // work-around for IE
1884 return 0;
1885 }
1886 submitted = true;
1887 return 1;
1888 }
1890 function help_window(helpurl, width, height) {
1891 HelpWin = window.open('%s' + helpurl, 'RoundupHelpWindow', 'scrollbars=yes,resizable=yes,toolbar=no,height='+height+',width='+width);
1892 }
1893 </script>
1894 '''%self.base
1896 def batch(self):
1897 ''' Return a batch object for results from the "current search"
1898 '''
1899 filterspec = self.filterspec
1900 sort = self.sort
1901 group = self.group
1903 # get the list of ids we're batching over
1904 klass = self.client.db.getclass(self.classname)
1905 if self.search_text:
1906 matches = self.client.db.indexer.search(
1907 re.findall(r'\b\w{2,25}\b', self.search_text), klass)
1908 else:
1909 matches = None
1910 l = klass.filter(matches, filterspec, sort, group)
1912 # return the batch object, using IDs only
1913 return Batch(self.client, l, self.pagesize, self.startwith,
1914 classname=self.classname)
1916 # extend the standard ZTUtils Batch object to remove dependency on
1917 # Acquisition and add a couple of useful methods
1918 class Batch(ZTUtils.Batch):
1919 ''' Use me to turn a list of items, or item ids of a given class, into a
1920 series of batches.
1922 ========= ========================================================
1923 Parameter Usage
1924 ========= ========================================================
1925 sequence a list of HTMLItems or item ids
1926 classname if sequence is a list of ids, this is the class of item
1927 size how big to make the sequence.
1928 start where to start (0-indexed) in the sequence.
1929 end where to end (0-indexed) in the sequence.
1930 orphan if the next batch would contain less items than this
1931 value, then it is combined with this batch
1932 overlap the number of items shared between adjacent batches
1933 ========= ========================================================
1935 Attributes: Note that the "start" attribute, unlike the
1936 argument, is a 1-based index (I know, lame). "first" is the
1937 0-based index. "length" is the actual number of elements in
1938 the batch.
1940 "sequence_length" is the length of the original, unbatched, sequence.
1941 '''
1942 def __init__(self, client, sequence, size, start, end=0, orphan=0,
1943 overlap=0, classname=None):
1944 self.client = client
1945 self.last_index = self.last_item = None
1946 self.current_item = None
1947 self.classname = classname
1948 self.sequence_length = len(sequence)
1949 ZTUtils.Batch.__init__(self, sequence, size, start, end, orphan,
1950 overlap)
1952 # overwrite so we can late-instantiate the HTMLItem instance
1953 def __getitem__(self, index):
1954 if index < 0:
1955 if index + self.end < self.first: raise IndexError, index
1956 return self._sequence[index + self.end]
1958 if index >= self.length:
1959 raise IndexError, index
1961 # move the last_item along - but only if the fetched index changes
1962 # (for some reason, index 0 is fetched twice)
1963 if index != self.last_index:
1964 self.last_item = self.current_item
1965 self.last_index = index
1967 item = self._sequence[index + self.first]
1968 if self.classname:
1969 # map the item ids to instances
1970 if self.classname == 'user':
1971 item = HTMLUser(self.client, self.classname, item)
1972 else:
1973 item = HTMLItem(self.client, self.classname, item)
1974 self.current_item = item
1975 return item
1977 def propchanged(self, property):
1978 ''' Detect if the property marked as being the group property
1979 changed in the last iteration fetch
1980 '''
1981 if (self.last_item is None or
1982 self.last_item[property] != self.current_item[property]):
1983 return 1
1984 return 0
1986 # override these 'cos we don't have access to acquisition
1987 def previous(self):
1988 if self.start == 1:
1989 return None
1990 return Batch(self.client, self._sequence, self._size,
1991 self.first - self._size + self.overlap, 0, self.orphan,
1992 self.overlap)
1994 def next(self):
1995 try:
1996 self._sequence[self.end]
1997 except IndexError:
1998 return None
1999 return Batch(self.client, self._sequence, self._size,
2000 self.end - self.overlap, 0, self.orphan, self.overlap)
2002 class TemplatingUtils:
2003 ''' Utilities for templating
2004 '''
2005 def __init__(self, client):
2006 self.client = client
2007 def Batch(self, sequence, size, start, end=0, orphan=0, overlap=0):
2008 return Batch(self.client, sequence, size, start, end, orphan,
2009 overlap)