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