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