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 prop = self._props[item]
381 # look up the correct HTMLProperty class
382 form = self._client.form
383 for klass, htmlklass in propclasses:
384 if not isinstance(prop, klass):
385 continue
386 if form.has_key(item):
387 if isinstance(prop, hyperdb.Multilink):
388 value = lookupIds(self._db, prop,
389 handleListCGIValue(form[item]), fail_ok=True)
390 elif isinstance(prop, hyperdb.Link):
391 value = form[item].value.strip()
392 if value:
393 value = lookupIds(self._db, prop, [value],
394 fail_ok=True)[0]
395 else:
396 value = None
397 else:
398 value = form[item].value.strip() or None
399 else:
400 if isinstance(prop, hyperdb.Multilink):
401 value = []
402 else:
403 value = None
404 return htmlklass(self._client, self._classname, '', prop, item,
405 value, self._anonymous)
407 # no good
408 raise KeyError, item
410 def __getattr__(self, attr):
411 ''' convenience access '''
412 try:
413 return self[attr]
414 except KeyError:
415 raise AttributeError, attr
417 def designator(self):
418 ''' Return this class' designator (classname) '''
419 return self._classname
421 def getItem(self, itemid, num_re=re.compile('-?\d+')):
422 ''' Get an item of this class by its item id.
423 '''
424 # make sure we're looking at an itemid
425 if not isinstance(itemid, type(1)) and not num_re.match(itemid):
426 itemid = self._klass.lookup(itemid)
428 if self.classname == 'user':
429 klass = HTMLUser
430 else:
431 klass = HTMLItem
433 return klass(self._client, self.classname, itemid)
435 def properties(self, sort=1):
436 ''' Return HTMLProperty for all of this class' properties.
437 '''
438 l = []
439 for name, prop in self._props.items():
440 for klass, htmlklass in propclasses:
441 if isinstance(prop, hyperdb.Multilink):
442 value = []
443 else:
444 value = None
445 if isinstance(prop, klass):
446 l.append(htmlklass(self._client, self._classname, '',
447 prop, name, value, self._anonymous))
448 if sort:
449 l.sort(lambda a,b:cmp(a._name, b._name))
450 return l
452 def list(self, sort_on=None):
453 ''' List all items in this class.
454 '''
455 if self.classname == 'user':
456 klass = HTMLUser
457 else:
458 klass = HTMLItem
460 # get the list and sort it nicely
461 l = self._klass.list()
462 sortfunc = make_sort_function(self._db, self.classname, sort_on)
463 l.sort(sortfunc)
465 l = [klass(self._client, self.classname, x) for x in l]
466 return l
468 def csv(self):
469 ''' Return the items of this class as a chunk of CSV text.
470 '''
471 if rcsv.error:
472 return rcsv.error
474 props = self.propnames()
475 s = StringIO.StringIO()
476 writer = rcsv.writer(s, rcsv.comma_separated)
477 writer.writerow(props)
478 for nodeid in self._klass.list():
479 l = []
480 for name in props:
481 value = self._klass.get(nodeid, name)
482 if value is None:
483 l.append('')
484 elif isinstance(value, type([])):
485 l.append(':'.join(map(str, value)))
486 else:
487 l.append(str(self._klass.get(nodeid, name)))
488 writer.writerow(l)
489 return s.getvalue()
491 def propnames(self):
492 ''' Return the list of the names of the properties of this class.
493 '''
494 idlessprops = self._klass.getprops(protected=0).keys()
495 idlessprops.sort()
496 return ['id'] + idlessprops
498 def filter(self, request=None, filterspec={}, sort=(None,None),
499 group=(None,None)):
500 ''' Return a list of items from this class, filtered and sorted
501 by the current requested filterspec/filter/sort/group args
503 "request" takes precedence over the other three arguments.
504 '''
505 if request is not None:
506 filterspec = request.filterspec
507 sort = request.sort
508 group = request.group
509 if self.classname == 'user':
510 klass = HTMLUser
511 else:
512 klass = HTMLItem
513 l = [klass(self._client, self.classname, x)
514 for x in self._klass.filter(None, filterspec, sort, group)]
515 return l
517 def classhelp(self, properties=None, label='(list)', width='500',
518 height='400', property=''):
519 ''' Pop up a javascript window with class help
521 This generates a link to a popup window which displays the
522 properties indicated by "properties" of the class named by
523 "classname". The "properties" should be a comma-separated list
524 (eg. 'id,name,description'). Properties defaults to all the
525 properties of a class (excluding id, creator, created and
526 activity).
528 You may optionally override the label displayed, the width and
529 height. The popup window will be resizable and scrollable.
531 If the "property" arg is given, it's passed through to the
532 javascript help_window function.
533 '''
534 if properties is None:
535 properties = self._klass.getprops(protected=0).keys()
536 properties.sort()
537 properties = ','.join(properties)
538 if property:
539 property = '&property=%s'%property
540 return '<a class="classhelp" href="javascript:help_window(\'%s?'\
541 '@startwith=0&@template=help&properties=%s%s\', \'%s\', \
542 \'%s\')">%s</a>'%(self.classname, properties, property, width,
543 height, label)
545 def submit(self, label="Submit New Entry"):
546 ''' Generate a submit button (and action hidden element)
547 '''
548 self.view_check()
549 if self.is_edit_ok():
550 return self.input(type="hidden",name="@action",value="new") + \
551 '\n' + self.input(type="submit",name="submit",value=label)
552 return ''
554 def history(self):
555 self.view_check()
556 return 'New node - no history'
558 def renderWith(self, name, **kwargs):
559 ''' Render this class with the given template.
560 '''
561 # create a new request and override the specified args
562 req = HTMLRequest(self._client)
563 req.classname = self.classname
564 req.update(kwargs)
566 # new template, using the specified classname and request
567 pt = Templates(self._db.config.TEMPLATES).get(self.classname, name)
569 # use our fabricated request
570 args = {
571 'ok_message': self._client.ok_message,
572 'error_message': self._client.error_message
573 }
574 return pt.render(self._client, self.classname, req, **args)
576 class HTMLItem(HTMLInputMixin, HTMLPermissions):
577 ''' Accesses through an *item*
578 '''
579 def __init__(self, client, classname, nodeid, anonymous=0):
580 self._client = client
581 self._db = client.db
582 self._classname = classname
583 self._nodeid = nodeid
584 self._klass = self._db.getclass(classname)
585 self._props = self._klass.getprops()
587 # do we prefix the form items with the item's identification?
588 self._anonymous = anonymous
590 HTMLInputMixin.__init__(self)
592 def __repr__(self):
593 return '<HTMLItem(0x%x) %s %s>'%(id(self), self._classname,
594 self._nodeid)
596 def __getitem__(self, item):
597 ''' return an HTMLProperty instance
598 '''
599 #print 'HTMLItem.getitem', (self, item)
600 if item == 'id':
601 return self._nodeid
603 # get the property
604 prop = self._props[item]
606 # get the value, handling missing values
607 value = None
608 if int(self._nodeid) > 0:
609 value = self._klass.get(self._nodeid, item, None)
610 if value is None:
611 if isinstance(self._props[item], hyperdb.Multilink):
612 value = []
614 # look up the correct HTMLProperty class
615 for klass, htmlklass in propclasses:
616 if isinstance(prop, klass):
617 return htmlklass(self._client, self._classname,
618 self._nodeid, prop, item, value, self._anonymous)
620 raise KeyError, item
622 def __getattr__(self, attr):
623 ''' convenience access to properties '''
624 try:
625 return self[attr]
626 except KeyError:
627 raise AttributeError, attr
629 def designator(self):
630 """Return this item's designator (classname + id)."""
631 return '%s%s'%(self._classname, self._nodeid)
633 def submit(self, label="Submit Changes"):
634 """Generate a submit button.
636 Also sneak in the lastactivity and action hidden elements.
637 """
638 return self.input(type="hidden", name="@lastactivity", value=date.Date('.')) + '\n' + \
639 self.input(type="hidden", name="@action", value="edit") + '\n' + \
640 self.input(type="submit", name="submit", value=label)
642 def journal(self, direction='descending'):
643 ''' Return a list of HTMLJournalEntry instances.
644 '''
645 # XXX do this
646 return []
648 def history(self, direction='descending', dre=re.compile('\d+')):
649 self.view_check()
651 l = ['<table class="history">'
652 '<tr><th colspan="4" class="header">',
653 _('History'),
654 '</th></tr><tr>',
655 _('<th>Date</th>'),
656 _('<th>User</th>'),
657 _('<th>Action</th>'),
658 _('<th>Args</th>'),
659 '</tr>']
660 current = {}
661 comments = {}
662 history = self._klass.history(self._nodeid)
663 history.sort()
664 timezone = self._db.getUserTimezone()
665 if direction == 'descending':
666 history.reverse()
667 for prop_n in self._props.keys():
668 prop = self[prop_n]
669 if isinstance(prop, HTMLProperty):
670 current[prop_n] = prop.plain()
671 # make link if hrefable
672 if (self._props.has_key(prop_n) and
673 isinstance(self._props[prop_n], hyperdb.Link)):
674 classname = self._props[prop_n].classname
675 try:
676 template = find_template(self._db.config.TEMPLATES,
677 classname, 'item')
678 if template[1].startswith('_generic'):
679 raise NoTemplate, 'not really...'
680 except NoTemplate:
681 pass
682 else:
683 id = self._klass.get(self._nodeid, prop_n, None)
684 current[prop_n] = '<a href="%s%s">%s</a>'%(
685 classname, id, current[prop_n])
687 for id, evt_date, user, action, args in history:
688 date_s = str(evt_date.local(timezone)).replace("."," ")
689 arg_s = ''
690 if action == 'link' and type(args) == type(()):
691 if len(args) == 3:
692 linkcl, linkid, key = args
693 arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
694 linkcl, linkid, key)
695 else:
696 arg_s = str(args)
698 elif action == 'unlink' and type(args) == type(()):
699 if len(args) == 3:
700 linkcl, linkid, key = args
701 arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
702 linkcl, linkid, key)
703 else:
704 arg_s = str(args)
706 elif type(args) == type({}):
707 cell = []
708 for k in args.keys():
709 # try to get the relevant property and treat it
710 # specially
711 try:
712 prop = self._props[k]
713 except KeyError:
714 prop = None
715 if prop is None:
716 # property no longer exists
717 comments['no_exist'] = _('''<em>The indicated property
718 no longer exists</em>''')
719 cell.append('<em>%s: %s</em>\n'%(k, str(args[k])))
720 continue
722 if args[k] and (isinstance(prop, hyperdb.Multilink) or
723 isinstance(prop, hyperdb.Link)):
724 # figure what the link class is
725 classname = prop.classname
726 try:
727 linkcl = self._db.getclass(classname)
728 except KeyError:
729 labelprop = None
730 comments[classname] = _('''The linked class
731 %(classname)s no longer exists''')%locals()
732 labelprop = linkcl.labelprop(1)
733 try:
734 template = find_template(self._db.config.TEMPLATES,
735 classname, 'item')
736 if template[1].startswith('_generic'):
737 raise NoTemplate, 'not really...'
738 hrefable = 1
739 except NoTemplate:
740 hrefable = 0
742 if isinstance(prop, hyperdb.Multilink) and args[k]:
743 ml = []
744 for linkid in args[k]:
745 if isinstance(linkid, type(())):
746 sublabel = linkid[0] + ' '
747 linkids = linkid[1]
748 else:
749 sublabel = ''
750 linkids = [linkid]
751 subml = []
752 for linkid in linkids:
753 label = classname + linkid
754 # if we have a label property, try to use it
755 # TODO: test for node existence even when
756 # there's no labelprop!
757 try:
758 if labelprop is not None and \
759 labelprop != 'id':
760 label = linkcl.get(linkid, labelprop)
761 except IndexError:
762 comments['no_link'] = _('''<strike>The
763 linked node no longer
764 exists</strike>''')
765 subml.append('<strike>%s</strike>'%label)
766 else:
767 if hrefable:
768 subml.append('<a href="%s%s">%s</a>'%(
769 classname, linkid, label))
770 else:
771 subml.append(label)
772 ml.append(sublabel + ', '.join(subml))
773 cell.append('%s:\n %s'%(k, ', '.join(ml)))
774 elif isinstance(prop, hyperdb.Link) and args[k]:
775 label = classname + args[k]
776 # if we have a label property, try to use it
777 # TODO: test for node existence even when
778 # there's no labelprop!
779 if labelprop is not None and labelprop != 'id':
780 try:
781 label = linkcl.get(args[k], labelprop)
782 except IndexError:
783 comments['no_link'] = _('''<strike>The
784 linked node no longer
785 exists</strike>''')
786 cell.append(' <strike>%s</strike>,\n'%label)
787 # "flag" this is done .... euwww
788 label = None
789 if label is not None:
790 if hrefable:
791 old = '<a href="%s%s">%s</a>'%(classname, args[k], label)
792 else:
793 old = label;
794 cell.append('%s: %s' % (k,old))
795 if current.has_key(k):
796 cell[-1] += ' -> %s'%current[k]
797 current[k] = old
799 elif isinstance(prop, hyperdb.Date) and args[k]:
800 d = date.Date(args[k]).local(timezone)
801 cell.append('%s: %s'%(k, str(d)))
802 if current.has_key(k):
803 cell[-1] += ' -> %s' % current[k]
804 current[k] = str(d)
806 elif isinstance(prop, hyperdb.Interval) and args[k]:
807 d = date.Interval(args[k])
808 cell.append('%s: %s'%(k, str(d)))
809 if current.has_key(k):
810 cell[-1] += ' -> %s'%current[k]
811 current[k] = str(d)
813 elif isinstance(prop, hyperdb.String) and args[k]:
814 cell.append('%s: %s'%(k, cgi.escape(args[k])))
815 if current.has_key(k):
816 cell[-1] += ' -> %s'%current[k]
817 current[k] = cgi.escape(args[k])
819 elif not args[k]:
820 if current.has_key(k):
821 cell.append('%s: %s'%(k, current[k]))
822 current[k] = '(no value)'
823 else:
824 cell.append('%s: (no value)'%k)
826 else:
827 cell.append('%s: %s'%(k, str(args[k])))
828 if current.has_key(k):
829 cell[-1] += ' -> %s'%current[k]
830 current[k] = str(args[k])
832 arg_s = '<br />'.join(cell)
833 else:
834 # unkown event!!
835 comments['unknown'] = _('''<strong><em>This event is not
836 handled by the history display!</em></strong>''')
837 arg_s = '<strong><em>' + str(args) + '</em></strong>'
838 date_s = date_s.replace(' ', ' ')
839 # if the user's an itemid, figure the username (older journals
840 # have the username)
841 if dre.match(user):
842 user = self._db.user.get(user, 'username')
843 l.append('<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>'%(
844 date_s, user, action, arg_s))
845 if comments:
846 l.append(_('<tr><td colspan=4><strong>Note:</strong></td></tr>'))
847 for entry in comments.values():
848 l.append('<tr><td colspan=4>%s</td></tr>'%entry)
849 l.append('</table>')
850 return '\n'.join(l)
852 def renderQueryForm(self):
853 ''' Render this item, which is a query, as a search form.
854 '''
855 # create a new request and override the specified args
856 req = HTMLRequest(self._client)
857 req.classname = self._klass.get(self._nodeid, 'klass')
858 name = self._klass.get(self._nodeid, 'name')
859 req.updateFromURL(self._klass.get(self._nodeid, 'url') +
860 '&@queryname=%s'%urllib.quote(name))
862 # new template, using the specified classname and request
863 pt = Templates(self._db.config.TEMPLATES).get(req.classname, 'search')
865 # use our fabricated request
866 return pt.render(self._client, req.classname, req)
868 class HTMLUserPermission:
870 def is_edit_ok(self):
871 ''' Is the user allowed to Edit the current class?
872 Also check whether this is the current user's info.
873 '''
874 return self._user_perm_check('Edit')
876 def is_view_ok(self):
877 ''' Is the user allowed to View the current class?
878 Also check whether this is the current user's info.
879 '''
880 return self._user_perm_check('View')
882 def _user_perm_check(self, type):
883 # some users may view / edit all users
884 s = self._db.security
885 userid = self._client.userid
886 if s.hasPermission(type, userid, self._classname):
887 return 1
889 # users may view their own info
890 is_anonymous = self._db.user.get(userid, 'username') == 'anonymous'
891 if getattr(self, '_nodeid', None) == userid and not is_anonymous:
892 return 1
894 # may anonymous users register?
895 if (is_anonymous and s.hasPermission('Web Registration', userid,
896 self._classname)):
897 return 1
899 # nope, no access here
900 return 0
902 class HTMLUserClass(HTMLUserPermission, HTMLClass):
903 pass
905 class HTMLUser(HTMLUserPermission, HTMLItem):
906 ''' Accesses through the *user* (a special case of item)
907 '''
908 def __init__(self, client, classname, nodeid, anonymous=0):
909 HTMLItem.__init__(self, client, 'user', nodeid, anonymous)
910 self._default_classname = client.classname
912 # used for security checks
913 self._security = client.db.security
915 _marker = []
916 def hasPermission(self, permission, classname=_marker):
917 ''' Determine if the user has the Permission.
919 The class being tested defaults to the template's class, but may
920 be overidden for this test by suppling an alternate classname.
921 '''
922 if classname is self._marker:
923 classname = self._default_classname
924 return self._security.hasPermission(permission, self._nodeid, classname)
926 class HTMLProperty(HTMLInputMixin, HTMLPermissions):
927 ''' String, Number, Date, Interval HTMLProperty
929 Has useful attributes:
931 _name the name of the property
932 _value the value of the property if any
934 A wrapper object which may be stringified for the plain() behaviour.
935 '''
936 def __init__(self, client, classname, nodeid, prop, name, value,
937 anonymous=0):
938 self._client = client
939 self._db = client.db
940 self._classname = classname
941 self._nodeid = nodeid
942 self._prop = prop
943 self._value = value
944 self._anonymous = anonymous
945 self._name = name
946 if not anonymous:
947 self._formname = '%s%s@%s'%(classname, nodeid, name)
948 else:
949 self._formname = name
951 HTMLInputMixin.__init__(self)
953 def __repr__(self):
954 return '<HTMLProperty(0x%x) %s %r %r>'%(id(self), self._formname,
955 self._prop, self._value)
956 def __str__(self):
957 return self.plain()
958 def __cmp__(self, other):
959 if isinstance(other, HTMLProperty):
960 return cmp(self._value, other._value)
961 return cmp(self._value, other)
963 def is_edit_ok(self):
964 ''' Is the user allowed to Edit the current class?
965 '''
966 thing = HTMLDatabase(self._client)[self._classname]
967 if self._nodeid:
968 # this is a special-case for the User class where permission's
969 # on a per-item basis :(
970 thing = thing.getItem(self._nodeid)
971 return thing.is_edit_ok()
973 def is_view_ok(self):
974 ''' Is the user allowed to View the current class?
975 '''
976 thing = HTMLDatabase(self._client)[self._classname]
977 if self._nodeid:
978 # this is a special-case for the User class where permission's
979 # on a per-item basis :(
980 thing = thing.getItem(self._nodeid)
981 return thing.is_view_ok()
983 class StringHTMLProperty(HTMLProperty):
984 hyper_re = re.compile(r'((?P<url>\w{3,6}://\S+)|'
985 r'(?P<email>[-+=%/\w\.]+@[\w\.\-]+)|'
986 r'(?P<item>(?P<class>[a-z_]+)(?P<id>\d+)))')
987 def _hyper_repl(self, match):
988 if match.group('url'):
989 s = match.group('url')
990 return '<a href="%s">%s</a>'%(s, s)
991 elif match.group('email'):
992 s = match.group('email')
993 return '<a href="mailto:%s">%s</a>'%(s, s)
994 else:
995 s = match.group('item')
996 s1 = match.group('class')
997 s2 = match.group('id')
998 try:
999 # make sure s1 is a valid tracker classname
1000 cl = self._db.getclass(s1)
1001 if not cl.hasnode(s2):
1002 raise KeyError, 'oops'
1003 return '<a href="%s">%s%s</a>'%(s, s1, s2)
1004 except KeyError:
1005 return '%s%s'%(s1, s2)
1007 def hyperlinked(self):
1008 ''' Render a "hyperlinked" version of the text '''
1009 return self.plain(hyperlink=1)
1011 def plain(self, escape=0, hyperlink=0):
1012 '''Render a "plain" representation of the property
1014 - "escape" turns on/off HTML quoting
1015 - "hyperlink" turns on/off in-text hyperlinking of URLs, email
1016 addresses and designators
1017 '''
1018 self.view_check()
1020 if self._value is None:
1021 return ''
1022 if escape:
1023 s = cgi.escape(str(self._value))
1024 else:
1025 s = str(self._value)
1026 if hyperlink:
1027 # no, we *must* escape this text
1028 if not escape:
1029 s = cgi.escape(s)
1030 s = self.hyper_re.sub(self._hyper_repl, s)
1031 return s
1033 def stext(self, escape=0):
1034 ''' Render the value of the property as StructuredText.
1036 This requires the StructureText module to be installed separately.
1037 '''
1038 self.view_check()
1040 s = self.plain(escape=escape)
1041 if not StructuredText:
1042 return s
1043 return StructuredText(s,level=1,header=0)
1045 def field(self, size = 30):
1046 ''' Render the property as a field in HTML.
1048 If not editable, just display the value via plain().
1049 '''
1050 self.view_check()
1052 if self._value is None:
1053 value = ''
1054 else:
1055 value = cgi.escape(str(self._value))
1057 if self.is_edit_ok():
1058 value = '"'.join(value.split('"'))
1059 return self.input(name=self._formname,value=value,size=size)
1061 return self.plain()
1063 def multiline(self, escape=0, rows=5, cols=40):
1064 ''' Render a multiline form edit field for the property.
1066 If not editable, just display the plain() value in a <pre> tag.
1067 '''
1068 self.view_check()
1070 if self._value is None:
1071 value = ''
1072 else:
1073 value = cgi.escape(str(self._value))
1075 if self.is_edit_ok():
1076 value = '"'.join(value.split('"'))
1077 return '<textarea name="%s" rows="%s" cols="%s">%s</textarea>'%(
1078 self._formname, rows, cols, value)
1080 return '<pre>%s</pre>'%self.plain()
1082 def email(self, escape=1):
1083 ''' Render the value of the property as an obscured email address
1084 '''
1085 self.view_check()
1087 if self._value is None:
1088 value = ''
1089 else:
1090 value = str(self._value)
1091 if value.find('@') != -1:
1092 name, domain = value.split('@')
1093 domain = ' '.join(domain.split('.')[:-1])
1094 name = name.replace('.', ' ')
1095 value = '%s at %s ...'%(name, domain)
1096 else:
1097 value = value.replace('.', ' ')
1098 if escape:
1099 value = cgi.escape(value)
1100 return value
1102 class PasswordHTMLProperty(HTMLProperty):
1103 def plain(self):
1104 ''' Render a "plain" representation of the property
1105 '''
1106 self.view_check()
1108 if self._value is None:
1109 return ''
1110 return _('*encrypted*')
1112 def field(self, size = 30):
1113 ''' Render a form edit field for the property.
1115 If not editable, just display the value via plain().
1116 '''
1117 self.view_check()
1119 if self.is_edit_ok():
1120 return self.input(type="password", name=self._formname, size=size)
1122 return self.plain()
1124 def confirm(self, size = 30):
1125 ''' Render a second form edit field for the property, used for
1126 confirmation that the user typed the password correctly. Generates
1127 a field with name "@confirm@name".
1129 If not editable, display nothing.
1130 '''
1131 self.view_check()
1133 if self.is_edit_ok():
1134 return self.input(type="password",
1135 name="@confirm@%s"%self._formname, size=size)
1137 return ''
1139 class NumberHTMLProperty(HTMLProperty):
1140 def plain(self):
1141 ''' Render a "plain" representation of the property
1142 '''
1143 self.view_check()
1145 return str(self._value)
1147 def field(self, size = 30):
1148 ''' Render a form edit field for the property.
1150 If not editable, just display the value via plain().
1151 '''
1152 self.view_check()
1154 if self._value is None:
1155 value = ''
1156 else:
1157 value = cgi.escape(str(self._value))
1159 if self.is_edit_ok():
1160 value = '"'.join(value.split('"'))
1161 return self.input(name=self._formname,value=value,size=size)
1163 return self.plain()
1165 def __int__(self):
1166 ''' Return an int of me
1167 '''
1168 return int(self._value)
1170 def __float__(self):
1171 ''' Return a float of me
1172 '''
1173 return float(self._value)
1176 class BooleanHTMLProperty(HTMLProperty):
1177 def plain(self):
1178 ''' Render a "plain" representation of the property
1179 '''
1180 self.view_check()
1182 if self._value is None:
1183 return ''
1184 return self._value and "Yes" or "No"
1186 def field(self):
1187 ''' Render a form edit field for the property
1189 If not editable, just display the value via plain().
1190 '''
1191 self.view_check()
1193 if not is_edit_ok():
1194 return self.plain()
1196 checked = self._value and "checked" or ""
1197 if self._value:
1198 s = self.input(type="radio", name=self._formname, value="yes",
1199 checked="checked")
1200 s += 'Yes'
1201 s +=self.input(type="radio", name=self._formname, value="no")
1202 s += 'No'
1203 else:
1204 s = self.input(type="radio", name=self._formname, value="yes")
1205 s += 'Yes'
1206 s +=self.input(type="radio", name=self._formname, value="no",
1207 checked="checked")
1208 s += 'No'
1209 return s
1211 class DateHTMLProperty(HTMLProperty):
1212 def plain(self):
1213 ''' Render a "plain" representation of the property
1214 '''
1215 self.view_check()
1217 if self._value is None:
1218 return ''
1219 return str(self._value.local(self._db.getUserTimezone()))
1221 def now(self):
1222 ''' Return the current time.
1224 This is useful for defaulting a new value. Returns a
1225 DateHTMLProperty.
1226 '''
1227 self.view_check()
1229 return DateHTMLProperty(self._client, self._classname, self._nodeid,
1230 self._prop, self._formname, date.Date('.'))
1232 def field(self, size = 30):
1233 ''' Render a form edit field for the property
1235 If not editable, just display the value via plain().
1236 '''
1237 self.view_check()
1239 if self._value is None:
1240 value = ''
1241 else:
1242 tz = self._db.getUserTimezone()
1243 value = cgi.escape(str(self._value.local(tz)))
1245 if is_edit_ok():
1246 value = '"'.join(value.split('"'))
1247 return self.input(name=self._formname,value=value,size=size)
1249 return self.plain()
1251 def reldate(self, pretty=1):
1252 ''' Render the interval between the date and now.
1254 If the "pretty" flag is true, then make the display pretty.
1255 '''
1256 self.view_check()
1258 if not self._value:
1259 return ''
1261 # figure the interval
1262 interval = self._value - date.Date('.')
1263 if pretty:
1264 return interval.pretty()
1265 return str(interval)
1267 _marker = []
1268 def pretty(self, format=_marker):
1269 ''' Render the date in a pretty format (eg. month names, spaces).
1271 The format string is a standard python strftime format string.
1272 Note that if the day is zero, and appears at the start of the
1273 string, then it'll be stripped from the output. This is handy
1274 for the situatin when a date only specifies a month and a year.
1275 '''
1276 self.view_check()
1278 if format is not self._marker:
1279 return self._value.pretty(format)
1280 else:
1281 return self._value.pretty()
1283 def local(self, offset):
1284 ''' Return the date/time as a local (timezone offset) date/time.
1285 '''
1286 self.view_check()
1288 return DateHTMLProperty(self._client, self._classname, self._nodeid,
1289 self._prop, self._formname, self._value.local(offset))
1291 class IntervalHTMLProperty(HTMLProperty):
1292 def plain(self):
1293 ''' Render a "plain" representation of the property
1294 '''
1295 self.view_check()
1297 if self._value is None:
1298 return ''
1299 return str(self._value)
1301 def pretty(self):
1302 ''' Render the interval in a pretty format (eg. "yesterday")
1303 '''
1304 self.view_check()
1306 return self._value.pretty()
1308 def field(self, size = 30):
1309 ''' Render a form edit field for the property
1311 If not editable, just display the value via plain().
1312 '''
1313 self.view_check()
1315 if self._value is None:
1316 value = ''
1317 else:
1318 value = cgi.escape(str(self._value))
1320 if is_edit_ok():
1321 value = '"'.join(value.split('"'))
1322 return self.input(name=self._formname,value=value,size=size)
1324 return self.plain()
1326 class LinkHTMLProperty(HTMLProperty):
1327 ''' Link HTMLProperty
1328 Include the above as well as being able to access the class
1329 information. Stringifying the object itself results in the value
1330 from the item being displayed. Accessing attributes of this object
1331 result in the appropriate entry from the class being queried for the
1332 property accessed (so item/assignedto/name would look up the user
1333 entry identified by the assignedto property on item, and then the
1334 name property of that user)
1335 '''
1336 def __init__(self, *args, **kw):
1337 HTMLProperty.__init__(self, *args, **kw)
1338 # if we're representing a form value, then the -1 from the form really
1339 # should be a None
1340 if str(self._value) == '-1':
1341 self._value = None
1343 def __getattr__(self, attr):
1344 ''' return a new HTMLItem '''
1345 #print 'Link.getattr', (self, attr, self._value)
1346 if not self._value:
1347 raise AttributeError, "Can't access missing value"
1348 if self._prop.classname == 'user':
1349 klass = HTMLUser
1350 else:
1351 klass = HTMLItem
1352 i = klass(self._client, self._prop.classname, self._value)
1353 return getattr(i, attr)
1355 def plain(self, escape=0):
1356 ''' Render a "plain" representation of the property
1357 '''
1358 self.view_check()
1360 if self._value is None:
1361 return ''
1362 linkcl = self._db.classes[self._prop.classname]
1363 k = linkcl.labelprop(1)
1364 value = str(linkcl.get(self._value, k))
1365 if escape:
1366 value = cgi.escape(value)
1367 return value
1369 def field(self, showid=0, size=None):
1370 ''' Render a form edit field for the property
1372 If not editable, just display the value via plain().
1373 '''
1374 self.view_check()
1376 if not self.is_edit_ok():
1377 return self.plain()
1379 # edit field
1380 linkcl = self._db.getclass(self._prop.classname)
1381 if self._value is None:
1382 value = ''
1383 else:
1384 k = linkcl.getkey()
1385 if k:
1386 label = linkcl.get(self._value, k)
1387 else:
1388 label = self._value
1389 value = cgi.escape(str(self._value))
1390 value = '"'.join(value.split('"'))
1391 return '<input name="%s" value="%s" size="%s">'%(self._formname,
1392 label, size)
1394 def menu(self, size=None, height=None, showid=0, additional=[],
1395 sort_on=None, **conditions):
1396 ''' Render a form select list for this property
1398 If not editable, just display the value via plain().
1399 '''
1400 self.view_check()
1402 if not self.is_edit_ok():
1403 return self.plain()
1405 value = self._value
1407 linkcl = self._db.getclass(self._prop.classname)
1408 l = ['<select name="%s">'%self._formname]
1409 k = linkcl.labelprop(1)
1410 s = ''
1411 if value is None:
1412 s = 'selected="selected" '
1413 l.append(_('<option %svalue="-1">- no selection -</option>')%s)
1414 if linkcl.getprops().has_key('order'):
1415 sort_on = ('+', 'order')
1416 else:
1417 if sort_on is None:
1418 sort_on = ('+', linkcl.labelprop())
1419 else:
1420 sort_on = ('+', sort_on)
1421 options = linkcl.filter(None, conditions, sort_on, (None, None))
1423 # make sure we list the current value if it's retired
1424 if self._value and self._value not in options:
1425 options.insert(0, self._value)
1427 for optionid in options:
1428 # get the option value, and if it's None use an empty string
1429 option = linkcl.get(optionid, k) or ''
1431 # figure if this option is selected
1432 s = ''
1433 if value in [optionid, option]:
1434 s = 'selected="selected" '
1436 # figure the label
1437 if showid:
1438 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1439 else:
1440 lab = option
1442 # truncate if it's too long
1443 if size is not None and len(lab) > size:
1444 lab = lab[:size-3] + '...'
1445 if additional:
1446 m = []
1447 for propname in additional:
1448 m.append(linkcl.get(optionid, propname))
1449 lab = lab + ' (%s)'%', '.join(map(str, m))
1451 # and generate
1452 lab = cgi.escape(lab)
1453 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
1454 l.append('</select>')
1455 return '\n'.join(l)
1456 # def checklist(self, ...)
1458 class MultilinkHTMLProperty(HTMLProperty):
1459 ''' Multilink HTMLProperty
1461 Also be iterable, returning a wrapper object like the Link case for
1462 each entry in the multilink.
1463 '''
1464 def __init__(self, *args, **kwargs):
1465 HTMLProperty.__init__(self, *args, **kwargs)
1466 if self._value:
1467 sortfun = make_sort_function(self._db, self._prop.classname)
1468 self._value.sort(sortfun)
1470 def __len__(self):
1471 ''' length of the multilink '''
1472 return len(self._value)
1474 def __getattr__(self, attr):
1475 ''' no extended attribute accesses make sense here '''
1476 raise AttributeError, attr
1478 def __getitem__(self, num):
1479 ''' iterate and return a new HTMLItem
1480 '''
1481 #print 'Multi.getitem', (self, num)
1482 value = self._value[num]
1483 if self._prop.classname == 'user':
1484 klass = HTMLUser
1485 else:
1486 klass = HTMLItem
1487 return klass(self._client, self._prop.classname, value)
1489 def __contains__(self, value):
1490 ''' Support the "in" operator. We have to make sure the passed-in
1491 value is a string first, not a HTMLProperty.
1492 '''
1493 return str(value) in self._value
1495 def reverse(self):
1496 ''' return the list in reverse order
1497 '''
1498 l = self._value[:]
1499 l.reverse()
1500 if self._prop.classname == 'user':
1501 klass = HTMLUser
1502 else:
1503 klass = HTMLItem
1504 return [klass(self._client, self._prop.classname, value) for value in l]
1506 def plain(self, escape=0):
1507 ''' Render a "plain" representation of the property
1508 '''
1509 self.view_check()
1511 linkcl = self._db.classes[self._prop.classname]
1512 k = linkcl.labelprop(1)
1513 labels = []
1514 for v in self._value:
1515 labels.append(linkcl.get(v, k))
1516 value = ', '.join(labels)
1517 if escape:
1518 value = cgi.escape(value)
1519 return value
1521 def field(self, size=30, showid=0):
1522 ''' Render a form edit field for the property
1524 If not editable, just display the value via plain().
1525 '''
1526 self.view_check()
1528 if not self.is_edit_ok():
1529 return self.plain()
1531 linkcl = self._db.getclass(self._prop.classname)
1532 value = self._value[:]
1533 # map the id to the label property
1534 if not linkcl.getkey():
1535 showid=1
1536 if not showid:
1537 k = linkcl.labelprop(1)
1538 value = [linkcl.get(v, k) for v in value]
1539 value = cgi.escape(','.join(value))
1540 return self.input(name=self._formname,size=size,value=value)
1542 def menu(self, size=None, height=None, showid=0, additional=[],
1543 sort_on=None, **conditions):
1544 ''' Render a form select list for this property
1546 If not editable, just display the value via plain().
1547 '''
1548 self.view_check()
1550 if not self.is_edit_ok():
1551 return self.plain()
1553 value = self._value
1555 linkcl = self._db.getclass(self._prop.classname)
1556 if sort_on is None:
1557 sort_on = ('+', find_sort_key(linkcl))
1558 else:
1559 sort_on = ('+', sort_on)
1560 options = linkcl.filter(None, conditions, sort_on)
1561 height = height or min(len(options), 7)
1562 l = ['<select multiple name="%s" size="%s">'%(self._formname, height)]
1563 k = linkcl.labelprop(1)
1565 # make sure we list the current values if they're retired
1566 for val in value:
1567 if val not in options:
1568 options.insert(0, val)
1570 for optionid in options:
1571 # get the option value, and if it's None use an empty string
1572 option = linkcl.get(optionid, k) or ''
1574 # figure if this option is selected
1575 s = ''
1576 if optionid in value or option in value:
1577 s = 'selected="selected" '
1579 # figure the label
1580 if showid:
1581 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1582 else:
1583 lab = option
1584 # truncate if it's too long
1585 if size is not None and len(lab) > size:
1586 lab = lab[:size-3] + '...'
1587 if additional:
1588 m = []
1589 for propname in additional:
1590 m.append(linkcl.get(optionid, propname))
1591 lab = lab + ' (%s)'%', '.join(m)
1593 # and generate
1594 lab = cgi.escape(lab)
1595 l.append('<option %svalue="%s">%s</option>'%(s, optionid,
1596 lab))
1597 l.append('</select>')
1598 return '\n'.join(l)
1600 # set the propclasses for HTMLItem
1601 propclasses = (
1602 (hyperdb.String, StringHTMLProperty),
1603 (hyperdb.Number, NumberHTMLProperty),
1604 (hyperdb.Boolean, BooleanHTMLProperty),
1605 (hyperdb.Date, DateHTMLProperty),
1606 (hyperdb.Interval, IntervalHTMLProperty),
1607 (hyperdb.Password, PasswordHTMLProperty),
1608 (hyperdb.Link, LinkHTMLProperty),
1609 (hyperdb.Multilink, MultilinkHTMLProperty),
1610 )
1612 def make_sort_function(db, classname, sort_on=None):
1613 '''Make a sort function for a given class
1614 '''
1615 linkcl = db.getclass(classname)
1616 if sort_on is None:
1617 sort_on = find_sort_key(linkcl)
1618 def sortfunc(a, b):
1619 return cmp(linkcl.get(a, sort_on), linkcl.get(b, sort_on))
1620 return sortfunc
1622 def find_sort_key(linkcl):
1623 if linkcl.getprops().has_key('order'):
1624 return 'order'
1625 else:
1626 return linkcl.labelprop()
1628 def handleListCGIValue(value):
1629 ''' Value is either a single item or a list of items. Each item has a
1630 .value that we're actually interested in.
1631 '''
1632 if isinstance(value, type([])):
1633 return [value.value for value in value]
1634 else:
1635 value = value.value.strip()
1636 if not value:
1637 return []
1638 return value.split(',')
1640 class ShowDict:
1641 ''' A convenience access to the :columns index parameters
1642 '''
1643 def __init__(self, columns):
1644 self.columns = {}
1645 for col in columns:
1646 self.columns[col] = 1
1647 def __getitem__(self, name):
1648 return self.columns.has_key(name)
1650 class HTMLRequest(HTMLInputMixin):
1651 '''The *request*, holding the CGI form and environment.
1653 - "form" the CGI form as a cgi.FieldStorage
1654 - "env" the CGI environment variables
1655 - "base" the base URL for this instance
1656 - "user" a HTMLUser instance for this user
1657 - "classname" the current classname (possibly None)
1658 - "template" the current template (suffix, also possibly None)
1660 Index args:
1662 - "columns" dictionary of the columns to display in an index page
1663 - "show" a convenience access to columns - request/show/colname will
1664 be true if the columns should be displayed, false otherwise
1665 - "sort" index sort column (direction, column name)
1666 - "group" index grouping property (direction, column name)
1667 - "filter" properties to filter the index on
1668 - "filterspec" values to filter the index on
1669 - "search_text" text to perform a full-text search on for an index
1670 '''
1671 def __init__(self, client):
1672 # _client is needed by HTMLInputMixin
1673 self._client = self.client = client
1675 # easier access vars
1676 self.form = client.form
1677 self.env = client.env
1678 self.base = client.base
1679 self.user = HTMLUser(client, 'user', client.userid)
1681 # store the current class name and action
1682 self.classname = client.classname
1683 self.template = client.template
1685 # the special char to use for special vars
1686 self.special_char = '@'
1688 HTMLInputMixin.__init__(self)
1690 self._post_init()
1692 def _post_init(self):
1693 ''' Set attributes based on self.form
1694 '''
1695 # extract the index display information from the form
1696 self.columns = []
1697 for name in ':columns @columns'.split():
1698 if self.form.has_key(name):
1699 self.special_char = name[0]
1700 self.columns = handleListCGIValue(self.form[name])
1701 break
1702 self.show = ShowDict(self.columns)
1704 # sorting
1705 self.sort = (None, None)
1706 for name in ':sort @sort'.split():
1707 if self.form.has_key(name):
1708 self.special_char = name[0]
1709 sort = self.form[name].value
1710 if sort.startswith('-'):
1711 self.sort = ('-', sort[1:])
1712 else:
1713 self.sort = ('+', sort)
1714 if self.form.has_key(self.special_char+'sortdir'):
1715 self.sort = ('-', self.sort[1])
1717 # grouping
1718 self.group = (None, None)
1719 for name in ':group @group'.split():
1720 if self.form.has_key(name):
1721 self.special_char = name[0]
1722 group = self.form[name].value
1723 if group.startswith('-'):
1724 self.group = ('-', group[1:])
1725 else:
1726 self.group = ('+', group)
1727 if self.form.has_key(self.special_char+'groupdir'):
1728 self.group = ('-', self.group[1])
1730 # filtering
1731 self.filter = []
1732 for name in ':filter @filter'.split():
1733 if self.form.has_key(name):
1734 self.special_char = name[0]
1735 self.filter = handleListCGIValue(self.form[name])
1737 self.filterspec = {}
1738 db = self.client.db
1739 if self.classname is not None:
1740 props = db.getclass(self.classname).getprops()
1741 for name in self.filter:
1742 if not self.form.has_key(name):
1743 continue
1744 prop = props[name]
1745 fv = self.form[name]
1746 if (isinstance(prop, hyperdb.Link) or
1747 isinstance(prop, hyperdb.Multilink)):
1748 self.filterspec[name] = lookupIds(db, prop,
1749 handleListCGIValue(fv))
1750 else:
1751 if isinstance(fv, type([])):
1752 self.filterspec[name] = [v.value for v in fv]
1753 else:
1754 self.filterspec[name] = fv.value
1756 # full-text search argument
1757 self.search_text = None
1758 for name in ':search_text @search_text'.split():
1759 if self.form.has_key(name):
1760 self.special_char = name[0]
1761 self.search_text = self.form[name].value
1763 # pagination - size and start index
1764 # figure batch args
1765 self.pagesize = 50
1766 for name in ':pagesize @pagesize'.split():
1767 if self.form.has_key(name):
1768 self.special_char = name[0]
1769 self.pagesize = int(self.form[name].value)
1771 self.startwith = 0
1772 for name in ':startwith @startwith'.split():
1773 if self.form.has_key(name):
1774 self.special_char = name[0]
1775 self.startwith = int(self.form[name].value)
1777 def updateFromURL(self, url):
1778 ''' Parse the URL for query args, and update my attributes using the
1779 values.
1780 '''
1781 env = {'QUERY_STRING': url}
1782 self.form = cgi.FieldStorage(environ=env)
1784 self._post_init()
1786 def update(self, kwargs):
1787 ''' Update my attributes using the keyword args
1788 '''
1789 self.__dict__.update(kwargs)
1790 if kwargs.has_key('columns'):
1791 self.show = ShowDict(self.columns)
1793 def description(self):
1794 ''' Return a description of the request - handle for the page title.
1795 '''
1796 s = [self.client.db.config.TRACKER_NAME]
1797 if self.classname:
1798 if self.client.nodeid:
1799 s.append('- %s%s'%(self.classname, self.client.nodeid))
1800 else:
1801 if self.template == 'item':
1802 s.append('- new %s'%self.classname)
1803 elif self.template == 'index':
1804 s.append('- %s index'%self.classname)
1805 else:
1806 s.append('- %s %s'%(self.classname, self.template))
1807 else:
1808 s.append('- home')
1809 return ' '.join(s)
1811 def __str__(self):
1812 d = {}
1813 d.update(self.__dict__)
1814 f = ''
1815 for k in self.form.keys():
1816 f += '\n %r=%r'%(k,handleListCGIValue(self.form[k]))
1817 d['form'] = f
1818 e = ''
1819 for k,v in self.env.items():
1820 e += '\n %r=%r'%(k, v)
1821 d['env'] = e
1822 return '''
1823 form: %(form)s
1824 base: %(base)r
1825 classname: %(classname)r
1826 template: %(template)r
1827 columns: %(columns)r
1828 sort: %(sort)r
1829 group: %(group)r
1830 filter: %(filter)r
1831 search_text: %(search_text)r
1832 pagesize: %(pagesize)r
1833 startwith: %(startwith)r
1834 env: %(env)s
1835 '''%d
1837 def indexargs_form(self, columns=1, sort=1, group=1, filter=1,
1838 filterspec=1):
1839 ''' return the current index args as form elements '''
1840 l = []
1841 sc = self.special_char
1842 s = self.input(type="hidden",name="%s",value="%s")
1843 if columns and self.columns:
1844 l.append(s%(sc+'columns', ','.join(self.columns)))
1845 if sort and self.sort[1] is not None:
1846 if self.sort[0] == '-':
1847 val = '-'+self.sort[1]
1848 else:
1849 val = self.sort[1]
1850 l.append(s%(sc+'sort', val))
1851 if group and self.group[1] is not None:
1852 if self.group[0] == '-':
1853 val = '-'+self.group[1]
1854 else:
1855 val = self.group[1]
1856 l.append(s%(sc+'group', val))
1857 if filter and self.filter:
1858 l.append(s%(sc+'filter', ','.join(self.filter)))
1859 if filterspec:
1860 for k,v in self.filterspec.items():
1861 if type(v) == type([]):
1862 l.append(s%(k, ','.join(v)))
1863 else:
1864 l.append(s%(k, v))
1865 if self.search_text:
1866 l.append(s%(sc+'search_text', self.search_text))
1867 l.append(s%(sc+'pagesize', self.pagesize))
1868 l.append(s%(sc+'startwith', self.startwith))
1869 return '\n'.join(l)
1871 def indexargs_url(self, url, args):
1872 ''' Embed the current index args in a URL
1873 '''
1874 sc = self.special_char
1875 l = ['%s=%s'%(k,v) for k,v in args.items()]
1877 # pull out the special values (prefixed by @ or :)
1878 specials = {}
1879 for key in args.keys():
1880 if key[0] in '@:':
1881 specials[key[1:]] = args[key]
1883 # ok, now handle the specials we received in the request
1884 if self.columns and not specials.has_key('columns'):
1885 l.append(sc+'columns=%s'%(','.join(self.columns)))
1886 if self.sort[1] is not None and not specials.has_key('sort'):
1887 if self.sort[0] == '-':
1888 val = '-'+self.sort[1]
1889 else:
1890 val = self.sort[1]
1891 l.append(sc+'sort=%s'%val)
1892 if self.group[1] is not None and not specials.has_key('group'):
1893 if self.group[0] == '-':
1894 val = '-'+self.group[1]
1895 else:
1896 val = self.group[1]
1897 l.append(sc+'group=%s'%val)
1898 if self.filter and not specials.has_key('filter'):
1899 l.append(sc+'filter=%s'%(','.join(self.filter)))
1900 if self.search_text and not specials.has_key('search_text'):
1901 l.append(sc+'search_text=%s'%self.search_text)
1902 if not specials.has_key('pagesize'):
1903 l.append(sc+'pagesize=%s'%self.pagesize)
1904 if not specials.has_key('startwith'):
1905 l.append(sc+'startwith=%s'%self.startwith)
1907 # finally, the remainder of the filter args in the request
1908 for k,v in self.filterspec.items():
1909 if not args.has_key(k):
1910 if type(v) == type([]):
1911 l.append('%s=%s'%(k, ','.join(v)))
1912 else:
1913 l.append('%s=%s'%(k, v))
1914 return '%s?%s'%(url, '&'.join(l))
1915 indexargs_href = indexargs_url
1917 def base_javascript(self):
1918 return '''
1919 <script type="text/javascript">
1920 submitted = false;
1921 function submit_once() {
1922 if (submitted) {
1923 alert("Your request is being processed.\\nPlease be patient.");
1924 event.returnValue = 0; // work-around for IE
1925 return 0;
1926 }
1927 submitted = true;
1928 return 1;
1929 }
1931 function help_window(helpurl, width, height) {
1932 HelpWin = window.open('%s' + helpurl, 'RoundupHelpWindow', 'scrollbars=yes,resizable=yes,toolbar=no,height='+height+',width='+width);
1933 }
1934 </script>
1935 '''%self.base
1937 def batch(self):
1938 ''' Return a batch object for results from the "current search"
1939 '''
1940 filterspec = self.filterspec
1941 sort = self.sort
1942 group = self.group
1944 # get the list of ids we're batching over
1945 klass = self.client.db.getclass(self.classname)
1946 if self.search_text:
1947 matches = self.client.db.indexer.search(
1948 re.findall(r'\b\w{2,25}\b', self.search_text), klass)
1949 else:
1950 matches = None
1951 l = klass.filter(matches, filterspec, sort, group)
1953 # return the batch object, using IDs only
1954 return Batch(self.client, l, self.pagesize, self.startwith,
1955 classname=self.classname)
1957 # extend the standard ZTUtils Batch object to remove dependency on
1958 # Acquisition and add a couple of useful methods
1959 class Batch(ZTUtils.Batch):
1960 ''' Use me to turn a list of items, or item ids of a given class, into a
1961 series of batches.
1963 ========= ========================================================
1964 Parameter Usage
1965 ========= ========================================================
1966 sequence a list of HTMLItems or item ids
1967 classname if sequence is a list of ids, this is the class of item
1968 size how big to make the sequence.
1969 start where to start (0-indexed) in the sequence.
1970 end where to end (0-indexed) in the sequence.
1971 orphan if the next batch would contain less items than this
1972 value, then it is combined with this batch
1973 overlap the number of items shared between adjacent batches
1974 ========= ========================================================
1976 Attributes: Note that the "start" attribute, unlike the
1977 argument, is a 1-based index (I know, lame). "first" is the
1978 0-based index. "length" is the actual number of elements in
1979 the batch.
1981 "sequence_length" is the length of the original, unbatched, sequence.
1982 '''
1983 def __init__(self, client, sequence, size, start, end=0, orphan=0,
1984 overlap=0, classname=None):
1985 self.client = client
1986 self.last_index = self.last_item = None
1987 self.current_item = None
1988 self.classname = classname
1989 self.sequence_length = len(sequence)
1990 ZTUtils.Batch.__init__(self, sequence, size, start, end, orphan,
1991 overlap)
1993 # overwrite so we can late-instantiate the HTMLItem instance
1994 def __getitem__(self, index):
1995 if index < 0:
1996 if index + self.end < self.first: raise IndexError, index
1997 return self._sequence[index + self.end]
1999 if index >= self.length:
2000 raise IndexError, index
2002 # move the last_item along - but only if the fetched index changes
2003 # (for some reason, index 0 is fetched twice)
2004 if index != self.last_index:
2005 self.last_item = self.current_item
2006 self.last_index = index
2008 item = self._sequence[index + self.first]
2009 if self.classname:
2010 # map the item ids to instances
2011 if self.classname == 'user':
2012 item = HTMLUser(self.client, self.classname, item)
2013 else:
2014 item = HTMLItem(self.client, self.classname, item)
2015 self.current_item = item
2016 return item
2018 def propchanged(self, property):
2019 ''' Detect if the property marked as being the group property
2020 changed in the last iteration fetch
2021 '''
2022 if (self.last_item is None or
2023 self.last_item[property] != self.current_item[property]):
2024 return 1
2025 return 0
2027 # override these 'cos we don't have access to acquisition
2028 def previous(self):
2029 if self.start == 1:
2030 return None
2031 return Batch(self.client, self._sequence, self._size,
2032 self.first - self._size + self.overlap, 0, self.orphan,
2033 self.overlap)
2035 def next(self):
2036 try:
2037 self._sequence[self.end]
2038 except IndexError:
2039 return None
2040 return Batch(self.client, self._sequence, self._size,
2041 self.end - self.overlap, 0, self.orphan, self.overlap)
2043 class TemplatingUtils:
2044 ''' Utilities for templating
2045 '''
2046 def __init__(self, client):
2047 self.client = client
2048 def Batch(self, sequence, size, start, end=0, orphan=0, overlap=0):
2049 return Batch(self.client, sequence, size, start, end, orphan,
2050 overlap)