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 if classname == 'user':
209 c['context'] = HTMLUserClass(client, classname, anonymous=1)
210 else:
211 c['context'] = HTMLClass(client, classname, anonymous=1)
212 return c
214 def render(self, client, classname, request, **options):
215 """Render this Page Template"""
217 if not self._v_cooked:
218 self._cook()
220 __traceback_supplement__ = (PageTemplate.PageTemplateTracebackSupplement, self)
222 if self._v_errors:
223 raise PageTemplate.PTRuntimeError, \
224 'Page Template %s has errors.'%self.id
226 # figure the context
227 classname = classname or client.classname
228 request = request or HTMLRequest(client)
229 c = self.getContext(client, classname, request)
230 c.update({'options': options})
232 # and go
233 output = StringIO.StringIO()
234 TALInterpreter(self._v_program, self.macros,
235 getEngine().getContext(c), output, tal=1, strictinsert=0)()
236 return output.getvalue()
238 def __repr__(self):
239 return '<Roundup PageTemplate %r>'%self.id
241 class HTMLDatabase:
242 ''' Return HTMLClasses for valid class fetches
243 '''
244 def __init__(self, client):
245 self._client = client
246 self._db = client.db
248 # we want config to be exposed
249 self.config = client.db.config
251 def __getitem__(self, item, desre=re.compile(r'(?P<cl>\w+)(?P<id>[-\d]+)')):
252 # check to see if we're actually accessing an item
253 m = desre.match(item)
254 if m:
255 self._client.db.getclass(m.group('cl'))
256 return HTMLItem(self._client, m.group('cl'), m.group('id'))
257 else:
258 self._client.db.getclass(item)
259 if item == 'user':
260 return HTMLUserClass(self._client, item)
261 return HTMLClass(self._client, item)
263 def __getattr__(self, attr):
264 try:
265 return self[attr]
266 except KeyError:
267 raise AttributeError, attr
269 def classes(self):
270 l = self._client.db.classes.keys()
271 l.sort()
272 r = []
273 for item in l:
274 if item == 'user':
275 m.append(HTMLUserClass(self._client, item))
276 m.append(HTMLClass(self._client, item))
277 return r
279 def lookupIds(db, prop, ids, num_re=re.compile('-?\d+')):
280 cl = db.getclass(prop.classname)
281 l = []
282 for entry in ids:
283 if num_re.match(entry):
284 l.append(entry)
285 else:
286 try:
287 l.append(cl.lookup(entry))
288 except KeyError:
289 # ignore invalid keys
290 pass
291 return l
293 class HTMLPermissions:
294 ''' Helpers that provide answers to commonly asked Permission questions.
295 '''
296 def is_edit_ok(self):
297 ''' Is the user allowed to Edit the current class?
298 '''
299 return self._db.security.hasPermission('Edit', self._client.userid,
300 self._classname)
302 def is_view_ok(self):
303 ''' Is the user allowed to View the current class?
304 '''
305 return self._db.security.hasPermission('View', self._client.userid,
306 self._classname)
308 def is_only_view_ok(self):
309 ''' Is the user only allowed to View (ie. not Edit) the current class?
310 '''
311 return self.is_view_ok() and not self.is_edit_ok()
313 def view_check(self):
314 ''' Raise the Unauthorised exception if the user's not permitted to
315 view this class.
316 '''
317 if not self.is_view_ok():
318 raise Unauthorised("view", self._classname)
320 def edit_check(self):
321 ''' Raise the Unauthorised exception if the user's not permitted to
322 edit this class.
323 '''
324 if not self.is_edit_ok():
325 raise Unauthorised("edit", self._classname)
327 def input_html4(**attrs):
328 """Generate an 'input' (html4) element with given attributes"""
329 return '<input %s>'%' '.join(['%s="%s"'%item for item in attrs.items()])
331 def input_xhtml(**attrs):
332 """Generate an 'input' (xhtml) element with given attributes"""
333 return '<input %s/>'%' '.join(['%s="%s"'%item for item in attrs.items()])
335 class HTMLInputMixin:
336 ''' requires a _client property '''
337 def __init__(self):
338 html_version = 'html4'
339 if hasattr(self._client.instance.config, 'HTML_VERSION'):
340 html_version = self._client.instance.config.HTML_VERSION
341 if html_version == 'xhtml':
342 self.input = input_xhtml
343 else:
344 self.input = input_html4
346 class HTMLClass(HTMLInputMixin, HTMLPermissions):
347 ''' Accesses through a class (either through *class* or *db.<classname>*)
348 '''
349 def __init__(self, client, classname, anonymous=0):
350 self._client = client
351 self._db = client.db
352 self._anonymous = anonymous
354 # we want classname to be exposed, but _classname gives a
355 # consistent API for extending Class/Item
356 self._classname = self.classname = classname
357 self._klass = self._db.getclass(self.classname)
358 self._props = self._klass.getprops()
360 HTMLInputMixin.__init__(self)
362 def __repr__(self):
363 return '<HTMLClass(0x%x) %s>'%(id(self), self.classname)
365 def __getitem__(self, item):
366 ''' return an HTMLProperty instance
367 '''
368 #print 'HTMLClass.getitem', (self, item)
370 # we don't exist
371 if item == 'id':
372 return None
374 # get the property
375 prop = self._props[item]
377 # look up the correct HTMLProperty class
378 form = self._client.form
379 for klass, htmlklass in propclasses:
380 if not isinstance(prop, klass):
381 continue
382 if form.has_key(item):
383 if isinstance(prop, hyperdb.Multilink):
384 value = lookupIds(self._db, prop,
385 handleListCGIValue(form[item]))
386 elif isinstance(prop, hyperdb.Link):
387 value = form[item].value.strip()
388 if value:
389 value = lookupIds(self._db, prop, [value])[0]
390 else:
391 value = None
392 else:
393 value = form[item].value.strip() or None
394 else:
395 if isinstance(prop, hyperdb.Multilink):
396 value = []
397 else:
398 value = None
399 return htmlklass(self._client, self._classname, '', prop, item,
400 value, self._anonymous)
402 # no good
403 raise KeyError, item
405 def __getattr__(self, attr):
406 ''' convenience access '''
407 try:
408 return self[attr]
409 except KeyError:
410 raise AttributeError, attr
412 def designator(self):
413 ''' Return this class' designator (classname) '''
414 return self._classname
416 def getItem(self, itemid, num_re=re.compile('-?\d+')):
417 ''' Get an item of this class by its item id.
418 '''
419 # make sure we're looking at an itemid
420 if not isinstance(itemid, type(1)) and not num_re.match(itemid):
421 itemid = self._klass.lookup(itemid)
423 if self.classname == 'user':
424 klass = HTMLUser
425 else:
426 klass = HTMLItem
428 return klass(self._client, self.classname, itemid)
430 def properties(self, sort=1):
431 ''' Return HTMLProperty for all of this class' properties.
432 '''
433 l = []
434 for name, prop in self._props.items():
435 for klass, htmlklass in propclasses:
436 if isinstance(prop, hyperdb.Multilink):
437 value = []
438 else:
439 value = None
440 if isinstance(prop, klass):
441 l.append(htmlklass(self._client, self._classname, '',
442 prop, name, value, self._anonymous))
443 if sort:
444 l.sort(lambda a,b:cmp(a._name, b._name))
445 return l
447 def list(self, sort_on=None):
448 ''' List all items in this class.
449 '''
450 if self.classname == 'user':
451 klass = HTMLUser
452 else:
453 klass = HTMLItem
455 # get the list and sort it nicely
456 l = self._klass.list()
457 sortfunc = make_sort_function(self._db, self.classname, sort_on)
458 l.sort(sortfunc)
460 l = [klass(self._client, self.classname, x) for x in l]
461 return l
463 def csv(self):
464 ''' Return the items of this class as a chunk of CSV text.
465 '''
466 if rcsv.error:
467 return rcsv.error
469 props = self.propnames()
470 s = StringIO.StringIO()
471 writer = rcsv.writer(s, rcsv.comma_separated)
472 writer.writerow(props)
473 for nodeid in self._klass.list():
474 l = []
475 for name in props:
476 value = self._klass.get(nodeid, name)
477 if value is None:
478 l.append('')
479 elif isinstance(value, type([])):
480 l.append(':'.join(map(str, value)))
481 else:
482 l.append(str(self._klass.get(nodeid, name)))
483 writer.writerow(l)
484 return s.getvalue()
486 def propnames(self):
487 ''' Return the list of the names of the properties of this class.
488 '''
489 idlessprops = self._klass.getprops(protected=0).keys()
490 idlessprops.sort()
491 return ['id'] + idlessprops
493 def filter(self, request=None, filterspec={}, sort=(None,None),
494 group=(None,None)):
495 ''' Return a list of items from this class, filtered and sorted
496 by the current requested filterspec/filter/sort/group args
498 "request" takes precedence over the other three arguments.
499 '''
500 if request is not None:
501 filterspec = request.filterspec
502 sort = request.sort
503 group = request.group
504 if self.classname == 'user':
505 klass = HTMLUser
506 else:
507 klass = HTMLItem
508 l = [klass(self._client, self.classname, x)
509 for x in self._klass.filter(None, filterspec, sort, group)]
510 return l
512 def classhelp(self, properties=None, label='(list)', width='500',
513 height='400', property=''):
514 ''' Pop up a javascript window with class help
516 This generates a link to a popup window which displays the
517 properties indicated by "properties" of the class named by
518 "classname". The "properties" should be a comma-separated list
519 (eg. 'id,name,description'). Properties defaults to all the
520 properties of a class (excluding id, creator, created and
521 activity).
523 You may optionally override the label displayed, the width and
524 height. The popup window will be resizable and scrollable.
526 If the "property" arg is given, it's passed through to the
527 javascript help_window function.
528 '''
529 if properties is None:
530 properties = self._klass.getprops(protected=0).keys()
531 properties.sort()
532 properties = ','.join(properties)
533 if property:
534 property = '&property=%s'%property
535 return '<a class="classhelp" href="javascript:help_window(\'%s?'\
536 '@startwith=0&@template=help&properties=%s%s\', \'%s\', \
537 \'%s\')">%s</a>'%(self.classname, properties, property, width,
538 height, label)
540 def submit(self, label="Submit New Entry"):
541 ''' Generate a submit button (and action hidden element)
542 '''
543 self.view_check()
544 if self.is_edit_ok():
545 return self.input(type="hidden",name="@action",value="new") + \
546 '\n' + self.input(type="submit",name="submit",value=label)
547 return ''
549 def history(self):
550 self.view_check()
551 return 'New node - no history'
553 def renderWith(self, name, **kwargs):
554 ''' Render this class with the given template.
555 '''
556 # create a new request and override the specified args
557 req = HTMLRequest(self._client)
558 req.classname = self.classname
559 req.update(kwargs)
561 # new template, using the specified classname and request
562 pt = Templates(self._db.config.TEMPLATES).get(self.classname, name)
564 # use our fabricated request
565 args = {
566 'ok_message': self._client.ok_message,
567 'error_message': self._client.error_message
568 }
569 return pt.render(self._client, self.classname, req, **args)
571 class HTMLItem(HTMLInputMixin, HTMLPermissions):
572 ''' Accesses through an *item*
573 '''
574 def __init__(self, client, classname, nodeid, anonymous=0):
575 self._client = client
576 self._db = client.db
577 self._classname = classname
578 self._nodeid = nodeid
579 self._klass = self._db.getclass(classname)
580 self._props = self._klass.getprops()
582 # do we prefix the form items with the item's identification?
583 self._anonymous = anonymous
585 HTMLInputMixin.__init__(self)
587 def __repr__(self):
588 return '<HTMLItem(0x%x) %s %s>'%(id(self), self._classname,
589 self._nodeid)
591 def __getitem__(self, item):
592 ''' return an HTMLProperty instance
593 '''
594 #print 'HTMLItem.getitem', (self, item)
595 if item == 'id':
596 return self._nodeid
598 # get the property
599 prop = self._props[item]
601 # get the value, handling missing values
602 value = None
603 if int(self._nodeid) > 0:
604 value = self._klass.get(self._nodeid, item, None)
605 if value is None:
606 if isinstance(self._props[item], hyperdb.Multilink):
607 value = []
609 # look up the correct HTMLProperty class
610 for klass, htmlklass in propclasses:
611 if isinstance(prop, klass):
612 return htmlklass(self._client, self._classname,
613 self._nodeid, prop, item, value, self._anonymous)
615 raise KeyError, item
617 def __getattr__(self, attr):
618 ''' convenience access to properties '''
619 try:
620 return self[attr]
621 except KeyError:
622 raise AttributeError, attr
624 def designator(self):
625 """Return this item's designator (classname + id)."""
626 return '%s%s'%(self._classname, self._nodeid)
628 def submit(self, label="Submit Changes"):
629 """Generate a submit button.
631 Also sneak in the lastactivity and action hidden elements.
632 """
633 return self.input(type="hidden", name="@lastactivity", value=date.Date('.')) + '\n' + \
634 self.input(type="hidden", name="@action", value="edit") + '\n' + \
635 self.input(type="submit", name="submit", value=label)
637 def journal(self, direction='descending'):
638 ''' Return a list of HTMLJournalEntry instances.
639 '''
640 # XXX do this
641 return []
643 def history(self, direction='descending', dre=re.compile('\d+')):
644 self.view_check()
646 l = ['<table class="history">'
647 '<tr><th colspan="4" class="header">',
648 _('History'),
649 '</th></tr><tr>',
650 _('<th>Date</th>'),
651 _('<th>User</th>'),
652 _('<th>Action</th>'),
653 _('<th>Args</th>'),
654 '</tr>']
655 current = {}
656 comments = {}
657 history = self._klass.history(self._nodeid)
658 history.sort()
659 timezone = self._db.getUserTimezone()
660 if direction == 'descending':
661 history.reverse()
662 for prop_n in self._props.keys():
663 prop = self[prop_n]
664 if isinstance(prop, HTMLProperty):
665 current[prop_n] = prop.plain()
666 # make link if hrefable
667 if (self._props.has_key(prop_n) and
668 isinstance(self._props[prop_n], hyperdb.Link)):
669 classname = self._props[prop_n].classname
670 try:
671 template = find_template(self._db.config.TEMPLATES,
672 classname, 'item')
673 if template[1].startswith('_generic'):
674 raise NoTemplate, 'not really...'
675 except NoTemplate:
676 pass
677 else:
678 id = self._klass.get(self._nodeid, prop_n, None)
679 current[prop_n] = '<a href="%s%s">%s</a>'%(
680 classname, id, current[prop_n])
682 for id, evt_date, user, action, args in history:
683 date_s = str(evt_date.local(timezone)).replace("."," ")
684 arg_s = ''
685 if action == 'link' and type(args) == type(()):
686 if len(args) == 3:
687 linkcl, linkid, key = args
688 arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
689 linkcl, linkid, key)
690 else:
691 arg_s = str(args)
693 elif action == 'unlink' and type(args) == type(()):
694 if len(args) == 3:
695 linkcl, linkid, key = args
696 arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
697 linkcl, linkid, key)
698 else:
699 arg_s = str(args)
701 elif type(args) == type({}):
702 cell = []
703 for k in args.keys():
704 # try to get the relevant property and treat it
705 # specially
706 try:
707 prop = self._props[k]
708 except KeyError:
709 prop = None
710 if prop is None:
711 # property no longer exists
712 comments['no_exist'] = _('''<em>The indicated property
713 no longer exists</em>''')
714 cell.append('<em>%s: %s</em>\n'%(k, str(args[k])))
715 continue
717 if args[k] and (isinstance(prop, hyperdb.Multilink) or
718 isinstance(prop, hyperdb.Link)):
719 # figure what the link class is
720 classname = prop.classname
721 try:
722 linkcl = self._db.getclass(classname)
723 except KeyError:
724 labelprop = None
725 comments[classname] = _('''The linked class
726 %(classname)s no longer exists''')%locals()
727 labelprop = linkcl.labelprop(1)
728 try:
729 template = find_template(self._db.config.TEMPLATES,
730 classname, 'item')
731 if template[1].startswith('_generic'):
732 raise NoTemplate, 'not really...'
733 hrefable = 1
734 except NoTemplate:
735 hrefable = 0
737 if isinstance(prop, hyperdb.Multilink) and args[k]:
738 ml = []
739 for linkid in args[k]:
740 if isinstance(linkid, type(())):
741 sublabel = linkid[0] + ' '
742 linkids = linkid[1]
743 else:
744 sublabel = ''
745 linkids = [linkid]
746 subml = []
747 for linkid in linkids:
748 label = classname + linkid
749 # if we have a label property, try to use it
750 # TODO: test for node existence even when
751 # there's no labelprop!
752 try:
753 if labelprop is not None and \
754 labelprop != 'id':
755 label = linkcl.get(linkid, labelprop)
756 except IndexError:
757 comments['no_link'] = _('''<strike>The
758 linked node no longer
759 exists</strike>''')
760 subml.append('<strike>%s</strike>'%label)
761 else:
762 if hrefable:
763 subml.append('<a href="%s%s">%s</a>'%(
764 classname, linkid, label))
765 else:
766 subml.append(label)
767 ml.append(sublabel + ', '.join(subml))
768 cell.append('%s:\n %s'%(k, ', '.join(ml)))
769 elif isinstance(prop, hyperdb.Link) and args[k]:
770 label = classname + args[k]
771 # if we have a label property, try to use it
772 # TODO: test for node existence even when
773 # there's no labelprop!
774 if labelprop is not None and labelprop != 'id':
775 try:
776 label = linkcl.get(args[k], labelprop)
777 except IndexError:
778 comments['no_link'] = _('''<strike>The
779 linked node no longer
780 exists</strike>''')
781 cell.append(' <strike>%s</strike>,\n'%label)
782 # "flag" this is done .... euwww
783 label = None
784 if label is not None:
785 if hrefable:
786 old = '<a href="%s%s">%s</a>'%(classname, args[k], label)
787 else:
788 old = label;
789 cell.append('%s: %s' % (k,old))
790 if current.has_key(k):
791 cell[-1] += ' -> %s'%current[k]
792 current[k] = old
794 elif isinstance(prop, hyperdb.Date) and args[k]:
795 d = date.Date(args[k]).local(timezone)
796 cell.append('%s: %s'%(k, str(d)))
797 if current.has_key(k):
798 cell[-1] += ' -> %s' % current[k]
799 current[k] = str(d)
801 elif isinstance(prop, hyperdb.Interval) and args[k]:
802 d = date.Interval(args[k])
803 cell.append('%s: %s'%(k, str(d)))
804 if current.has_key(k):
805 cell[-1] += ' -> %s'%current[k]
806 current[k] = str(d)
808 elif isinstance(prop, hyperdb.String) and args[k]:
809 cell.append('%s: %s'%(k, cgi.escape(args[k])))
810 if current.has_key(k):
811 cell[-1] += ' -> %s'%current[k]
812 current[k] = cgi.escape(args[k])
814 elif not args[k]:
815 if current.has_key(k):
816 cell.append('%s: %s'%(k, current[k]))
817 current[k] = '(no value)'
818 else:
819 cell.append('%s: (no value)'%k)
821 else:
822 cell.append('%s: %s'%(k, str(args[k])))
823 if current.has_key(k):
824 cell[-1] += ' -> %s'%current[k]
825 current[k] = str(args[k])
827 arg_s = '<br />'.join(cell)
828 else:
829 # unkown event!!
830 comments['unknown'] = _('''<strong><em>This event is not
831 handled by the history display!</em></strong>''')
832 arg_s = '<strong><em>' + str(args) + '</em></strong>'
833 date_s = date_s.replace(' ', ' ')
834 # if the user's an itemid, figure the username (older journals
835 # have the username)
836 if dre.match(user):
837 user = self._db.user.get(user, 'username')
838 l.append('<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>'%(
839 date_s, user, action, arg_s))
840 if comments:
841 l.append(_('<tr><td colspan=4><strong>Note:</strong></td></tr>'))
842 for entry in comments.values():
843 l.append('<tr><td colspan=4>%s</td></tr>'%entry)
844 l.append('</table>')
845 return '\n'.join(l)
847 def renderQueryForm(self):
848 ''' Render this item, which is a query, as a search form.
849 '''
850 # create a new request and override the specified args
851 req = HTMLRequest(self._client)
852 req.classname = self._klass.get(self._nodeid, 'klass')
853 name = self._klass.get(self._nodeid, 'name')
854 req.updateFromURL(self._klass.get(self._nodeid, 'url') +
855 '&@queryname=%s'%urllib.quote(name))
857 # new template, using the specified classname and request
858 pt = Templates(self._db.config.TEMPLATES).get(req.classname, 'search')
860 # use our fabricated request
861 return pt.render(self._client, req.classname, req)
863 class HTMLUserPermission:
865 def is_edit_ok(self):
866 ''' Is the user allowed to Edit the current class?
867 Also check whether this is the current user's info.
868 '''
869 return self._user_perm_check('Edit')
871 def is_view_ok(self):
872 ''' Is the user allowed to View the current class?
873 Also check whether this is the current user's info.
874 '''
875 return self._user_perm_check('View')
877 def _user_perm_check(self, type):
878 # some users may view / edit all users
879 s = self._db.security
880 userid = self._client.userid
881 if s.hasPermission(type, userid, self._classname):
882 return 1
884 # users may view their own info
885 is_anonymous = self._db.user.get(userid, 'username') == 'anonymous'
886 if getattr(self, '_nodeid', None) == userid and not is_anonymous:
887 return 1
889 # may anonymous users register?
890 if (is_anonymous and s.hasPermission('Web Registration', userid,
891 self._classname)):
892 return 1
894 # nope, no access here
895 return 0
897 class HTMLUserClass(HTMLUserPermission, HTMLClass):
898 pass
900 class HTMLUser(HTMLUserPermission, HTMLItem):
901 ''' Accesses through the *user* (a special case of item)
902 '''
903 def __init__(self, client, classname, nodeid, anonymous=0):
904 HTMLItem.__init__(self, client, 'user', nodeid, anonymous)
905 self._default_classname = client.classname
907 # used for security checks
908 self._security = client.db.security
910 _marker = []
911 def hasPermission(self, permission, classname=_marker):
912 ''' Determine if the user has the Permission.
914 The class being tested defaults to the template's class, but may
915 be overidden for this test by suppling an alternate classname.
916 '''
917 if classname is self._marker:
918 classname = self._default_classname
919 return self._security.hasPermission(permission, self._nodeid, classname)
921 class HTMLProperty(HTMLInputMixin, HTMLPermissions):
922 ''' String, Number, Date, Interval HTMLProperty
924 Has useful attributes:
926 _name the name of the property
927 _value the value of the property if any
929 A wrapper object which may be stringified for the plain() behaviour.
930 '''
931 def __init__(self, client, classname, nodeid, prop, name, value,
932 anonymous=0):
933 self._client = client
934 self._db = client.db
935 self._classname = classname
936 self._nodeid = nodeid
937 self._prop = prop
938 self._value = value
939 self._anonymous = anonymous
940 self._name = name
941 if not anonymous:
942 self._formname = '%s%s@%s'%(classname, nodeid, name)
943 else:
944 self._formname = name
946 HTMLInputMixin.__init__(self)
948 def __repr__(self):
949 return '<HTMLProperty(0x%x) %s %r %r>'%(id(self), self._formname,
950 self._prop, self._value)
951 def __str__(self):
952 return self.plain()
953 def __cmp__(self, other):
954 if isinstance(other, HTMLProperty):
955 return cmp(self._value, other._value)
956 return cmp(self._value, other)
958 def is_edit_ok(self):
959 ''' Is the user allowed to Edit the current class?
960 '''
961 thing = HTMLDatabase(self._client)[self._classname]
962 if self._nodeid:
963 # this is a special-case for the User class where permission's
964 # on a per-item basis :(
965 thing = thing.getItem(self._nodeid)
966 return thing.is_edit_ok()
968 def is_view_ok(self):
969 ''' Is the user allowed to View the current class?
970 '''
971 thing = HTMLDatabase(self._client)[self._classname]
972 if self._nodeid:
973 # this is a special-case for the User class where permission's
974 # on a per-item basis :(
975 thing = thing.getItem(self._nodeid)
976 return thing.is_view_ok()
978 class StringHTMLProperty(HTMLProperty):
979 hyper_re = re.compile(r'((?P<url>\w{3,6}://\S+)|'
980 r'(?P<email>[-+=%/\w\.]+@[\w\.\-]+)|'
981 r'(?P<item>(?P<class>[a-z_]+)(?P<id>\d+)))')
982 def _hyper_repl(self, match):
983 if match.group('url'):
984 s = match.group('url')
985 return '<a href="%s">%s</a>'%(s, s)
986 elif match.group('email'):
987 s = match.group('email')
988 return '<a href="mailto:%s">%s</a>'%(s, s)
989 else:
990 s = match.group('item')
991 s1 = match.group('class')
992 s2 = match.group('id')
993 try:
994 # make sure s1 is a valid tracker classname
995 cl = self._db.getclass(s1)
996 if not cl.hasnode(s2):
997 raise KeyError, 'oops'
998 return '<a href="%s">%s%s</a>'%(s, s1, s2)
999 except KeyError:
1000 return '%s%s'%(s1, s2)
1002 def hyperlinked(self):
1003 ''' Render a "hyperlinked" version of the text '''
1004 return self.plain(hyperlink=1)
1006 def plain(self, escape=0, hyperlink=0):
1007 '''Render a "plain" representation of the property
1009 - "escape" turns on/off HTML quoting
1010 - "hyperlink" turns on/off in-text hyperlinking of URLs, email
1011 addresses and designators
1012 '''
1013 self.view_check()
1015 if self._value is None:
1016 return ''
1017 if escape:
1018 s = cgi.escape(str(self._value))
1019 else:
1020 s = str(self._value)
1021 if hyperlink:
1022 # no, we *must* escape this text
1023 if not escape:
1024 s = cgi.escape(s)
1025 s = self.hyper_re.sub(self._hyper_repl, s)
1026 return s
1028 def stext(self, escape=0):
1029 ''' Render the value of the property as StructuredText.
1031 This requires the StructureText module to be installed separately.
1032 '''
1033 self.view_check()
1035 s = self.plain(escape=escape)
1036 if not StructuredText:
1037 return s
1038 return StructuredText(s,level=1,header=0)
1040 def field(self, size = 30):
1041 ''' Render the property as a field in HTML.
1043 If not editable, just display the value via plain().
1044 '''
1045 self.view_check()
1047 if self._value is None:
1048 value = ''
1049 else:
1050 value = cgi.escape(str(self._value))
1052 if self.is_edit_ok():
1053 value = '"'.join(value.split('"'))
1054 return self.input(name=self._formname,value=value,size=size)
1056 return self.plain()
1058 def multiline(self, escape=0, rows=5, cols=40):
1059 ''' Render a multiline form edit field for the property.
1061 If not editable, just display the plain() value in a <pre> tag.
1062 '''
1063 self.view_check()
1065 if self._value is None:
1066 value = ''
1067 else:
1068 value = cgi.escape(str(self._value))
1070 if self.is_edit_ok():
1071 value = '"'.join(value.split('"'))
1072 return '<textarea name="%s" rows="%s" cols="%s">%s</textarea>'%(
1073 self._formname, rows, cols, value)
1075 return '<pre>%s</pre>'%self.plain()
1077 def email(self, escape=1):
1078 ''' Render the value of the property as an obscured email address
1079 '''
1080 self.view_check()
1082 if self._value is None:
1083 value = ''
1084 else:
1085 value = str(self._value)
1086 if value.find('@') != -1:
1087 name, domain = value.split('@')
1088 domain = ' '.join(domain.split('.')[:-1])
1089 name = name.replace('.', ' ')
1090 value = '%s at %s ...'%(name, domain)
1091 else:
1092 value = value.replace('.', ' ')
1093 if escape:
1094 value = cgi.escape(value)
1095 return value
1097 class PasswordHTMLProperty(HTMLProperty):
1098 def plain(self):
1099 ''' Render a "plain" representation of the property
1100 '''
1101 self.view_check()
1103 if self._value is None:
1104 return ''
1105 return _('*encrypted*')
1107 def field(self, size = 30):
1108 ''' Render a form edit field for the property.
1110 If not editable, just display the value via plain().
1111 '''
1112 self.view_check()
1114 if self.is_edit_ok():
1115 return self.input(type="password", name=self._formname, size=size)
1117 return self.plain()
1119 def confirm(self, size = 30):
1120 ''' Render a second form edit field for the property, used for
1121 confirmation that the user typed the password correctly. Generates
1122 a field with name "@confirm@name".
1124 If not editable, display nothing.
1125 '''
1126 self.view_check()
1128 if self.is_edit_ok():
1129 return self.input(type="password",
1130 name="@confirm@%s"%self._formname, size=size)
1132 return ''
1134 class NumberHTMLProperty(HTMLProperty):
1135 def plain(self):
1136 ''' Render a "plain" representation of the property
1137 '''
1138 self.view_check()
1140 return str(self._value)
1142 def field(self, size = 30):
1143 ''' Render a form edit field for the property.
1145 If not editable, just display the value via plain().
1146 '''
1147 self.view_check()
1149 if self._value is None:
1150 value = ''
1151 else:
1152 value = cgi.escape(str(self._value))
1154 if self.is_edit_ok():
1155 value = '"'.join(value.split('"'))
1156 return self.input(name=self._formname,value=value,size=size)
1158 return self.plain()
1160 def __int__(self):
1161 ''' Return an int of me
1162 '''
1163 return int(self._value)
1165 def __float__(self):
1166 ''' Return a float of me
1167 '''
1168 return float(self._value)
1171 class BooleanHTMLProperty(HTMLProperty):
1172 def plain(self):
1173 ''' Render a "plain" representation of the property
1174 '''
1175 self.view_check()
1177 if self._value is None:
1178 return ''
1179 return self._value and "Yes" or "No"
1181 def field(self):
1182 ''' Render a form edit field for the property
1184 If not editable, just display the value via plain().
1185 '''
1186 self.view_check()
1188 if not is_edit_ok():
1189 return self.plain()
1191 checked = self._value and "checked" or ""
1192 if self._value:
1193 s = self.input(type="radio", name=self._formname, value="yes",
1194 checked="checked")
1195 s += 'Yes'
1196 s +=self.input(type="radio", name=self._formname, value="no")
1197 s += 'No'
1198 else:
1199 s = self.input(type="radio", name=self._formname, value="yes")
1200 s += 'Yes'
1201 s +=self.input(type="radio", name=self._formname, value="no",
1202 checked="checked")
1203 s += 'No'
1204 return s
1206 class DateHTMLProperty(HTMLProperty):
1207 def plain(self):
1208 ''' Render a "plain" representation of the property
1209 '''
1210 self.view_check()
1212 if self._value is None:
1213 return ''
1214 return str(self._value.local(self._db.getUserTimezone()))
1216 def now(self):
1217 ''' Return the current time.
1219 This is useful for defaulting a new value. Returns a
1220 DateHTMLProperty.
1221 '''
1222 self.view_check()
1224 return DateHTMLProperty(self._client, self._classname, self._nodeid,
1225 self._prop, self._formname, date.Date('.'))
1227 def field(self, size = 30):
1228 ''' Render a form edit field for the property
1230 If not editable, just display the value via plain().
1231 '''
1232 self.view_check()
1234 if self._value is None:
1235 value = ''
1236 else:
1237 tz = self._db.getUserTimezone()
1238 value = cgi.escape(str(self._value.local(tz)))
1240 if is_edit_ok():
1241 value = '"'.join(value.split('"'))
1242 return self.input(name=self._formname,value=value,size=size)
1244 return self.plain()
1246 def reldate(self, pretty=1):
1247 ''' Render the interval between the date and now.
1249 If the "pretty" flag is true, then make the display pretty.
1250 '''
1251 self.view_check()
1253 if not self._value:
1254 return ''
1256 # figure the interval
1257 interval = self._value - date.Date('.')
1258 if pretty:
1259 return interval.pretty()
1260 return str(interval)
1262 _marker = []
1263 def pretty(self, format=_marker):
1264 ''' Render the date in a pretty format (eg. month names, spaces).
1266 The format string is a standard python strftime format string.
1267 Note that if the day is zero, and appears at the start of the
1268 string, then it'll be stripped from the output. This is handy
1269 for the situatin when a date only specifies a month and a year.
1270 '''
1271 self.view_check()
1273 if format is not self._marker:
1274 return self._value.pretty(format)
1275 else:
1276 return self._value.pretty()
1278 def local(self, offset):
1279 ''' Return the date/time as a local (timezone offset) date/time.
1280 '''
1281 self.view_check()
1283 return DateHTMLProperty(self._client, self._classname, self._nodeid,
1284 self._prop, self._formname, self._value.local(offset))
1286 class IntervalHTMLProperty(HTMLProperty):
1287 def plain(self):
1288 ''' Render a "plain" representation of the property
1289 '''
1290 self.view_check()
1292 if self._value is None:
1293 return ''
1294 return str(self._value)
1296 def pretty(self):
1297 ''' Render the interval in a pretty format (eg. "yesterday")
1298 '''
1299 self.view_check()
1301 return self._value.pretty()
1303 def field(self, size = 30):
1304 ''' Render a form edit field for the property
1306 If not editable, just display the value via plain().
1307 '''
1308 self.view_check()
1310 if self._value is None:
1311 value = ''
1312 else:
1313 value = cgi.escape(str(self._value))
1315 if is_edit_ok():
1316 value = '"'.join(value.split('"'))
1317 return self.input(name=self._formname,value=value,size=size)
1319 return self.plain()
1321 class LinkHTMLProperty(HTMLProperty):
1322 ''' Link HTMLProperty
1323 Include the above as well as being able to access the class
1324 information. Stringifying the object itself results in the value
1325 from the item being displayed. Accessing attributes of this object
1326 result in the appropriate entry from the class being queried for the
1327 property accessed (so item/assignedto/name would look up the user
1328 entry identified by the assignedto property on item, and then the
1329 name property of that user)
1330 '''
1331 def __init__(self, *args, **kw):
1332 HTMLProperty.__init__(self, *args, **kw)
1333 # if we're representing a form value, then the -1 from the form really
1334 # should be a None
1335 if str(self._value) == '-1':
1336 self._value = None
1338 def __getattr__(self, attr):
1339 ''' return a new HTMLItem '''
1340 #print 'Link.getattr', (self, attr, self._value)
1341 if not self._value:
1342 raise AttributeError, "Can't access missing value"
1343 if self._prop.classname == 'user':
1344 klass = HTMLUser
1345 else:
1346 klass = HTMLItem
1347 i = klass(self._client, self._prop.classname, self._value)
1348 return getattr(i, attr)
1350 def plain(self, escape=0):
1351 ''' Render a "plain" representation of the property
1352 '''
1353 self.view_check()
1355 if self._value is None:
1356 return ''
1357 linkcl = self._db.classes[self._prop.classname]
1358 k = linkcl.labelprop(1)
1359 value = str(linkcl.get(self._value, k))
1360 if escape:
1361 value = cgi.escape(value)
1362 return value
1364 def field(self, showid=0, size=None):
1365 ''' Render a form edit field for the property
1367 If not editable, just display the value via plain().
1368 '''
1369 self.view_check()
1371 if not self.is_edit_ok():
1372 return self.plain()
1374 # edit field
1375 linkcl = self._db.getclass(self._prop.classname)
1376 if self._value is None:
1377 value = ''
1378 else:
1379 k = linkcl.getkey()
1380 if k:
1381 label = linkcl.get(self._value, k)
1382 else:
1383 label = self._value
1384 value = cgi.escape(str(self._value))
1385 value = '"'.join(value.split('"'))
1386 return '<input name="%s" value="%s" size="%s">'%(self._formname,
1387 label, size)
1389 def menu(self, size=None, height=None, showid=0, additional=[],
1390 sort_on=None, **conditions):
1391 ''' Render a form select list for this property
1393 If not editable, just display the value via plain().
1394 '''
1395 self.view_check()
1397 if not self.is_edit_ok():
1398 return self.plain()
1400 value = self._value
1402 linkcl = self._db.getclass(self._prop.classname)
1403 l = ['<select name="%s">'%self._formname]
1404 k = linkcl.labelprop(1)
1405 s = ''
1406 if value is None:
1407 s = 'selected="selected" '
1408 l.append(_('<option %svalue="-1">- no selection -</option>')%s)
1409 if linkcl.getprops().has_key('order'):
1410 sort_on = ('+', 'order')
1411 else:
1412 if sort_on is None:
1413 sort_on = ('+', linkcl.labelprop())
1414 else:
1415 sort_on = ('+', sort_on)
1416 options = linkcl.filter(None, conditions, sort_on, (None, None))
1418 # make sure we list the current value if it's retired
1419 if self._value and self._value not in options:
1420 options.insert(0, self._value)
1422 for optionid in options:
1423 # get the option value, and if it's None use an empty string
1424 option = linkcl.get(optionid, k) or ''
1426 # figure if this option is selected
1427 s = ''
1428 if value in [optionid, option]:
1429 s = 'selected="selected" '
1431 # figure the label
1432 if showid:
1433 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1434 else:
1435 lab = option
1437 # truncate if it's too long
1438 if size is not None and len(lab) > size:
1439 lab = lab[:size-3] + '...'
1440 if additional:
1441 m = []
1442 for propname in additional:
1443 m.append(linkcl.get(optionid, propname))
1444 lab = lab + ' (%s)'%', '.join(map(str, m))
1446 # and generate
1447 lab = cgi.escape(lab)
1448 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
1449 l.append('</select>')
1450 return '\n'.join(l)
1451 # def checklist(self, ...)
1453 class MultilinkHTMLProperty(HTMLProperty):
1454 ''' Multilink HTMLProperty
1456 Also be iterable, returning a wrapper object like the Link case for
1457 each entry in the multilink.
1458 '''
1459 def __init__(self, *args, **kwargs):
1460 HTMLProperty.__init__(self, *args, **kwargs)
1461 if self._value:
1462 sortfun = make_sort_function(self._db, self._prop.classname)
1463 self._value.sort(sortfun)
1465 def __len__(self):
1466 ''' length of the multilink '''
1467 return len(self._value)
1469 def __getattr__(self, attr):
1470 ''' no extended attribute accesses make sense here '''
1471 raise AttributeError, attr
1473 def __getitem__(self, num):
1474 ''' iterate and return a new HTMLItem
1475 '''
1476 #print 'Multi.getitem', (self, num)
1477 value = self._value[num]
1478 if self._prop.classname == 'user':
1479 klass = HTMLUser
1480 else:
1481 klass = HTMLItem
1482 return klass(self._client, self._prop.classname, value)
1484 def __contains__(self, value):
1485 ''' Support the "in" operator. We have to make sure the passed-in
1486 value is a string first, not a HTMLProperty.
1487 '''
1488 return str(value) in self._value
1490 def reverse(self):
1491 ''' return the list in reverse order
1492 '''
1493 l = self._value[:]
1494 l.reverse()
1495 if self._prop.classname == 'user':
1496 klass = HTMLUser
1497 else:
1498 klass = HTMLItem
1499 return [klass(self._client, self._prop.classname, value) for value in l]
1501 def plain(self, escape=0):
1502 ''' Render a "plain" representation of the property
1503 '''
1504 self.view_check()
1506 linkcl = self._db.classes[self._prop.classname]
1507 k = linkcl.labelprop(1)
1508 labels = []
1509 for v in self._value:
1510 labels.append(linkcl.get(v, k))
1511 value = ', '.join(labels)
1512 if escape:
1513 value = cgi.escape(value)
1514 return value
1516 def field(self, size=30, showid=0):
1517 ''' Render a form edit field for the property
1519 If not editable, just display the value via plain().
1520 '''
1521 self.view_check()
1523 if not self.is_edit_ok():
1524 return self.plain()
1526 linkcl = self._db.getclass(self._prop.classname)
1527 value = self._value[:]
1528 # map the id to the label property
1529 if not linkcl.getkey():
1530 showid=1
1531 if not showid:
1532 k = linkcl.labelprop(1)
1533 value = [linkcl.get(v, k) for v in value]
1534 value = cgi.escape(','.join(value))
1535 return self.input(name=self._formname,size=size,value=value)
1537 def menu(self, size=None, height=None, showid=0, additional=[],
1538 sort_on=None, **conditions):
1539 ''' Render a form select list for this property
1541 If not editable, just display the value via plain().
1542 '''
1543 self.view_check()
1545 if not self.is_edit_ok():
1546 return self.plain()
1548 value = self._value
1550 linkcl = self._db.getclass(self._prop.classname)
1551 if sort_on is None:
1552 sort_on = ('+', find_sort_key(linkcl))
1553 else:
1554 sort_on = ('+', sort_on)
1555 options = linkcl.filter(None, conditions, sort_on)
1556 height = height or min(len(options), 7)
1557 l = ['<select multiple name="%s" size="%s">'%(self._formname, height)]
1558 k = linkcl.labelprop(1)
1560 # make sure we list the current values if they're retired
1561 for val in value:
1562 if val not in options:
1563 options.insert(0, val)
1565 for optionid in options:
1566 # get the option value, and if it's None use an empty string
1567 option = linkcl.get(optionid, k) or ''
1569 # figure if this option is selected
1570 s = ''
1571 if optionid in value or option in value:
1572 s = 'selected="selected" '
1574 # figure the label
1575 if showid:
1576 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1577 else:
1578 lab = option
1579 # truncate if it's too long
1580 if size is not None and len(lab) > size:
1581 lab = lab[:size-3] + '...'
1582 if additional:
1583 m = []
1584 for propname in additional:
1585 m.append(linkcl.get(optionid, propname))
1586 lab = lab + ' (%s)'%', '.join(m)
1588 # and generate
1589 lab = cgi.escape(lab)
1590 l.append('<option %svalue="%s">%s</option>'%(s, optionid,
1591 lab))
1592 l.append('</select>')
1593 return '\n'.join(l)
1595 # set the propclasses for HTMLItem
1596 propclasses = (
1597 (hyperdb.String, StringHTMLProperty),
1598 (hyperdb.Number, NumberHTMLProperty),
1599 (hyperdb.Boolean, BooleanHTMLProperty),
1600 (hyperdb.Date, DateHTMLProperty),
1601 (hyperdb.Interval, IntervalHTMLProperty),
1602 (hyperdb.Password, PasswordHTMLProperty),
1603 (hyperdb.Link, LinkHTMLProperty),
1604 (hyperdb.Multilink, MultilinkHTMLProperty),
1605 )
1607 def make_sort_function(db, classname, sort_on=None):
1608 '''Make a sort function for a given class
1609 '''
1610 linkcl = db.getclass(classname)
1611 if sort_on is None:
1612 sort_on = find_sort_key(linkcl)
1613 def sortfunc(a, b):
1614 return cmp(linkcl.get(a, sort_on), linkcl.get(b, sort_on))
1615 return sortfunc
1617 def find_sort_key(linkcl):
1618 if linkcl.getprops().has_key('order'):
1619 return 'order'
1620 else:
1621 return linkcl.labelprop()
1623 def handleListCGIValue(value):
1624 ''' Value is either a single item or a list of items. Each item has a
1625 .value that we're actually interested in.
1626 '''
1627 if isinstance(value, type([])):
1628 return [value.value for value in value]
1629 else:
1630 value = value.value.strip()
1631 if not value:
1632 return []
1633 return value.split(',')
1635 class ShowDict:
1636 ''' A convenience access to the :columns index parameters
1637 '''
1638 def __init__(self, columns):
1639 self.columns = {}
1640 for col in columns:
1641 self.columns[col] = 1
1642 def __getitem__(self, name):
1643 return self.columns.has_key(name)
1645 class HTMLRequest(HTMLInputMixin):
1646 '''The *request*, holding the CGI form and environment.
1648 - "form" the CGI form as a cgi.FieldStorage
1649 - "env" the CGI environment variables
1650 - "base" the base URL for this instance
1651 - "user" a HTMLUser instance for this user
1652 - "classname" the current classname (possibly None)
1653 - "template" the current template (suffix, also possibly None)
1655 Index args:
1657 - "columns" dictionary of the columns to display in an index page
1658 - "show" a convenience access to columns - request/show/colname will
1659 be true if the columns should be displayed, false otherwise
1660 - "sort" index sort column (direction, column name)
1661 - "group" index grouping property (direction, column name)
1662 - "filter" properties to filter the index on
1663 - "filterspec" values to filter the index on
1664 - "search_text" text to perform a full-text search on for an index
1665 '''
1666 def __init__(self, client):
1667 # _client is needed by HTMLInputMixin
1668 self._client = self.client = client
1670 # easier access vars
1671 self.form = client.form
1672 self.env = client.env
1673 self.base = client.base
1674 self.user = HTMLUser(client, 'user', client.userid)
1676 # store the current class name and action
1677 self.classname = client.classname
1678 self.template = client.template
1680 # the special char to use for special vars
1681 self.special_char = '@'
1683 HTMLInputMixin.__init__(self)
1685 self._post_init()
1687 def _post_init(self):
1688 ''' Set attributes based on self.form
1689 '''
1690 # extract the index display information from the form
1691 self.columns = []
1692 for name in ':columns @columns'.split():
1693 if self.form.has_key(name):
1694 self.special_char = name[0]
1695 self.columns = handleListCGIValue(self.form[name])
1696 break
1697 self.show = ShowDict(self.columns)
1699 # sorting
1700 self.sort = (None, None)
1701 for name in ':sort @sort'.split():
1702 if self.form.has_key(name):
1703 self.special_char = name[0]
1704 sort = self.form[name].value
1705 if sort.startswith('-'):
1706 self.sort = ('-', sort[1:])
1707 else:
1708 self.sort = ('+', sort)
1709 if self.form.has_key(self.special_char+'sortdir'):
1710 self.sort = ('-', self.sort[1])
1712 # grouping
1713 self.group = (None, None)
1714 for name in ':group @group'.split():
1715 if self.form.has_key(name):
1716 self.special_char = name[0]
1717 group = self.form[name].value
1718 if group.startswith('-'):
1719 self.group = ('-', group[1:])
1720 else:
1721 self.group = ('+', group)
1722 if self.form.has_key(self.special_char+'groupdir'):
1723 self.group = ('-', self.group[1])
1725 # filtering
1726 self.filter = []
1727 for name in ':filter @filter'.split():
1728 if self.form.has_key(name):
1729 self.special_char = name[0]
1730 self.filter = handleListCGIValue(self.form[name])
1732 self.filterspec = {}
1733 db = self.client.db
1734 if self.classname is not None:
1735 props = db.getclass(self.classname).getprops()
1736 for name in self.filter:
1737 if not self.form.has_key(name):
1738 continue
1739 prop = props[name]
1740 fv = self.form[name]
1741 if (isinstance(prop, hyperdb.Link) or
1742 isinstance(prop, hyperdb.Multilink)):
1743 self.filterspec[name] = lookupIds(db, prop,
1744 handleListCGIValue(fv))
1745 else:
1746 if isinstance(fv, type([])):
1747 self.filterspec[name] = [v.value for v in fv]
1748 else:
1749 self.filterspec[name] = fv.value
1751 # full-text search argument
1752 self.search_text = None
1753 for name in ':search_text @search_text'.split():
1754 if self.form.has_key(name):
1755 self.special_char = name[0]
1756 self.search_text = self.form[name].value
1758 # pagination - size and start index
1759 # figure batch args
1760 self.pagesize = 50
1761 for name in ':pagesize @pagesize'.split():
1762 if self.form.has_key(name):
1763 self.special_char = name[0]
1764 self.pagesize = int(self.form[name].value)
1766 self.startwith = 0
1767 for name in ':startwith @startwith'.split():
1768 if self.form.has_key(name):
1769 self.special_char = name[0]
1770 self.startwith = int(self.form[name].value)
1772 def updateFromURL(self, url):
1773 ''' Parse the URL for query args, and update my attributes using the
1774 values.
1775 '''
1776 env = {'QUERY_STRING': url}
1777 self.form = cgi.FieldStorage(environ=env)
1779 self._post_init()
1781 def update(self, kwargs):
1782 ''' Update my attributes using the keyword args
1783 '''
1784 self.__dict__.update(kwargs)
1785 if kwargs.has_key('columns'):
1786 self.show = ShowDict(self.columns)
1788 def description(self):
1789 ''' Return a description of the request - handle for the page title.
1790 '''
1791 s = [self.client.db.config.TRACKER_NAME]
1792 if self.classname:
1793 if self.client.nodeid:
1794 s.append('- %s%s'%(self.classname, self.client.nodeid))
1795 else:
1796 if self.template == 'item':
1797 s.append('- new %s'%self.classname)
1798 elif self.template == 'index':
1799 s.append('- %s index'%self.classname)
1800 else:
1801 s.append('- %s %s'%(self.classname, self.template))
1802 else:
1803 s.append('- home')
1804 return ' '.join(s)
1806 def __str__(self):
1807 d = {}
1808 d.update(self.__dict__)
1809 f = ''
1810 for k in self.form.keys():
1811 f += '\n %r=%r'%(k,handleListCGIValue(self.form[k]))
1812 d['form'] = f
1813 e = ''
1814 for k,v in self.env.items():
1815 e += '\n %r=%r'%(k, v)
1816 d['env'] = e
1817 return '''
1818 form: %(form)s
1819 base: %(base)r
1820 classname: %(classname)r
1821 template: %(template)r
1822 columns: %(columns)r
1823 sort: %(sort)r
1824 group: %(group)r
1825 filter: %(filter)r
1826 search_text: %(search_text)r
1827 pagesize: %(pagesize)r
1828 startwith: %(startwith)r
1829 env: %(env)s
1830 '''%d
1832 def indexargs_form(self, columns=1, sort=1, group=1, filter=1,
1833 filterspec=1):
1834 ''' return the current index args as form elements '''
1835 l = []
1836 sc = self.special_char
1837 s = self.input(type="hidden",name="%s",value="%s")
1838 if columns and self.columns:
1839 l.append(s%(sc+'columns', ','.join(self.columns)))
1840 if sort and self.sort[1] is not None:
1841 if self.sort[0] == '-':
1842 val = '-'+self.sort[1]
1843 else:
1844 val = self.sort[1]
1845 l.append(s%(sc+'sort', val))
1846 if group and self.group[1] is not None:
1847 if self.group[0] == '-':
1848 val = '-'+self.group[1]
1849 else:
1850 val = self.group[1]
1851 l.append(s%(sc+'group', val))
1852 if filter and self.filter:
1853 l.append(s%(sc+'filter', ','.join(self.filter)))
1854 if filterspec:
1855 for k,v in self.filterspec.items():
1856 if type(v) == type([]):
1857 l.append(s%(k, ','.join(v)))
1858 else:
1859 l.append(s%(k, v))
1860 if self.search_text:
1861 l.append(s%(sc+'search_text', self.search_text))
1862 l.append(s%(sc+'pagesize', self.pagesize))
1863 l.append(s%(sc+'startwith', self.startwith))
1864 return '\n'.join(l)
1866 def indexargs_url(self, url, args):
1867 ''' Embed the current index args in a URL
1868 '''
1869 sc = self.special_char
1870 l = ['%s=%s'%(k,v) for k,v in args.items()]
1872 # pull out the special values (prefixed by @ or :)
1873 specials = {}
1874 for key in args.keys():
1875 if key[0] in '@:':
1876 specials[key[1:]] = args[key]
1878 # ok, now handle the specials we received in the request
1879 if self.columns and not specials.has_key('columns'):
1880 l.append(sc+'columns=%s'%(','.join(self.columns)))
1881 if self.sort[1] is not None and not specials.has_key('sort'):
1882 if self.sort[0] == '-':
1883 val = '-'+self.sort[1]
1884 else:
1885 val = self.sort[1]
1886 l.append(sc+'sort=%s'%val)
1887 if self.group[1] is not None and not specials.has_key('group'):
1888 if self.group[0] == '-':
1889 val = '-'+self.group[1]
1890 else:
1891 val = self.group[1]
1892 l.append(sc+'group=%s'%val)
1893 if self.filter and not specials.has_key('filter'):
1894 l.append(sc+'filter=%s'%(','.join(self.filter)))
1895 if self.search_text and not specials.has_key('search_text'):
1896 l.append(sc+'search_text=%s'%self.search_text)
1897 if not specials.has_key('pagesize'):
1898 l.append(sc+'pagesize=%s'%self.pagesize)
1899 if not specials.has_key('startwith'):
1900 l.append(sc+'startwith=%s'%self.startwith)
1902 # finally, the remainder of the filter args in the request
1903 for k,v in self.filterspec.items():
1904 if not args.has_key(k):
1905 if type(v) == type([]):
1906 l.append('%s=%s'%(k, ','.join(v)))
1907 else:
1908 l.append('%s=%s'%(k, v))
1909 return '%s?%s'%(url, '&'.join(l))
1910 indexargs_href = indexargs_url
1912 def base_javascript(self):
1913 return '''
1914 <script type="text/javascript">
1915 submitted = false;
1916 function submit_once() {
1917 if (submitted) {
1918 alert("Your request is being processed.\\nPlease be patient.");
1919 event.returnValue = 0; // work-around for IE
1920 return 0;
1921 }
1922 submitted = true;
1923 return 1;
1924 }
1926 function help_window(helpurl, width, height) {
1927 HelpWin = window.open('%s' + helpurl, 'RoundupHelpWindow', 'scrollbars=yes,resizable=yes,toolbar=no,height='+height+',width='+width);
1928 }
1929 </script>
1930 '''%self.base
1932 def batch(self):
1933 ''' Return a batch object for results from the "current search"
1934 '''
1935 filterspec = self.filterspec
1936 sort = self.sort
1937 group = self.group
1939 # get the list of ids we're batching over
1940 klass = self.client.db.getclass(self.classname)
1941 if self.search_text:
1942 matches = self.client.db.indexer.search(
1943 re.findall(r'\b\w{2,25}\b', self.search_text), klass)
1944 else:
1945 matches = None
1946 l = klass.filter(matches, filterspec, sort, group)
1948 # return the batch object, using IDs only
1949 return Batch(self.client, l, self.pagesize, self.startwith,
1950 classname=self.classname)
1952 # extend the standard ZTUtils Batch object to remove dependency on
1953 # Acquisition and add a couple of useful methods
1954 class Batch(ZTUtils.Batch):
1955 ''' Use me to turn a list of items, or item ids of a given class, into a
1956 series of batches.
1958 ========= ========================================================
1959 Parameter Usage
1960 ========= ========================================================
1961 sequence a list of HTMLItems or item ids
1962 classname if sequence is a list of ids, this is the class of item
1963 size how big to make the sequence.
1964 start where to start (0-indexed) in the sequence.
1965 end where to end (0-indexed) in the sequence.
1966 orphan if the next batch would contain less items than this
1967 value, then it is combined with this batch
1968 overlap the number of items shared between adjacent batches
1969 ========= ========================================================
1971 Attributes: Note that the "start" attribute, unlike the
1972 argument, is a 1-based index (I know, lame). "first" is the
1973 0-based index. "length" is the actual number of elements in
1974 the batch.
1976 "sequence_length" is the length of the original, unbatched, sequence.
1977 '''
1978 def __init__(self, client, sequence, size, start, end=0, orphan=0,
1979 overlap=0, classname=None):
1980 self.client = client
1981 self.last_index = self.last_item = None
1982 self.current_item = None
1983 self.classname = classname
1984 self.sequence_length = len(sequence)
1985 ZTUtils.Batch.__init__(self, sequence, size, start, end, orphan,
1986 overlap)
1988 # overwrite so we can late-instantiate the HTMLItem instance
1989 def __getitem__(self, index):
1990 if index < 0:
1991 if index + self.end < self.first: raise IndexError, index
1992 return self._sequence[index + self.end]
1994 if index >= self.length:
1995 raise IndexError, index
1997 # move the last_item along - but only if the fetched index changes
1998 # (for some reason, index 0 is fetched twice)
1999 if index != self.last_index:
2000 self.last_item = self.current_item
2001 self.last_index = index
2003 item = self._sequence[index + self.first]
2004 if self.classname:
2005 # map the item ids to instances
2006 if self.classname == 'user':
2007 item = HTMLUser(self.client, self.classname, item)
2008 else:
2009 item = HTMLItem(self.client, self.classname, item)
2010 self.current_item = item
2011 return item
2013 def propchanged(self, property):
2014 ''' Detect if the property marked as being the group property
2015 changed in the last iteration fetch
2016 '''
2017 if (self.last_item is None or
2018 self.last_item[property] != self.current_item[property]):
2019 return 1
2020 return 0
2022 # override these 'cos we don't have access to acquisition
2023 def previous(self):
2024 if self.start == 1:
2025 return None
2026 return Batch(self.client, self._sequence, self._size,
2027 self.first - self._size + self.overlap, 0, self.orphan,
2028 self.overlap)
2030 def next(self):
2031 try:
2032 self._sequence[self.end]
2033 except IndexError:
2034 return None
2035 return Batch(self.client, self._sequence, self._size,
2036 self.end - self.overlap, 0, self.orphan, self.overlap)
2038 class TemplatingUtils:
2039 ''' Utilities for templating
2040 '''
2041 def __init__(self, client):
2042 self.client = client
2043 def Batch(self, sequence, size, start, end=0, orphan=0, overlap=0):
2044 return Batch(self.client, sequence, size, start, end, orphan,
2045 overlap)