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