1 import sys, cgi, urllib, os, re, os.path, time, errno, mimetypes
3 from roundup import hyperdb, date, rcsv
4 from roundup.i18n import _
6 try:
7 import cPickle as pickle
8 except ImportError:
9 import pickle
10 try:
11 import cStringIO as StringIO
12 except ImportError:
13 import StringIO
14 try:
15 import StructuredText
16 except ImportError:
17 StructuredText = None
19 # bring in the templating support
20 from roundup.cgi.PageTemplates import PageTemplate
21 from roundup.cgi.PageTemplates.Expressions import getEngine
22 from roundup.cgi.TAL.TALInterpreter import TALInterpreter
23 from roundup.cgi import ZTUtils
25 class NoTemplate(Exception):
26 pass
28 def find_template(dir, name, extension):
29 ''' Find a template in the nominated dir
30 '''
31 # find the source
32 if extension:
33 filename = '%s.%s'%(name, extension)
34 else:
35 filename = name
37 # try old-style
38 src = os.path.join(dir, filename)
39 if os.path.exists(src):
40 return (src, filename)
42 # try with a .html extension (new-style)
43 filename = filename + '.html'
44 src = os.path.join(dir, filename)
45 if os.path.exists(src):
46 return (src, filename)
48 # no extension == no generic template is possible
49 if not extension:
50 raise NoTemplate, 'Template file "%s" doesn\'t exist'%name
52 # try for a _generic template
53 generic = '_generic.%s'%extension
54 src = os.path.join(dir, generic)
55 if os.path.exists(src):
56 return (src, generic)
58 # finally, try _generic.html
59 generic = generic + '.html'
60 src = os.path.join(dir, generic)
61 if os.path.exists(src):
62 return (src, generic)
64 raise NoTemplate, 'No template file exists for templating "%s" '\
65 'with template "%s" (neither "%s" nor "%s")'%(name, extension,
66 filename, generic)
68 class Templates:
69 templates = {}
71 def __init__(self, dir):
72 self.dir = dir
74 def precompileTemplates(self):
75 ''' Go through a directory and precompile all the templates therein
76 '''
77 for filename in os.listdir(self.dir):
78 if os.path.isdir(filename): continue
79 if '.' in filename:
80 name, extension = filename.split('.')
81 self.get(name, extension)
82 else:
83 self.get(filename, None)
85 def get(self, name, extension=None):
86 ''' Interface to get a template, possibly loading a compiled template.
88 "name" and "extension" indicate the template we're after, which in
89 most cases will be "name.extension". If "extension" is None, then
90 we look for a template just called "name" with no extension.
92 If the file "name.extension" doesn't exist, we look for
93 "_generic.extension" as a fallback.
94 '''
95 # default the name to "home"
96 if name is None:
97 name = 'home'
98 elif extension is None and '.' in name:
99 # split name
100 name, extension = name.split('.')
102 # find the source
103 src, filename = find_template(self.dir, name, extension)
105 # has it changed?
106 try:
107 stime = os.stat(src)[os.path.stat.ST_MTIME]
108 except os.error, error:
109 if error.errno != errno.ENOENT:
110 raise
112 if self.templates.has_key(src) and \
113 stime < self.templates[src].mtime:
114 # compiled template is up to date
115 return self.templates[src]
117 # compile the template
118 self.templates[src] = pt = RoundupPageTemplate()
119 # use pt_edit so we can pass the content_type guess too
120 content_type = mimetypes.guess_type(filename)[0] or 'text/html'
121 pt.pt_edit(open(src).read(), content_type)
122 pt.id = filename
123 pt.mtime = time.time()
124 return pt
126 def __getitem__(self, name):
127 name, extension = os.path.splitext(name)
128 if extension:
129 extension = extension[1:]
130 try:
131 return self.get(name, extension)
132 except NoTemplate, message:
133 raise KeyError, message
135 class RoundupPageTemplate(PageTemplate.PageTemplate):
136 ''' A Roundup-specific PageTemplate.
138 Interrogate the client to set up the various template variables to
139 be available:
141 *context*
142 this is one of three things:
143 1. None - we're viewing a "home" page
144 2. The current class of item being displayed. This is an HTMLClass
145 instance.
146 3. The current item from the database, if we're viewing a specific
147 item, as an HTMLItem instance.
148 *request*
149 Includes information about the current request, including:
150 - the url
151 - the current index information (``filterspec``, ``filter`` args,
152 ``properties``, etc) parsed out of the form.
153 - methods for easy filterspec link generation
154 - *user*, the current user node as an HTMLItem instance
155 - *form*, the current CGI form information as a FieldStorage
156 *config*
157 The current tracker config.
158 *db*
159 The current database, used to access arbitrary database items.
160 *utils*
161 This is a special class that has its base in the TemplatingUtils
162 class in this file. If the tracker interfaces module defines a
163 TemplatingUtils class then it is mixed in, overriding the methods
164 in the base class.
165 '''
166 def getContext(self, client, classname, request):
167 # construct the TemplatingUtils class
168 utils = TemplatingUtils
169 if hasattr(client.instance.interfaces, 'TemplatingUtils'):
170 class utils(client.instance.interfaces.TemplatingUtils, utils):
171 pass
173 c = {
174 'options': {},
175 'nothing': None,
176 'request': request,
177 'db': HTMLDatabase(client),
178 'config': client.instance.config,
179 'tracker': client.instance,
180 'utils': utils(client),
181 'templates': Templates(client.instance.config.TEMPLATES),
182 }
183 # add in the item if there is one
184 if client.nodeid:
185 if classname == 'user':
186 c['context'] = HTMLUser(client, classname, client.nodeid,
187 anonymous=1)
188 else:
189 c['context'] = HTMLItem(client, classname, client.nodeid,
190 anonymous=1)
191 elif client.db.classes.has_key(classname):
192 c['context'] = HTMLClass(client, classname, anonymous=1)
193 return c
195 def render(self, client, classname, request, **options):
196 """Render this Page Template"""
198 if not self._v_cooked:
199 self._cook()
201 __traceback_supplement__ = (PageTemplate.PageTemplateTracebackSupplement, self)
203 if self._v_errors:
204 raise PageTemplate.PTRuntimeError, \
205 'Page Template %s has errors.'%self.id
207 # figure the context
208 classname = classname or client.classname
209 request = request or HTMLRequest(client)
210 c = self.getContext(client, classname, request)
211 c.update({'options': options})
213 # and go
214 output = StringIO.StringIO()
215 TALInterpreter(self._v_program, self.macros,
216 getEngine().getContext(c), output, tal=1, strictinsert=0)()
217 return output.getvalue()
219 class HTMLDatabase:
220 ''' Return HTMLClasses for valid class fetches
221 '''
222 def __init__(self, client):
223 self._client = client
224 self._db = client.db
226 # we want config to be exposed
227 self.config = client.db.config
229 def __getitem__(self, item, desre=re.compile(r'(?P<cl>\w+)(?P<id>[-\d]+)')):
230 # check to see if we're actually accessing an item
231 m = desre.match(item)
232 if m:
233 self._client.db.getclass(m.group('cl'))
234 return HTMLItem(self._client, m.group('cl'), m.group('id'))
235 else:
236 self._client.db.getclass(item)
237 return HTMLClass(self._client, item)
239 def __getattr__(self, attr):
240 try:
241 return self[attr]
242 except KeyError:
243 raise AttributeError, attr
245 def classes(self):
246 l = self._client.db.classes.keys()
247 l.sort()
248 return [HTMLClass(self._client, cn) for cn in l]
250 def lookupIds(db, prop, ids, num_re=re.compile('-?\d+')):
251 cl = db.getclass(prop.classname)
252 l = []
253 for entry in ids:
254 if num_re.match(entry):
255 l.append(entry)
256 else:
257 try:
258 l.append(cl.lookup(entry))
259 except KeyError:
260 # ignore invalid keys
261 pass
262 return l
264 class HTMLPermissions:
265 ''' Helpers that provide answers to commonly asked Permission questions.
266 '''
267 def is_edit_ok(self):
268 ''' Is the user allowed to Edit the current class?
269 '''
270 return self._db.security.hasPermission('Edit', self._client.userid,
271 self._classname)
272 def is_view_ok(self):
273 ''' Is the user allowed to View the current class?
274 '''
275 return self._db.security.hasPermission('View', self._client.userid,
276 self._classname)
277 def is_only_view_ok(self):
278 ''' Is the user only allowed to View (ie. not Edit) the current class?
279 '''
280 return self.is_view_ok() and not self.is_edit_ok()
282 class HTMLClass(HTMLPermissions):
283 ''' Accesses through a class (either through *class* or *db.<classname>*)
284 '''
285 def __init__(self, client, classname, anonymous=0):
286 self._client = client
287 self._db = client.db
288 self._anonymous = anonymous
290 # we want classname to be exposed, but _classname gives a
291 # consistent API for extending Class/Item
292 self._classname = self.classname = classname
293 self._klass = self._db.getclass(self.classname)
294 self._props = self._klass.getprops()
296 def __repr__(self):
297 return '<HTMLClass(0x%x) %s>'%(id(self), self.classname)
299 def __getitem__(self, item):
300 ''' return an HTMLProperty instance
301 '''
302 #print 'HTMLClass.getitem', (self, item)
304 # we don't exist
305 if item == 'id':
306 return None
308 # get the property
309 prop = self._props[item]
311 # look up the correct HTMLProperty class
312 form = self._client.form
313 for klass, htmlklass in propclasses:
314 if not isinstance(prop, klass):
315 continue
316 if form.has_key(item):
317 if isinstance(prop, hyperdb.Multilink):
318 value = lookupIds(self._db, prop,
319 handleListCGIValue(form[item]))
320 elif isinstance(prop, hyperdb.Link):
321 value = form[item].value.strip()
322 if value:
323 value = lookupIds(self._db, prop, [value])[0]
324 else:
325 value = None
326 else:
327 value = form[item].value.strip() or None
328 else:
329 if isinstance(prop, hyperdb.Multilink):
330 value = []
331 else:
332 value = None
333 return htmlklass(self._client, self._classname, '', prop, item,
334 value, self._anonymous)
336 # no good
337 raise KeyError, item
339 def __getattr__(self, attr):
340 ''' convenience access '''
341 try:
342 return self[attr]
343 except KeyError:
344 raise AttributeError, attr
346 def designator(self):
347 ''' Return this class' designator (classname) '''
348 return self._classname
350 def getItem(self, itemid, num_re=re.compile('-?\d+')):
351 ''' Get an item of this class by its item id.
352 '''
353 # make sure we're looking at an itemid
354 if not num_re.match(itemid):
355 itemid = self._klass.lookup(itemid)
357 if self.classname == 'user':
358 klass = HTMLUser
359 else:
360 klass = HTMLItem
362 return klass(self._client, self.classname, itemid)
364 def properties(self, sort=1):
365 ''' Return HTMLProperty for all of this class' properties.
366 '''
367 l = []
368 for name, prop in self._props.items():
369 for klass, htmlklass in propclasses:
370 if isinstance(prop, hyperdb.Multilink):
371 value = []
372 else:
373 value = None
374 if isinstance(prop, klass):
375 l.append(htmlklass(self._client, self._classname, '',
376 prop, name, value, self._anonymous))
377 if sort:
378 l.sort(lambda a,b:cmp(a._name, b._name))
379 return l
381 def list(self):
382 ''' List all items in this class.
383 '''
384 if self.classname == 'user':
385 klass = HTMLUser
386 else:
387 klass = HTMLItem
389 # get the list and sort it nicely
390 l = self._klass.list()
391 sortfunc = make_sort_function(self._db, self.classname)
392 l.sort(sortfunc)
394 l = [klass(self._client, self.classname, x) for x in l]
395 return l
397 def csv(self):
398 ''' Return the items of this class as a chunk of CSV text.
399 '''
400 if rcsv.error:
401 return rcsv.error
403 props = self.propnames()
404 s = StringIO.StringIO()
405 writer = rcsv.writer(s, rcsv.comma_separated)
406 writer.writerow(props)
407 for nodeid in self._klass.list():
408 l = []
409 for name in props:
410 value = self._klass.get(nodeid, name)
411 if value is None:
412 l.append('')
413 elif isinstance(value, type([])):
414 l.append(':'.join(map(str, value)))
415 else:
416 l.append(str(self._klass.get(nodeid, name)))
417 writer.writerow(l)
418 return s.getvalue()
420 def propnames(self):
421 ''' Return the list of the names of the properties of this class.
422 '''
423 idlessprops = self._klass.getprops(protected=0).keys()
424 idlessprops.sort()
425 return ['id'] + idlessprops
427 def filter(self, request=None):
428 ''' Return a list of items from this class, filtered and sorted
429 by the current requested filterspec/filter/sort/group args
430 '''
431 # XXX allow direct specification of the filterspec etc.
432 if request is not None:
433 filterspec = request.filterspec
434 sort = request.sort
435 group = request.group
436 else:
437 filterspec = {}
438 sort = (None,None)
439 group = (None,None)
440 if self.classname == 'user':
441 klass = HTMLUser
442 else:
443 klass = HTMLItem
444 l = [klass(self._client, self.classname, x)
445 for x in self._klass.filter(None, filterspec, sort, group)]
446 return l
448 def classhelp(self, properties=None, label='(list)', width='500',
449 height='400', property=''):
450 ''' Pop up a javascript window with class help
452 This generates a link to a popup window which displays the
453 properties indicated by "properties" of the class named by
454 "classname". The "properties" should be a comma-separated list
455 (eg. 'id,name,description'). Properties defaults to all the
456 properties of a class (excluding id, creator, created and
457 activity).
459 You may optionally override the label displayed, the width and
460 height. The popup window will be resizable and scrollable.
462 If the "property" arg is given, it's passed through to the
463 javascript help_window function.
464 '''
465 if properties is None:
466 properties = self._klass.getprops(protected=0).keys()
467 properties.sort()
468 properties = ','.join(properties)
469 if property:
470 property = '&property=%s'%property
471 return '<a class="classhelp" href="javascript:help_window(\'%s?'\
472 '@startwith=0&@template=help&properties=%s%s\', \'%s\', \
473 \'%s\')">%s</a>'%(self.classname, properties, property, width,
474 height, label)
476 def submit(self, label="Submit New Entry"):
477 ''' Generate a submit button (and action hidden element)
478 '''
479 return ' <input type="hidden" name="@action" value="new">\n'\
480 ' <input type="submit" name="submit" value="%s">'%label
482 def history(self):
483 return 'New node - no history'
485 def renderWith(self, name, **kwargs):
486 ''' Render this class with the given template.
487 '''
488 # create a new request and override the specified args
489 req = HTMLRequest(self._client)
490 req.classname = self.classname
491 req.update(kwargs)
493 # new template, using the specified classname and request
494 pt = Templates(self._db.config.TEMPLATES).get(self.classname, name)
496 # use our fabricated request
497 return pt.render(self._client, self.classname, req)
499 class HTMLItem(HTMLPermissions):
500 ''' Accesses through an *item*
501 '''
502 def __init__(self, client, classname, nodeid, anonymous=0):
503 self._client = client
504 self._db = client.db
505 self._classname = classname
506 self._nodeid = nodeid
507 self._klass = self._db.getclass(classname)
508 self._props = self._klass.getprops()
510 # do we prefix the form items with the item's identification?
511 self._anonymous = anonymous
513 def __repr__(self):
514 return '<HTMLItem(0x%x) %s %s>'%(id(self), self._classname,
515 self._nodeid)
517 def __getitem__(self, item):
518 ''' return an HTMLProperty instance
519 '''
520 #print 'HTMLItem.getitem', (self, item)
521 if item == 'id':
522 return self._nodeid
524 # get the property
525 prop = self._props[item]
527 # get the value, handling missing values
528 value = None
529 if int(self._nodeid) > 0:
530 value = self._klass.get(self._nodeid, item, None)
531 if value is None:
532 if isinstance(self._props[item], hyperdb.Multilink):
533 value = []
535 # look up the correct HTMLProperty class
536 for klass, htmlklass in propclasses:
537 if isinstance(prop, klass):
538 return htmlklass(self._client, self._classname,
539 self._nodeid, prop, item, value, self._anonymous)
541 raise KeyError, item
543 def __getattr__(self, attr):
544 ''' convenience access to properties '''
545 try:
546 return self[attr]
547 except KeyError:
548 raise AttributeError, attr
550 def designator(self):
551 ''' Return this item's designator (classname + id) '''
552 return '%s%s'%(self._classname, self._nodeid)
554 def submit(self, label="Submit Changes"):
555 ''' Generate a submit button (and action hidden element)
556 '''
557 return ' <input type="hidden" name="@action" value="edit">\n'\
558 ' <input type="submit" name="submit" value="%s">'%label
560 def journal(self, direction='descending'):
561 ''' Return a list of HTMLJournalEntry instances.
562 '''
563 # XXX do this
564 return []
566 def history(self, direction='descending', dre=re.compile('\d+')):
567 l = ['<table class="history">'
568 '<tr><th colspan="4" class="header">',
569 _('History'),
570 '</th></tr><tr>',
571 _('<th>Date</th>'),
572 _('<th>User</th>'),
573 _('<th>Action</th>'),
574 _('<th>Args</th>'),
575 '</tr>']
576 current = {}
577 comments = {}
578 history = self._klass.history(self._nodeid)
579 history.sort()
580 timezone = self._db.getUserTimezone()
581 if direction == 'descending':
582 history.reverse()
583 for prop_n in self._props.keys():
584 prop = self[prop_n]
585 if isinstance(prop, HTMLProperty):
586 current[prop_n] = prop.plain()
587 # make link if hrefable
588 if (self._props.has_key(prop_n) and
589 isinstance(self._props[prop_n], hyperdb.Link)):
590 classname = self._props[prop_n].classname
591 try:
592 template = find_template(self._db.config.TEMPLATES,
593 classname, 'item')
594 if template[1].startswith('_generic'):
595 raise NoTemplate, 'not really...'
596 except NoTemplate:
597 pass
598 else:
599 id = self._klass.get(self._nodeid, prop_n, None)
600 current[prop_n] = '<a href="%s%s">%s</a>'%(
601 classname, id, current[prop_n])
603 for id, evt_date, user, action, args in history:
604 date_s = str(evt_date.local(timezone)).replace("."," ")
605 arg_s = ''
606 if action == 'link' and type(args) == type(()):
607 if len(args) == 3:
608 linkcl, linkid, key = args
609 arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
610 linkcl, linkid, key)
611 else:
612 arg_s = str(args)
614 elif action == 'unlink' and type(args) == type(()):
615 if len(args) == 3:
616 linkcl, linkid, key = args
617 arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
618 linkcl, linkid, key)
619 else:
620 arg_s = str(args)
622 elif type(args) == type({}):
623 cell = []
624 for k in args.keys():
625 # try to get the relevant property and treat it
626 # specially
627 try:
628 prop = self._props[k]
629 except KeyError:
630 prop = None
631 if prop is None:
632 # property no longer exists
633 comments['no_exist'] = _('''<em>The indicated property
634 no longer exists</em>''')
635 cell.append('<em>%s: %s</em>\n'%(k, str(args[k])))
636 continue
638 if args[k] and (isinstance(prop, hyperdb.Multilink) or
639 isinstance(prop, hyperdb.Link)):
640 # figure what the link class is
641 classname = prop.classname
642 try:
643 linkcl = self._db.getclass(classname)
644 except KeyError:
645 labelprop = None
646 comments[classname] = _('''The linked class
647 %(classname)s no longer exists''')%locals()
648 labelprop = linkcl.labelprop(1)
649 try:
650 template = find_template(self._db.config.TEMPLATES,
651 classname, 'item')
652 if template[1].startswith('_generic'):
653 raise NoTemplate, 'not really...'
654 hrefable = 1
655 except NoTemplate:
656 hrefable = 0
658 if isinstance(prop, hyperdb.Multilink) and args[k]:
659 ml = []
660 for linkid in args[k]:
661 if isinstance(linkid, type(())):
662 sublabel = linkid[0] + ' '
663 linkids = linkid[1]
664 else:
665 sublabel = ''
666 linkids = [linkid]
667 subml = []
668 for linkid in linkids:
669 label = classname + linkid
670 # if we have a label property, try to use it
671 # TODO: test for node existence even when
672 # there's no labelprop!
673 try:
674 if labelprop is not None and \
675 labelprop != 'id':
676 label = linkcl.get(linkid, labelprop)
677 except IndexError:
678 comments['no_link'] = _('''<strike>The
679 linked node no longer
680 exists</strike>''')
681 subml.append('<strike>%s</strike>'%label)
682 else:
683 if hrefable:
684 subml.append('<a href="%s%s">%s</a>'%(
685 classname, linkid, label))
686 else:
687 subml.append(label)
688 ml.append(sublabel + ', '.join(subml))
689 cell.append('%s:\n %s'%(k, ', '.join(ml)))
690 elif isinstance(prop, hyperdb.Link) and args[k]:
691 label = classname + args[k]
692 # if we have a label property, try to use it
693 # TODO: test for node existence even when
694 # there's no labelprop!
695 if labelprop is not None and labelprop != 'id':
696 try:
697 label = linkcl.get(args[k], labelprop)
698 except IndexError:
699 comments['no_link'] = _('''<strike>The
700 linked node no longer
701 exists</strike>''')
702 cell.append(' <strike>%s</strike>,\n'%label)
703 # "flag" this is done .... euwww
704 label = None
705 if label is not None:
706 if hrefable:
707 old = '<a href="%s%s">%s</a>'%(classname, args[k], label)
708 else:
709 old = label;
710 cell.append('%s: %s' % (k,old))
711 if current.has_key(k):
712 cell[-1] += ' -> %s'%current[k]
713 current[k] = old
715 elif isinstance(prop, hyperdb.Date) and args[k]:
716 d = date.Date(args[k]).local(timezone)
717 cell.append('%s: %s'%(k, str(d)))
718 if current.has_key(k):
719 cell[-1] += ' -> %s' % current[k]
720 current[k] = str(d)
722 elif isinstance(prop, hyperdb.Interval) and args[k]:
723 d = date.Interval(args[k])
724 cell.append('%s: %s'%(k, str(d)))
725 if current.has_key(k):
726 cell[-1] += ' -> %s'%current[k]
727 current[k] = str(d)
729 elif isinstance(prop, hyperdb.String) and args[k]:
730 cell.append('%s: %s'%(k, cgi.escape(args[k])))
731 if current.has_key(k):
732 cell[-1] += ' -> %s'%current[k]
733 current[k] = cgi.escape(args[k])
735 elif not args[k]:
736 if current.has_key(k):
737 cell.append('%s: %s'%(k, current[k]))
738 current[k] = '(no value)'
739 else:
740 cell.append('%s: (no value)'%k)
742 else:
743 cell.append('%s: %s'%(k, str(args[k])))
744 if current.has_key(k):
745 cell[-1] += ' -> %s'%current[k]
746 current[k] = str(args[k])
748 arg_s = '<br />'.join(cell)
749 else:
750 # unkown event!!
751 comments['unknown'] = _('''<strong><em>This event is not
752 handled by the history display!</em></strong>''')
753 arg_s = '<strong><em>' + str(args) + '</em></strong>'
754 date_s = date_s.replace(' ', ' ')
755 # if the user's an itemid, figure the username (older journals
756 # have the username)
757 if dre.match(user):
758 user = self._db.user.get(user, 'username')
759 l.append('<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>'%(
760 date_s, user, action, arg_s))
761 if comments:
762 l.append(_('<tr><td colspan=4><strong>Note:</strong></td></tr>'))
763 for entry in comments.values():
764 l.append('<tr><td colspan=4>%s</td></tr>'%entry)
765 l.append('</table>')
766 return '\n'.join(l)
768 def renderQueryForm(self):
769 ''' Render this item, which is a query, as a search form.
770 '''
771 # create a new request and override the specified args
772 req = HTMLRequest(self._client)
773 req.classname = self._klass.get(self._nodeid, 'klass')
774 name = self._klass.get(self._nodeid, 'name')
775 req.updateFromURL(self._klass.get(self._nodeid, 'url') +
776 '&@queryname=%s'%urllib.quote(name))
778 # new template, using the specified classname and request
779 pt = Templates(self._db.config.TEMPLATES).get(req.classname, 'search')
781 # use our fabricated request
782 return pt.render(self._client, req.classname, req)
784 class HTMLUser(HTMLItem):
785 ''' Accesses through the *user* (a special case of item)
786 '''
787 def __init__(self, client, classname, nodeid, anonymous=0):
788 HTMLItem.__init__(self, client, 'user', nodeid, anonymous)
789 self._default_classname = client.classname
791 # used for security checks
792 self._security = client.db.security
794 _marker = []
795 def hasPermission(self, permission, classname=_marker):
796 ''' Determine if the user has the Permission.
798 The class being tested defaults to the template's class, but may
799 be overidden for this test by suppling an alternate classname.
800 '''
801 if classname is self._marker:
802 classname = self._default_classname
803 return self._security.hasPermission(permission, self._nodeid, classname)
805 def is_edit_ok(self):
806 ''' Is the user allowed to Edit the current class?
807 Also check whether this is the current user's info.
808 '''
809 return self._db.security.hasPermission('Edit', self._client.userid,
810 self._classname) or (self._nodeid == self._client.userid and
811 self._db.user.get(self._client.userid, 'username') != 'anonymous')
813 def is_view_ok(self):
814 ''' Is the user allowed to View the current class?
815 Also check whether this is the current user's info.
816 '''
817 return self._db.security.hasPermission('Edit', self._client.userid,
818 self._classname) or (self._nodeid == self._client.userid and
819 self._db.user.get(self._client.userid, 'username') != 'anonymous')
821 class HTMLProperty:
822 ''' String, Number, Date, Interval HTMLProperty
824 Has useful attributes:
826 _name the name of the property
827 _value the value of the property if any
829 A wrapper object which may be stringified for the plain() behaviour.
830 '''
831 def __init__(self, client, classname, nodeid, prop, name, value,
832 anonymous=0):
833 self._client = client
834 self._db = client.db
835 self._classname = classname
836 self._nodeid = nodeid
837 self._prop = prop
838 self._value = value
839 self._anonymous = anonymous
840 self._name = name
841 if not anonymous:
842 self._formname = '%s%s@%s'%(classname, nodeid, name)
843 else:
844 self._formname = name
845 def __repr__(self):
846 return '<HTMLProperty(0x%x) %s %r %r>'%(id(self), self._formname,
847 self._prop, self._value)
848 def __str__(self):
849 return self.plain()
850 def __cmp__(self, other):
851 if isinstance(other, HTMLProperty):
852 return cmp(self._value, other._value)
853 return cmp(self._value, other)
855 class StringHTMLProperty(HTMLProperty):
856 hyper_re = re.compile(r'((?P<url>\w{3,6}://\S+)|'
857 r'(?P<email>[-+=%/\w\.]+@[\w\.\-]+)|'
858 r'(?P<item>(?P<class>[a-z_]+)(?P<id>\d+)))')
859 def _hyper_repl(self, match):
860 if match.group('url'):
861 s = match.group('url')
862 return '<a href="%s">%s</a>'%(s, s)
863 elif match.group('email'):
864 s = match.group('email')
865 return '<a href="mailto:%s">%s</a>'%(s, s)
866 else:
867 s = match.group('item')
868 s1 = match.group('class')
869 s2 = match.group('id')
870 try:
871 # make sure s1 is a valid tracker classname
872 self._db.getclass(s1)
873 return '<a href="%s">%s %s</a>'%(s, s1, s2)
874 except KeyError:
875 return '%s%s'%(s1, s2)
877 def hyperlinked(self):
878 ''' Render a "hyperlinked" version of the text '''
879 return self.plain(hyperlink=1)
881 def plain(self, escape=0, hyperlink=0):
882 ''' Render a "plain" representation of the property
884 "escape" turns on/off HTML quoting
885 "hyperlink" turns on/off in-text hyperlinking of URLs, email
886 addresses and designators
887 '''
888 if self._value is None:
889 return ''
890 if escape:
891 s = cgi.escape(str(self._value))
892 else:
893 s = str(self._value)
894 if hyperlink:
895 # no, we *must* escape this text
896 if not escape:
897 s = cgi.escape(s)
898 s = self.hyper_re.sub(self._hyper_repl, s)
899 return s
901 def stext(self, escape=0):
902 ''' Render the value of the property as StructuredText.
904 This requires the StructureText module to be installed separately.
905 '''
906 s = self.plain(escape=escape)
907 if not StructuredText:
908 return s
909 return StructuredText(s,level=1,header=0)
911 def field(self, size = 30):
912 ''' Render a form edit field for the property
913 '''
914 if self._value is None:
915 value = ''
916 else:
917 value = cgi.escape(str(self._value))
918 value = '"'.join(value.split('"'))
919 return '<input name="%s" value="%s" size="%s">'%(self._formname, value, size)
921 def multiline(self, escape=0, rows=5, cols=40):
922 ''' Render a multiline form edit field for the property
923 '''
924 if self._value is None:
925 value = ''
926 else:
927 value = cgi.escape(str(self._value))
928 value = '"'.join(value.split('"'))
929 return '<textarea name="%s" rows="%s" cols="%s">%s</textarea>'%(
930 self._formname, rows, cols, value)
932 def email(self, escape=1):
933 ''' Render the value of the property as an obscured email address
934 '''
935 if self._value is None: value = ''
936 else: value = str(self._value)
937 if value.find('@') != -1:
938 name, domain = value.split('@')
939 domain = ' '.join(domain.split('.')[:-1])
940 name = name.replace('.', ' ')
941 value = '%s at %s ...'%(name, domain)
942 else:
943 value = value.replace('.', ' ')
944 if escape:
945 value = cgi.escape(value)
946 return value
948 class PasswordHTMLProperty(HTMLProperty):
949 def plain(self):
950 ''' Render a "plain" representation of the property
951 '''
952 if self._value is None:
953 return ''
954 return _('*encrypted*')
956 def field(self, size = 30):
957 ''' Render a form edit field for the property.
958 '''
959 return '<input type="password" name="%s" size="%s">'%(self._formname, size)
961 def confirm(self, size = 30):
962 ''' Render a second form edit field for the property, used for
963 confirmation that the user typed the password correctly. Generates
964 a field with name "@confirm@name".
965 '''
966 return '<input type="password" name="@confirm@%s" size="%s">'%(
967 self._formname, size)
969 class NumberHTMLProperty(HTMLProperty):
970 def plain(self):
971 ''' Render a "plain" representation of the property
972 '''
973 return str(self._value)
975 def field(self, size = 30):
976 ''' Render a form edit field for the property
977 '''
978 if self._value is None:
979 value = ''
980 else:
981 value = cgi.escape(str(self._value))
982 value = '"'.join(value.split('"'))
983 return '<input name="%s" value="%s" size="%s">'%(self._formname, value, size)
985 def __int__(self):
986 ''' Return an int of me
987 '''
988 return int(self._value)
990 def __float__(self):
991 ''' Return a float of me
992 '''
993 return float(self._value)
996 class BooleanHTMLProperty(HTMLProperty):
997 def plain(self):
998 ''' Render a "plain" representation of the property
999 '''
1000 if self._value is None:
1001 return ''
1002 return self._value and "Yes" or "No"
1004 def field(self):
1005 ''' Render a form edit field for the property
1006 '''
1007 checked = self._value and "checked" or ""
1008 s = '<input type="radio" name="%s" value="yes" %s>Yes'%(self._formname,
1009 checked)
1010 if checked:
1011 checked = ""
1012 else:
1013 checked = "checked"
1014 s += '<input type="radio" name="%s" value="no" %s>No'%(self._formname,
1015 checked)
1016 return s
1018 class DateHTMLProperty(HTMLProperty):
1019 def plain(self):
1020 ''' Render a "plain" representation of the property
1021 '''
1022 if self._value is None:
1023 return ''
1024 return str(self._value.local(self._db.getUserTimezone()))
1026 def now(self):
1027 ''' Return the current time.
1029 This is useful for defaulting a new value. Returns a
1030 DateHTMLProperty.
1031 '''
1032 return DateHTMLProperty(self._client, self._nodeid, self._prop,
1033 self._formname, date.Date('.'))
1035 def field(self, size = 30):
1036 ''' Render a form edit field for the property
1037 '''
1038 if self._value is None:
1039 value = ''
1040 else:
1041 value = cgi.escape(str(self._value.local(self._db.getUserTimezone())))
1042 value = '"'.join(value.split('"'))
1043 return '<input name="%s" value="%s" size="%s">'%(self._formname, value, size)
1045 def reldate(self, pretty=1):
1046 ''' Render the interval between the date and now.
1048 If the "pretty" flag is true, then make the display pretty.
1049 '''
1050 if not self._value:
1051 return ''
1053 # figure the interval
1054 interval = date.Date('.') - self._value
1055 if pretty:
1056 return interval.pretty()
1057 return str(interval)
1059 _marker = []
1060 def pretty(self, format=_marker):
1061 ''' Render the date in a pretty format (eg. month names, spaces).
1063 The format string is a standard python strftime format string.
1064 Note that if the day is zero, and appears at the start of the
1065 string, then it'll be stripped from the output. This is handy
1066 for the situatin when a date only specifies a month and a year.
1067 '''
1068 if format is not self._marker:
1069 return self._value.pretty(format)
1070 else:
1071 return self._value.pretty()
1073 def local(self, offset):
1074 ''' Return the date/time as a local (timezone offset) date/time.
1075 '''
1076 return DateHTMLProperty(self._client, self._nodeid, self._prop,
1077 self._formname, self._value.local(offset))
1079 class IntervalHTMLProperty(HTMLProperty):
1080 def plain(self):
1081 ''' Render a "plain" representation of the property
1082 '''
1083 if self._value is None:
1084 return ''
1085 return str(self._value)
1087 def pretty(self):
1088 ''' Render the interval in a pretty format (eg. "yesterday")
1089 '''
1090 return self._value.pretty()
1092 def field(self, size = 30):
1093 ''' Render a form edit field for the property
1094 '''
1095 if self._value is None:
1096 value = ''
1097 else:
1098 value = cgi.escape(str(self._value))
1099 value = '"'.join(value.split('"'))
1100 return '<input name="%s" value="%s" size="%s">'%(self._formname, value, size)
1102 class LinkHTMLProperty(HTMLProperty):
1103 ''' Link HTMLProperty
1104 Include the above as well as being able to access the class
1105 information. Stringifying the object itself results in the value
1106 from the item being displayed. Accessing attributes of this object
1107 result in the appropriate entry from the class being queried for the
1108 property accessed (so item/assignedto/name would look up the user
1109 entry identified by the assignedto property on item, and then the
1110 name property of that user)
1111 '''
1112 def __init__(self, *args, **kw):
1113 HTMLProperty.__init__(self, *args, **kw)
1114 # if we're representing a form value, then the -1 from the form really
1115 # should be a None
1116 if str(self._value) == '-1':
1117 self._value = None
1119 def __getattr__(self, attr):
1120 ''' return a new HTMLItem '''
1121 #print 'Link.getattr', (self, attr, self._value)
1122 if not self._value:
1123 raise AttributeError, "Can't access missing value"
1124 if self._prop.classname == 'user':
1125 klass = HTMLUser
1126 else:
1127 klass = HTMLItem
1128 i = klass(self._client, self._prop.classname, self._value)
1129 return getattr(i, attr)
1131 def plain(self, escape=0):
1132 ''' Render a "plain" representation of the property
1133 '''
1134 if self._value is None:
1135 return ''
1136 linkcl = self._db.classes[self._prop.classname]
1137 k = linkcl.labelprop(1)
1138 value = str(linkcl.get(self._value, k))
1139 if escape:
1140 value = cgi.escape(value)
1141 return value
1143 def field(self, showid=0, size=None):
1144 ''' Render a form edit field for the property
1145 '''
1146 linkcl = self._db.getclass(self._prop.classname)
1147 if linkcl.getprops().has_key('order'):
1148 sort_on = 'order'
1149 else:
1150 sort_on = linkcl.labelprop()
1151 options = linkcl.filter(None, {}, ('+', sort_on), (None, None))
1152 # TODO: make this a field display, not a menu one!
1153 l = ['<select name="%s">'%self._formname]
1154 k = linkcl.labelprop(1)
1155 if self._value is None:
1156 s = 'selected '
1157 else:
1158 s = ''
1159 l.append(_('<option %svalue="-1">- no selection -</option>')%s)
1161 # make sure we list the current value if it's retired
1162 if self._value and self._value not in options:
1163 options.insert(0, self._value)
1165 for optionid in options:
1166 # get the option value, and if it's None use an empty string
1167 option = linkcl.get(optionid, k) or ''
1169 # figure if this option is selected
1170 s = ''
1171 if optionid == self._value:
1172 s = 'selected '
1174 # figure the label
1175 if showid:
1176 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1177 else:
1178 lab = option
1180 # truncate if it's too long
1181 if size is not None and len(lab) > size:
1182 lab = lab[:size-3] + '...'
1184 # and generate
1185 lab = cgi.escape(lab)
1186 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
1187 l.append('</select>')
1188 return '\n'.join(l)
1190 def menu(self, size=None, height=None, showid=0, additional=[],
1191 **conditions):
1192 ''' Render a form select list for this property
1193 '''
1194 value = self._value
1196 # sort function
1197 sortfunc = make_sort_function(self._db, self._prop.classname)
1199 linkcl = self._db.getclass(self._prop.classname)
1200 l = ['<select name="%s">'%self._formname]
1201 k = linkcl.labelprop(1)
1202 s = ''
1203 if value is None:
1204 s = 'selected '
1205 l.append(_('<option %svalue="-1">- no selection -</option>')%s)
1206 if linkcl.getprops().has_key('order'):
1207 sort_on = ('+', 'order')
1208 else:
1209 sort_on = ('+', linkcl.labelprop())
1210 options = linkcl.filter(None, conditions, sort_on, (None, None))
1212 # make sure we list the current value if it's retired
1213 if self._value and self._value not in options:
1214 options.insert(0, self._value)
1216 for optionid in options:
1217 # get the option value, and if it's None use an empty string
1218 option = linkcl.get(optionid, k) or ''
1220 # figure if this option is selected
1221 s = ''
1222 if value in [optionid, option]:
1223 s = 'selected '
1225 # figure the label
1226 if showid:
1227 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1228 else:
1229 lab = option
1231 # truncate if it's too long
1232 if size is not None and len(lab) > size:
1233 lab = lab[:size-3] + '...'
1234 if additional:
1235 m = []
1236 for propname in additional:
1237 m.append(linkcl.get(optionid, propname))
1238 lab = lab + ' (%s)'%', '.join(map(str, m))
1240 # and generate
1241 lab = cgi.escape(lab)
1242 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
1243 l.append('</select>')
1244 return '\n'.join(l)
1245 # def checklist(self, ...)
1247 class MultilinkHTMLProperty(HTMLProperty):
1248 ''' Multilink HTMLProperty
1250 Also be iterable, returning a wrapper object like the Link case for
1251 each entry in the multilink.
1252 '''
1253 def __len__(self):
1254 ''' length of the multilink '''
1255 return len(self._value)
1257 def __getattr__(self, attr):
1258 ''' no extended attribute accesses make sense here '''
1259 raise AttributeError, attr
1261 def __getitem__(self, num):
1262 ''' iterate and return a new HTMLItem
1263 '''
1264 #print 'Multi.getitem', (self, num)
1265 value = self._value[num]
1266 if self._prop.classname == 'user':
1267 klass = HTMLUser
1268 else:
1269 klass = HTMLItem
1270 return klass(self._client, self._prop.classname, value)
1272 def __contains__(self, value):
1273 ''' Support the "in" operator. We have to make sure the passed-in
1274 value is a string first, not a *HTMLProperty.
1275 '''
1276 return str(value) in self._value
1278 def reverse(self):
1279 ''' return the list in reverse order
1280 '''
1281 l = self._value[:]
1282 l.reverse()
1283 if self._prop.classname == 'user':
1284 klass = HTMLUser
1285 else:
1286 klass = HTMLItem
1287 return [klass(self._client, self._prop.classname, value) for value in l]
1289 def plain(self, escape=0):
1290 ''' Render a "plain" representation of the property
1291 '''
1292 linkcl = self._db.classes[self._prop.classname]
1293 k = linkcl.labelprop(1)
1294 labels = []
1295 for v in self._value:
1296 labels.append(linkcl.get(v, k))
1297 value = ', '.join(labels)
1298 if escape:
1299 value = cgi.escape(value)
1300 return value
1302 def field(self, size=30, showid=0):
1303 ''' Render a form edit field for the property
1304 '''
1305 sortfunc = make_sort_function(self._db, self._prop.classname)
1306 linkcl = self._db.getclass(self._prop.classname)
1307 value = self._value[:]
1308 if value:
1309 value.sort(sortfunc)
1310 # map the id to the label property
1311 if not linkcl.getkey():
1312 showid=1
1313 if not showid:
1314 k = linkcl.labelprop(1)
1315 value = [linkcl.get(v, k) for v in value]
1316 value = cgi.escape(','.join(value))
1317 return '<input name="%s" size="%s" value="%s">'%(self._formname, size, value)
1319 def menu(self, size=None, height=None, showid=0, additional=[],
1320 **conditions):
1321 ''' Render a form select list for this property
1322 '''
1323 value = self._value
1325 # sort function
1326 sortfunc = make_sort_function(self._db, self._prop.classname)
1328 linkcl = self._db.getclass(self._prop.classname)
1329 if linkcl.getprops().has_key('order'):
1330 sort_on = ('+', 'order')
1331 else:
1332 sort_on = ('+', linkcl.labelprop())
1333 options = linkcl.filter(None, conditions, sort_on, (None,None))
1334 height = height or min(len(options), 7)
1335 l = ['<select multiple name="%s" size="%s">'%(self._formname, height)]
1336 k = linkcl.labelprop(1)
1338 # make sure we list the current values if they're retired
1339 for val in value:
1340 if val not in options:
1341 options.insert(0, val)
1343 for optionid in options:
1344 # get the option value, and if it's None use an empty string
1345 option = linkcl.get(optionid, k) or ''
1347 # figure if this option is selected
1348 s = ''
1349 if optionid in value or option in value:
1350 s = 'selected '
1352 # figure the label
1353 if showid:
1354 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1355 else:
1356 lab = option
1357 # truncate if it's too long
1358 if size is not None and len(lab) > size:
1359 lab = lab[:size-3] + '...'
1360 if additional:
1361 m = []
1362 for propname in additional:
1363 m.append(linkcl.get(optionid, propname))
1364 lab = lab + ' (%s)'%', '.join(m)
1366 # and generate
1367 lab = cgi.escape(lab)
1368 l.append('<option %svalue="%s">%s</option>'%(s, optionid,
1369 lab))
1370 l.append('</select>')
1371 return '\n'.join(l)
1373 # set the propclasses for HTMLItem
1374 propclasses = (
1375 (hyperdb.String, StringHTMLProperty),
1376 (hyperdb.Number, NumberHTMLProperty),
1377 (hyperdb.Boolean, BooleanHTMLProperty),
1378 (hyperdb.Date, DateHTMLProperty),
1379 (hyperdb.Interval, IntervalHTMLProperty),
1380 (hyperdb.Password, PasswordHTMLProperty),
1381 (hyperdb.Link, LinkHTMLProperty),
1382 (hyperdb.Multilink, MultilinkHTMLProperty),
1383 )
1385 def make_sort_function(db, classname):
1386 '''Make a sort function for a given class
1387 '''
1388 linkcl = db.getclass(classname)
1389 if linkcl.getprops().has_key('order'):
1390 sort_on = 'order'
1391 else:
1392 sort_on = linkcl.labelprop()
1393 def sortfunc(a, b, linkcl=linkcl, sort_on=sort_on):
1394 return cmp(linkcl.get(a, sort_on), linkcl.get(b, sort_on))
1395 return sortfunc
1397 def handleListCGIValue(value):
1398 ''' Value is either a single item or a list of items. Each item has a
1399 .value that we're actually interested in.
1400 '''
1401 if isinstance(value, type([])):
1402 return [value.value for value in value]
1403 else:
1404 value = value.value.strip()
1405 if not value:
1406 return []
1407 return value.split(',')
1409 class ShowDict:
1410 ''' A convenience access to the :columns index parameters
1411 '''
1412 def __init__(self, columns):
1413 self.columns = {}
1414 for col in columns:
1415 self.columns[col] = 1
1416 def __getitem__(self, name):
1417 return self.columns.has_key(name)
1419 class HTMLRequest:
1420 ''' The *request*, holding the CGI form and environment.
1422 "form" the CGI form as a cgi.FieldStorage
1423 "env" the CGI environment variables
1424 "base" the base URL for this instance
1425 "user" a HTMLUser instance for this user
1426 "classname" the current classname (possibly None)
1427 "template" the current template (suffix, also possibly None)
1429 Index args:
1430 "columns" dictionary of the columns to display in an index page
1431 "show" a convenience access to columns - request/show/colname will
1432 be true if the columns should be displayed, false otherwise
1433 "sort" index sort column (direction, column name)
1434 "group" index grouping property (direction, column name)
1435 "filter" properties to filter the index on
1436 "filterspec" values to filter the index on
1437 "search_text" text to perform a full-text search on for an index
1439 '''
1440 def __init__(self, client):
1441 self.client = client
1443 # easier access vars
1444 self.form = client.form
1445 self.env = client.env
1446 self.base = client.base
1447 self.user = HTMLUser(client, 'user', client.userid)
1449 # store the current class name and action
1450 self.classname = client.classname
1451 self.template = client.template
1453 # the special char to use for special vars
1454 self.special_char = '@'
1456 self._post_init()
1458 def _post_init(self):
1459 ''' Set attributes based on self.form
1460 '''
1461 # extract the index display information from the form
1462 self.columns = []
1463 for name in ':columns @columns'.split():
1464 if self.form.has_key(name):
1465 self.special_char = name[0]
1466 self.columns = handleListCGIValue(self.form[name])
1467 break
1468 self.show = ShowDict(self.columns)
1470 # sorting
1471 self.sort = (None, None)
1472 for name in ':sort @sort'.split():
1473 if self.form.has_key(name):
1474 self.special_char = name[0]
1475 sort = self.form[name].value
1476 if sort.startswith('-'):
1477 self.sort = ('-', sort[1:])
1478 else:
1479 self.sort = ('+', sort)
1480 if self.form.has_key(self.special_char+'sortdir'):
1481 self.sort = ('-', self.sort[1])
1483 # grouping
1484 self.group = (None, None)
1485 for name in ':group @group'.split():
1486 if self.form.has_key(name):
1487 self.special_char = name[0]
1488 group = self.form[name].value
1489 if group.startswith('-'):
1490 self.group = ('-', group[1:])
1491 else:
1492 self.group = ('+', group)
1493 if self.form.has_key(self.special_char+'groupdir'):
1494 self.group = ('-', self.group[1])
1496 # filtering
1497 self.filter = []
1498 for name in ':filter @filter'.split():
1499 if self.form.has_key(name):
1500 self.special_char = name[0]
1501 self.filter = handleListCGIValue(self.form[name])
1503 self.filterspec = {}
1504 db = self.client.db
1505 if self.classname is not None:
1506 props = db.getclass(self.classname).getprops()
1507 for name in self.filter:
1508 if not self.form.has_key(name):
1509 continue
1510 prop = props[name]
1511 fv = self.form[name]
1512 if (isinstance(prop, hyperdb.Link) or
1513 isinstance(prop, hyperdb.Multilink)):
1514 self.filterspec[name] = lookupIds(db, prop,
1515 handleListCGIValue(fv))
1516 else:
1517 if isinstance(fv, type([])):
1518 self.filterspec[name] = [v.value for v in fv]
1519 else:
1520 self.filterspec[name] = fv.value
1522 # full-text search argument
1523 self.search_text = None
1524 for name in ':search_text @search_text'.split():
1525 if self.form.has_key(name):
1526 self.special_char = name[0]
1527 self.search_text = self.form[name].value
1529 # pagination - size and start index
1530 # figure batch args
1531 self.pagesize = 50
1532 for name in ':pagesize @pagesize'.split():
1533 if self.form.has_key(name):
1534 self.special_char = name[0]
1535 self.pagesize = int(self.form[name].value)
1537 self.startwith = 0
1538 for name in ':startwith @startwith'.split():
1539 if self.form.has_key(name):
1540 self.special_char = name[0]
1541 self.startwith = int(self.form[name].value)
1543 def updateFromURL(self, url):
1544 ''' Parse the URL for query args, and update my attributes using the
1545 values.
1546 '''
1547 env = {'QUERY_STRING': url}
1548 self.form = cgi.FieldStorage(environ=env)
1550 self._post_init()
1552 def update(self, kwargs):
1553 ''' Update my attributes using the keyword args
1554 '''
1555 self.__dict__.update(kwargs)
1556 if kwargs.has_key('columns'):
1557 self.show = ShowDict(self.columns)
1559 def description(self):
1560 ''' Return a description of the request - handle for the page title.
1561 '''
1562 s = [self.client.db.config.TRACKER_NAME]
1563 if self.classname:
1564 if self.client.nodeid:
1565 s.append('- %s%s'%(self.classname, self.client.nodeid))
1566 else:
1567 if self.template == 'item':
1568 s.append('- new %s'%self.classname)
1569 elif self.template == 'index':
1570 s.append('- %s index'%self.classname)
1571 else:
1572 s.append('- %s %s'%(self.classname, self.template))
1573 else:
1574 s.append('- home')
1575 return ' '.join(s)
1577 def __str__(self):
1578 d = {}
1579 d.update(self.__dict__)
1580 f = ''
1581 for k in self.form.keys():
1582 f += '\n %r=%r'%(k,handleListCGIValue(self.form[k]))
1583 d['form'] = f
1584 e = ''
1585 for k,v in self.env.items():
1586 e += '\n %r=%r'%(k, v)
1587 d['env'] = e
1588 return '''
1589 form: %(form)s
1590 base: %(base)r
1591 classname: %(classname)r
1592 template: %(template)r
1593 columns: %(columns)r
1594 sort: %(sort)r
1595 group: %(group)r
1596 filter: %(filter)r
1597 search_text: %(search_text)r
1598 pagesize: %(pagesize)r
1599 startwith: %(startwith)r
1600 env: %(env)s
1601 '''%d
1603 def indexargs_form(self, columns=1, sort=1, group=1, filter=1,
1604 filterspec=1):
1605 ''' return the current index args as form elements '''
1606 l = []
1607 sc = self.special_char
1608 s = '<input type="hidden" name="%s" value="%s">'
1609 if columns and self.columns:
1610 l.append(s%(sc+'columns', ','.join(self.columns)))
1611 if sort and self.sort[1] is not None:
1612 if self.sort[0] == '-':
1613 val = '-'+self.sort[1]
1614 else:
1615 val = self.sort[1]
1616 l.append(s%(sc+'sort', val))
1617 if group and self.group[1] is not None:
1618 if self.group[0] == '-':
1619 val = '-'+self.group[1]
1620 else:
1621 val = self.group[1]
1622 l.append(s%(sc+'group', val))
1623 if filter and self.filter:
1624 l.append(s%(sc+'filter', ','.join(self.filter)))
1625 if filterspec:
1626 for k,v in self.filterspec.items():
1627 if type(v) == type([]):
1628 l.append(s%(k, ','.join(v)))
1629 else:
1630 l.append(s%(k, v))
1631 if self.search_text:
1632 l.append(s%(sc+'search_text', self.search_text))
1633 l.append(s%(sc+'pagesize', self.pagesize))
1634 l.append(s%(sc+'startwith', self.startwith))
1635 return '\n'.join(l)
1637 def indexargs_url(self, url, args):
1638 ''' Embed the current index args in a URL
1639 '''
1640 sc = self.special_char
1641 l = ['%s=%s'%(k,v) for k,v in args.items()]
1643 # pull out the special values (prefixed by @ or :)
1644 specials = {}
1645 for key in args.keys():
1646 if key[0] in '@:':
1647 specials[key[1:]] = args[key]
1649 # ok, now handle the specials we received in the request
1650 if self.columns and not specials.has_key('columns'):
1651 l.append(sc+'columns=%s'%(','.join(self.columns)))
1652 if self.sort[1] is not None and not specials.has_key('sort'):
1653 if self.sort[0] == '-':
1654 val = '-'+self.sort[1]
1655 else:
1656 val = self.sort[1]
1657 l.append(sc+'sort=%s'%val)
1658 if self.group[1] is not None and not specials.has_key('group'):
1659 if self.group[0] == '-':
1660 val = '-'+self.group[1]
1661 else:
1662 val = self.group[1]
1663 l.append(sc+'group=%s'%val)
1664 if self.filter and not specials.has_key('filter'):
1665 l.append(sc+'filter=%s'%(','.join(self.filter)))
1666 if self.search_text and not specials.has_key('search_text'):
1667 l.append(sc+'search_text=%s'%self.search_text)
1668 if not specials.has_key('pagesize'):
1669 l.append(sc+'pagesize=%s'%self.pagesize)
1670 if not specials.has_key('startwith'):
1671 l.append(sc+'startwith=%s'%self.startwith)
1673 # finally, the remainder of the filter args in the request
1674 for k,v in self.filterspec.items():
1675 if not args.has_key(k):
1676 if type(v) == type([]):
1677 l.append('%s=%s'%(k, ','.join(v)))
1678 else:
1679 l.append('%s=%s'%(k, v))
1680 return '%s?%s'%(url, '&'.join(l))
1681 indexargs_href = indexargs_url
1683 def base_javascript(self):
1684 return '''
1685 <script type="text/javascript">
1686 submitted = false;
1687 function submit_once() {
1688 if (submitted) {
1689 alert("Your request is being processed.\\nPlease be patient.");
1690 return 0;
1691 }
1692 submitted = true;
1693 return 1;
1694 }
1696 function help_window(helpurl, width, height) {
1697 HelpWin = window.open('%s' + helpurl, 'RoundupHelpWindow', 'scrollbars=yes,resizable=yes,toolbar=no,height='+height+',width='+width);
1698 }
1699 </script>
1700 '''%self.base
1702 def batch(self):
1703 ''' Return a batch object for results from the "current search"
1704 '''
1705 filterspec = self.filterspec
1706 sort = self.sort
1707 group = self.group
1709 # get the list of ids we're batching over
1710 klass = self.client.db.getclass(self.classname)
1711 if self.search_text:
1712 matches = self.client.db.indexer.search(
1713 re.findall(r'\b\w{2,25}\b', self.search_text), klass)
1714 else:
1715 matches = None
1716 l = klass.filter(matches, filterspec, sort, group)
1718 # return the batch object, using IDs only
1719 return Batch(self.client, l, self.pagesize, self.startwith,
1720 classname=self.classname)
1722 # extend the standard ZTUtils Batch object to remove dependency on
1723 # Acquisition and add a couple of useful methods
1724 class Batch(ZTUtils.Batch):
1725 ''' Use me to turn a list of items, or item ids of a given class, into a
1726 series of batches.
1728 ========= ========================================================
1729 Parameter Usage
1730 ========= ========================================================
1731 sequence a list of HTMLItems or item ids
1732 classname if sequence is a list of ids, this is the class of item
1733 size how big to make the sequence.
1734 start where to start (0-indexed) in the sequence.
1735 end where to end (0-indexed) in the sequence.
1736 orphan if the next batch would contain less items than this
1737 value, then it is combined with this batch
1738 overlap the number of items shared between adjacent batches
1739 ========= ========================================================
1741 Attributes: Note that the "start" attribute, unlike the
1742 argument, is a 1-based index (I know, lame). "first" is the
1743 0-based index. "length" is the actual number of elements in
1744 the batch.
1746 "sequence_length" is the length of the original, unbatched, sequence.
1747 '''
1748 def __init__(self, client, sequence, size, start, end=0, orphan=0,
1749 overlap=0, classname=None):
1750 self.client = client
1751 self.last_index = self.last_item = None
1752 self.current_item = None
1753 self.classname = classname
1754 self.sequence_length = len(sequence)
1755 ZTUtils.Batch.__init__(self, sequence, size, start, end, orphan,
1756 overlap)
1758 # overwrite so we can late-instantiate the HTMLItem instance
1759 def __getitem__(self, index):
1760 if index < 0:
1761 if index + self.end < self.first: raise IndexError, index
1762 return self._sequence[index + self.end]
1764 if index >= self.length:
1765 raise IndexError, index
1767 # move the last_item along - but only if the fetched index changes
1768 # (for some reason, index 0 is fetched twice)
1769 if index != self.last_index:
1770 self.last_item = self.current_item
1771 self.last_index = index
1773 item = self._sequence[index + self.first]
1774 if self.classname:
1775 # map the item ids to instances
1776 if self.classname == 'user':
1777 item = HTMLUser(self.client, self.classname, item)
1778 else:
1779 item = HTMLItem(self.client, self.classname, item)
1780 self.current_item = item
1781 return item
1783 def propchanged(self, property):
1784 ''' Detect if the property marked as being the group property
1785 changed in the last iteration fetch
1786 '''
1787 if (self.last_item is None or
1788 self.last_item[property] != self.current_item[property]):
1789 return 1
1790 return 0
1792 # override these 'cos we don't have access to acquisition
1793 def previous(self):
1794 if self.start == 1:
1795 return None
1796 return Batch(self.client, self._sequence, self._size,
1797 self.first - self._size + self.overlap, 0, self.orphan,
1798 self.overlap)
1800 def next(self):
1801 try:
1802 self._sequence[self.end]
1803 except IndexError:
1804 return None
1805 return Batch(self.client, self._sequence, self._size,
1806 self.end - self.overlap, 0, self.orphan, self.overlap)
1808 class TemplatingUtils:
1809 ''' Utilities for templating
1810 '''
1811 def __init__(self, client):
1812 self.client = client
1813 def Batch(self, sequence, size, start, end=0, orphan=0, overlap=0):
1814 return Batch(self.client, sequence, size, start, end, orphan,
1815 overlap)