dc8482639db6cb89ae14e0bc38438776989c6691
1 import sys, cgi, urllib, os, re, os.path, time, errno
3 from roundup import hyperdb, date
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 pt.write(open(src).read())
120 pt.id = filename
121 pt.mtime = time.time()
122 return pt
124 def __getitem__(self, name):
125 name, extension = os.path.splitext(name)
126 if extension:
127 extension = extension[1:]
128 try:
129 return self.get(name, extension)
130 except NoTemplate, message:
131 raise KeyError, message
133 class RoundupPageTemplate(PageTemplate.PageTemplate):
134 ''' A Roundup-specific PageTemplate.
136 Interrogate the client to set up the various template variables to
137 be available:
139 *context*
140 this is one of three things:
141 1. None - we're viewing a "home" page
142 2. The current class of item being displayed. This is an HTMLClass
143 instance.
144 3. The current item from the database, if we're viewing a specific
145 item, as an HTMLItem instance.
146 *request*
147 Includes information about the current request, including:
148 - the url
149 - the current index information (``filterspec``, ``filter`` args,
150 ``properties``, etc) parsed out of the form.
151 - methods for easy filterspec link generation
152 - *user*, the current user node as an HTMLItem instance
153 - *form*, the current CGI form information as a FieldStorage
154 *config*
155 The current tracker config.
156 *db*
157 The current database, used to access arbitrary database items.
158 *utils*
159 This is a special class that has its base in the TemplatingUtils
160 class in this file. If the tracker interfaces module defines a
161 TemplatingUtils class then it is mixed in, overriding the methods
162 in the base class.
163 '''
164 def getContext(self, client, classname, request):
165 # construct the TemplatingUtils class
166 utils = TemplatingUtils
167 if hasattr(client.instance.interfaces, 'TemplatingUtils'):
168 class utils(client.instance.interfaces.TemplatingUtils, utils):
169 pass
171 c = {
172 'options': {},
173 'nothing': None,
174 'request': request,
175 'db': HTMLDatabase(client),
176 'config': client.instance.config,
177 'tracker': client.instance,
178 'utils': utils(client),
179 'templates': Templates(client.instance.config.TEMPLATES),
180 }
181 # add in the item if there is one
182 if client.nodeid:
183 if classname == 'user':
184 c['context'] = HTMLUser(client, classname, client.nodeid,
185 anonymous=1)
186 else:
187 c['context'] = HTMLItem(client, classname, client.nodeid,
188 anonymous=1)
189 elif client.db.classes.has_key(classname):
190 c['context'] = HTMLClass(client, classname, anonymous=1)
191 return c
193 def render(self, client, classname, request, **options):
194 """Render this Page Template"""
196 if not self._v_cooked:
197 self._cook()
199 __traceback_supplement__ = (PageTemplate.PageTemplateTracebackSupplement, self)
201 if self._v_errors:
202 raise PageTemplate.PTRuntimeError, \
203 'Page Template %s has errors.'%self.id
205 # figure the context
206 classname = classname or client.classname
207 request = request or HTMLRequest(client)
208 c = self.getContext(client, classname, request)
209 c.update({'options': options})
211 # and go
212 output = StringIO.StringIO()
213 TALInterpreter(self._v_program, self.macros,
214 getEngine().getContext(c), output, tal=1, strictinsert=0)()
215 return output.getvalue()
217 class HTMLDatabase:
218 ''' Return HTMLClasses for valid class fetches
219 '''
220 def __init__(self, client):
221 self._client = client
222 self._db = client.db
224 # we want config to be exposed
225 self.config = client.db.config
227 def __getitem__(self, item, desre=re.compile(r'(?P<cl>\w+)(?P<id>[-\d]+)')):
228 # check to see if we're actually accessing an item
229 m = desre.match(item)
230 if m:
231 self._client.db.getclass(m.group('cl'))
232 return HTMLItem(self._client, m.group('cl'), m.group('id'))
233 else:
234 self._client.db.getclass(item)
235 return HTMLClass(self._client, item)
237 def __getattr__(self, attr):
238 try:
239 return self[attr]
240 except KeyError:
241 raise AttributeError, attr
243 def classes(self):
244 l = self._client.db.classes.keys()
245 l.sort()
246 return [HTMLClass(self._client, cn) for cn in l]
248 def lookupIds(db, prop, ids, num_re=re.compile('-?\d+')):
249 cl = db.getclass(prop.classname)
250 l = []
251 for entry in ids:
252 if num_re.match(entry):
253 l.append(entry)
254 else:
255 try:
256 l.append(cl.lookup(entry))
257 except KeyError:
258 # ignore invalid keys
259 pass
260 return l
262 class HTMLPermissions:
263 ''' Helpers that provide answers to commonly asked Permission questions.
264 '''
265 def is_edit_ok(self):
266 ''' Is the user allowed to Edit the current class?
267 '''
268 return self._db.security.hasPermission('Edit', self._client.userid,
269 self._classname)
270 def is_view_ok(self):
271 ''' Is the user allowed to View the current class?
272 '''
273 return self._db.security.hasPermission('View', self._client.userid,
274 self._classname)
275 def is_only_view_ok(self):
276 ''' Is the user only allowed to View (ie. not Edit) the current class?
277 '''
278 return self.is_view_ok() and not self.is_edit_ok()
280 class HTMLClass(HTMLPermissions):
281 ''' Accesses through a class (either through *class* or *db.<classname>*)
282 '''
283 def __init__(self, client, classname, anonymous=0):
284 self._client = client
285 self._db = client.db
286 self._anonymous = anonymous
288 # we want classname to be exposed, but _classname gives a
289 # consistent API for extending Class/Item
290 self._classname = self.classname = classname
291 self._klass = self._db.getclass(self.classname)
292 self._props = self._klass.getprops()
294 def __repr__(self):
295 return '<HTMLClass(0x%x) %s>'%(id(self), self.classname)
297 def __getitem__(self, item):
298 ''' return an HTMLProperty instance
299 '''
300 #print 'HTMLClass.getitem', (self, item)
302 # we don't exist
303 if item == 'id':
304 return None
306 # get the property
307 prop = self._props[item]
309 # look up the correct HTMLProperty class
310 form = self._client.form
311 for klass, htmlklass in propclasses:
312 if not isinstance(prop, klass):
313 continue
314 if form.has_key(item):
315 if isinstance(prop, hyperdb.Multilink):
316 value = lookupIds(self._db, prop,
317 handleListCGIValue(form[item]))
318 elif isinstance(prop, hyperdb.Link):
319 value = form[item].value.strip()
320 if value:
321 value = lookupIds(self._db, prop, [value])[0]
322 else:
323 value = None
324 else:
325 value = form[item].value.strip() or None
326 else:
327 if isinstance(prop, hyperdb.Multilink):
328 value = []
329 else:
330 value = None
331 return htmlklass(self._client, self._classname, '', prop, item,
332 value, self._anonymous)
334 # no good
335 raise KeyError, item
337 def __getattr__(self, attr):
338 ''' convenience access '''
339 try:
340 return self[attr]
341 except KeyError:
342 raise AttributeError, attr
344 def getItem(self, itemid, num_re=re.compile('\d+')):
345 ''' Get an item of this class by its item id.
346 '''
347 # make sure we're looking at an itemid
348 if not num_re.match(itemid):
349 itemid = self._klass.lookup(itemid)
351 if self.classname == 'user':
352 klass = HTMLUser
353 else:
354 klass = HTMLItem
356 return klass(self._client, self.classname, itemid)
358 def properties(self, sort=1):
359 ''' Return HTMLProperty for all of this class' properties.
360 '''
361 l = []
362 for name, prop in self._props.items():
363 for klass, htmlklass in propclasses:
364 if isinstance(prop, hyperdb.Multilink):
365 value = []
366 else:
367 value = None
368 if isinstance(prop, klass):
369 l.append(htmlklass(self._client, self._classname, '',
370 prop, name, value, self._anonymous))
371 if sort:
372 l.sort(lambda a,b:cmp(a._name, b._name))
373 return l
375 def list(self):
376 ''' List all items in this class.
377 '''
378 if self.classname == 'user':
379 klass = HTMLUser
380 else:
381 klass = HTMLItem
383 # get the list and sort it nicely
384 l = self._klass.list()
385 sortfunc = make_sort_function(self._db, self.classname)
386 l.sort(sortfunc)
388 l = [klass(self._client, self.classname, x) for x in l]
389 return l
391 def csv(self):
392 ''' Return the items of this class as a chunk of CSV text.
393 '''
394 # get the CSV module
395 try:
396 import csv
397 except ImportError:
398 return 'Sorry, you need the csv module to use this function.\n'\
399 'Get it from: http://www.object-craft.com.au/projects/csv/'
401 props = self.propnames()
402 p = csv.parser()
403 s = StringIO.StringIO()
404 s.write(p.join(props) + '\n')
405 for nodeid in self._klass.list():
406 l = []
407 for name in props:
408 value = self._klass.get(nodeid, name)
409 if value is None:
410 l.append('')
411 elif isinstance(value, type([])):
412 l.append(':'.join(map(str, value)))
413 else:
414 l.append(str(self._klass.get(nodeid, name)))
415 s.write(p.join(l) + '\n')
416 return s.getvalue()
418 def propnames(self):
419 ''' Return the list of the names of the properties of this class.
420 '''
421 idlessprops = self._klass.getprops(protected=0).keys()
422 idlessprops.sort()
423 return ['id'] + idlessprops
425 def filter(self, request=None):
426 ''' Return a list of items from this class, filtered and sorted
427 by the current requested filterspec/filter/sort/group args
428 '''
429 # XXX allow direct specification of the filterspec etc.
430 if request is not None:
431 filterspec = request.filterspec
432 sort = request.sort
433 group = request.group
434 else:
435 filterspec = {}
436 sort = (None,None)
437 group = (None,None)
438 if self.classname == 'user':
439 klass = HTMLUser
440 else:
441 klass = HTMLItem
442 l = [klass(self._client, self.classname, x)
443 for x in self._klass.filter(None, filterspec, sort, group)]
444 return l
446 def classhelp(self, properties=None, label='(list)', width='500',
447 height='400', property=''):
448 ''' Pop up a javascript window with class help
450 This generates a link to a popup window which displays the
451 properties indicated by "properties" of the class named by
452 "classname". The "properties" should be a comma-separated list
453 (eg. 'id,name,description'). Properties defaults to all the
454 properties of a class (excluding id, creator, created and
455 activity).
457 You may optionally override the label displayed, the width and
458 height. The popup window will be resizable and scrollable.
460 If the "property" arg is given, it's passed through to the
461 javascript help_window function.
462 '''
463 if properties is None:
464 properties = self._klass.getprops(protected=0).keys()
465 properties.sort()
466 properties = ','.join(properties)
467 if property:
468 property = '&property=%s'%property
469 return '<a class="classhelp" href="javascript:help_window(\'%s?'\
470 ':startwith=0&:template=help&properties=%s%s\', \'%s\', \
471 \'%s\')">%s</a>'%(self.classname, properties, property, width,
472 height, label)
474 def submit(self, label="Submit New Entry"):
475 ''' Generate a submit button (and action hidden element)
476 '''
477 return ' <input type="hidden" name=":action" value="new">\n'\
478 ' <input type="submit" name="submit" value="%s">'%label
480 def history(self):
481 return 'New node - no history'
483 def renderWith(self, name, **kwargs):
484 ''' Render this class with the given template.
485 '''
486 # create a new request and override the specified args
487 req = HTMLRequest(self._client)
488 req.classname = self.classname
489 req.update(kwargs)
491 # new template, using the specified classname and request
492 pt = Templates(self._db.config.TEMPLATES).get(self.classname, name)
494 # use our fabricated request
495 return pt.render(self._client, self.classname, req)
497 class HTMLItem(HTMLPermissions):
498 ''' Accesses through an *item*
499 '''
500 def __init__(self, client, classname, nodeid, anonymous=0):
501 self._client = client
502 self._db = client.db
503 self._classname = classname
504 self._nodeid = nodeid
505 self._klass = self._db.getclass(classname)
506 self._props = self._klass.getprops()
508 # do we prefix the form items with the item's identification?
509 self._anonymous = anonymous
511 def __repr__(self):
512 return '<HTMLItem(0x%x) %s %s>'%(id(self), self._classname,
513 self._nodeid)
515 def __getitem__(self, item):
516 ''' return an HTMLProperty instance
517 '''
518 #print 'HTMLItem.getitem', (self, item)
519 if item == 'id':
520 return self._nodeid
522 # get the property
523 prop = self._props[item]
525 # get the value, handling missing values
526 value = None
527 if int(self._nodeid) > 0:
528 value = self._klass.get(self._nodeid, item, None)
529 if value is None:
530 if isinstance(self._props[item], hyperdb.Multilink):
531 value = []
533 # look up the correct HTMLProperty class
534 for klass, htmlklass in propclasses:
535 if isinstance(prop, klass):
536 return htmlklass(self._client, self._classname,
537 self._nodeid, prop, item, value, self._anonymous)
539 raise KeyError, item
541 def __getattr__(self, attr):
542 ''' convenience access to properties '''
543 try:
544 return self[attr]
545 except KeyError:
546 raise AttributeError, attr
548 def submit(self, label="Submit Changes"):
549 ''' Generate a submit button (and action hidden element)
550 '''
551 return ' <input type="hidden" name=":action" value="edit">\n'\
552 ' <input type="submit" name="submit" value="%s">'%label
554 def journal(self, direction='descending'):
555 ''' Return a list of HTMLJournalEntry instances.
556 '''
557 # XXX do this
558 return []
560 def history(self, direction='descending', dre=re.compile('\d+')):
561 l = ['<table class="history">'
562 '<tr><th colspan="4" class="header">',
563 _('History'),
564 '</th></tr><tr>',
565 _('<th>Date</th>'),
566 _('<th>User</th>'),
567 _('<th>Action</th>'),
568 _('<th>Args</th>'),
569 '</tr>']
570 current = {}
571 comments = {}
572 history = self._klass.history(self._nodeid)
573 history.sort()
574 timezone = self._db.getUserTimezone()
575 if direction == 'descending':
576 history.reverse()
577 for prop_n in self._props.keys():
578 prop = self[prop_n]
579 if isinstance(prop, HTMLProperty):
580 current[prop_n] = prop.plain()
581 # make link if hrefable
582 if (self._props.has_key(prop_n) and
583 isinstance(self._props[prop_n], hyperdb.Link)):
584 classname = self._props[prop_n].classname
585 try:
586 find_template(self._db.config.TEMPLATES,
587 classname, 'item')
588 except NoTemplate:
589 pass
590 else:
591 id = self._klass.get(self._nodeid, prop_n, None)
592 current[prop_n] = '<a href="%s%s">%s</a>'%(
593 classname, id, current[prop_n])
595 for id, evt_date, user, action, args in history:
596 date_s = str(evt_date.local(timezone)).replace("."," ")
597 arg_s = ''
598 if action == 'link' and type(args) == type(()):
599 if len(args) == 3:
600 linkcl, linkid, key = args
601 arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
602 linkcl, linkid, key)
603 else:
604 arg_s = str(args)
606 elif action == 'unlink' 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 type(args) == type({}):
615 cell = []
616 for k in args.keys():
617 # try to get the relevant property and treat it
618 # specially
619 try:
620 prop = self._props[k]
621 except KeyError:
622 prop = None
623 if prop is not None:
624 if args[k] and (isinstance(prop, hyperdb.Multilink) or
625 isinstance(prop, hyperdb.Link)):
626 # figure what the link class is
627 classname = prop.classname
628 try:
629 linkcl = self._db.getclass(classname)
630 except KeyError:
631 labelprop = None
632 comments[classname] = _('''The linked class
633 %(classname)s no longer exists''')%locals()
634 labelprop = linkcl.labelprop(1)
635 hrefable = os.path.exists(
636 os.path.join(self._db.config.TEMPLATES,
637 classname+'.item'))
639 if isinstance(prop, hyperdb.Multilink) and args[k]:
640 ml = []
641 for linkid in args[k]:
642 if isinstance(linkid, type(())):
643 sublabel = linkid[0] + ' '
644 linkids = linkid[1]
645 else:
646 sublabel = ''
647 linkids = [linkid]
648 subml = []
649 for linkid in linkids:
650 label = classname + linkid
651 # if we have a label property, try to use it
652 # TODO: test for node existence even when
653 # there's no labelprop!
654 try:
655 if labelprop is not None and \
656 labelprop != 'id':
657 label = linkcl.get(linkid, labelprop)
658 except IndexError:
659 comments['no_link'] = _('''<strike>The
660 linked node no longer
661 exists</strike>''')
662 subml.append('<strike>%s</strike>'%label)
663 else:
664 if hrefable:
665 subml.append('<a href="%s%s">%s</a>'%(
666 classname, linkid, label))
667 else:
668 subml.append(label)
669 ml.append(sublabel + ', '.join(subml))
670 cell.append('%s:\n %s'%(k, ', '.join(ml)))
671 elif isinstance(prop, hyperdb.Link) and args[k]:
672 label = classname + args[k]
673 # if we have a label property, try to use it
674 # TODO: test for node existence even when
675 # there's no labelprop!
676 if labelprop is not None and labelprop != 'id':
677 try:
678 label = linkcl.get(args[k], labelprop)
679 except IndexError:
680 comments['no_link'] = _('''<strike>The
681 linked node no longer
682 exists</strike>''')
683 cell.append(' <strike>%s</strike>,\n'%label)
684 # "flag" this is done .... euwww
685 label = None
686 if label is not None:
687 if hrefable:
688 old = '<a href="%s%s">%s</a>'%(classname, args[k], label)
689 else:
690 old = label;
691 cell.append('%s: %s' % (k,old))
692 if current.has_key(k):
693 cell[-1] += ' -> %s'%current[k]
694 current[k] = old
696 elif isinstance(prop, hyperdb.Date) and args[k]:
697 d = date.Date(args[k]).local(timezone)
698 cell.append('%s: %s'%(k, str(d)))
699 if current.has_key(k):
700 cell[-1] += ' -> %s' % current[k]
701 current[k] = str(d)
703 elif isinstance(prop, hyperdb.Interval) and args[k]:
704 d = date.Interval(args[k])
705 cell.append('%s: %s'%(k, str(d)))
706 if current.has_key(k):
707 cell[-1] += ' -> %s'%current[k]
708 current[k] = str(d)
710 elif isinstance(prop, hyperdb.String) and args[k]:
711 cell.append('%s: %s'%(k, cgi.escape(args[k])))
712 if current.has_key(k):
713 cell[-1] += ' -> %s'%current[k]
714 current[k] = cgi.escape(args[k])
716 elif not args[k]:
717 if current.has_key(k):
718 cell.append('%s: %s'%(k, current[k]))
719 current[k] = '(no value)'
720 else:
721 cell.append('%s: (no value)'%k)
723 else:
724 cell.append('%s: %s'%(k, str(args[k])))
725 if current.has_key(k):
726 cell[-1] += ' -> %s'%current[k]
727 current[k] = str(args[k])
728 else:
729 # property no longer exists
730 comments['no_exist'] = _('''<em>The indicated property
731 no longer exists</em>''')
732 cell.append('<em>%s: %s</em>\n'%(k, str(args[k])))
733 arg_s = '<br />'.join(cell)
734 else:
735 # unkown event!!
736 comments['unknown'] = _('''<strong><em>This event is not
737 handled by the history display!</em></strong>''')
738 arg_s = '<strong><em>' + str(args) + '</em></strong>'
739 date_s = date_s.replace(' ', ' ')
740 # if the user's an itemid, figure the username (older journals
741 # have the username)
742 if dre.match(user):
743 user = self._db.user.get(user, 'username')
744 l.append('<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>'%(
745 date_s, user, action, arg_s))
746 if comments:
747 l.append(_('<tr><td colspan=4><strong>Note:</strong></td></tr>'))
748 for entry in comments.values():
749 l.append('<tr><td colspan=4>%s</td></tr>'%entry)
750 l.append('</table>')
751 return '\n'.join(l)
753 def renderQueryForm(self):
754 ''' Render this item, which is a query, as a search form.
755 '''
756 # create a new request and override the specified args
757 req = HTMLRequest(self._client)
758 req.classname = self._klass.get(self._nodeid, 'klass')
759 name = self._klass.get(self._nodeid, 'name')
760 req.updateFromURL(self._klass.get(self._nodeid, 'url') +
761 '&:queryname=%s'%urllib.quote(name))
763 # new template, using the specified classname and request
764 pt = Templates(self._db.config.TEMPLATES).get(req.classname, 'search')
766 # use our fabricated request
767 return pt.render(self._client, req.classname, req)
769 class HTMLUser(HTMLItem):
770 ''' Accesses through the *user* (a special case of item)
771 '''
772 def __init__(self, client, classname, nodeid, anonymous=0):
773 HTMLItem.__init__(self, client, 'user', nodeid, anonymous)
774 self._default_classname = client.classname
776 # used for security checks
777 self._security = client.db.security
779 _marker = []
780 def hasPermission(self, permission, classname=_marker):
781 ''' Determine if the user has the Permission.
783 The class being tested defaults to the template's class, but may
784 be overidden for this test by suppling an alternate classname.
785 '''
786 if classname is self._marker:
787 classname = self._default_classname
788 return self._security.hasPermission(permission, self._nodeid, classname)
790 def is_edit_ok(self):
791 ''' Is the user allowed to Edit the current class?
792 Also check whether this is the current user's info.
793 '''
794 return self._db.security.hasPermission('Edit', self._client.userid,
795 self._classname) or self._nodeid == self._client.userid
797 def is_view_ok(self):
798 ''' Is the user allowed to View the current class?
799 Also check whether this is the current user's info.
800 '''
801 return self._db.security.hasPermission('Edit', self._client.userid,
802 self._classname) or self._nodeid == self._client.userid
804 class HTMLProperty:
805 ''' String, Number, Date, Interval HTMLProperty
807 Has useful attributes:
809 _name the name of the property
810 _value the value of the property if any
812 A wrapper object which may be stringified for the plain() behaviour.
813 '''
814 def __init__(self, client, classname, nodeid, prop, name, value,
815 anonymous=0):
816 self._client = client
817 self._db = client.db
818 self._classname = classname
819 self._nodeid = nodeid
820 self._prop = prop
821 self._value = value
822 self._anonymous = anonymous
823 self._name = name
824 if not anonymous:
825 self._formname = '%s%s@%s'%(classname, nodeid, name)
826 else:
827 self._formname = name
828 def __repr__(self):
829 return '<HTMLProperty(0x%x) %s %r %r>'%(id(self), self._formname,
830 self._prop, self._value)
831 def __str__(self):
832 return self.plain()
833 def __cmp__(self, other):
834 if isinstance(other, HTMLProperty):
835 return cmp(self._value, other._value)
836 return cmp(self._value, other)
838 class StringHTMLProperty(HTMLProperty):
839 hyper_re = re.compile(r'((?P<url>\w{3,6}://\S+)|'
840 r'(?P<email>[-+=%/\w\.]+@[\w\.\-]+)|'
841 r'(?P<item>(?P<class>[a-z_]+)(?P<id>\d+)))')
842 def _hyper_repl(self, match):
843 if match.group('url'):
844 s = match.group('url')
845 return '<a href="%s">%s</a>'%(s, s)
846 elif match.group('email'):
847 s = match.group('email')
848 return '<a href="mailto:%s">%s</a>'%(s, s)
849 else:
850 s = match.group('item')
851 s1 = match.group('class')
852 s2 = match.group('id')
853 try:
854 # make sure s1 is a valid tracker classname
855 self._db.getclass(s1)
856 return '<a href="%s">%s %s</a>'%(s, s1, s2)
857 except KeyError:
858 return '%s%s'%(s1, s2)
860 def plain(self, escape=0, hyperlink=0):
861 ''' Render a "plain" representation of the property
863 "escape" turns on/off HTML quoting
864 "hyperlink" turns on/off in-text hyperlinking of URLs, email
865 addresses and designators
866 '''
867 if self._value is None:
868 return ''
869 if escape:
870 s = cgi.escape(str(self._value))
871 else:
872 s = str(self._value)
873 if hyperlink:
874 if not escape:
875 s = cgi.escape(s)
876 s = self.hyper_re.sub(self._hyper_repl, s)
877 return s
879 def stext(self, escape=0):
880 ''' Render the value of the property as StructuredText.
882 This requires the StructureText module to be installed separately.
883 '''
884 s = self.plain(escape=escape)
885 if not StructuredText:
886 return s
887 return StructuredText(s,level=1,header=0)
889 def field(self, size = 30):
890 ''' Render a form edit field for the property
891 '''
892 if self._value is None:
893 value = ''
894 else:
895 value = cgi.escape(str(self._value))
896 value = '"'.join(value.split('"'))
897 return '<input name="%s" value="%s" size="%s">'%(self._formname, value, size)
899 def multiline(self, escape=0, rows=5, cols=40):
900 ''' Render a multiline form edit field for the property
901 '''
902 if self._value is None:
903 value = ''
904 else:
905 value = cgi.escape(str(self._value))
906 value = '"'.join(value.split('"'))
907 return '<textarea name="%s" rows="%s" cols="%s">%s</textarea>'%(
908 self._formname, rows, cols, value)
910 def email(self, escape=1):
911 ''' Render the value of the property as an obscured email address
912 '''
913 if self._value is None: value = ''
914 else: value = str(self._value)
915 if value.find('@') != -1:
916 name, domain = value.split('@')
917 domain = ' '.join(domain.split('.')[:-1])
918 name = name.replace('.', ' ')
919 value = '%s at %s ...'%(name, domain)
920 else:
921 value = value.replace('.', ' ')
922 if escape:
923 value = cgi.escape(value)
924 return value
926 class PasswordHTMLProperty(HTMLProperty):
927 def plain(self):
928 ''' Render a "plain" representation of the property
929 '''
930 if self._value is None:
931 return ''
932 return _('*encrypted*')
934 def field(self, size = 30):
935 ''' Render a form edit field for the property.
936 '''
937 return '<input type="password" name="%s" size="%s">'%(self._formname, size)
939 def confirm(self, size = 30):
940 ''' Render a second form edit field for the property, used for
941 confirmation that the user typed the password correctly. Generates
942 a field with name ":confirm:name".
943 '''
944 return '<input type="password" name=":confirm:%s" size="%s">'%(
945 self._formname, size)
947 class NumberHTMLProperty(HTMLProperty):
948 def plain(self):
949 ''' Render a "plain" representation of the property
950 '''
951 return str(self._value)
953 def field(self, size = 30):
954 ''' Render a form edit field for the property
955 '''
956 if self._value is None:
957 value = ''
958 else:
959 value = cgi.escape(str(self._value))
960 value = '"'.join(value.split('"'))
961 return '<input name="%s" value="%s" size="%s">'%(self._formname, value, size)
963 def __int__(self):
964 ''' Return an int of me
965 '''
966 return int(self._value)
968 def __float__(self):
969 ''' Return a float of me
970 '''
971 return float(self._value)
974 class BooleanHTMLProperty(HTMLProperty):
975 def plain(self):
976 ''' Render a "plain" representation of the property
977 '''
978 if self._value is None:
979 return ''
980 return self._value and "Yes" or "No"
982 def field(self):
983 ''' Render a form edit field for the property
984 '''
985 checked = self._value and "checked" or ""
986 s = '<input type="radio" name="%s" value="yes" %s>Yes'%(self._formname,
987 checked)
988 if checked:
989 checked = ""
990 else:
991 checked = "checked"
992 s += '<input type="radio" name="%s" value="no" %s>No'%(self._formname,
993 checked)
994 return s
996 class DateHTMLProperty(HTMLProperty):
997 def plain(self):
998 ''' Render a "plain" representation of the property
999 '''
1000 if self._value is None:
1001 return ''
1002 return str(self._value.local(self._db.getUserTimezone()))
1004 def now(self):
1005 ''' Return the current time.
1007 This is useful for defaulting a new value. Returns a
1008 DateHTMLProperty.
1009 '''
1010 return DateHTMLProperty(self._client, self._nodeid, self._prop,
1011 self._formname, date.Date('.'))
1013 def field(self, size = 30):
1014 ''' Render a form edit field for the property
1015 '''
1016 if self._value is None:
1017 value = ''
1018 else:
1019 value = cgi.escape(str(self._value.local(self._db.getUserTimezone())))
1020 value = '"'.join(value.split('"'))
1021 return '<input name="%s" value="%s" size="%s">'%(self._formname, value, size)
1023 def reldate(self, pretty=1):
1024 ''' Render the interval between the date and now.
1026 If the "pretty" flag is true, then make the display pretty.
1027 '''
1028 if not self._value:
1029 return ''
1031 # figure the interval
1032 interval = date.Date('.') - self._value
1033 if pretty:
1034 return interval.pretty()
1035 return str(interval)
1037 _marker = []
1038 def pretty(self, format=_marker):
1039 ''' Render the date in a pretty format (eg. month names, spaces).
1041 The format string is a standard python strftime format string.
1042 Note that if the day is zero, and appears at the start of the
1043 string, then it'll be stripped from the output. This is handy
1044 for the situatin when a date only specifies a month and a year.
1045 '''
1046 if format is not self._marker:
1047 return self._value.pretty(format)
1048 else:
1049 return self._value.pretty()
1051 def local(self, offset):
1052 ''' Return the date/time as a local (timezone offset) date/time.
1053 '''
1054 return DateHTMLProperty(self._client, self._nodeid, self._prop,
1055 self._formname, self._value.local(offset))
1057 class IntervalHTMLProperty(HTMLProperty):
1058 def plain(self):
1059 ''' Render a "plain" representation of the property
1060 '''
1061 if self._value is None:
1062 return ''
1063 return str(self._value)
1065 def pretty(self):
1066 ''' Render the interval in a pretty format (eg. "yesterday")
1067 '''
1068 return self._value.pretty()
1070 def field(self, size = 30):
1071 ''' Render a form edit field for the property
1072 '''
1073 if self._value is None:
1074 value = ''
1075 else:
1076 value = cgi.escape(str(self._value))
1077 value = '"'.join(value.split('"'))
1078 return '<input name="%s" value="%s" size="%s">'%(self._formname, value, size)
1080 class LinkHTMLProperty(HTMLProperty):
1081 ''' Link HTMLProperty
1082 Include the above as well as being able to access the class
1083 information. Stringifying the object itself results in the value
1084 from the item being displayed. Accessing attributes of this object
1085 result in the appropriate entry from the class being queried for the
1086 property accessed (so item/assignedto/name would look up the user
1087 entry identified by the assignedto property on item, and then the
1088 name property of that user)
1089 '''
1090 def __init__(self, *args, **kw):
1091 HTMLProperty.__init__(self, *args, **kw)
1092 # if we're representing a form value, then the -1 from the form really
1093 # should be a None
1094 if str(self._value) == '-1':
1095 self._value = None
1097 def __getattr__(self, attr):
1098 ''' return a new HTMLItem '''
1099 #print 'Link.getattr', (self, attr, self._value)
1100 if not self._value:
1101 raise AttributeError, "Can't access missing value"
1102 if self._prop.classname == 'user':
1103 klass = HTMLUser
1104 else:
1105 klass = HTMLItem
1106 i = klass(self._client, self._prop.classname, self._value)
1107 return getattr(i, attr)
1109 def plain(self, escape=0):
1110 ''' Render a "plain" representation of the property
1111 '''
1112 if self._value is None:
1113 return ''
1114 linkcl = self._db.classes[self._prop.classname]
1115 k = linkcl.labelprop(1)
1116 value = str(linkcl.get(self._value, k))
1117 if escape:
1118 value = cgi.escape(value)
1119 return value
1121 def field(self, showid=0, size=None):
1122 ''' Render a form edit field for the property
1123 '''
1124 linkcl = self._db.getclass(self._prop.classname)
1125 if linkcl.getprops().has_key('order'):
1126 sort_on = 'order'
1127 else:
1128 sort_on = linkcl.labelprop()
1129 options = linkcl.filter(None, {}, ('+', sort_on), (None, None))
1130 # TODO: make this a field display, not a menu one!
1131 l = ['<select name="%s">'%self._formname]
1132 k = linkcl.labelprop(1)
1133 if self._value is None:
1134 s = 'selected '
1135 else:
1136 s = ''
1137 l.append(_('<option %svalue="-1">- no selection -</option>')%s)
1139 # make sure we list the current value if it's retired
1140 if self._value and self._value not in options:
1141 options.insert(0, self._value)
1143 for optionid in options:
1144 # get the option value, and if it's None use an empty string
1145 option = linkcl.get(optionid, k) or ''
1147 # figure if this option is selected
1148 s = ''
1149 if optionid == self._value:
1150 s = 'selected '
1152 # figure the label
1153 if showid:
1154 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1155 else:
1156 lab = option
1158 # truncate if it's too long
1159 if size is not None and len(lab) > size:
1160 lab = lab[:size-3] + '...'
1162 # and generate
1163 lab = cgi.escape(lab)
1164 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
1165 l.append('</select>')
1166 return '\n'.join(l)
1168 def menu(self, size=None, height=None, showid=0, additional=[],
1169 **conditions):
1170 ''' Render a form select list for this property
1171 '''
1172 value = self._value
1174 # sort function
1175 sortfunc = make_sort_function(self._db, self._prop.classname)
1177 linkcl = self._db.getclass(self._prop.classname)
1178 l = ['<select name="%s">'%self._formname]
1179 k = linkcl.labelprop(1)
1180 s = ''
1181 if value is None:
1182 s = 'selected '
1183 l.append(_('<option %svalue="-1">- no selection -</option>')%s)
1184 if linkcl.getprops().has_key('order'):
1185 sort_on = ('+', 'order')
1186 else:
1187 sort_on = ('+', linkcl.labelprop())
1188 options = linkcl.filter(None, conditions, sort_on, (None, None))
1190 # make sure we list the current value if it's retired
1191 if self._value and self._value not in options:
1192 options.insert(0, self._value)
1194 for optionid in options:
1195 # get the option value, and if it's None use an empty string
1196 option = linkcl.get(optionid, k) or ''
1198 # figure if this option is selected
1199 s = ''
1200 if value in [optionid, option]:
1201 s = 'selected '
1203 # figure the label
1204 if showid:
1205 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1206 else:
1207 lab = option
1209 # truncate if it's too long
1210 if size is not None and len(lab) > size:
1211 lab = lab[:size-3] + '...'
1212 if additional:
1213 m = []
1214 for propname in additional:
1215 m.append(linkcl.get(optionid, propname))
1216 lab = lab + ' (%s)'%', '.join(map(str, m))
1218 # and generate
1219 lab = cgi.escape(lab)
1220 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
1221 l.append('</select>')
1222 return '\n'.join(l)
1223 # def checklist(self, ...)
1225 class MultilinkHTMLProperty(HTMLProperty):
1226 ''' Multilink HTMLProperty
1228 Also be iterable, returning a wrapper object like the Link case for
1229 each entry in the multilink.
1230 '''
1231 def __len__(self):
1232 ''' length of the multilink '''
1233 return len(self._value)
1235 def __getattr__(self, attr):
1236 ''' no extended attribute accesses make sense here '''
1237 raise AttributeError, attr
1239 def __getitem__(self, num):
1240 ''' iterate and return a new HTMLItem
1241 '''
1242 #print 'Multi.getitem', (self, num)
1243 value = self._value[num]
1244 if self._prop.classname == 'user':
1245 klass = HTMLUser
1246 else:
1247 klass = HTMLItem
1248 return klass(self._client, self._prop.classname, value)
1250 def __contains__(self, value):
1251 ''' Support the "in" operator. We have to make sure the passed-in
1252 value is a string first, not a *HTMLProperty.
1253 '''
1254 return str(value) in self._value
1256 def reverse(self):
1257 ''' return the list in reverse order
1258 '''
1259 l = self._value[:]
1260 l.reverse()
1261 if self._prop.classname == 'user':
1262 klass = HTMLUser
1263 else:
1264 klass = HTMLItem
1265 return [klass(self._client, self._prop.classname, value) for value in l]
1267 def plain(self, escape=0):
1268 ''' Render a "plain" representation of the property
1269 '''
1270 linkcl = self._db.classes[self._prop.classname]
1271 k = linkcl.labelprop(1)
1272 labels = []
1273 for v in self._value:
1274 labels.append(linkcl.get(v, k))
1275 value = ', '.join(labels)
1276 if escape:
1277 value = cgi.escape(value)
1278 return value
1280 def field(self, size=30, showid=0):
1281 ''' Render a form edit field for the property
1282 '''
1283 sortfunc = make_sort_function(self._db, self._prop.classname)
1284 linkcl = self._db.getclass(self._prop.classname)
1285 value = self._value[:]
1286 if value:
1287 value.sort(sortfunc)
1288 # map the id to the label property
1289 if not linkcl.getkey():
1290 showid=1
1291 if not showid:
1292 k = linkcl.labelprop(1)
1293 value = [linkcl.get(v, k) for v in value]
1294 value = cgi.escape(','.join(value))
1295 return '<input name="%s" size="%s" value="%s">'%(self._formname, size, value)
1297 def menu(self, size=None, height=None, showid=0, additional=[],
1298 **conditions):
1299 ''' Render a form select list for this property
1300 '''
1301 value = self._value
1303 # sort function
1304 sortfunc = make_sort_function(self._db, self._prop.classname)
1306 linkcl = self._db.getclass(self._prop.classname)
1307 if linkcl.getprops().has_key('order'):
1308 sort_on = ('+', 'order')
1309 else:
1310 sort_on = ('+', linkcl.labelprop())
1311 options = linkcl.filter(None, conditions, sort_on, (None,None))
1312 height = height or min(len(options), 7)
1313 l = ['<select multiple name="%s" size="%s">'%(self._formname, height)]
1314 k = linkcl.labelprop(1)
1316 # make sure we list the current values if they're retired
1317 for val in value:
1318 if val not in options:
1319 options.insert(0, val)
1321 for optionid in options:
1322 # get the option value, and if it's None use an empty string
1323 option = linkcl.get(optionid, k) or ''
1325 # figure if this option is selected
1326 s = ''
1327 if optionid in value or option in value:
1328 s = 'selected '
1330 # figure the label
1331 if showid:
1332 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1333 else:
1334 lab = option
1335 # truncate if it's too long
1336 if size is not None and len(lab) > size:
1337 lab = lab[:size-3] + '...'
1338 if additional:
1339 m = []
1340 for propname in additional:
1341 m.append(linkcl.get(optionid, propname))
1342 lab = lab + ' (%s)'%', '.join(m)
1344 # and generate
1345 lab = cgi.escape(lab)
1346 l.append('<option %svalue="%s">%s</option>'%(s, optionid,
1347 lab))
1348 l.append('</select>')
1349 return '\n'.join(l)
1351 # set the propclasses for HTMLItem
1352 propclasses = (
1353 (hyperdb.String, StringHTMLProperty),
1354 (hyperdb.Number, NumberHTMLProperty),
1355 (hyperdb.Boolean, BooleanHTMLProperty),
1356 (hyperdb.Date, DateHTMLProperty),
1357 (hyperdb.Interval, IntervalHTMLProperty),
1358 (hyperdb.Password, PasswordHTMLProperty),
1359 (hyperdb.Link, LinkHTMLProperty),
1360 (hyperdb.Multilink, MultilinkHTMLProperty),
1361 )
1363 def make_sort_function(db, classname):
1364 '''Make a sort function for a given class
1365 '''
1366 linkcl = db.getclass(classname)
1367 if linkcl.getprops().has_key('order'):
1368 sort_on = 'order'
1369 else:
1370 sort_on = linkcl.labelprop()
1371 def sortfunc(a, b, linkcl=linkcl, sort_on=sort_on):
1372 return cmp(linkcl.get(a, sort_on), linkcl.get(b, sort_on))
1373 return sortfunc
1375 def handleListCGIValue(value):
1376 ''' Value is either a single item or a list of items. Each item has a
1377 .value that we're actually interested in.
1378 '''
1379 if isinstance(value, type([])):
1380 return [value.value for value in value]
1381 else:
1382 value = value.value.strip()
1383 if not value:
1384 return []
1385 return value.split(',')
1387 class ShowDict:
1388 ''' A convenience access to the :columns index parameters
1389 '''
1390 def __init__(self, columns):
1391 self.columns = {}
1392 for col in columns:
1393 self.columns[col] = 1
1394 def __getitem__(self, name):
1395 return self.columns.has_key(name)
1397 class HTMLRequest:
1398 ''' The *request*, holding the CGI form and environment.
1400 "form" the CGI form as a cgi.FieldStorage
1401 "env" the CGI environment variables
1402 "base" the base URL for this instance
1403 "user" a HTMLUser instance for this user
1404 "classname" the current classname (possibly None)
1405 "template" the current template (suffix, also possibly None)
1407 Index args:
1408 "columns" dictionary of the columns to display in an index page
1409 "show" a convenience access to columns - request/show/colname will
1410 be true if the columns should be displayed, false otherwise
1411 "sort" index sort column (direction, column name)
1412 "group" index grouping property (direction, column name)
1413 "filter" properties to filter the index on
1414 "filterspec" values to filter the index on
1415 "search_text" text to perform a full-text search on for an index
1417 '''
1418 def __init__(self, client):
1419 self.client = client
1421 # easier access vars
1422 self.form = client.form
1423 self.env = client.env
1424 self.base = client.base
1425 self.user = HTMLUser(client, 'user', client.userid)
1427 # store the current class name and action
1428 self.classname = client.classname
1429 self.template = client.template
1431 # the special char to use for special vars
1432 self.special_char = '@'
1434 self._post_init()
1436 def _post_init(self):
1437 ''' Set attributes based on self.form
1438 '''
1439 # extract the index display information from the form
1440 self.columns = []
1441 for name in ':columns @columns'.split():
1442 if self.form.has_key(name):
1443 self.special_char = name[0]
1444 self.columns = handleListCGIValue(self.form[name])
1445 break
1446 self.show = ShowDict(self.columns)
1448 # sorting
1449 self.sort = (None, None)
1450 for name in ':sort @sort'.split():
1451 if self.form.has_key(name):
1452 self.special_char = name[0]
1453 sort = self.form[name].value
1454 if sort.startswith('-'):
1455 self.sort = ('-', sort[1:])
1456 else:
1457 self.sort = ('+', sort)
1458 if self.form.has_key(self.special_char+'sortdir'):
1459 self.sort = ('-', self.sort[1])
1461 # grouping
1462 self.group = (None, None)
1463 for name in ':group @group'.split():
1464 if self.form.has_key(name):
1465 self.special_char = name[0]
1466 group = self.form[name].value
1467 if group.startswith('-'):
1468 self.group = ('-', group[1:])
1469 else:
1470 self.group = ('+', group)
1471 if self.form.has_key(self.special_char+'groupdir'):
1472 self.group = ('-', self.group[1])
1474 # filtering
1475 self.filter = []
1476 for name in ':filter @filter'.split():
1477 if self.form.has_key(name):
1478 self.special_char = name[0]
1479 self.filter = handleListCGIValue(self.form[name])
1481 self.filterspec = {}
1482 db = self.client.db
1483 if self.classname is not None:
1484 props = db.getclass(self.classname).getprops()
1485 for name in self.filter:
1486 if not self.form.has_key(name):
1487 continue
1488 prop = props[name]
1489 fv = self.form[name]
1490 if (isinstance(prop, hyperdb.Link) or
1491 isinstance(prop, hyperdb.Multilink)):
1492 self.filterspec[name] = lookupIds(db, prop,
1493 handleListCGIValue(fv))
1494 else:
1495 if isinstance(fv, type([])):
1496 self.filterspec[name] = [v.value for v in fv]
1497 else:
1498 self.filterspec[name] = fv.value
1500 # full-text search argument
1501 self.search_text = None
1502 for name in ':search_text @search_text'.split():
1503 if self.form.has_key(name):
1504 self.special_char = name[0]
1505 self.search_text = self.form[name].value
1507 # pagination - size and start index
1508 # figure batch args
1509 self.pagesize = 50
1510 for name in ':pagesize @pagesize'.split():
1511 if self.form.has_key(name):
1512 self.special_char = name[0]
1513 self.pagesize = int(self.form[name].value)
1515 self.startwith = 0
1516 for name in ':startwith @startwith'.split():
1517 if self.form.has_key(name):
1518 self.special_char = name[0]
1519 self.startwith = int(self.form[name].value)
1521 def updateFromURL(self, url):
1522 ''' Parse the URL for query args, and update my attributes using the
1523 values.
1524 '''
1525 env = {'QUERY_STRING': url}
1526 self.form = cgi.FieldStorage(environ=env)
1528 self._post_init()
1530 def update(self, kwargs):
1531 ''' Update my attributes using the keyword args
1532 '''
1533 self.__dict__.update(kwargs)
1534 if kwargs.has_key('columns'):
1535 self.show = ShowDict(self.columns)
1537 def description(self):
1538 ''' Return a description of the request - handle for the page title.
1539 '''
1540 s = [self.client.db.config.TRACKER_NAME]
1541 if self.classname:
1542 if self.client.nodeid:
1543 s.append('- %s%s'%(self.classname, self.client.nodeid))
1544 else:
1545 if self.template == 'item':
1546 s.append('- new %s'%self.classname)
1547 elif self.template == 'index':
1548 s.append('- %s index'%self.classname)
1549 else:
1550 s.append('- %s %s'%(self.classname, self.template))
1551 else:
1552 s.append('- home')
1553 return ' '.join(s)
1555 def __str__(self):
1556 d = {}
1557 d.update(self.__dict__)
1558 f = ''
1559 for k in self.form.keys():
1560 f += '\n %r=%r'%(k,handleListCGIValue(self.form[k]))
1561 d['form'] = f
1562 e = ''
1563 for k,v in self.env.items():
1564 e += '\n %r=%r'%(k, v)
1565 d['env'] = e
1566 return '''
1567 form: %(form)s
1568 base: %(base)r
1569 classname: %(classname)r
1570 template: %(template)r
1571 columns: %(columns)r
1572 sort: %(sort)r
1573 group: %(group)r
1574 filter: %(filter)r
1575 search_text: %(search_text)r
1576 pagesize: %(pagesize)r
1577 startwith: %(startwith)r
1578 env: %(env)s
1579 '''%d
1581 def indexargs_form(self, columns=1, sort=1, group=1, filter=1,
1582 filterspec=1):
1583 ''' return the current index args as form elements '''
1584 l = []
1585 sc = self.special_char
1586 s = '<input type="hidden" name="%s" value="%s">'
1587 if columns and self.columns:
1588 l.append(s%(sc+'columns', ','.join(self.columns)))
1589 if sort and self.sort[1] is not None:
1590 if self.sort[0] == '-':
1591 val = '-'+self.sort[1]
1592 else:
1593 val = self.sort[1]
1594 l.append(s%(sc+'sort', val))
1595 if group and self.group[1] is not None:
1596 if self.group[0] == '-':
1597 val = '-'+self.group[1]
1598 else:
1599 val = self.group[1]
1600 l.append(s%(sc+'group', val))
1601 if filter and self.filter:
1602 l.append(s%(sc+'filter', ','.join(self.filter)))
1603 if filterspec:
1604 for k,v in self.filterspec.items():
1605 if type(v) == type([]):
1606 l.append(s%(k, ','.join(v)))
1607 else:
1608 l.append(s%(k, v))
1609 if self.search_text:
1610 l.append(s%(sc+'search_text', self.search_text))
1611 l.append(s%(sc+'pagesize', self.pagesize))
1612 l.append(s%(sc+'startwith', self.startwith))
1613 return '\n'.join(l)
1615 def indexargs_url(self, url, args):
1616 ''' Embed the current index args in a URL
1617 '''
1618 sc = self.special_char
1619 l = ['%s=%s'%(k,v) for k,v in args.items()]
1621 # pull out the special values (prefixed by @ or :)
1622 specials = {}
1623 for key in args.keys():
1624 if key[0] in '@:':
1625 specials[key[1:]] = args[key]
1627 # ok, now handle the specials we received in the request
1628 if self.columns and not specials.has_key('columns'):
1629 l.append(sc+'columns=%s'%(','.join(self.columns)))
1630 if self.sort[1] is not None and not specials.has_key('sort'):
1631 if self.sort[0] == '-':
1632 val = '-'+self.sort[1]
1633 else:
1634 val = self.sort[1]
1635 l.append(sc+'sort=%s'%val)
1636 if self.group[1] is not None and not specials.has_key('group'):
1637 if self.group[0] == '-':
1638 val = '-'+self.group[1]
1639 else:
1640 val = self.group[1]
1641 l.append(sc+'group=%s'%val)
1642 if self.filter and not specials.has_key('filter'):
1643 l.append(sc+'filter=%s'%(','.join(self.filter)))
1644 if self.search_text and not specials.has_key('search_text'):
1645 l.append(sc+'search_text=%s'%self.search_text)
1646 if not specials.has_key('pagesize'):
1647 l.append(sc+'pagesize=%s'%self.pagesize)
1648 if not specials.has_key('startwith'):
1649 l.append(sc+'startwith=%s'%self.startwith)
1651 # finally, the remainder of the filter args in the request
1652 for k,v in self.filterspec.items():
1653 if not args.has_key(k):
1654 if type(v) == type([]):
1655 l.append('%s=%s'%(k, ','.join(v)))
1656 else:
1657 l.append('%s=%s'%(k, v))
1658 return '%s?%s'%(url, '&'.join(l))
1659 indexargs_href = indexargs_url
1661 def base_javascript(self):
1662 return '''
1663 <script language="javascript">
1664 submitted = false;
1665 function submit_once() {
1666 if (submitted) {
1667 alert("Your request is being processed.\\nPlease be patient.");
1668 return 0;
1669 }
1670 submitted = true;
1671 return 1;
1672 }
1674 function help_window(helpurl, width, height) {
1675 HelpWin = window.open('%s' + helpurl, 'RoundupHelpWindow', 'scrollbars=yes,resizable=yes,toolbar=no,height='+height+',width='+width);
1676 }
1677 </script>
1678 '''%self.base
1680 def batch(self):
1681 ''' Return a batch object for results from the "current search"
1682 '''
1683 filterspec = self.filterspec
1684 sort = self.sort
1685 group = self.group
1687 # get the list of ids we're batching over
1688 klass = self.client.db.getclass(self.classname)
1689 if self.search_text:
1690 matches = self.client.db.indexer.search(
1691 re.findall(r'\b\w{2,25}\b', self.search_text), klass)
1692 else:
1693 matches = None
1694 l = klass.filter(matches, filterspec, sort, group)
1696 # return the batch object, using IDs only
1697 return Batch(self.client, l, self.pagesize, self.startwith,
1698 classname=self.classname)
1700 # extend the standard ZTUtils Batch object to remove dependency on
1701 # Acquisition and add a couple of useful methods
1702 class Batch(ZTUtils.Batch):
1703 ''' Use me to turn a list of items, or item ids of a given class, into a
1704 series of batches.
1706 ========= ========================================================
1707 Parameter Usage
1708 ========= ========================================================
1709 sequence a list of HTMLItems or item ids
1710 classname if sequence is a list of ids, this is the class of item
1711 size how big to make the sequence.
1712 start where to start (0-indexed) in the sequence.
1713 end where to end (0-indexed) in the sequence.
1714 orphan if the next batch would contain less items than this
1715 value, then it is combined with this batch
1716 overlap the number of items shared between adjacent batches
1717 ========= ========================================================
1719 Attributes: Note that the "start" attribute, unlike the
1720 argument, is a 1-based index (I know, lame). "first" is the
1721 0-based index. "length" is the actual number of elements in
1722 the batch.
1724 "sequence_length" is the length of the original, unbatched, sequence.
1725 '''
1726 def __init__(self, client, sequence, size, start, end=0, orphan=0,
1727 overlap=0, classname=None):
1728 self.client = client
1729 self.last_index = self.last_item = None
1730 self.current_item = None
1731 self.classname = classname
1732 self.sequence_length = len(sequence)
1733 ZTUtils.Batch.__init__(self, sequence, size, start, end, orphan,
1734 overlap)
1736 # overwrite so we can late-instantiate the HTMLItem instance
1737 def __getitem__(self, index):
1738 if index < 0:
1739 if index + self.end < self.first: raise IndexError, index
1740 return self._sequence[index + self.end]
1742 if index >= self.length:
1743 raise IndexError, index
1745 # move the last_item along - but only if the fetched index changes
1746 # (for some reason, index 0 is fetched twice)
1747 if index != self.last_index:
1748 self.last_item = self.current_item
1749 self.last_index = index
1751 item = self._sequence[index + self.first]
1752 if self.classname:
1753 # map the item ids to instances
1754 if self.classname == 'user':
1755 item = HTMLUser(self.client, self.classname, item)
1756 else:
1757 item = HTMLItem(self.client, self.classname, item)
1758 self.current_item = item
1759 return item
1761 def propchanged(self, property):
1762 ''' Detect if the property marked as being the group property
1763 changed in the last iteration fetch
1764 '''
1765 if (self.last_item is None or
1766 self.last_item[property] != self.current_item[property]):
1767 return 1
1768 return 0
1770 # override these 'cos we don't have access to acquisition
1771 def previous(self):
1772 if self.start == 1:
1773 return None
1774 return Batch(self.client, self._sequence, self._size,
1775 self.first - self._size + self.overlap, 0, self.orphan,
1776 self.overlap)
1778 def next(self):
1779 try:
1780 self._sequence[self.end]
1781 except IndexError:
1782 return None
1783 return Batch(self.client, self._sequence, self._size,
1784 self.end - self.overlap, 0, self.orphan, self.overlap)
1786 class TemplatingUtils:
1787 ''' Utilities for templating
1788 '''
1789 def __init__(self, client):
1790 self.client = client
1791 def Batch(self, sequence, size, start, end=0, orphan=0, overlap=0):
1792 return Batch(self.client, sequence, size, start, end, orphan,
1793 overlap)