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