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('View', 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 def is_edit_ok(self):
919 ''' Is the user allowed to Edit the current class?
920 '''
921 thing = HTMLDatabase(self._client)[self._classname]
922 if self._nodeid:
923 # this is a special-case for the User class where permission's
924 # on a per-item basis :(
925 thing = thing.getItem(self._nodeid)
926 return thing.is_edit_ok()
928 def is_view_ok(self):
929 ''' Is the user allowed to View the current class?
930 '''
931 thing = HTMLDatabase(self._client)[self._classname]
932 if self._nodeid:
933 # this is a special-case for the User class where permission's
934 # on a per-item basis :(
935 thing = thing.getItem(self._nodeid)
936 return thing.is_view_ok()
938 class StringHTMLProperty(HTMLProperty):
939 hyper_re = re.compile(r'((?P<url>\w{3,6}://\S+)|'
940 r'(?P<email>[-+=%/\w\.]+@[\w\.\-]+)|'
941 r'(?P<item>(?P<class>[a-z_]+)(?P<id>\d+)))')
942 def _hyper_repl(self, match):
943 if match.group('url'):
944 s = match.group('url')
945 return '<a href="%s">%s</a>'%(s, s)
946 elif match.group('email'):
947 s = match.group('email')
948 return '<a href="mailto:%s">%s</a>'%(s, s)
949 else:
950 s = match.group('item')
951 s1 = match.group('class')
952 s2 = match.group('id')
953 try:
954 # make sure s1 is a valid tracker classname
955 self._db.getclass(s1)
956 return '<a href="%s">%s %s</a>'%(s, s1, s2)
957 except KeyError:
958 return '%s%s'%(s1, s2)
960 def hyperlinked(self):
961 ''' Render a "hyperlinked" version of the text '''
962 return self.plain(hyperlink=1)
964 def plain(self, escape=0, hyperlink=0):
965 ''' Render a "plain" representation of the property
967 "escape" turns on/off HTML quoting
968 "hyperlink" turns on/off in-text hyperlinking of URLs, email
969 addresses and designators
970 '''
971 self.view_check()
973 if self._value is None:
974 return ''
975 if escape:
976 s = cgi.escape(str(self._value))
977 else:
978 s = str(self._value)
979 if hyperlink:
980 # no, we *must* escape this text
981 if not escape:
982 s = cgi.escape(s)
983 s = self.hyper_re.sub(self._hyper_repl, s)
984 return s
986 def stext(self, escape=0):
987 ''' Render the value of the property as StructuredText.
989 This requires the StructureText module to be installed separately.
990 '''
991 self.view_check()
993 s = self.plain(escape=escape)
994 if not StructuredText:
995 return s
996 return StructuredText(s,level=1,header=0)
998 def field(self, size = 30):
999 ''' Render the property as a field in HTML.
1001 If not editable, just display the value via plain().
1002 '''
1003 self.view_check()
1005 if self._value is None:
1006 value = ''
1007 else:
1008 value = cgi.escape(str(self._value))
1010 if self.is_edit_ok():
1011 value = '"'.join(value.split('"'))
1012 return self.input(name=self._formname,value=value,size=size)
1014 return self.plain()
1016 def multiline(self, escape=0, rows=5, cols=40):
1017 ''' Render a multiline form edit field for the property.
1019 If not editable, just display the plain() value in a <pre> tag.
1020 '''
1021 self.view_check()
1023 if self._value is None:
1024 value = ''
1025 else:
1026 value = cgi.escape(str(self._value))
1028 if self.is_edit_ok():
1029 value = '"'.join(value.split('"'))
1030 return '<textarea name="%s" rows="%s" cols="%s">%s</textarea>'%(
1031 self._formname, rows, cols, value)
1033 return '<pre>%s</pre>'%self.plain()
1035 def email(self, escape=1):
1036 ''' Render the value of the property as an obscured email address
1037 '''
1038 self.view_check()
1040 if self._value is None:
1041 value = ''
1042 else:
1043 value = str(self._value)
1044 if value.find('@') != -1:
1045 name, domain = value.split('@')
1046 domain = ' '.join(domain.split('.')[:-1])
1047 name = name.replace('.', ' ')
1048 value = '%s at %s ...'%(name, domain)
1049 else:
1050 value = value.replace('.', ' ')
1051 if escape:
1052 value = cgi.escape(value)
1053 return value
1055 class PasswordHTMLProperty(HTMLProperty):
1056 def plain(self):
1057 ''' Render a "plain" representation of the property
1058 '''
1059 self.view_check()
1061 if self._value is None:
1062 return ''
1063 return _('*encrypted*')
1065 def field(self, size = 30):
1066 ''' Render a form edit field for the property.
1068 If not editable, just display the value via plain().
1069 '''
1070 self.view_check()
1072 if self.is_edit_ok():
1073 return self.input(type="password", name=self._formname, size=size)
1075 return self.plain()
1077 def confirm(self, size = 30):
1078 ''' Render a second form edit field for the property, used for
1079 confirmation that the user typed the password correctly. Generates
1080 a field with name "@confirm@name".
1082 If not editable, display nothing.
1083 '''
1084 self.view_check()
1086 if self.is_edit_ok():
1087 return self.input(type="password",
1088 name="@confirm@%s"%self._formname, size=size)
1090 return ''
1092 class NumberHTMLProperty(HTMLProperty):
1093 def plain(self):
1094 ''' Render a "plain" representation of the property
1095 '''
1096 self.view_check()
1098 return str(self._value)
1100 def field(self, size = 30):
1101 ''' Render a form edit field for the property.
1103 If not editable, just display the value via plain().
1104 '''
1105 self.view_check()
1107 if self._value is None:
1108 value = ''
1109 else:
1110 value = cgi.escape(str(self._value))
1112 if self.is_edit_ok():
1113 value = '"'.join(value.split('"'))
1114 return self.input(name=self._formname,value=value,size=size)
1116 return self.plain()
1118 def __int__(self):
1119 ''' Return an int of me
1120 '''
1121 return int(self._value)
1123 def __float__(self):
1124 ''' Return a float of me
1125 '''
1126 return float(self._value)
1129 class BooleanHTMLProperty(HTMLProperty):
1130 def plain(self):
1131 ''' Render a "plain" representation of the property
1132 '''
1133 self.view_check()
1135 if self._value is None:
1136 return ''
1137 return self._value and "Yes" or "No"
1139 def field(self):
1140 ''' Render a form edit field for the property
1142 If not editable, just display the value via plain().
1143 '''
1144 self.view_check()
1146 if not is_edit_ok():
1147 return self.plain()
1149 checked = self._value and "checked" or ""
1150 if self._value:
1151 s = self.input(type="radio", name=self._formname, value="yes",
1152 checked="checked")
1153 s += 'Yes'
1154 s +=self.input(type="radio", name=self._formname, value="no")
1155 s += 'No'
1156 else:
1157 s = self.input(type="radio", name=self._formname, value="yes")
1158 s += 'Yes'
1159 s +=self.input(type="radio", name=self._formname, value="no",
1160 checked="checked")
1161 s += 'No'
1162 return s
1164 class DateHTMLProperty(HTMLProperty):
1165 def plain(self):
1166 ''' Render a "plain" representation of the property
1167 '''
1168 self.view_check()
1170 if self._value is None:
1171 return ''
1172 return str(self._value.local(self._db.getUserTimezone()))
1174 def now(self):
1175 ''' Return the current time.
1177 This is useful for defaulting a new value. Returns a
1178 DateHTMLProperty.
1179 '''
1180 self.view_check()
1182 return DateHTMLProperty(self._client, self._nodeid, self._prop,
1183 self._formname, date.Date('.'))
1185 def field(self, size = 30):
1186 ''' Render a form edit field for the property
1188 If not editable, just display the value via plain().
1189 '''
1190 self.view_check()
1192 if self._value is None:
1193 value = ''
1194 else:
1195 tz = self._db.getUserTimezone()
1196 value = cgi.escape(str(self._value.local(tz)))
1198 if is_edit_ok():
1199 value = '"'.join(value.split('"'))
1200 return self.input(name=self._formname,value=value,size=size)
1202 return self.plain()
1204 def reldate(self, pretty=1):
1205 ''' Render the interval between the date and now.
1207 If the "pretty" flag is true, then make the display pretty.
1208 '''
1209 self.view_check()
1211 if not self._value:
1212 return ''
1214 # figure the interval
1215 interval = self._value - date.Date('.')
1216 if pretty:
1217 return interval.pretty()
1218 return str(interval)
1220 _marker = []
1221 def pretty(self, format=_marker):
1222 ''' Render the date in a pretty format (eg. month names, spaces).
1224 The format string is a standard python strftime format string.
1225 Note that if the day is zero, and appears at the start of the
1226 string, then it'll be stripped from the output. This is handy
1227 for the situatin when a date only specifies a month and a year.
1228 '''
1229 self.view_check()
1231 if format is not self._marker:
1232 return self._value.pretty(format)
1233 else:
1234 return self._value.pretty()
1236 def local(self, offset):
1237 ''' Return the date/time as a local (timezone offset) date/time.
1238 '''
1239 self.view_check()
1241 return DateHTMLProperty(self._client, self._nodeid, self._prop,
1242 self._formname, self._value.local(offset))
1244 class IntervalHTMLProperty(HTMLProperty):
1245 def plain(self):
1246 ''' Render a "plain" representation of the property
1247 '''
1248 self.view_check()
1250 if self._value is None:
1251 return ''
1252 return str(self._value)
1254 def pretty(self):
1255 ''' Render the interval in a pretty format (eg. "yesterday")
1256 '''
1257 self.view_check()
1259 return self._value.pretty()
1261 def field(self, size = 30):
1262 ''' Render a form edit field for the property
1264 If not editable, just display the value via plain().
1265 '''
1266 self.view_check()
1268 if self._value is None:
1269 value = ''
1270 else:
1271 value = cgi.escape(str(self._value))
1273 if is_edit_ok():
1274 value = '"'.join(value.split('"'))
1275 return self.input(name=self._formname,value=value,size=size)
1277 return self.plain()
1279 class LinkHTMLProperty(HTMLProperty):
1280 ''' Link HTMLProperty
1281 Include the above as well as being able to access the class
1282 information. Stringifying the object itself results in the value
1283 from the item being displayed. Accessing attributes of this object
1284 result in the appropriate entry from the class being queried for the
1285 property accessed (so item/assignedto/name would look up the user
1286 entry identified by the assignedto property on item, and then the
1287 name property of that user)
1288 '''
1289 def __init__(self, *args, **kw):
1290 HTMLProperty.__init__(self, *args, **kw)
1291 # if we're representing a form value, then the -1 from the form really
1292 # should be a None
1293 if str(self._value) == '-1':
1294 self._value = None
1296 def __getattr__(self, attr):
1297 ''' return a new HTMLItem '''
1298 #print 'Link.getattr', (self, attr, self._value)
1299 if not self._value:
1300 raise AttributeError, "Can't access missing value"
1301 if self._prop.classname == 'user':
1302 klass = HTMLUser
1303 else:
1304 klass = HTMLItem
1305 i = klass(self._client, self._prop.classname, self._value)
1306 return getattr(i, attr)
1308 def plain(self, escape=0):
1309 ''' Render a "plain" representation of the property
1310 '''
1311 self.view_check()
1313 if self._value is None:
1314 return ''
1315 linkcl = self._db.classes[self._prop.classname]
1316 k = linkcl.labelprop(1)
1317 value = str(linkcl.get(self._value, k))
1318 if escape:
1319 value = cgi.escape(value)
1320 return value
1322 def field(self, showid=0, size=None):
1323 ''' Render a form edit field for the property
1325 If not editable, just display the value via plain().
1326 '''
1327 self.view_check()
1329 if not self.is_edit_ok():
1330 return self.plain()
1332 # edit field
1333 linkcl = self._db.getclass(self._prop.classname)
1334 if self._value is None:
1335 value = ''
1336 else:
1337 k = linkcl.getkey()
1338 if k:
1339 label = linkcl.get(self._value, k)
1340 else:
1341 label = self._value
1342 value = cgi.escape(str(self._value))
1343 value = '"'.join(value.split('"'))
1344 return '<input name="%s" value="%s" size="%s">'%(self._formname,
1345 label, size)
1347 def menu(self, size=None, height=None, showid=0, additional=[],
1348 sort_on=None, **conditions):
1349 ''' Render a form select list for this property
1351 If not editable, just display the value via plain().
1352 '''
1353 self.view_check()
1355 if not self.is_edit_ok():
1356 return self.plain()
1358 value = self._value
1360 linkcl = self._db.getclass(self._prop.classname)
1361 l = ['<select name="%s">'%self._formname]
1362 k = linkcl.labelprop(1)
1363 s = ''
1364 if value is None:
1365 s = 'selected="selected" '
1366 l.append(_('<option %svalue="-1">- no selection -</option>')%s)
1367 if linkcl.getprops().has_key('order'):
1368 sort_on = ('+', 'order')
1369 else:
1370 if sort_on is None:
1371 sort_on = ('+', linkcl.labelprop())
1372 else:
1373 sort_on = ('+', sort_on)
1374 options = linkcl.filter(None, conditions, sort_on, (None, None))
1376 # make sure we list the current value if it's retired
1377 if self._value and self._value not in options:
1378 options.insert(0, self._value)
1380 for optionid in options:
1381 # get the option value, and if it's None use an empty string
1382 option = linkcl.get(optionid, k) or ''
1384 # figure if this option is selected
1385 s = ''
1386 if value in [optionid, option]:
1387 s = 'selected="selected" '
1389 # figure the label
1390 if showid:
1391 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1392 else:
1393 lab = option
1395 # truncate if it's too long
1396 if size is not None and len(lab) > size:
1397 lab = lab[:size-3] + '...'
1398 if additional:
1399 m = []
1400 for propname in additional:
1401 m.append(linkcl.get(optionid, propname))
1402 lab = lab + ' (%s)'%', '.join(map(str, m))
1404 # and generate
1405 lab = cgi.escape(lab)
1406 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
1407 l.append('</select>')
1408 return '\n'.join(l)
1409 # def checklist(self, ...)
1411 class MultilinkHTMLProperty(HTMLProperty):
1412 ''' Multilink HTMLProperty
1414 Also be iterable, returning a wrapper object like the Link case for
1415 each entry in the multilink.
1416 '''
1417 def __init__(self, *args, **kwargs):
1418 HTMLProperty.__init__(self, *args, **kwargs)
1419 if self._value:
1420 sortfun = make_sort_function(self._db, self._prop.classname)
1421 self._value.sort(sortfun)
1423 def __len__(self):
1424 ''' length of the multilink '''
1425 return len(self._value)
1427 def __getattr__(self, attr):
1428 ''' no extended attribute accesses make sense here '''
1429 raise AttributeError, attr
1431 def __getitem__(self, num):
1432 ''' iterate and return a new HTMLItem
1433 '''
1434 #print 'Multi.getitem', (self, num)
1435 value = self._value[num]
1436 if self._prop.classname == 'user':
1437 klass = HTMLUser
1438 else:
1439 klass = HTMLItem
1440 return klass(self._client, self._prop.classname, value)
1442 def __contains__(self, value):
1443 ''' Support the "in" operator. We have to make sure the passed-in
1444 value is a string first, not a *HTMLProperty.
1445 '''
1446 return str(value) in self._value
1448 def reverse(self):
1449 ''' return the list in reverse order
1450 '''
1451 l = self._value[:]
1452 l.reverse()
1453 if self._prop.classname == 'user':
1454 klass = HTMLUser
1455 else:
1456 klass = HTMLItem
1457 return [klass(self._client, self._prop.classname, value) for value in l]
1459 def plain(self, escape=0):
1460 ''' Render a "plain" representation of the property
1461 '''
1462 self.view_check()
1464 linkcl = self._db.classes[self._prop.classname]
1465 k = linkcl.labelprop(1)
1466 labels = []
1467 for v in self._value:
1468 labels.append(linkcl.get(v, k))
1469 value = ', '.join(labels)
1470 if escape:
1471 value = cgi.escape(value)
1472 return value
1474 def field(self, size=30, showid=0):
1475 ''' Render a form edit field for the property
1477 If not editable, just display the value via plain().
1478 '''
1479 self.view_check()
1481 if not self.is_edit_ok():
1482 return self.plain()
1484 linkcl = self._db.getclass(self._prop.classname)
1485 value = self._value[:]
1486 # map the id to the label property
1487 if not linkcl.getkey():
1488 showid=1
1489 if not showid:
1490 k = linkcl.labelprop(1)
1491 value = [linkcl.get(v, k) for v in value]
1492 value = cgi.escape(','.join(value))
1493 return self.input(name=self._formname,size=size,value=value)
1495 def menu(self, size=None, height=None, showid=0, additional=[],
1496 sort_on=None, **conditions):
1497 ''' Render a form select list for this property
1499 If not editable, just display the value via plain().
1500 '''
1501 self.view_check()
1503 if not self.is_edit_ok():
1504 return self.plain()
1506 value = self._value
1508 linkcl = self._db.getclass(self._prop.classname)
1509 if sort_on is None:
1510 sort_on = ('+', find_sort_key(linkcl))
1511 else:
1512 sort_on = ('+', sort_on)
1513 options = linkcl.filter(None, conditions, sort_on)
1514 height = height or min(len(options), 7)
1515 l = ['<select multiple name="%s" size="%s">'%(self._formname, height)]
1516 k = linkcl.labelprop(1)
1518 # make sure we list the current values if they're retired
1519 for val in value:
1520 if val not in options:
1521 options.insert(0, val)
1523 for optionid in options:
1524 # get the option value, and if it's None use an empty string
1525 option = linkcl.get(optionid, k) or ''
1527 # figure if this option is selected
1528 s = ''
1529 if optionid in value or option in value:
1530 s = 'selected="selected" '
1532 # figure the label
1533 if showid:
1534 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1535 else:
1536 lab = option
1537 # truncate if it's too long
1538 if size is not None and len(lab) > size:
1539 lab = lab[:size-3] + '...'
1540 if additional:
1541 m = []
1542 for propname in additional:
1543 m.append(linkcl.get(optionid, propname))
1544 lab = lab + ' (%s)'%', '.join(m)
1546 # and generate
1547 lab = cgi.escape(lab)
1548 l.append('<option %svalue="%s">%s</option>'%(s, optionid,
1549 lab))
1550 l.append('</select>')
1551 return '\n'.join(l)
1553 # set the propclasses for HTMLItem
1554 propclasses = (
1555 (hyperdb.String, StringHTMLProperty),
1556 (hyperdb.Number, NumberHTMLProperty),
1557 (hyperdb.Boolean, BooleanHTMLProperty),
1558 (hyperdb.Date, DateHTMLProperty),
1559 (hyperdb.Interval, IntervalHTMLProperty),
1560 (hyperdb.Password, PasswordHTMLProperty),
1561 (hyperdb.Link, LinkHTMLProperty),
1562 (hyperdb.Multilink, MultilinkHTMLProperty),
1563 )
1565 def make_sort_function(db, classname, sort_on=None):
1566 '''Make a sort function for a given class
1567 '''
1568 linkcl = db.getclass(classname)
1569 if sort_on is None:
1570 sort_on = find_sort_key(linkcl)
1571 def sortfunc(a, b):
1572 return cmp(linkcl.get(a, sort_on), linkcl.get(b, sort_on))
1573 return sortfunc
1575 def find_sort_key(linkcl):
1576 if linkcl.getprops().has_key('order'):
1577 return 'order'
1578 else:
1579 return linkcl.labelprop()
1581 def handleListCGIValue(value):
1582 ''' Value is either a single item or a list of items. Each item has a
1583 .value that we're actually interested in.
1584 '''
1585 if isinstance(value, type([])):
1586 return [value.value for value in value]
1587 else:
1588 value = value.value.strip()
1589 if not value:
1590 return []
1591 return value.split(',')
1593 class ShowDict:
1594 ''' A convenience access to the :columns index parameters
1595 '''
1596 def __init__(self, columns):
1597 self.columns = {}
1598 for col in columns:
1599 self.columns[col] = 1
1600 def __getitem__(self, name):
1601 return self.columns.has_key(name)
1603 class HTMLRequest(HTMLInputMixin):
1604 ''' The *request*, holding the CGI form and environment.
1606 "form" the CGI form as a cgi.FieldStorage
1607 "env" the CGI environment variables
1608 "base" the base URL for this instance
1609 "user" a HTMLUser instance for this user
1610 "classname" the current classname (possibly None)
1611 "template" the current template (suffix, also possibly None)
1613 Index args:
1614 "columns" dictionary of the columns to display in an index page
1615 "show" a convenience access to columns - request/show/colname will
1616 be true if the columns should be displayed, false otherwise
1617 "sort" index sort column (direction, column name)
1618 "group" index grouping property (direction, column name)
1619 "filter" properties to filter the index on
1620 "filterspec" values to filter the index on
1621 "search_text" text to perform a full-text search on for an index
1623 '''
1624 def __init__(self, client):
1625 # _client is needed by HTMLInputMixin
1626 self._client = self.client = client
1628 # easier access vars
1629 self.form = client.form
1630 self.env = client.env
1631 self.base = client.base
1632 self.user = HTMLUser(client, 'user', client.userid)
1634 # store the current class name and action
1635 self.classname = client.classname
1636 self.template = client.template
1638 # the special char to use for special vars
1639 self.special_char = '@'
1641 HTMLInputMixin.__init__(self)
1643 self._post_init()
1645 def _post_init(self):
1646 ''' Set attributes based on self.form
1647 '''
1648 # extract the index display information from the form
1649 self.columns = []
1650 for name in ':columns @columns'.split():
1651 if self.form.has_key(name):
1652 self.special_char = name[0]
1653 self.columns = handleListCGIValue(self.form[name])
1654 break
1655 self.show = ShowDict(self.columns)
1657 # sorting
1658 self.sort = (None, None)
1659 for name in ':sort @sort'.split():
1660 if self.form.has_key(name):
1661 self.special_char = name[0]
1662 sort = self.form[name].value
1663 if sort.startswith('-'):
1664 self.sort = ('-', sort[1:])
1665 else:
1666 self.sort = ('+', sort)
1667 if self.form.has_key(self.special_char+'sortdir'):
1668 self.sort = ('-', self.sort[1])
1670 # grouping
1671 self.group = (None, None)
1672 for name in ':group @group'.split():
1673 if self.form.has_key(name):
1674 self.special_char = name[0]
1675 group = self.form[name].value
1676 if group.startswith('-'):
1677 self.group = ('-', group[1:])
1678 else:
1679 self.group = ('+', group)
1680 if self.form.has_key(self.special_char+'groupdir'):
1681 self.group = ('-', self.group[1])
1683 # filtering
1684 self.filter = []
1685 for name in ':filter @filter'.split():
1686 if self.form.has_key(name):
1687 self.special_char = name[0]
1688 self.filter = handleListCGIValue(self.form[name])
1690 self.filterspec = {}
1691 db = self.client.db
1692 if self.classname is not None:
1693 props = db.getclass(self.classname).getprops()
1694 for name in self.filter:
1695 if not self.form.has_key(name):
1696 continue
1697 prop = props[name]
1698 fv = self.form[name]
1699 if (isinstance(prop, hyperdb.Link) or
1700 isinstance(prop, hyperdb.Multilink)):
1701 self.filterspec[name] = lookupIds(db, prop,
1702 handleListCGIValue(fv))
1703 else:
1704 if isinstance(fv, type([])):
1705 self.filterspec[name] = [v.value for v in fv]
1706 else:
1707 self.filterspec[name] = fv.value
1709 # full-text search argument
1710 self.search_text = None
1711 for name in ':search_text @search_text'.split():
1712 if self.form.has_key(name):
1713 self.special_char = name[0]
1714 self.search_text = self.form[name].value
1716 # pagination - size and start index
1717 # figure batch args
1718 self.pagesize = 50
1719 for name in ':pagesize @pagesize'.split():
1720 if self.form.has_key(name):
1721 self.special_char = name[0]
1722 self.pagesize = int(self.form[name].value)
1724 self.startwith = 0
1725 for name in ':startwith @startwith'.split():
1726 if self.form.has_key(name):
1727 self.special_char = name[0]
1728 self.startwith = int(self.form[name].value)
1730 def updateFromURL(self, url):
1731 ''' Parse the URL for query args, and update my attributes using the
1732 values.
1733 '''
1734 env = {'QUERY_STRING': url}
1735 self.form = cgi.FieldStorage(environ=env)
1737 self._post_init()
1739 def update(self, kwargs):
1740 ''' Update my attributes using the keyword args
1741 '''
1742 self.__dict__.update(kwargs)
1743 if kwargs.has_key('columns'):
1744 self.show = ShowDict(self.columns)
1746 def description(self):
1747 ''' Return a description of the request - handle for the page title.
1748 '''
1749 s = [self.client.db.config.TRACKER_NAME]
1750 if self.classname:
1751 if self.client.nodeid:
1752 s.append('- %s%s'%(self.classname, self.client.nodeid))
1753 else:
1754 if self.template == 'item':
1755 s.append('- new %s'%self.classname)
1756 elif self.template == 'index':
1757 s.append('- %s index'%self.classname)
1758 else:
1759 s.append('- %s %s'%(self.classname, self.template))
1760 else:
1761 s.append('- home')
1762 return ' '.join(s)
1764 def __str__(self):
1765 d = {}
1766 d.update(self.__dict__)
1767 f = ''
1768 for k in self.form.keys():
1769 f += '\n %r=%r'%(k,handleListCGIValue(self.form[k]))
1770 d['form'] = f
1771 e = ''
1772 for k,v in self.env.items():
1773 e += '\n %r=%r'%(k, v)
1774 d['env'] = e
1775 return '''
1776 form: %(form)s
1777 base: %(base)r
1778 classname: %(classname)r
1779 template: %(template)r
1780 columns: %(columns)r
1781 sort: %(sort)r
1782 group: %(group)r
1783 filter: %(filter)r
1784 search_text: %(search_text)r
1785 pagesize: %(pagesize)r
1786 startwith: %(startwith)r
1787 env: %(env)s
1788 '''%d
1790 def indexargs_form(self, columns=1, sort=1, group=1, filter=1,
1791 filterspec=1):
1792 ''' return the current index args as form elements '''
1793 l = []
1794 sc = self.special_char
1795 s = self.input(type="hidden",name="%s",value="%s")
1796 if columns and self.columns:
1797 l.append(s%(sc+'columns', ','.join(self.columns)))
1798 if sort and self.sort[1] is not None:
1799 if self.sort[0] == '-':
1800 val = '-'+self.sort[1]
1801 else:
1802 val = self.sort[1]
1803 l.append(s%(sc+'sort', val))
1804 if group and self.group[1] is not None:
1805 if self.group[0] == '-':
1806 val = '-'+self.group[1]
1807 else:
1808 val = self.group[1]
1809 l.append(s%(sc+'group', val))
1810 if filter and self.filter:
1811 l.append(s%(sc+'filter', ','.join(self.filter)))
1812 if filterspec:
1813 for k,v in self.filterspec.items():
1814 if type(v) == type([]):
1815 l.append(s%(k, ','.join(v)))
1816 else:
1817 l.append(s%(k, v))
1818 if self.search_text:
1819 l.append(s%(sc+'search_text', self.search_text))
1820 l.append(s%(sc+'pagesize', self.pagesize))
1821 l.append(s%(sc+'startwith', self.startwith))
1822 return '\n'.join(l)
1824 def indexargs_url(self, url, args):
1825 ''' Embed the current index args in a URL
1826 '''
1827 sc = self.special_char
1828 l = ['%s=%s'%(k,v) for k,v in args.items()]
1830 # pull out the special values (prefixed by @ or :)
1831 specials = {}
1832 for key in args.keys():
1833 if key[0] in '@:':
1834 specials[key[1:]] = args[key]
1836 # ok, now handle the specials we received in the request
1837 if self.columns and not specials.has_key('columns'):
1838 l.append(sc+'columns=%s'%(','.join(self.columns)))
1839 if self.sort[1] is not None and not specials.has_key('sort'):
1840 if self.sort[0] == '-':
1841 val = '-'+self.sort[1]
1842 else:
1843 val = self.sort[1]
1844 l.append(sc+'sort=%s'%val)
1845 if self.group[1] is not None and not specials.has_key('group'):
1846 if self.group[0] == '-':
1847 val = '-'+self.group[1]
1848 else:
1849 val = self.group[1]
1850 l.append(sc+'group=%s'%val)
1851 if self.filter and not specials.has_key('filter'):
1852 l.append(sc+'filter=%s'%(','.join(self.filter)))
1853 if self.search_text and not specials.has_key('search_text'):
1854 l.append(sc+'search_text=%s'%self.search_text)
1855 if not specials.has_key('pagesize'):
1856 l.append(sc+'pagesize=%s'%self.pagesize)
1857 if not specials.has_key('startwith'):
1858 l.append(sc+'startwith=%s'%self.startwith)
1860 # finally, the remainder of the filter args in the request
1861 for k,v in self.filterspec.items():
1862 if not args.has_key(k):
1863 if type(v) == type([]):
1864 l.append('%s=%s'%(k, ','.join(v)))
1865 else:
1866 l.append('%s=%s'%(k, v))
1867 return '%s?%s'%(url, '&'.join(l))
1868 indexargs_href = indexargs_url
1870 def base_javascript(self):
1871 return '''
1872 <script type="text/javascript">
1873 submitted = false;
1874 function submit_once() {
1875 if (submitted) {
1876 alert("Your request is being processed.\\nPlease be patient.");
1877 event.returnValue = 0; // work-around for IE
1878 return 0;
1879 }
1880 submitted = true;
1881 return 1;
1882 }
1884 function help_window(helpurl, width, height) {
1885 HelpWin = window.open('%s' + helpurl, 'RoundupHelpWindow', 'scrollbars=yes,resizable=yes,toolbar=no,height='+height+',width='+width);
1886 }
1887 </script>
1888 '''%self.base
1890 def batch(self):
1891 ''' Return a batch object for results from the "current search"
1892 '''
1893 filterspec = self.filterspec
1894 sort = self.sort
1895 group = self.group
1897 # get the list of ids we're batching over
1898 klass = self.client.db.getclass(self.classname)
1899 if self.search_text:
1900 matches = self.client.db.indexer.search(
1901 re.findall(r'\b\w{2,25}\b', self.search_text), klass)
1902 else:
1903 matches = None
1904 l = klass.filter(matches, filterspec, sort, group)
1906 # return the batch object, using IDs only
1907 return Batch(self.client, l, self.pagesize, self.startwith,
1908 classname=self.classname)
1910 # extend the standard ZTUtils Batch object to remove dependency on
1911 # Acquisition and add a couple of useful methods
1912 class Batch(ZTUtils.Batch):
1913 ''' Use me to turn a list of items, or item ids of a given class, into a
1914 series of batches.
1916 ========= ========================================================
1917 Parameter Usage
1918 ========= ========================================================
1919 sequence a list of HTMLItems or item ids
1920 classname if sequence is a list of ids, this is the class of item
1921 size how big to make the sequence.
1922 start where to start (0-indexed) in the sequence.
1923 end where to end (0-indexed) in the sequence.
1924 orphan if the next batch would contain less items than this
1925 value, then it is combined with this batch
1926 overlap the number of items shared between adjacent batches
1927 ========= ========================================================
1929 Attributes: Note that the "start" attribute, unlike the
1930 argument, is a 1-based index (I know, lame). "first" is the
1931 0-based index. "length" is the actual number of elements in
1932 the batch.
1934 "sequence_length" is the length of the original, unbatched, sequence.
1935 '''
1936 def __init__(self, client, sequence, size, start, end=0, orphan=0,
1937 overlap=0, classname=None):
1938 self.client = client
1939 self.last_index = self.last_item = None
1940 self.current_item = None
1941 self.classname = classname
1942 self.sequence_length = len(sequence)
1943 ZTUtils.Batch.__init__(self, sequence, size, start, end, orphan,
1944 overlap)
1946 # overwrite so we can late-instantiate the HTMLItem instance
1947 def __getitem__(self, index):
1948 if index < 0:
1949 if index + self.end < self.first: raise IndexError, index
1950 return self._sequence[index + self.end]
1952 if index >= self.length:
1953 raise IndexError, index
1955 # move the last_item along - but only if the fetched index changes
1956 # (for some reason, index 0 is fetched twice)
1957 if index != self.last_index:
1958 self.last_item = self.current_item
1959 self.last_index = index
1961 item = self._sequence[index + self.first]
1962 if self.classname:
1963 # map the item ids to instances
1964 if self.classname == 'user':
1965 item = HTMLUser(self.client, self.classname, item)
1966 else:
1967 item = HTMLItem(self.client, self.classname, item)
1968 self.current_item = item
1969 return item
1971 def propchanged(self, property):
1972 ''' Detect if the property marked as being the group property
1973 changed in the last iteration fetch
1974 '''
1975 if (self.last_item is None or
1976 self.last_item[property] != self.current_item[property]):
1977 return 1
1978 return 0
1980 # override these 'cos we don't have access to acquisition
1981 def previous(self):
1982 if self.start == 1:
1983 return None
1984 return Batch(self.client, self._sequence, self._size,
1985 self.first - self._size + self.overlap, 0, self.orphan,
1986 self.overlap)
1988 def next(self):
1989 try:
1990 self._sequence[self.end]
1991 except IndexError:
1992 return None
1993 return Batch(self.client, self._sequence, self._size,
1994 self.end - self.overlap, 0, self.orphan, self.overlap)
1996 class TemplatingUtils:
1997 ''' Utilities for templating
1998 '''
1999 def __init__(self, client):
2000 self.client = client
2001 def Batch(self, sequence, size, start, end=0, orphan=0, overlap=0):
2002 return Batch(self.client, sequence, size, start, end, orphan,
2003 overlap)