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 class Templates:
29 templates = {}
31 def __init__(self, dir):
32 self.dir = dir
34 def precompileTemplates(self):
35 ''' Go through a directory and precompile all the templates therein
36 '''
37 for filename in os.listdir(self.dir):
38 if os.path.isdir(filename): continue
39 if '.' in filename:
40 name, extension = filename.split('.')
41 self.getTemplate(name, extension)
42 else:
43 self.getTemplate(filename, None)
45 def get(self, name, extension):
46 ''' Interface to get a template, possibly loading a compiled template.
48 "name" and "extension" indicate the template we're after, which in
49 most cases will be "name.extension". If "extension" is None, then
50 we look for a template just called "name" with no extension.
52 If the file "name.extension" doesn't exist, we look for
53 "_generic.extension" as a fallback.
54 '''
55 # default the name to "home"
56 if name is None:
57 name = 'home'
59 # find the source, figure the time it was last modified
60 if extension:
61 filename = '%s.%s'%(name, extension)
62 else:
63 filename = name
64 src = os.path.join(self.dir, filename)
65 try:
66 stime = os.stat(src)[os.path.stat.ST_MTIME]
67 except os.error, error:
68 if error.errno != errno.ENOENT:
69 raise
70 if not extension:
71 raise NoTemplate, 'Template file "%s" doesn\'t exist'%name
73 # try for a generic template
74 generic = '_generic.%s'%extension
75 src = os.path.join(self.dir, generic)
76 try:
77 stime = os.stat(src)[os.path.stat.ST_MTIME]
78 except os.error, error:
79 if error.errno != errno.ENOENT:
80 raise
81 # nicer error
82 raise NoTemplate, 'No template file exists for templating '\
83 '"%s" with template "%s" (neither "%s" nor "%s")'%(name,
84 extension, filename, generic)
85 filename = generic
87 if self.templates.has_key(src) and \
88 stime < self.templates[src].mtime:
89 # compiled template is up to date
90 return self.templates[src]
92 # compile the template
93 self.templates[src] = pt = RoundupPageTemplate()
94 pt.write(open(src).read())
95 pt.id = filename
96 pt.mtime = time.time()
97 return pt
99 def __getitem__(self, name):
100 name, extension = os.path.splitext(name)
101 if extension:
102 extension = extension[1:]
103 try:
104 return self.get(name, extension)
105 except NoTemplate, message:
106 raise KeyError, message
108 class RoundupPageTemplate(PageTemplate.PageTemplate):
109 ''' A Roundup-specific PageTemplate.
111 Interrogate the client to set up the various template variables to
112 be available:
114 *context*
115 this is one of three things:
116 1. None - we're viewing a "home" page
117 2. The current class of item being displayed. This is an HTMLClass
118 instance.
119 3. The current item from the database, if we're viewing a specific
120 item, as an HTMLItem instance.
121 *request*
122 Includes information about the current request, including:
123 - the url
124 - the current index information (``filterspec``, ``filter`` args,
125 ``properties``, etc) parsed out of the form.
126 - methods for easy filterspec link generation
127 - *user*, the current user node as an HTMLItem instance
128 - *form*, the current CGI form information as a FieldStorage
129 *config*
130 The current tracker config.
131 *db*
132 The current database, used to access arbitrary database items.
133 *utils*
134 This is a special class that has its base in the TemplatingUtils
135 class in this file. If the tracker interfaces module defines a
136 TemplatingUtils class then it is mixed in, overriding the methods
137 in the base class.
138 '''
139 def getContext(self, client, classname, request):
140 # construct the TemplatingUtils class
141 utils = TemplatingUtils
142 if hasattr(client.instance.interfaces, 'TemplatingUtils'):
143 class utils(client.instance.interfaces.TemplatingUtils, utils):
144 pass
146 c = {
147 'options': {},
148 'nothing': None,
149 'request': request,
150 'db': HTMLDatabase(client),
151 'config': client.instance.config,
152 'tracker': client.instance,
153 'utils': utils(client),
154 'templates': Templates(client.instance.config.TEMPLATES),
155 }
156 # add in the item if there is one
157 if client.nodeid:
158 if classname == 'user':
159 c['context'] = HTMLUser(client, classname, client.nodeid)
160 else:
161 c['context'] = HTMLItem(client, classname, client.nodeid)
162 elif client.db.classes.has_key(classname):
163 c['context'] = HTMLClass(client, classname)
164 return c
166 def render(self, client, classname, request, **options):
167 """Render this Page Template"""
169 if not self._v_cooked:
170 self._cook()
172 __traceback_supplement__ = (PageTemplate.PageTemplateTracebackSupplement, self)
174 if self._v_errors:
175 raise PageTemplate.PTRuntimeError, \
176 'Page Template %s has errors.'%self.id
178 # figure the context
179 classname = classname or client.classname
180 request = request or HTMLRequest(client)
181 c = self.getContext(client, classname, request)
182 c.update({'options': options})
184 # and go
185 output = StringIO.StringIO()
186 TALInterpreter(self._v_program, self.macros,
187 getEngine().getContext(c), output, tal=1, strictinsert=0)()
188 return output.getvalue()
190 class HTMLDatabase:
191 ''' Return HTMLClasses for valid class fetches
192 '''
193 def __init__(self, client):
194 self._client = client
196 # we want config to be exposed
197 self.config = client.db.config
199 def __getitem__(self, item):
200 self._client.db.getclass(item)
201 return HTMLClass(self._client, item)
203 def __getattr__(self, attr):
204 try:
205 return self[attr]
206 except KeyError:
207 raise AttributeError, attr
209 def classes(self):
210 l = self._client.db.classes.keys()
211 l.sort()
212 return [HTMLClass(self._client, cn) for cn in l]
214 def lookupIds(db, prop, ids, num_re=re.compile('-?\d+')):
215 cl = db.getclass(prop.classname)
216 l = []
217 for entry in ids:
218 if num_re.match(entry):
219 l.append(entry)
220 else:
221 try:
222 l.append(cl.lookup(entry))
223 except KeyError:
224 # ignore invalid keys
225 pass
226 return l
228 class HTMLPermissions:
229 ''' Helpers that provide answers to commonly asked Permission questions.
230 '''
231 def is_edit_ok(self):
232 ''' Is the user allowed to Edit the current class?
233 '''
234 return self._db.security.hasPermission('Edit', self._client.userid,
235 self._classname)
236 def is_view_ok(self):
237 ''' Is the user allowed to View the current class?
238 '''
239 return self._db.security.hasPermission('View', self._client.userid,
240 self._classname)
241 def is_only_view_ok(self):
242 ''' Is the user only allowed to View (ie. not Edit) the current class?
243 '''
244 return self.is_view_ok() and not self.is_edit_ok()
246 class HTMLClass(HTMLPermissions):
247 ''' Accesses through a class (either through *class* or *db.<classname>*)
248 '''
249 def __init__(self, client, classname):
250 self._client = client
251 self._db = client.db
253 # we want classname to be exposed, but _classname gives a
254 # consistent API for extending Class/Item
255 self._classname = self.classname = classname
256 self._klass = self._db.getclass(self.classname)
257 self._props = self._klass.getprops()
259 def __repr__(self):
260 return '<HTMLClass(0x%x) %s>'%(id(self), self.classname)
262 def __getitem__(self, item):
263 ''' return an HTMLProperty instance
264 '''
265 #print 'HTMLClass.getitem', (self, item)
267 # we don't exist
268 if item == 'id':
269 return None
271 # get the property
272 prop = self._props[item]
274 # look up the correct HTMLProperty class
275 form = self._client.form
276 for klass, htmlklass in propclasses:
277 if not isinstance(prop, klass):
278 continue
279 if form.has_key(item):
280 if isinstance(prop, hyperdb.Multilink):
281 value = lookupIds(self._db, prop,
282 handleListCGIValue(form[item]))
283 elif isinstance(prop, hyperdb.Link):
284 value = form[item].value.strip()
285 if value:
286 value = lookupIds(self._db, prop, [value])[0]
287 else:
288 value = None
289 else:
290 value = form[item].value.strip() or None
291 else:
292 if isinstance(prop, hyperdb.Multilink):
293 value = []
294 else:
295 value = None
296 return htmlklass(self._client, '', prop, item, value)
298 # no good
299 raise KeyError, item
301 def __getattr__(self, attr):
302 ''' convenience access '''
303 try:
304 return self[attr]
305 except KeyError:
306 raise AttributeError, attr
308 def getItem(self, itemid, num_re=re.compile('\d+')):
309 ''' Get an item of this class by its item id.
310 '''
311 # make sure we're looking at an itemid
312 if not num_re.match(itemid):
313 itemid = self._klass.lookup(itemid)
315 if self.classname == 'user':
316 klass = HTMLUser
317 else:
318 klass = HTMLItem
320 return klass(self._client, self.classname, itemid)
322 def properties(self):
323 ''' Return HTMLProperty for all of this class' properties.
324 '''
325 l = []
326 for name, prop in self._props.items():
327 for klass, htmlklass in propclasses:
328 if isinstance(prop, hyperdb.Multilink):
329 value = []
330 else:
331 value = None
332 if isinstance(prop, klass):
333 l.append(htmlklass(self._client, '', prop, name, value))
334 return l
336 def list(self):
337 ''' List all items in this class.
338 '''
339 if self.classname == 'user':
340 klass = HTMLUser
341 else:
342 klass = HTMLItem
344 # get the list and sort it nicely
345 l = self._klass.list()
346 sortfunc = make_sort_function(self._db, self.classname)
347 l.sort(sortfunc)
349 l = [klass(self._client, self.classname, x) for x in l]
350 return l
352 def csv(self):
353 ''' Return the items of this class as a chunk of CSV text.
354 '''
355 # get the CSV module
356 try:
357 import csv
358 except ImportError:
359 return 'Sorry, you need the csv module to use this function.\n'\
360 'Get it from: http://www.object-craft.com.au/projects/csv/'
362 props = self.propnames()
363 p = csv.parser()
364 s = StringIO.StringIO()
365 s.write(p.join(props) + '\n')
366 for nodeid in self._klass.list():
367 l = []
368 for name in props:
369 value = self._klass.get(nodeid, name)
370 if value is None:
371 l.append('')
372 elif isinstance(value, type([])):
373 l.append(':'.join(map(str, value)))
374 else:
375 l.append(str(self._klass.get(nodeid, name)))
376 s.write(p.join(l) + '\n')
377 return s.getvalue()
379 def propnames(self):
380 ''' Return the list of the names of the properties of this class.
381 '''
382 idlessprops = self._klass.getprops(protected=0).keys()
383 idlessprops.sort()
384 return ['id'] + idlessprops
386 def filter(self, request=None):
387 ''' Return a list of items from this class, filtered and sorted
388 by the current requested filterspec/filter/sort/group args
389 '''
390 if request is not None:
391 filterspec = request.filterspec
392 sort = request.sort
393 group = request.group
394 else:
395 filterspec = {}
396 sort = (None,None)
397 group = (None,None)
398 if self.classname == 'user':
399 klass = HTMLUser
400 else:
401 klass = HTMLItem
402 l = [klass(self._client, self.classname, x)
403 for x in self._klass.filter(None, filterspec, sort, group)]
404 return l
406 def classhelp(self, properties=None, label='list', width='500',
407 height='400'):
408 ''' Pop up a javascript window with class help
410 This generates a link to a popup window which displays the
411 properties indicated by "properties" of the class named by
412 "classname". The "properties" should be a comma-separated list
413 (eg. 'id,name,description'). Properties defaults to all the
414 properties of a class (excluding id, creator, created and
415 activity).
417 You may optionally override the label displayed, the width and
418 height. The popup window will be resizable and scrollable.
419 '''
420 if properties is None:
421 properties = self._klass.getprops(protected=0).keys()
422 properties.sort()
423 properties = ','.join(properties)
424 return '<a href="javascript:help_window(\'%s?:template=help&' \
425 'properties=%s\', \'%s\', \'%s\')"><b>(%s)</b></a>'%(
426 self.classname, properties, width, height, label)
428 def submit(self, label="Submit New Entry"):
429 ''' Generate a submit button (and action hidden element)
430 '''
431 return ' <input type="hidden" name=":action" value="new">\n'\
432 ' <input type="submit" name="submit" value="%s">'%label
434 def history(self):
435 return 'New node - no history'
437 def renderWith(self, name, **kwargs):
438 ''' Render this class with the given template.
439 '''
440 # create a new request and override the specified args
441 req = HTMLRequest(self._client)
442 req.classname = self.classname
443 req.update(kwargs)
445 # new template, using the specified classname and request
446 pt = Templates(self._db.config.TEMPLATES).get(self.classname, name)
448 # use our fabricated request
449 return pt.render(self._client, self.classname, req)
451 class HTMLItem(HTMLPermissions):
452 ''' Accesses through an *item*
453 '''
454 def __init__(self, client, classname, nodeid):
455 self._client = client
456 self._db = client.db
457 self._classname = classname
458 self._nodeid = nodeid
459 self._klass = self._db.getclass(classname)
460 self._props = self._klass.getprops()
462 def __repr__(self):
463 return '<HTMLItem(0x%x) %s %s>'%(id(self), self._classname,
464 self._nodeid)
466 def __getitem__(self, item):
467 ''' return an HTMLProperty instance
468 '''
469 #print 'HTMLItem.getitem', (self, item)
470 if item == 'id':
471 return self._nodeid
473 # get the property
474 prop = self._props[item]
476 # get the value, handling missing values
477 value = self._klass.get(self._nodeid, item, None)
478 if value is None:
479 if isinstance(self._props[item], hyperdb.Multilink):
480 value = []
482 # look up the correct HTMLProperty class
483 for klass, htmlklass in propclasses:
484 if isinstance(prop, klass):
485 return htmlklass(self._client, self._nodeid, prop, item, value)
487 raise KeyError, item
489 def __getattr__(self, attr):
490 ''' convenience access to properties '''
491 try:
492 return self[attr]
493 except KeyError:
494 raise AttributeError, attr
496 def submit(self, label="Submit Changes"):
497 ''' Generate a submit button (and action hidden element)
498 '''
499 return ' <input type="hidden" name=":action" value="edit">\n'\
500 ' <input type="submit" name="submit" value="%s">'%label
502 def journal(self, direction='descending'):
503 ''' Return a list of HTMLJournalEntry instances.
504 '''
505 # XXX do this
506 return []
508 def history(self, direction='descending', dre=re.compile('\d+')):
509 l = ['<table class="history">'
510 '<tr><th colspan="4" class="header">',
511 _('History'),
512 '</th></tr><tr>',
513 _('<th>Date</th>'),
514 _('<th>User</th>'),
515 _('<th>Action</th>'),
516 _('<th>Args</th>'),
517 '</tr>']
518 current = {}
519 comments = {}
520 history = self._klass.history(self._nodeid)
521 history.sort()
522 timezone = self._db.getUserTimezone()
523 if direction == 'descending':
524 history.reverse()
525 for prop_n in self._props.keys():
526 prop = self[prop_n]
527 if isinstance(prop, HTMLProperty):
528 current[prop_n] = prop.plain()
529 # make link if hrefable
530 if (self._props.has_key(prop_n) and
531 isinstance(self._props[prop_n], hyperdb.Link)):
532 classname = self._props[prop_n].classname
533 if os.path.exists(os.path.join(self._db.config.TEMPLATES, classname + '.item')):
534 current[prop_n] = '<a href="%s%s">%s</a>'%(classname,
535 self._klass.get(self._nodeid, prop_n, None), current[prop_n])
537 for id, evt_date, user, action, args in history:
538 date_s = str(evt_date.local(timezone)).replace("."," ")
539 arg_s = ''
540 if action == 'link' and type(args) == type(()):
541 if len(args) == 3:
542 linkcl, linkid, key = args
543 arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
544 linkcl, linkid, key)
545 else:
546 arg_s = str(args)
548 elif action == 'unlink' and type(args) == type(()):
549 if len(args) == 3:
550 linkcl, linkid, key = args
551 arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
552 linkcl, linkid, key)
553 else:
554 arg_s = str(args)
556 elif type(args) == type({}):
557 cell = []
558 for k in args.keys():
559 # try to get the relevant property and treat it
560 # specially
561 try:
562 prop = self._props[k]
563 except KeyError:
564 prop = None
565 if prop is not None:
566 if args[k] and (isinstance(prop, hyperdb.Multilink) or
567 isinstance(prop, hyperdb.Link)):
568 # figure what the link class is
569 classname = prop.classname
570 try:
571 linkcl = self._db.getclass(classname)
572 except KeyError:
573 labelprop = None
574 comments[classname] = _('''The linked class
575 %(classname)s no longer exists''')%locals()
576 labelprop = linkcl.labelprop(1)
577 hrefable = os.path.exists(
578 os.path.join(self._db.config.TEMPLATES,
579 classname+'.item'))
581 if isinstance(prop, hyperdb.Multilink) and \
582 len(args[k]) > 0:
583 ml = []
584 for linkid in args[k]:
585 if isinstance(linkid, type(())):
586 sublabel = linkid[0] + ' '
587 linkids = linkid[1]
588 else:
589 sublabel = ''
590 linkids = [linkid]
591 subml = []
592 for linkid in linkids:
593 label = classname + linkid
594 # if we have a label property, try to use it
595 # TODO: test for node existence even when
596 # there's no labelprop!
597 try:
598 if labelprop is not None and \
599 labelprop != 'id':
600 label = linkcl.get(linkid, labelprop)
601 except IndexError:
602 comments['no_link'] = _('''<strike>The
603 linked node no longer
604 exists</strike>''')
605 subml.append('<strike>%s</strike>'%label)
606 else:
607 if hrefable:
608 subml.append('<a href="%s%s">%s</a>'%(
609 classname, linkid, label))
610 else:
611 subml.append(label)
612 ml.append(sublabel + ', '.join(subml))
613 cell.append('%s:\n %s'%(k, ', '.join(ml)))
614 elif isinstance(prop, hyperdb.Link) and args[k]:
615 label = classname + args[k]
616 # if we have a label property, try to use it
617 # TODO: test for node existence even when
618 # there's no labelprop!
619 if labelprop is not None and labelprop != 'id':
620 try:
621 label = linkcl.get(args[k], labelprop)
622 except IndexError:
623 comments['no_link'] = _('''<strike>The
624 linked node no longer
625 exists</strike>''')
626 cell.append(' <strike>%s</strike>,\n'%label)
627 # "flag" this is done .... euwww
628 label = None
629 if label is not None:
630 if hrefable:
631 old = '<a href="%s%s">%s</a>'%(classname, args[k], label)
632 else:
633 old = label;
634 cell.append('%s: %s' % (k,old))
635 if current.has_key(k):
636 cell[-1] += ' -> %s'%current[k]
637 current[k] = old
639 elif isinstance(prop, hyperdb.Date) and args[k]:
640 d = date.Date(args[k]).local(timezone)
641 cell.append('%s: %s'%(k, str(d)))
642 if current.has_key(k):
643 if not current[k] == '(no value)' and current[k]:
644 current[k] = date.Date(current[k]).local(timezone)
645 cell[-1] += ' -> %s' % current[k]
646 current[k] = str(d)
648 elif isinstance(prop, hyperdb.Interval) and args[k]:
649 d = date.Interval(args[k])
650 cell.append('%s: %s'%(k, str(d)))
651 if current.has_key(k):
652 cell[-1] += ' -> %s'%current[k]
653 current[k] = str(d)
655 elif isinstance(prop, hyperdb.String) and args[k]:
656 cell.append('%s: %s'%(k, cgi.escape(args[k])))
657 if current.has_key(k):
658 cell[-1] += ' -> %s'%current[k]
659 current[k] = cgi.escape(args[k])
661 elif not args[k]:
662 if current.has_key(k):
663 cell.append('%s: %s'%(k, current[k]))
664 current[k] = '(no value)'
665 else:
666 cell.append('%s: (no value)'%k)
668 else:
669 cell.append('%s: %s'%(k, str(args[k])))
670 if current.has_key(k):
671 cell[-1] += ' -> %s'%current[k]
672 current[k] = str(args[k])
673 else:
674 # property no longer exists
675 comments['no_exist'] = _('''<em>The indicated property
676 no longer exists</em>''')
677 cell.append('<em>%s: %s</em>\n'%(k, str(args[k])))
678 arg_s = '<br />'.join(cell)
679 else:
680 # unkown event!!
681 comments['unknown'] = _('''<strong><em>This event is not
682 handled by the history display!</em></strong>''')
683 arg_s = '<strong><em>' + str(args) + '</em></strong>'
684 date_s = date_s.replace(' ', ' ')
685 # if the user's an itemid, figure the username (older journals
686 # have the username)
687 if dre.match(user):
688 user = self._db.user.get(user, 'username')
689 l.append('<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>'%(
690 date_s, user, action, arg_s))
691 if comments:
692 l.append(_('<tr><td colspan=4><strong>Note:</strong></td></tr>'))
693 for entry in comments.values():
694 l.append('<tr><td colspan=4>%s</td></tr>'%entry)
695 l.append('</table>')
696 return '\n'.join(l)
698 def renderQueryForm(self):
699 ''' Render this item, which is a query, as a search form.
700 '''
701 # create a new request and override the specified args
702 req = HTMLRequest(self._client)
703 req.classname = self._klass.get(self._nodeid, 'klass')
704 name = self._klass.get(self._nodeid, 'name')
705 req.updateFromURL(self._klass.get(self._nodeid, 'url') +
706 '&:queryname=%s'%urllib.quote(name))
708 # new template, using the specified classname and request
709 pt = Templates(self._db.config.TEMPLATES).get(req.classname, 'search')
711 # use our fabricated request
712 return pt.render(self._client, req.classname, req)
714 class HTMLUser(HTMLItem):
715 ''' Accesses through the *user* (a special case of item)
716 '''
717 def __init__(self, client, classname, nodeid):
718 HTMLItem.__init__(self, client, 'user', nodeid)
719 self._default_classname = client.classname
721 # used for security checks
722 self._security = client.db.security
724 _marker = []
725 def hasPermission(self, role, classname=_marker):
726 ''' Determine if the user has the Role.
728 The class being tested defaults to the template's class, but may
729 be overidden for this test by suppling an alternate classname.
730 '''
731 if classname is self._marker:
732 classname = self._default_classname
733 return self._security.hasPermission(role, self._nodeid, classname)
735 def is_edit_ok(self):
736 ''' Is the user allowed to Edit the current class?
737 Also check whether this is the current user's info.
738 '''
739 return self._db.security.hasPermission('Edit', self._client.userid,
740 self._classname) or self._nodeid == self._client.userid
742 def is_view_ok(self):
743 ''' Is the user allowed to View the current class?
744 Also check whether this is the current user's info.
745 '''
746 return self._db.security.hasPermission('Edit', self._client.userid,
747 self._classname) or self._nodeid == self._client.userid
749 class HTMLProperty:
750 ''' String, Number, Date, Interval HTMLProperty
752 Has useful attributes:
754 _name the name of the property
755 _value the value of the property if any
757 A wrapper object which may be stringified for the plain() behaviour.
758 '''
759 def __init__(self, client, nodeid, prop, name, value):
760 self._client = client
761 self._db = client.db
762 self._nodeid = nodeid
763 self._prop = prop
764 self._name = name
765 self._value = value
766 def __repr__(self):
767 return '<HTMLProperty(0x%x) %s %r %r>'%(id(self), self._name, self._prop, self._value)
768 def __str__(self):
769 return self.plain()
770 def __cmp__(self, other):
771 if isinstance(other, HTMLProperty):
772 return cmp(self._value, other._value)
773 return cmp(self._value, other)
775 class StringHTMLProperty(HTMLProperty):
776 hyper_re = re.compile(r'((?P<url>\w{3,6}://\S+)|'
777 r'(?P<email>[\w\.]+@[\w\.\-]+)|'
778 r'(?P<item>(?P<class>[a-z_]+)(?P<id>\d+)))')
779 def _hyper_repl(self, match):
780 if match.group('url'):
781 s = match.group('url')
782 return '<a href="%s">%s</a>'%(s, s)
783 elif match.group('email'):
784 s = match.group('email')
785 return '<a href="mailto:%s">%s</a>'%(s, s)
786 else:
787 s = match.group('item')
788 s1 = match.group('class')
789 s2 = match.group('id')
790 try:
791 # make sure s1 is a valid tracker classname
792 self._db.getclass(s1)
793 return '<a href="%s">%s %s</a>'%(s, s1, s2)
794 except KeyError:
795 return '%s%s'%(s1, s2)
797 def plain(self, escape=0, hyperlink=0):
798 ''' Render a "plain" representation of the property
800 "escape" turns on/off HTML quoting
801 "hyperlink" turns on/off in-text hyperlinking of URLs, email
802 addresses and designators
803 '''
804 if self._value is None:
805 return ''
806 if escape:
807 s = cgi.escape(str(self._value))
808 else:
809 s = str(self._value)
810 if hyperlink:
811 if not escape:
812 s = cgi.escape(s)
813 s = self.hyper_re.sub(self._hyper_repl, s)
814 return s
816 def stext(self, escape=0):
817 ''' Render the value of the property as StructuredText.
819 This requires the StructureText module to be installed separately.
820 '''
821 s = self.plain(escape=escape)
822 if not StructuredText:
823 return s
824 return StructuredText(s,level=1,header=0)
826 def field(self, size = 30):
827 ''' Render a form edit field for the property
828 '''
829 if self._value is None:
830 value = ''
831 else:
832 value = cgi.escape(str(self._value))
833 value = '"'.join(value.split('"'))
834 return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
836 def multiline(self, escape=0, rows=5, cols=40):
837 ''' Render a multiline form edit field for the property
838 '''
839 if self._value is None:
840 value = ''
841 else:
842 value = cgi.escape(str(self._value))
843 value = '"'.join(value.split('"'))
844 return '<textarea name="%s" rows="%s" cols="%s">%s</textarea>'%(
845 self._name, rows, cols, value)
847 def email(self, escape=1):
848 ''' Render the value of the property as an obscured email address
849 '''
850 if self._value is None: value = ''
851 else: value = str(self._value)
852 if value.find('@') != -1:
853 name, domain = value.split('@')
854 domain = ' '.join(domain.split('.')[:-1])
855 name = name.replace('.', ' ')
856 value = '%s at %s ...'%(name, domain)
857 else:
858 value = value.replace('.', ' ')
859 if escape:
860 value = cgi.escape(value)
861 return value
863 class PasswordHTMLProperty(HTMLProperty):
864 def plain(self):
865 ''' Render a "plain" representation of the property
866 '''
867 if self._value is None:
868 return ''
869 return _('*encrypted*')
871 def field(self, size = 30):
872 ''' Render a form edit field for the property.
873 '''
874 return '<input type="password" name="%s" size="%s">'%(self._name, size)
876 def confirm(self, size = 30):
877 ''' Render a second form edit field for the property, used for
878 confirmation that the user typed the password correctly. Generates
879 a field with name "name:confirm".
880 '''
881 return '<input type="password" name="%s:confirm" size="%s">'%(
882 self._name, size)
884 class NumberHTMLProperty(HTMLProperty):
885 def plain(self):
886 ''' Render a "plain" representation of the property
887 '''
888 return str(self._value)
890 def field(self, size = 30):
891 ''' Render a form edit field for the property
892 '''
893 if self._value is None:
894 value = ''
895 else:
896 value = cgi.escape(str(self._value))
897 value = '"'.join(value.split('"'))
898 return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
900 class BooleanHTMLProperty(HTMLProperty):
901 def plain(self):
902 ''' Render a "plain" representation of the property
903 '''
904 if self._value is None:
905 return ''
906 return self._value and "Yes" or "No"
908 def field(self):
909 ''' Render a form edit field for the property
910 '''
911 checked = self._value and "checked" or ""
912 s = '<input type="radio" name="%s" value="yes" %s>Yes'%(self._name,
913 checked)
914 if checked:
915 checked = ""
916 else:
917 checked = "checked"
918 s += '<input type="radio" name="%s" value="no" %s>No'%(self._name,
919 checked)
920 return s
922 class DateHTMLProperty(HTMLProperty):
923 def plain(self):
924 ''' Render a "plain" representation of the property
925 '''
926 if self._value is None:
927 return ''
928 return str(self._value.local(self._db.getUserTimezone()))
930 def now(self):
931 ''' Return the current time.
933 This is useful for defaulting a new value. Returns a
934 DateHTMLProperty.
935 '''
936 return DateHTMLProperty(self._client, self._nodeid, self._prop,
937 self._name, date.Date('.'))
939 def field(self, size = 30):
940 ''' Render a form edit field for the property
941 '''
942 if self._value is None:
943 value = ''
944 else:
945 value = cgi.escape(str(self._value.local(self._db.getUserTimezone())))
946 value = '"'.join(value.split('"'))
947 return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
949 def reldate(self, pretty=1):
950 ''' Render the interval between the date and now.
952 If the "pretty" flag is true, then make the display pretty.
953 '''
954 if not self._value:
955 return ''
957 # figure the interval
958 interval = date.Date('.') - self._value
959 if pretty:
960 return interval.pretty()
961 return str(interval)
963 def pretty(self, format='%d %B %Y'):
964 ''' Render the date in a pretty format (eg. month names, spaces).
966 The format string is a standard python strftime format string.
967 Note that if the day is zero, and appears at the start of the
968 string, then it'll be stripped from the output. This is handy
969 for the situatin when a date only specifies a month and a year.
970 '''
971 return self._value.pretty()
973 def local(self, offset):
974 ''' Return the date/time as a local (timezone offset) date/time.
975 '''
976 return DateHTMLProperty(self._client, self._nodeid, self._prop,
977 self._name, self._value.local(offset))
979 class IntervalHTMLProperty(HTMLProperty):
980 def plain(self):
981 ''' Render a "plain" representation of the property
982 '''
983 if self._value is None:
984 return ''
985 return str(self._value)
987 def pretty(self):
988 ''' Render the interval in a pretty format (eg. "yesterday")
989 '''
990 return self._value.pretty()
992 def field(self, size = 30):
993 ''' Render a form edit field for the property
994 '''
995 if self._value is None:
996 value = ''
997 else:
998 value = cgi.escape(str(self._value))
999 value = '"'.join(value.split('"'))
1000 return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
1002 class LinkHTMLProperty(HTMLProperty):
1003 ''' Link HTMLProperty
1004 Include the above as well as being able to access the class
1005 information. Stringifying the object itself results in the value
1006 from the item being displayed. Accessing attributes of this object
1007 result in the appropriate entry from the class being queried for the
1008 property accessed (so item/assignedto/name would look up the user
1009 entry identified by the assignedto property on item, and then the
1010 name property of that user)
1011 '''
1012 def __init__(self, *args):
1013 HTMLProperty.__init__(self, *args)
1014 # if we're representing a form value, then the -1 from the form really
1015 # should be a None
1016 if str(self._value) == '-1':
1017 self._value = None
1019 def __getattr__(self, attr):
1020 ''' return a new HTMLItem '''
1021 #print 'Link.getattr', (self, attr, self._value)
1022 if not self._value:
1023 raise AttributeError, "Can't access missing value"
1024 if self._prop.classname == 'user':
1025 klass = HTMLUser
1026 else:
1027 klass = HTMLItem
1028 i = klass(self._client, self._prop.classname, self._value)
1029 return getattr(i, attr)
1031 def plain(self, escape=0):
1032 ''' Render a "plain" representation of the property
1033 '''
1034 if self._value is None:
1035 return ''
1036 linkcl = self._db.classes[self._prop.classname]
1037 k = linkcl.labelprop(1)
1038 value = str(linkcl.get(self._value, k))
1039 if escape:
1040 value = cgi.escape(value)
1041 return value
1043 def field(self, showid=0, size=None):
1044 ''' Render a form edit field for the property
1045 '''
1046 linkcl = self._db.getclass(self._prop.classname)
1047 if linkcl.getprops().has_key('order'):
1048 sort_on = 'order'
1049 else:
1050 sort_on = linkcl.labelprop()
1051 options = linkcl.filter(None, {}, ('+', sort_on), (None, None))
1052 # TODO: make this a field display, not a menu one!
1053 l = ['<select name="%s">'%self._name]
1054 k = linkcl.labelprop(1)
1055 if self._value is None:
1056 s = 'selected '
1057 else:
1058 s = ''
1059 l.append(_('<option %svalue="-1">- no selection -</option>')%s)
1061 # make sure we list the current value if it's retired
1062 if self._value and self._value not in options:
1063 options.insert(0, self._value)
1065 for optionid in options:
1066 # get the option value, and if it's None use an empty string
1067 option = linkcl.get(optionid, k) or ''
1069 # figure if this option is selected
1070 s = ''
1071 if optionid == self._value:
1072 s = 'selected '
1074 # figure the label
1075 if showid:
1076 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1077 else:
1078 lab = option
1080 # truncate if it's too long
1081 if size is not None and len(lab) > size:
1082 lab = lab[:size-3] + '...'
1084 # and generate
1085 lab = cgi.escape(lab)
1086 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
1087 l.append('</select>')
1088 return '\n'.join(l)
1090 def menu(self, size=None, height=None, showid=0, additional=[],
1091 **conditions):
1092 ''' Render a form select list for this property
1093 '''
1094 value = self._value
1096 # sort function
1097 sortfunc = make_sort_function(self._db, self._prop.classname)
1099 linkcl = self._db.getclass(self._prop.classname)
1100 l = ['<select name="%s">'%self._name]
1101 k = linkcl.labelprop(1)
1102 s = ''
1103 if value is None:
1104 s = 'selected '
1105 l.append(_('<option %svalue="-1">- no selection -</option>')%s)
1106 if linkcl.getprops().has_key('order'):
1107 sort_on = ('+', 'order')
1108 else:
1109 sort_on = ('+', linkcl.labelprop())
1110 options = linkcl.filter(None, conditions, sort_on, (None, None))
1112 # make sure we list the current value if it's retired
1113 if self._value and self._value not in options:
1114 options.insert(0, self._value)
1116 for optionid in options:
1117 # get the option value, and if it's None use an empty string
1118 option = linkcl.get(optionid, k) or ''
1120 # figure if this option is selected
1121 s = ''
1122 if value in [optionid, option]:
1123 s = 'selected '
1125 # figure the label
1126 if showid:
1127 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1128 else:
1129 lab = option
1131 # truncate if it's too long
1132 if size is not None and len(lab) > size:
1133 lab = lab[:size-3] + '...'
1134 if additional:
1135 m = []
1136 for propname in additional:
1137 m.append(linkcl.get(optionid, propname))
1138 lab = lab + ' (%s)'%', '.join(map(str, m))
1140 # and generate
1141 lab = cgi.escape(lab)
1142 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
1143 l.append('</select>')
1144 return '\n'.join(l)
1145 # def checklist(self, ...)
1147 class MultilinkHTMLProperty(HTMLProperty):
1148 ''' Multilink HTMLProperty
1150 Also be iterable, returning a wrapper object like the Link case for
1151 each entry in the multilink.
1152 '''
1153 def __len__(self):
1154 ''' length of the multilink '''
1155 return len(self._value)
1157 def __getattr__(self, attr):
1158 ''' no extended attribute accesses make sense here '''
1159 raise AttributeError, attr
1161 def __getitem__(self, num):
1162 ''' iterate and return a new HTMLItem
1163 '''
1164 #print 'Multi.getitem', (self, num)
1165 value = self._value[num]
1166 if self._prop.classname == 'user':
1167 klass = HTMLUser
1168 else:
1169 klass = HTMLItem
1170 return klass(self._client, self._prop.classname, value)
1172 def __contains__(self, value):
1173 ''' Support the "in" operator. We have to make sure the passed-in
1174 value is a string first, not a *HTMLProperty.
1175 '''
1176 return str(value) in self._value
1178 def reverse(self):
1179 ''' return the list in reverse order
1180 '''
1181 l = self._value[:]
1182 l.reverse()
1183 if self._prop.classname == 'user':
1184 klass = HTMLUser
1185 else:
1186 klass = HTMLItem
1187 return [klass(self._client, self._prop.classname, value) for value in l]
1189 def plain(self, escape=0):
1190 ''' Render a "plain" representation of the property
1191 '''
1192 linkcl = self._db.classes[self._prop.classname]
1193 k = linkcl.labelprop(1)
1194 labels = []
1195 for v in self._value:
1196 labels.append(linkcl.get(v, k))
1197 value = ', '.join(labels)
1198 if escape:
1199 value = cgi.escape(value)
1200 return value
1202 def field(self, size=30, showid=0):
1203 ''' Render a form edit field for the property
1204 '''
1205 sortfunc = make_sort_function(self._db, self._prop.classname)
1206 linkcl = self._db.getclass(self._prop.classname)
1207 value = self._value[:]
1208 if value:
1209 value.sort(sortfunc)
1210 # map the id to the label property
1211 if not linkcl.getkey():
1212 showid=1
1213 if not showid:
1214 k = linkcl.labelprop(1)
1215 value = [linkcl.get(v, k) for v in value]
1216 value = cgi.escape(','.join(value))
1217 return '<input name="%s" size="%s" value="%s">'%(self._name, size, value)
1219 def menu(self, size=None, height=None, showid=0, additional=[],
1220 **conditions):
1221 ''' Render a form select list for this property
1222 '''
1223 value = self._value
1225 # sort function
1226 sortfunc = make_sort_function(self._db, self._prop.classname)
1228 linkcl = self._db.getclass(self._prop.classname)
1229 if linkcl.getprops().has_key('order'):
1230 sort_on = ('+', 'order')
1231 else:
1232 sort_on = ('+', linkcl.labelprop())
1233 options = linkcl.filter(None, conditions, sort_on, (None,None))
1234 height = height or min(len(options), 7)
1235 l = ['<select multiple name="%s" size="%s">'%(self._name, height)]
1236 k = linkcl.labelprop(1)
1238 # make sure we list the current values if they're retired
1239 for val in value:
1240 if val not in options:
1241 options.insert(0, val)
1243 for optionid in options:
1244 # get the option value, and if it's None use an empty string
1245 option = linkcl.get(optionid, k) or ''
1247 # figure if this option is selected
1248 s = ''
1249 if optionid in value or option in value:
1250 s = 'selected '
1252 # figure the label
1253 if showid:
1254 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1255 else:
1256 lab = option
1257 # truncate if it's too long
1258 if size is not None and len(lab) > size:
1259 lab = lab[:size-3] + '...'
1260 if additional:
1261 m = []
1262 for propname in additional:
1263 m.append(linkcl.get(optionid, propname))
1264 lab = lab + ' (%s)'%', '.join(m)
1266 # and generate
1267 lab = cgi.escape(lab)
1268 l.append('<option %svalue="%s">%s</option>'%(s, optionid,
1269 lab))
1270 l.append('</select>')
1271 return '\n'.join(l)
1273 # set the propclasses for HTMLItem
1274 propclasses = (
1275 (hyperdb.String, StringHTMLProperty),
1276 (hyperdb.Number, NumberHTMLProperty),
1277 (hyperdb.Boolean, BooleanHTMLProperty),
1278 (hyperdb.Date, DateHTMLProperty),
1279 (hyperdb.Interval, IntervalHTMLProperty),
1280 (hyperdb.Password, PasswordHTMLProperty),
1281 (hyperdb.Link, LinkHTMLProperty),
1282 (hyperdb.Multilink, MultilinkHTMLProperty),
1283 )
1285 def make_sort_function(db, classname):
1286 '''Make a sort function for a given class
1287 '''
1288 linkcl = db.getclass(classname)
1289 if linkcl.getprops().has_key('order'):
1290 sort_on = 'order'
1291 else:
1292 sort_on = linkcl.labelprop()
1293 def sortfunc(a, b, linkcl=linkcl, sort_on=sort_on):
1294 return cmp(linkcl.get(a, sort_on), linkcl.get(b, sort_on))
1295 return sortfunc
1297 def handleListCGIValue(value):
1298 ''' Value is either a single item or a list of items. Each item has a
1299 .value that we're actually interested in.
1300 '''
1301 if isinstance(value, type([])):
1302 return [value.value for value in value]
1303 else:
1304 value = value.value.strip()
1305 if not value:
1306 return []
1307 return value.split(',')
1309 class ShowDict:
1310 ''' A convenience access to the :columns index parameters
1311 '''
1312 def __init__(self, columns):
1313 self.columns = {}
1314 for col in columns:
1315 self.columns[col] = 1
1316 def __getitem__(self, name):
1317 return self.columns.has_key(name)
1319 class HTMLRequest:
1320 ''' The *request*, holding the CGI form and environment.
1322 "form" the CGI form as a cgi.FieldStorage
1323 "env" the CGI environment variables
1324 "base" the base URL for this instance
1325 "user" a HTMLUser instance for this user
1326 "classname" the current classname (possibly None)
1327 "template" the current template (suffix, also possibly None)
1329 Index args:
1330 "columns" dictionary of the columns to display in an index page
1331 "show" a convenience access to columns - request/show/colname will
1332 be true if the columns should be displayed, false otherwise
1333 "sort" index sort column (direction, column name)
1334 "group" index grouping property (direction, column name)
1335 "filter" properties to filter the index on
1336 "filterspec" values to filter the index on
1337 "search_text" text to perform a full-text search on for an index
1339 '''
1340 def __init__(self, client):
1341 self.client = client
1343 # easier access vars
1344 self.form = client.form
1345 self.env = client.env
1346 self.base = client.base
1347 self.user = HTMLUser(client, 'user', client.userid)
1349 # store the current class name and action
1350 self.classname = client.classname
1351 self.template = client.template
1353 # the special char to use for special vars
1354 self.special_char = '@'
1356 self._post_init()
1358 def _post_init(self):
1359 ''' Set attributes based on self.form
1360 '''
1361 # extract the index display information from the form
1362 self.columns = []
1363 for name in ':columns @columns'.split():
1364 if self.form.has_key(name):
1365 self.special_char = name[0]
1366 self.columns = handleListCGIValue(self.form[name])
1367 break
1368 self.show = ShowDict(self.columns)
1370 # sorting
1371 self.sort = (None, None)
1372 for name in ':sort @sort'.split():
1373 if self.form.has_key(name):
1374 self.special_char = name[0]
1375 sort = self.form[name].value
1376 if sort.startswith('-'):
1377 self.sort = ('-', sort[1:])
1378 else:
1379 self.sort = ('+', sort)
1380 if self.form.has_key(self.special_char+'sortdir'):
1381 self.sort = ('-', self.sort[1])
1383 # grouping
1384 self.group = (None, None)
1385 for name in ':group @group'.split():
1386 if self.form.has_key(name):
1387 self.special_char = name[0]
1388 group = self.form[name].value
1389 if group.startswith('-'):
1390 self.group = ('-', group[1:])
1391 else:
1392 self.group = ('+', group)
1393 if self.form.has_key(self.special_char+'groupdir'):
1394 self.group = ('-', self.group[1])
1396 # filtering
1397 self.filter = []
1398 for name in ':filter @filter'.split():
1399 if self.form.has_key(name):
1400 self.special_char = name[0]
1401 self.filter = handleListCGIValue(self.form[name])
1403 self.filterspec = {}
1404 db = self.client.db
1405 if self.classname is not None:
1406 props = db.getclass(self.classname).getprops()
1407 for name in self.filter:
1408 if self.form.has_key(name):
1409 prop = props[name]
1410 fv = self.form[name]
1411 if (isinstance(prop, hyperdb.Link) or
1412 isinstance(prop, hyperdb.Multilink)):
1413 self.filterspec[name] = lookupIds(db, prop,
1414 handleListCGIValue(fv))
1415 else:
1416 self.filterspec[name] = fv.value
1418 # full-text search argument
1419 self.search_text = None
1420 for name in ':search_text @search_text'.split():
1421 if self.form.has_key(name):
1422 self.special_char = name[0]
1423 self.search_text = self.form[name].value
1425 # pagination - size and start index
1426 # figure batch args
1427 self.pagesize = 50
1428 for name in ':pagesize @pagesize'.split():
1429 if self.form.has_key(name):
1430 self.special_char = name[0]
1431 self.pagesize = int(self.form[name].value)
1433 self.startwith = 0
1434 for name in ':startwith @startwith'.split():
1435 if self.form.has_key(name):
1436 self.special_char = name[0]
1437 self.startwith = int(self.form[name].value)
1439 def updateFromURL(self, url):
1440 ''' Parse the URL for query args, and update my attributes using the
1441 values.
1442 '''
1443 self.form = {}
1444 for name, value in cgi.parse_qsl(url):
1445 if self.form.has_key(name):
1446 if isinstance(self.form[name], type([])):
1447 self.form[name].append(cgi.MiniFieldStorage(name, value))
1448 else:
1449 self.form[name] = [self.form[name],
1450 cgi.MiniFieldStorage(name, value)]
1451 else:
1452 self.form[name] = cgi.MiniFieldStorage(name, value)
1453 self._post_init()
1455 def update(self, kwargs):
1456 ''' Update my attributes using the keyword args
1457 '''
1458 self.__dict__.update(kwargs)
1459 if kwargs.has_key('columns'):
1460 self.show = ShowDict(self.columns)
1462 def description(self):
1463 ''' Return a description of the request - handle for the page title.
1464 '''
1465 s = [self.client.db.config.TRACKER_NAME]
1466 if self.classname:
1467 if self.client.nodeid:
1468 s.append('- %s%s'%(self.classname, self.client.nodeid))
1469 else:
1470 if self.template == 'item':
1471 s.append('- new %s'%self.classname)
1472 elif self.template == 'index':
1473 s.append('- %s index'%self.classname)
1474 else:
1475 s.append('- %s %s'%(self.classname, self.template))
1476 else:
1477 s.append('- home')
1478 return ' '.join(s)
1480 def __str__(self):
1481 d = {}
1482 d.update(self.__dict__)
1483 f = ''
1484 for k in self.form.keys():
1485 f += '\n %r=%r'%(k,handleListCGIValue(self.form[k]))
1486 d['form'] = f
1487 e = ''
1488 for k,v in self.env.items():
1489 e += '\n %r=%r'%(k, v)
1490 d['env'] = e
1491 return '''
1492 form: %(form)s
1493 base: %(base)r
1494 classname: %(classname)r
1495 template: %(template)r
1496 columns: %(columns)r
1497 sort: %(sort)r
1498 group: %(group)r
1499 filter: %(filter)r
1500 search_text: %(search_text)r
1501 pagesize: %(pagesize)r
1502 startwith: %(startwith)r
1503 env: %(env)s
1504 '''%d
1506 def indexargs_form(self, columns=1, sort=1, group=1, filter=1,
1507 filterspec=1):
1508 ''' return the current index args as form elements '''
1509 l = []
1510 sc = self.special_char
1511 s = '<input type="hidden" name="%s" value="%s">'
1512 if columns and self.columns:
1513 l.append(s%(sc+'columns', ','.join(self.columns)))
1514 if sort and self.sort[1] is not None:
1515 if self.sort[0] == '-':
1516 val = '-'+self.sort[1]
1517 else:
1518 val = self.sort[1]
1519 l.append(s%(sc+'sort', val))
1520 if group and self.group[1] is not None:
1521 if self.group[0] == '-':
1522 val = '-'+self.group[1]
1523 else:
1524 val = self.group[1]
1525 l.append(s%(sc+'group', val))
1526 if filter and self.filter:
1527 l.append(s%(sc+'filter', ','.join(self.filter)))
1528 if filterspec:
1529 for k,v in self.filterspec.items():
1530 if type(v) == type([]):
1531 l.append(s%(k, ','.join(v)))
1532 else:
1533 l.append(s%(k, v))
1534 if self.search_text:
1535 l.append(s%(sc+'search_text', self.search_text))
1536 l.append(s%(sc+'pagesize', self.pagesize))
1537 l.append(s%(sc+'startwith', self.startwith))
1538 return '\n'.join(l)
1540 def indexargs_url(self, url, args):
1541 ''' embed the current index args in a URL '''
1542 sc = self.special_char
1543 l = ['%s=%s'%(k,v) for k,v in args.items()]
1544 if self.columns and not args.has_key(':columns'):
1545 l.append(sc+'columns=%s'%(','.join(self.columns)))
1546 if self.sort[1] is not None and not args.has_key(':sort'):
1547 if self.sort[0] == '-':
1548 val = '-'+self.sort[1]
1549 else:
1550 val = self.sort[1]
1551 l.append(sc+'sort=%s'%val)
1552 if self.group[1] is not None and not args.has_key(':group'):
1553 if self.group[0] == '-':
1554 val = '-'+self.group[1]
1555 else:
1556 val = self.group[1]
1557 l.append(sc+'group=%s'%val)
1558 if self.filter and not args.has_key(':filter'):
1559 l.append(sc+'filter=%s'%(','.join(self.filter)))
1560 for k,v in self.filterspec.items():
1561 if not args.has_key(k):
1562 if type(v) == type([]):
1563 l.append('%s=%s'%(k, ','.join(v)))
1564 else:
1565 l.append('%s=%s'%(k, v))
1566 if self.search_text and not args.has_key(':search_text'):
1567 l.append(sc+'search_text=%s'%self.search_text)
1568 if not args.has_key(':pagesize'):
1569 l.append(sc+'pagesize=%s'%self.pagesize)
1570 if not args.has_key(':startwith'):
1571 l.append(sc+'startwith=%s'%self.startwith)
1572 return '%s?%s'%(url, '&'.join(l))
1573 indexargs_href = indexargs_url
1575 def base_javascript(self):
1576 return '''
1577 <script language="javascript">
1578 submitted = false;
1579 function submit_once() {
1580 if (submitted) {
1581 alert("Your request is being processed.\\nPlease be patient.");
1582 return 0;
1583 }
1584 submitted = true;
1585 return 1;
1586 }
1588 function help_window(helpurl, width, height) {
1589 HelpWin = window.open('%s' + helpurl, 'RoundupHelpWindow', 'scrollbars=yes,resizable=yes,toolbar=no,height='+height+',width='+width);
1590 }
1591 </script>
1592 '''%self.base
1594 def batch(self):
1595 ''' Return a batch object for results from the "current search"
1596 '''
1597 filterspec = self.filterspec
1598 sort = self.sort
1599 group = self.group
1601 # get the list of ids we're batching over
1602 klass = self.client.db.getclass(self.classname)
1603 if self.search_text:
1604 matches = self.client.db.indexer.search(
1605 re.findall(r'\b\w{2,25}\b', self.search_text), klass)
1606 else:
1607 matches = None
1608 l = klass.filter(matches, filterspec, sort, group)
1610 # return the batch object, using IDs only
1611 return Batch(self.client, l, self.pagesize, self.startwith,
1612 classname=self.classname)
1614 # extend the standard ZTUtils Batch object to remove dependency on
1615 # Acquisition and add a couple of useful methods
1616 class Batch(ZTUtils.Batch):
1617 ''' Use me to turn a list of items, or item ids of a given class, into a
1618 series of batches.
1620 ========= ========================================================
1621 Parameter Usage
1622 ========= ========================================================
1623 sequence a list of HTMLItems or item ids
1624 classname if sequence is a list of ids, this is the class of item
1625 size how big to make the sequence.
1626 start where to start (0-indexed) in the sequence.
1627 end where to end (0-indexed) in the sequence.
1628 orphan if the next batch would contain less items than this
1629 value, then it is combined with this batch
1630 overlap the number of items shared between adjacent batches
1631 ========= ========================================================
1633 Attributes: Note that the "start" attribute, unlike the
1634 argument, is a 1-based index (I know, lame). "first" is the
1635 0-based index. "length" is the actual number of elements in
1636 the batch.
1638 "sequence_length" is the length of the original, unbatched, sequence.
1639 '''
1640 def __init__(self, client, sequence, size, start, end=0, orphan=0,
1641 overlap=0, classname=None):
1642 self.client = client
1643 self.last_index = self.last_item = None
1644 self.current_item = None
1645 self.classname = classname
1646 self.sequence_length = len(sequence)
1647 ZTUtils.Batch.__init__(self, sequence, size, start, end, orphan,
1648 overlap)
1650 # overwrite so we can late-instantiate the HTMLItem instance
1651 def __getitem__(self, index):
1652 if index < 0:
1653 if index + self.end < self.first: raise IndexError, index
1654 return self._sequence[index + self.end]
1656 if index >= self.length:
1657 raise IndexError, index
1659 # move the last_item along - but only if the fetched index changes
1660 # (for some reason, index 0 is fetched twice)
1661 if index != self.last_index:
1662 self.last_item = self.current_item
1663 self.last_index = index
1665 item = self._sequence[index + self.first]
1666 if self.classname:
1667 # map the item ids to instances
1668 if self.classname == 'user':
1669 item = HTMLUser(self.client, self.classname, item)
1670 else:
1671 item = HTMLItem(self.client, self.classname, item)
1672 self.current_item = item
1673 return item
1675 def propchanged(self, property):
1676 ''' Detect if the property marked as being the group property
1677 changed in the last iteration fetch
1678 '''
1679 if (self.last_item is None or
1680 self.last_item[property] != self.current_item[property]):
1681 return 1
1682 return 0
1684 # override these 'cos we don't have access to acquisition
1685 def previous(self):
1686 if self.start == 1:
1687 return None
1688 return Batch(self.client, self._sequence, self._size,
1689 self.first - self._size + self.overlap, 0, self.orphan,
1690 self.overlap)
1692 def next(self):
1693 try:
1694 self._sequence[self.end]
1695 except IndexError:
1696 return None
1697 return Batch(self.client, self._sequence, self._size,
1698 self.end - self.overlap, 0, self.orphan, self.overlap)
1700 class TemplatingUtils:
1701 ''' Utilities for templating
1702 '''
1703 def __init__(self, client):
1704 self.client = client
1705 def Batch(self, sequence, size, start, end=0, orphan=0, overlap=0):
1706 return Batch(self.client, sequence, size, start, end, orphan,
1707 overlap)