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