4b1b374afdc0dc4b1feda843174a7195c69c0aec
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
812 def is_view_ok(self):
813 ''' Is the user allowed to View the current class?
814 Also check whether this is the current user's info.
815 '''
816 return self._db.security.hasPermission('Edit', self._client.userid,
817 self._classname) or self._nodeid == self._client.userid
819 class HTMLProperty:
820 ''' String, Number, Date, Interval HTMLProperty
822 Has useful attributes:
824 _name the name of the property
825 _value the value of the property if any
827 A wrapper object which may be stringified for the plain() behaviour.
828 '''
829 def __init__(self, client, classname, nodeid, prop, name, value,
830 anonymous=0):
831 self._client = client
832 self._db = client.db
833 self._classname = classname
834 self._nodeid = nodeid
835 self._prop = prop
836 self._value = value
837 self._anonymous = anonymous
838 self._name = name
839 if not anonymous:
840 self._formname = '%s%s@%s'%(classname, nodeid, name)
841 else:
842 self._formname = name
843 def __repr__(self):
844 return '<HTMLProperty(0x%x) %s %r %r>'%(id(self), self._formname,
845 self._prop, self._value)
846 def __str__(self):
847 return self.plain()
848 def __cmp__(self, other):
849 if isinstance(other, HTMLProperty):
850 return cmp(self._value, other._value)
851 return cmp(self._value, other)
853 class StringHTMLProperty(HTMLProperty):
854 hyper_re = re.compile(r'((?P<url>\w{3,6}://\S+)|'
855 r'(?P<email>[-+=%/\w\.]+@[\w\.\-]+)|'
856 r'(?P<item>(?P<class>[a-z_]+)(?P<id>\d+)))')
857 def _hyper_repl(self, match):
858 if match.group('url'):
859 s = match.group('url')
860 return '<a href="%s">%s</a>'%(s, s)
861 elif match.group('email'):
862 s = match.group('email')
863 return '<a href="mailto:%s">%s</a>'%(s, s)
864 else:
865 s = match.group('item')
866 s1 = match.group('class')
867 s2 = match.group('id')
868 try:
869 # make sure s1 is a valid tracker classname
870 self._db.getclass(s1)
871 return '<a href="%s">%s %s</a>'%(s, s1, s2)
872 except KeyError:
873 return '%s%s'%(s1, s2)
875 def hyperlinked(self):
876 ''' Render a "hyperlinked" version of the text '''
877 return self.plain(hyperlink=1)
879 def plain(self, escape=0, hyperlink=0):
880 ''' Render a "plain" representation of the property
882 "escape" turns on/off HTML quoting
883 "hyperlink" turns on/off in-text hyperlinking of URLs, email
884 addresses and designators
885 '''
886 if self._value is None:
887 return ''
888 if escape:
889 s = cgi.escape(str(self._value))
890 else:
891 s = str(self._value)
892 if hyperlink:
893 # no, we *must* escape this text
894 if not escape:
895 s = cgi.escape(s)
896 s = self.hyper_re.sub(self._hyper_repl, s)
897 return s
899 def stext(self, escape=0):
900 ''' Render the value of the property as StructuredText.
902 This requires the StructureText module to be installed separately.
903 '''
904 s = self.plain(escape=escape)
905 if not StructuredText:
906 return s
907 return StructuredText(s,level=1,header=0)
909 def field(self, size = 30):
910 ''' Render a form edit field for the property
911 '''
912 if self._value is None:
913 value = ''
914 else:
915 value = cgi.escape(str(self._value))
916 value = '"'.join(value.split('"'))
917 return '<input name="%s" value="%s" size="%s">'%(self._formname, value, size)
919 def multiline(self, escape=0, rows=5, cols=40):
920 ''' Render a multiline form edit field for the property
921 '''
922 if self._value is None:
923 value = ''
924 else:
925 value = cgi.escape(str(self._value))
926 value = '"'.join(value.split('"'))
927 return '<textarea name="%s" rows="%s" cols="%s">%s</textarea>'%(
928 self._formname, rows, cols, value)
930 def email(self, escape=1):
931 ''' Render the value of the property as an obscured email address
932 '''
933 if self._value is None: value = ''
934 else: value = str(self._value)
935 if value.find('@') != -1:
936 name, domain = value.split('@')
937 domain = ' '.join(domain.split('.')[:-1])
938 name = name.replace('.', ' ')
939 value = '%s at %s ...'%(name, domain)
940 else:
941 value = value.replace('.', ' ')
942 if escape:
943 value = cgi.escape(value)
944 return value
946 class PasswordHTMLProperty(HTMLProperty):
947 def plain(self):
948 ''' Render a "plain" representation of the property
949 '''
950 if self._value is None:
951 return ''
952 return _('*encrypted*')
954 def field(self, size = 30):
955 ''' Render a form edit field for the property.
956 '''
957 return '<input type="password" name="%s" size="%s">'%(self._formname, size)
959 def confirm(self, size = 30):
960 ''' Render a second form edit field for the property, used for
961 confirmation that the user typed the password correctly. Generates
962 a field with name ":confirm:name".
963 '''
964 return '<input type="password" name=":confirm:%s" size="%s">'%(
965 self._formname, size)
967 class NumberHTMLProperty(HTMLProperty):
968 def plain(self):
969 ''' Render a "plain" representation of the property
970 '''
971 return str(self._value)
973 def field(self, size = 30):
974 ''' Render a form edit field for the property
975 '''
976 if self._value is None:
977 value = ''
978 else:
979 value = cgi.escape(str(self._value))
980 value = '"'.join(value.split('"'))
981 return '<input name="%s" value="%s" size="%s">'%(self._formname, value, size)
983 def __int__(self):
984 ''' Return an int of me
985 '''
986 return int(self._value)
988 def __float__(self):
989 ''' Return a float of me
990 '''
991 return float(self._value)
994 class BooleanHTMLProperty(HTMLProperty):
995 def plain(self):
996 ''' Render a "plain" representation of the property
997 '''
998 if self._value is None:
999 return ''
1000 return self._value and "Yes" or "No"
1002 def field(self):
1003 ''' Render a form edit field for the property
1004 '''
1005 checked = self._value and "checked" or ""
1006 s = '<input type="radio" name="%s" value="yes" %s>Yes'%(self._formname,
1007 checked)
1008 if checked:
1009 checked = ""
1010 else:
1011 checked = "checked"
1012 s += '<input type="radio" name="%s" value="no" %s>No'%(self._formname,
1013 checked)
1014 return s
1016 class DateHTMLProperty(HTMLProperty):
1017 def plain(self):
1018 ''' Render a "plain" representation of the property
1019 '''
1020 if self._value is None:
1021 return ''
1022 return str(self._value.local(self._db.getUserTimezone()))
1024 def now(self):
1025 ''' Return the current time.
1027 This is useful for defaulting a new value. Returns a
1028 DateHTMLProperty.
1029 '''
1030 return DateHTMLProperty(self._client, self._nodeid, self._prop,
1031 self._formname, date.Date('.'))
1033 def field(self, size = 30):
1034 ''' Render a form edit field for the property
1035 '''
1036 if self._value is None:
1037 value = ''
1038 else:
1039 value = cgi.escape(str(self._value.local(self._db.getUserTimezone())))
1040 value = '"'.join(value.split('"'))
1041 return '<input name="%s" value="%s" size="%s">'%(self._formname, value, size)
1043 def reldate(self, pretty=1):
1044 ''' Render the interval between the date and now.
1046 If the "pretty" flag is true, then make the display pretty.
1047 '''
1048 if not self._value:
1049 return ''
1051 # figure the interval
1052 interval = date.Date('.') - self._value
1053 if pretty:
1054 return interval.pretty()
1055 return str(interval)
1057 _marker = []
1058 def pretty(self, format=_marker):
1059 ''' Render the date in a pretty format (eg. month names, spaces).
1061 The format string is a standard python strftime format string.
1062 Note that if the day is zero, and appears at the start of the
1063 string, then it'll be stripped from the output. This is handy
1064 for the situatin when a date only specifies a month and a year.
1065 '''
1066 if format is not self._marker:
1067 return self._value.pretty(format)
1068 else:
1069 return self._value.pretty()
1071 def local(self, offset):
1072 ''' Return the date/time as a local (timezone offset) date/time.
1073 '''
1074 return DateHTMLProperty(self._client, self._nodeid, self._prop,
1075 self._formname, self._value.local(offset))
1077 class IntervalHTMLProperty(HTMLProperty):
1078 def plain(self):
1079 ''' Render a "plain" representation of the property
1080 '''
1081 if self._value is None:
1082 return ''
1083 return str(self._value)
1085 def pretty(self):
1086 ''' Render the interval in a pretty format (eg. "yesterday")
1087 '''
1088 return self._value.pretty()
1090 def field(self, size = 30):
1091 ''' Render a form edit field for the property
1092 '''
1093 if self._value is None:
1094 value = ''
1095 else:
1096 value = cgi.escape(str(self._value))
1097 value = '"'.join(value.split('"'))
1098 return '<input name="%s" value="%s" size="%s">'%(self._formname, value, size)
1100 class LinkHTMLProperty(HTMLProperty):
1101 ''' Link HTMLProperty
1102 Include the above as well as being able to access the class
1103 information. Stringifying the object itself results in the value
1104 from the item being displayed. Accessing attributes of this object
1105 result in the appropriate entry from the class being queried for the
1106 property accessed (so item/assignedto/name would look up the user
1107 entry identified by the assignedto property on item, and then the
1108 name property of that user)
1109 '''
1110 def __init__(self, *args, **kw):
1111 HTMLProperty.__init__(self, *args, **kw)
1112 # if we're representing a form value, then the -1 from the form really
1113 # should be a None
1114 if str(self._value) == '-1':
1115 self._value = None
1117 def __getattr__(self, attr):
1118 ''' return a new HTMLItem '''
1119 #print 'Link.getattr', (self, attr, self._value)
1120 if not self._value:
1121 raise AttributeError, "Can't access missing value"
1122 if self._prop.classname == 'user':
1123 klass = HTMLUser
1124 else:
1125 klass = HTMLItem
1126 i = klass(self._client, self._prop.classname, self._value)
1127 return getattr(i, attr)
1129 def plain(self, escape=0):
1130 ''' Render a "plain" representation of the property
1131 '''
1132 if self._value is None:
1133 return ''
1134 linkcl = self._db.classes[self._prop.classname]
1135 k = linkcl.labelprop(1)
1136 value = str(linkcl.get(self._value, k))
1137 if escape:
1138 value = cgi.escape(value)
1139 return value
1141 def field(self, showid=0, size=None):
1142 ''' Render a form edit field for the property
1143 '''
1144 linkcl = self._db.getclass(self._prop.classname)
1145 if linkcl.getprops().has_key('order'):
1146 sort_on = 'order'
1147 else:
1148 sort_on = linkcl.labelprop()
1149 options = linkcl.filter(None, {}, ('+', sort_on), (None, None))
1150 # TODO: make this a field display, not a menu one!
1151 l = ['<select name="%s">'%self._formname]
1152 k = linkcl.labelprop(1)
1153 if self._value is None:
1154 s = 'selected '
1155 else:
1156 s = ''
1157 l.append(_('<option %svalue="-1">- no selection -</option>')%s)
1159 # make sure we list the current value if it's retired
1160 if self._value and self._value not in options:
1161 options.insert(0, self._value)
1163 for optionid in options:
1164 # get the option value, and if it's None use an empty string
1165 option = linkcl.get(optionid, k) or ''
1167 # figure if this option is selected
1168 s = ''
1169 if optionid == self._value:
1170 s = 'selected '
1172 # figure the label
1173 if showid:
1174 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1175 else:
1176 lab = option
1178 # truncate if it's too long
1179 if size is not None and len(lab) > size:
1180 lab = lab[:size-3] + '...'
1182 # and generate
1183 lab = cgi.escape(lab)
1184 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
1185 l.append('</select>')
1186 return '\n'.join(l)
1188 def menu(self, size=None, height=None, showid=0, additional=[],
1189 **conditions):
1190 ''' Render a form select list for this property
1191 '''
1192 value = self._value
1194 # sort function
1195 sortfunc = make_sort_function(self._db, self._prop.classname)
1197 linkcl = self._db.getclass(self._prop.classname)
1198 l = ['<select name="%s">'%self._formname]
1199 k = linkcl.labelprop(1)
1200 s = ''
1201 if value is None:
1202 s = 'selected '
1203 l.append(_('<option %svalue="-1">- no selection -</option>')%s)
1204 if linkcl.getprops().has_key('order'):
1205 sort_on = ('+', 'order')
1206 else:
1207 sort_on = ('+', linkcl.labelprop())
1208 options = linkcl.filter(None, conditions, sort_on, (None, None))
1210 # make sure we list the current value if it's retired
1211 if self._value and self._value not in options:
1212 options.insert(0, self._value)
1214 for optionid in options:
1215 # get the option value, and if it's None use an empty string
1216 option = linkcl.get(optionid, k) or ''
1218 # figure if this option is selected
1219 s = ''
1220 if value in [optionid, option]:
1221 s = 'selected '
1223 # figure the label
1224 if showid:
1225 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1226 else:
1227 lab = option
1229 # truncate if it's too long
1230 if size is not None and len(lab) > size:
1231 lab = lab[:size-3] + '...'
1232 if additional:
1233 m = []
1234 for propname in additional:
1235 m.append(linkcl.get(optionid, propname))
1236 lab = lab + ' (%s)'%', '.join(map(str, m))
1238 # and generate
1239 lab = cgi.escape(lab)
1240 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
1241 l.append('</select>')
1242 return '\n'.join(l)
1243 # def checklist(self, ...)
1245 class MultilinkHTMLProperty(HTMLProperty):
1246 ''' Multilink HTMLProperty
1248 Also be iterable, returning a wrapper object like the Link case for
1249 each entry in the multilink.
1250 '''
1251 def __len__(self):
1252 ''' length of the multilink '''
1253 return len(self._value)
1255 def __getattr__(self, attr):
1256 ''' no extended attribute accesses make sense here '''
1257 raise AttributeError, attr
1259 def __getitem__(self, num):
1260 ''' iterate and return a new HTMLItem
1261 '''
1262 #print 'Multi.getitem', (self, num)
1263 value = self._value[num]
1264 if self._prop.classname == 'user':
1265 klass = HTMLUser
1266 else:
1267 klass = HTMLItem
1268 return klass(self._client, self._prop.classname, value)
1270 def __contains__(self, value):
1271 ''' Support the "in" operator. We have to make sure the passed-in
1272 value is a string first, not a *HTMLProperty.
1273 '''
1274 return str(value) in self._value
1276 def reverse(self):
1277 ''' return the list in reverse order
1278 '''
1279 l = self._value[:]
1280 l.reverse()
1281 if self._prop.classname == 'user':
1282 klass = HTMLUser
1283 else:
1284 klass = HTMLItem
1285 return [klass(self._client, self._prop.classname, value) for value in l]
1287 def plain(self, escape=0):
1288 ''' Render a "plain" representation of the property
1289 '''
1290 linkcl = self._db.classes[self._prop.classname]
1291 k = linkcl.labelprop(1)
1292 labels = []
1293 for v in self._value:
1294 labels.append(linkcl.get(v, k))
1295 value = ', '.join(labels)
1296 if escape:
1297 value = cgi.escape(value)
1298 return value
1300 def field(self, size=30, showid=0):
1301 ''' Render a form edit field for the property
1302 '''
1303 sortfunc = make_sort_function(self._db, self._prop.classname)
1304 linkcl = self._db.getclass(self._prop.classname)
1305 value = self._value[:]
1306 if value:
1307 value.sort(sortfunc)
1308 # map the id to the label property
1309 if not linkcl.getkey():
1310 showid=1
1311 if not showid:
1312 k = linkcl.labelprop(1)
1313 value = [linkcl.get(v, k) for v in value]
1314 value = cgi.escape(','.join(value))
1315 return '<input name="%s" size="%s" value="%s">'%(self._formname, size, value)
1317 def menu(self, size=None, height=None, showid=0, additional=[],
1318 **conditions):
1319 ''' Render a form select list for this property
1320 '''
1321 value = self._value
1323 # sort function
1324 sortfunc = make_sort_function(self._db, self._prop.classname)
1326 linkcl = self._db.getclass(self._prop.classname)
1327 if linkcl.getprops().has_key('order'):
1328 sort_on = ('+', 'order')
1329 else:
1330 sort_on = ('+', linkcl.labelprop())
1331 options = linkcl.filter(None, conditions, sort_on, (None,None))
1332 height = height or min(len(options), 7)
1333 l = ['<select multiple name="%s" size="%s">'%(self._formname, height)]
1334 k = linkcl.labelprop(1)
1336 # make sure we list the current values if they're retired
1337 for val in value:
1338 if val not in options:
1339 options.insert(0, val)
1341 for optionid in options:
1342 # get the option value, and if it's None use an empty string
1343 option = linkcl.get(optionid, k) or ''
1345 # figure if this option is selected
1346 s = ''
1347 if optionid in value or option in value:
1348 s = 'selected '
1350 # figure the label
1351 if showid:
1352 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1353 else:
1354 lab = option
1355 # truncate if it's too long
1356 if size is not None and len(lab) > size:
1357 lab = lab[:size-3] + '...'
1358 if additional:
1359 m = []
1360 for propname in additional:
1361 m.append(linkcl.get(optionid, propname))
1362 lab = lab + ' (%s)'%', '.join(m)
1364 # and generate
1365 lab = cgi.escape(lab)
1366 l.append('<option %svalue="%s">%s</option>'%(s, optionid,
1367 lab))
1368 l.append('</select>')
1369 return '\n'.join(l)
1371 # set the propclasses for HTMLItem
1372 propclasses = (
1373 (hyperdb.String, StringHTMLProperty),
1374 (hyperdb.Number, NumberHTMLProperty),
1375 (hyperdb.Boolean, BooleanHTMLProperty),
1376 (hyperdb.Date, DateHTMLProperty),
1377 (hyperdb.Interval, IntervalHTMLProperty),
1378 (hyperdb.Password, PasswordHTMLProperty),
1379 (hyperdb.Link, LinkHTMLProperty),
1380 (hyperdb.Multilink, MultilinkHTMLProperty),
1381 )
1383 def make_sort_function(db, classname):
1384 '''Make a sort function for a given class
1385 '''
1386 linkcl = db.getclass(classname)
1387 if linkcl.getprops().has_key('order'):
1388 sort_on = 'order'
1389 else:
1390 sort_on = linkcl.labelprop()
1391 def sortfunc(a, b, linkcl=linkcl, sort_on=sort_on):
1392 return cmp(linkcl.get(a, sort_on), linkcl.get(b, sort_on))
1393 return sortfunc
1395 def handleListCGIValue(value):
1396 ''' Value is either a single item or a list of items. Each item has a
1397 .value that we're actually interested in.
1398 '''
1399 if isinstance(value, type([])):
1400 return [value.value for value in value]
1401 else:
1402 value = value.value.strip()
1403 if not value:
1404 return []
1405 return value.split(',')
1407 class ShowDict:
1408 ''' A convenience access to the :columns index parameters
1409 '''
1410 def __init__(self, columns):
1411 self.columns = {}
1412 for col in columns:
1413 self.columns[col] = 1
1414 def __getitem__(self, name):
1415 return self.columns.has_key(name)
1417 class HTMLRequest:
1418 ''' The *request*, holding the CGI form and environment.
1420 "form" the CGI form as a cgi.FieldStorage
1421 "env" the CGI environment variables
1422 "base" the base URL for this instance
1423 "user" a HTMLUser instance for this user
1424 "classname" the current classname (possibly None)
1425 "template" the current template (suffix, also possibly None)
1427 Index args:
1428 "columns" dictionary of the columns to display in an index page
1429 "show" a convenience access to columns - request/show/colname will
1430 be true if the columns should be displayed, false otherwise
1431 "sort" index sort column (direction, column name)
1432 "group" index grouping property (direction, column name)
1433 "filter" properties to filter the index on
1434 "filterspec" values to filter the index on
1435 "search_text" text to perform a full-text search on for an index
1437 '''
1438 def __init__(self, client):
1439 self.client = client
1441 # easier access vars
1442 self.form = client.form
1443 self.env = client.env
1444 self.base = client.base
1445 self.user = HTMLUser(client, 'user', client.userid)
1447 # store the current class name and action
1448 self.classname = client.classname
1449 self.template = client.template
1451 # the special char to use for special vars
1452 self.special_char = '@'
1454 self._post_init()
1456 def _post_init(self):
1457 ''' Set attributes based on self.form
1458 '''
1459 # extract the index display information from the form
1460 self.columns = []
1461 for name in ':columns @columns'.split():
1462 if self.form.has_key(name):
1463 self.special_char = name[0]
1464 self.columns = handleListCGIValue(self.form[name])
1465 break
1466 self.show = ShowDict(self.columns)
1468 # sorting
1469 self.sort = (None, None)
1470 for name in ':sort @sort'.split():
1471 if self.form.has_key(name):
1472 self.special_char = name[0]
1473 sort = self.form[name].value
1474 if sort.startswith('-'):
1475 self.sort = ('-', sort[1:])
1476 else:
1477 self.sort = ('+', sort)
1478 if self.form.has_key(self.special_char+'sortdir'):
1479 self.sort = ('-', self.sort[1])
1481 # grouping
1482 self.group = (None, None)
1483 for name in ':group @group'.split():
1484 if self.form.has_key(name):
1485 self.special_char = name[0]
1486 group = self.form[name].value
1487 if group.startswith('-'):
1488 self.group = ('-', group[1:])
1489 else:
1490 self.group = ('+', group)
1491 if self.form.has_key(self.special_char+'groupdir'):
1492 self.group = ('-', self.group[1])
1494 # filtering
1495 self.filter = []
1496 for name in ':filter @filter'.split():
1497 if self.form.has_key(name):
1498 self.special_char = name[0]
1499 self.filter = handleListCGIValue(self.form[name])
1501 self.filterspec = {}
1502 db = self.client.db
1503 if self.classname is not None:
1504 props = db.getclass(self.classname).getprops()
1505 for name in self.filter:
1506 if not self.form.has_key(name):
1507 continue
1508 prop = props[name]
1509 fv = self.form[name]
1510 if (isinstance(prop, hyperdb.Link) or
1511 isinstance(prop, hyperdb.Multilink)):
1512 self.filterspec[name] = lookupIds(db, prop,
1513 handleListCGIValue(fv))
1514 else:
1515 if isinstance(fv, type([])):
1516 self.filterspec[name] = [v.value for v in fv]
1517 else:
1518 self.filterspec[name] = fv.value
1520 # full-text search argument
1521 self.search_text = None
1522 for name in ':search_text @search_text'.split():
1523 if self.form.has_key(name):
1524 self.special_char = name[0]
1525 self.search_text = self.form[name].value
1527 # pagination - size and start index
1528 # figure batch args
1529 self.pagesize = 50
1530 for name in ':pagesize @pagesize'.split():
1531 if self.form.has_key(name):
1532 self.special_char = name[0]
1533 self.pagesize = int(self.form[name].value)
1535 self.startwith = 0
1536 for name in ':startwith @startwith'.split():
1537 if self.form.has_key(name):
1538 self.special_char = name[0]
1539 self.startwith = int(self.form[name].value)
1541 def updateFromURL(self, url):
1542 ''' Parse the URL for query args, and update my attributes using the
1543 values.
1544 '''
1545 env = {'QUERY_STRING': url}
1546 self.form = cgi.FieldStorage(environ=env)
1548 self._post_init()
1550 def update(self, kwargs):
1551 ''' Update my attributes using the keyword args
1552 '''
1553 self.__dict__.update(kwargs)
1554 if kwargs.has_key('columns'):
1555 self.show = ShowDict(self.columns)
1557 def description(self):
1558 ''' Return a description of the request - handle for the page title.
1559 '''
1560 s = [self.client.db.config.TRACKER_NAME]
1561 if self.classname:
1562 if self.client.nodeid:
1563 s.append('- %s%s'%(self.classname, self.client.nodeid))
1564 else:
1565 if self.template == 'item':
1566 s.append('- new %s'%self.classname)
1567 elif self.template == 'index':
1568 s.append('- %s index'%self.classname)
1569 else:
1570 s.append('- %s %s'%(self.classname, self.template))
1571 else:
1572 s.append('- home')
1573 return ' '.join(s)
1575 def __str__(self):
1576 d = {}
1577 d.update(self.__dict__)
1578 f = ''
1579 for k in self.form.keys():
1580 f += '\n %r=%r'%(k,handleListCGIValue(self.form[k]))
1581 d['form'] = f
1582 e = ''
1583 for k,v in self.env.items():
1584 e += '\n %r=%r'%(k, v)
1585 d['env'] = e
1586 return '''
1587 form: %(form)s
1588 base: %(base)r
1589 classname: %(classname)r
1590 template: %(template)r
1591 columns: %(columns)r
1592 sort: %(sort)r
1593 group: %(group)r
1594 filter: %(filter)r
1595 search_text: %(search_text)r
1596 pagesize: %(pagesize)r
1597 startwith: %(startwith)r
1598 env: %(env)s
1599 '''%d
1601 def indexargs_form(self, columns=1, sort=1, group=1, filter=1,
1602 filterspec=1):
1603 ''' return the current index args as form elements '''
1604 l = []
1605 sc = self.special_char
1606 s = '<input type="hidden" name="%s" value="%s">'
1607 if columns and self.columns:
1608 l.append(s%(sc+'columns', ','.join(self.columns)))
1609 if sort and self.sort[1] is not None:
1610 if self.sort[0] == '-':
1611 val = '-'+self.sort[1]
1612 else:
1613 val = self.sort[1]
1614 l.append(s%(sc+'sort', val))
1615 if group and self.group[1] is not None:
1616 if self.group[0] == '-':
1617 val = '-'+self.group[1]
1618 else:
1619 val = self.group[1]
1620 l.append(s%(sc+'group', val))
1621 if filter and self.filter:
1622 l.append(s%(sc+'filter', ','.join(self.filter)))
1623 if filterspec:
1624 for k,v in self.filterspec.items():
1625 if type(v) == type([]):
1626 l.append(s%(k, ','.join(v)))
1627 else:
1628 l.append(s%(k, v))
1629 if self.search_text:
1630 l.append(s%(sc+'search_text', self.search_text))
1631 l.append(s%(sc+'pagesize', self.pagesize))
1632 l.append(s%(sc+'startwith', self.startwith))
1633 return '\n'.join(l)
1635 def indexargs_url(self, url, args):
1636 ''' Embed the current index args in a URL
1637 '''
1638 sc = self.special_char
1639 l = ['%s=%s'%(k,v) for k,v in args.items()]
1641 # pull out the special values (prefixed by @ or :)
1642 specials = {}
1643 for key in args.keys():
1644 if key[0] in '@:':
1645 specials[key[1:]] = args[key]
1647 # ok, now handle the specials we received in the request
1648 if self.columns and not specials.has_key('columns'):
1649 l.append(sc+'columns=%s'%(','.join(self.columns)))
1650 if self.sort[1] is not None and not specials.has_key('sort'):
1651 if self.sort[0] == '-':
1652 val = '-'+self.sort[1]
1653 else:
1654 val = self.sort[1]
1655 l.append(sc+'sort=%s'%val)
1656 if self.group[1] is not None and not specials.has_key('group'):
1657 if self.group[0] == '-':
1658 val = '-'+self.group[1]
1659 else:
1660 val = self.group[1]
1661 l.append(sc+'group=%s'%val)
1662 if self.filter and not specials.has_key('filter'):
1663 l.append(sc+'filter=%s'%(','.join(self.filter)))
1664 if self.search_text and not specials.has_key('search_text'):
1665 l.append(sc+'search_text=%s'%self.search_text)
1666 if not specials.has_key('pagesize'):
1667 l.append(sc+'pagesize=%s'%self.pagesize)
1668 if not specials.has_key('startwith'):
1669 l.append(sc+'startwith=%s'%self.startwith)
1671 # finally, the remainder of the filter args in the request
1672 for k,v in self.filterspec.items():
1673 if not args.has_key(k):
1674 if type(v) == type([]):
1675 l.append('%s=%s'%(k, ','.join(v)))
1676 else:
1677 l.append('%s=%s'%(k, v))
1678 return '%s?%s'%(url, '&'.join(l))
1679 indexargs_href = indexargs_url
1681 def base_javascript(self):
1682 return '''
1683 <script language="javascript">
1684 submitted = false;
1685 function submit_once() {
1686 if (submitted) {
1687 alert("Your request is being processed.\\nPlease be patient.");
1688 return 0;
1689 }
1690 submitted = true;
1691 return 1;
1692 }
1694 function help_window(helpurl, width, height) {
1695 HelpWin = window.open('%s' + helpurl, 'RoundupHelpWindow', 'scrollbars=yes,resizable=yes,toolbar=no,height='+height+',width='+width);
1696 }
1697 </script>
1698 '''%self.base
1700 def batch(self):
1701 ''' Return a batch object for results from the "current search"
1702 '''
1703 filterspec = self.filterspec
1704 sort = self.sort
1705 group = self.group
1707 # get the list of ids we're batching over
1708 klass = self.client.db.getclass(self.classname)
1709 if self.search_text:
1710 matches = self.client.db.indexer.search(
1711 re.findall(r'\b\w{2,25}\b', self.search_text), klass)
1712 else:
1713 matches = None
1714 l = klass.filter(matches, filterspec, sort, group)
1716 # return the batch object, using IDs only
1717 return Batch(self.client, l, self.pagesize, self.startwith,
1718 classname=self.classname)
1720 # extend the standard ZTUtils Batch object to remove dependency on
1721 # Acquisition and add a couple of useful methods
1722 class Batch(ZTUtils.Batch):
1723 ''' Use me to turn a list of items, or item ids of a given class, into a
1724 series of batches.
1726 ========= ========================================================
1727 Parameter Usage
1728 ========= ========================================================
1729 sequence a list of HTMLItems or item ids
1730 classname if sequence is a list of ids, this is the class of item
1731 size how big to make the sequence.
1732 start where to start (0-indexed) in the sequence.
1733 end where to end (0-indexed) in the sequence.
1734 orphan if the next batch would contain less items than this
1735 value, then it is combined with this batch
1736 overlap the number of items shared between adjacent batches
1737 ========= ========================================================
1739 Attributes: Note that the "start" attribute, unlike the
1740 argument, is a 1-based index (I know, lame). "first" is the
1741 0-based index. "length" is the actual number of elements in
1742 the batch.
1744 "sequence_length" is the length of the original, unbatched, sequence.
1745 '''
1746 def __init__(self, client, sequence, size, start, end=0, orphan=0,
1747 overlap=0, classname=None):
1748 self.client = client
1749 self.last_index = self.last_item = None
1750 self.current_item = None
1751 self.classname = classname
1752 self.sequence_length = len(sequence)
1753 ZTUtils.Batch.__init__(self, sequence, size, start, end, orphan,
1754 overlap)
1756 # overwrite so we can late-instantiate the HTMLItem instance
1757 def __getitem__(self, index):
1758 if index < 0:
1759 if index + self.end < self.first: raise IndexError, index
1760 return self._sequence[index + self.end]
1762 if index >= self.length:
1763 raise IndexError, index
1765 # move the last_item along - but only if the fetched index changes
1766 # (for some reason, index 0 is fetched twice)
1767 if index != self.last_index:
1768 self.last_item = self.current_item
1769 self.last_index = index
1771 item = self._sequence[index + self.first]
1772 if self.classname:
1773 # map the item ids to instances
1774 if self.classname == 'user':
1775 item = HTMLUser(self.client, self.classname, item)
1776 else:
1777 item = HTMLItem(self.client, self.classname, item)
1778 self.current_item = item
1779 return item
1781 def propchanged(self, property):
1782 ''' Detect if the property marked as being the group property
1783 changed in the last iteration fetch
1784 '''
1785 if (self.last_item is None or
1786 self.last_item[property] != self.current_item[property]):
1787 return 1
1788 return 0
1790 # override these 'cos we don't have access to acquisition
1791 def previous(self):
1792 if self.start == 1:
1793 return None
1794 return Batch(self.client, self._sequence, self._size,
1795 self.first - self._size + self.overlap, 0, self.orphan,
1796 self.overlap)
1798 def next(self):
1799 try:
1800 self._sequence[self.end]
1801 except IndexError:
1802 return None
1803 return Batch(self.client, self._sequence, self._size,
1804 self.end - self.overlap, 0, self.orphan, self.overlap)
1806 class TemplatingUtils:
1807 ''' Utilities for templating
1808 '''
1809 def __init__(self, client):
1810 self.client = client
1811 def Batch(self, sequence, size, start, end=0, orphan=0, overlap=0):
1812 return Batch(self.client, sequence, size, start, end, orphan,
1813 overlap)