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 if not current[k] == '(no value)' and current[k]:
665 current[k] = date.Date(current[k]).local(timezone)
666 cell[-1] += ' -> %s' % current[k]
667 current[k] = str(d)
669 elif isinstance(prop, hyperdb.Interval) and args[k]:
670 d = date.Interval(args[k])
671 cell.append('%s: %s'%(k, str(d)))
672 if current.has_key(k):
673 cell[-1] += ' -> %s'%current[k]
674 current[k] = str(d)
676 elif isinstance(prop, hyperdb.String) and args[k]:
677 cell.append('%s: %s'%(k, cgi.escape(args[k])))
678 if current.has_key(k):
679 cell[-1] += ' -> %s'%current[k]
680 current[k] = cgi.escape(args[k])
682 elif not args[k]:
683 if current.has_key(k):
684 cell.append('%s: %s'%(k, current[k]))
685 current[k] = '(no value)'
686 else:
687 cell.append('%s: (no value)'%k)
689 else:
690 cell.append('%s: %s'%(k, str(args[k])))
691 if current.has_key(k):
692 cell[-1] += ' -> %s'%current[k]
693 current[k] = str(args[k])
694 else:
695 # property no longer exists
696 comments['no_exist'] = _('''<em>The indicated property
697 no longer exists</em>''')
698 cell.append('<em>%s: %s</em>\n'%(k, str(args[k])))
699 arg_s = '<br />'.join(cell)
700 else:
701 # unkown event!!
702 comments['unknown'] = _('''<strong><em>This event is not
703 handled by the history display!</em></strong>''')
704 arg_s = '<strong><em>' + str(args) + '</em></strong>'
705 date_s = date_s.replace(' ', ' ')
706 # if the user's an itemid, figure the username (older journals
707 # have the username)
708 if dre.match(user):
709 user = self._db.user.get(user, 'username')
710 l.append('<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>'%(
711 date_s, user, action, arg_s))
712 if comments:
713 l.append(_('<tr><td colspan=4><strong>Note:</strong></td></tr>'))
714 for entry in comments.values():
715 l.append('<tr><td colspan=4>%s</td></tr>'%entry)
716 l.append('</table>')
717 return '\n'.join(l)
719 def renderQueryForm(self):
720 ''' Render this item, which is a query, as a search form.
721 '''
722 # create a new request and override the specified args
723 req = HTMLRequest(self._client)
724 req.classname = self._klass.get(self._nodeid, 'klass')
725 name = self._klass.get(self._nodeid, 'name')
726 req.updateFromURL(self._klass.get(self._nodeid, 'url') +
727 '&:queryname=%s'%urllib.quote(name))
729 # new template, using the specified classname and request
730 pt = Templates(self._db.config.TEMPLATES).get(req.classname, 'search')
732 # use our fabricated request
733 return pt.render(self._client, req.classname, req)
735 class HTMLUser(HTMLItem):
736 ''' Accesses through the *user* (a special case of item)
737 '''
738 def __init__(self, client, classname, nodeid, anonymous=0):
739 HTMLItem.__init__(self, client, 'user', nodeid, anonymous)
740 self._default_classname = client.classname
742 # used for security checks
743 self._security = client.db.security
745 _marker = []
746 def hasPermission(self, permission, classname=_marker):
747 ''' Determine if the user has the Permission.
749 The class being tested defaults to the template's class, but may
750 be overidden for this test by suppling an alternate classname.
751 '''
752 if classname is self._marker:
753 classname = self._default_classname
754 return self._security.hasPermission(permission, self._nodeid, classname)
756 def is_edit_ok(self):
757 ''' Is the user allowed to Edit the current class?
758 Also check whether this is the current user's info.
759 '''
760 return self._db.security.hasPermission('Edit', self._client.userid,
761 self._classname) or self._nodeid == self._client.userid
763 def is_view_ok(self):
764 ''' Is the user allowed to View the current class?
765 Also check whether this is the current user's info.
766 '''
767 return self._db.security.hasPermission('Edit', self._client.userid,
768 self._classname) or self._nodeid == self._client.userid
770 class HTMLProperty:
771 ''' String, Number, Date, Interval HTMLProperty
773 Has useful attributes:
775 _name the name of the property
776 _value the value of the property if any
778 A wrapper object which may be stringified for the plain() behaviour.
779 '''
780 def __init__(self, client, classname, nodeid, prop, name, value,
781 anonymous=0):
782 self._client = client
783 self._db = client.db
784 self._classname = classname
785 self._nodeid = nodeid
786 self._prop = prop
787 self._value = value
788 self._anonymous = anonymous
789 if not anonymous:
790 self._name = '%s%s@%s'%(classname, nodeid, name)
791 else:
792 self._name = name
793 def __repr__(self):
794 return '<HTMLProperty(0x%x) %s %r %r>'%(id(self), self._name,
795 self._prop, self._value)
796 def __str__(self):
797 return self.plain()
798 def __cmp__(self, other):
799 if isinstance(other, HTMLProperty):
800 return cmp(self._value, other._value)
801 return cmp(self._value, other)
803 class StringHTMLProperty(HTMLProperty):
804 hyper_re = re.compile(r'((?P<url>\w{3,6}://\S+)|'
805 r'(?P<email>[\w\.]+@[\w\.\-]+)|'
806 r'(?P<item>(?P<class>[a-z_]+)(?P<id>\d+)))')
807 def _hyper_repl(self, match):
808 if match.group('url'):
809 s = match.group('url')
810 return '<a href="%s">%s</a>'%(s, s)
811 elif match.group('email'):
812 s = match.group('email')
813 return '<a href="mailto:%s">%s</a>'%(s, s)
814 else:
815 s = match.group('item')
816 s1 = match.group('class')
817 s2 = match.group('id')
818 try:
819 # make sure s1 is a valid tracker classname
820 self._db.getclass(s1)
821 return '<a href="%s">%s %s</a>'%(s, s1, s2)
822 except KeyError:
823 return '%s%s'%(s1, s2)
825 def plain(self, escape=0, hyperlink=0):
826 ''' Render a "plain" representation of the property
828 "escape" turns on/off HTML quoting
829 "hyperlink" turns on/off in-text hyperlinking of URLs, email
830 addresses and designators
831 '''
832 if self._value is None:
833 return ''
834 if escape:
835 s = cgi.escape(str(self._value))
836 else:
837 s = str(self._value)
838 if hyperlink:
839 if not escape:
840 s = cgi.escape(s)
841 s = self.hyper_re.sub(self._hyper_repl, s)
842 return s
844 def stext(self, escape=0):
845 ''' Render the value of the property as StructuredText.
847 This requires the StructureText module to be installed separately.
848 '''
849 s = self.plain(escape=escape)
850 if not StructuredText:
851 return s
852 return StructuredText(s,level=1,header=0)
854 def field(self, size = 30):
855 ''' Render a form edit field for the property
856 '''
857 if self._value is None:
858 value = ''
859 else:
860 value = cgi.escape(str(self._value))
861 value = '"'.join(value.split('"'))
862 return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
864 def multiline(self, escape=0, rows=5, cols=40):
865 ''' Render a multiline form edit field for the property
866 '''
867 if self._value is None:
868 value = ''
869 else:
870 value = cgi.escape(str(self._value))
871 value = '"'.join(value.split('"'))
872 return '<textarea name="%s" rows="%s" cols="%s">%s</textarea>'%(
873 self._name, rows, cols, value)
875 def email(self, escape=1):
876 ''' Render the value of the property as an obscured email address
877 '''
878 if self._value is None: value = ''
879 else: value = str(self._value)
880 if value.find('@') != -1:
881 name, domain = value.split('@')
882 domain = ' '.join(domain.split('.')[:-1])
883 name = name.replace('.', ' ')
884 value = '%s at %s ...'%(name, domain)
885 else:
886 value = value.replace('.', ' ')
887 if escape:
888 value = cgi.escape(value)
889 return value
891 class PasswordHTMLProperty(HTMLProperty):
892 def plain(self):
893 ''' Render a "plain" representation of the property
894 '''
895 if self._value is None:
896 return ''
897 return _('*encrypted*')
899 def field(self, size = 30):
900 ''' Render a form edit field for the property.
901 '''
902 return '<input type="password" name="%s" size="%s">'%(self._name, size)
904 def confirm(self, size = 30):
905 ''' Render a second form edit field for the property, used for
906 confirmation that the user typed the password correctly. Generates
907 a field with name ":confirm:name".
908 '''
909 return '<input type="password" name=":confirm:%s" size="%s">'%(
910 self._name, size)
912 class NumberHTMLProperty(HTMLProperty):
913 def plain(self):
914 ''' Render a "plain" representation of the property
915 '''
916 return str(self._value)
918 def field(self, size = 30):
919 ''' Render a form edit field for the property
920 '''
921 if self._value is None:
922 value = ''
923 else:
924 value = cgi.escape(str(self._value))
925 value = '"'.join(value.split('"'))
926 return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
928 class BooleanHTMLProperty(HTMLProperty):
929 def plain(self):
930 ''' Render a "plain" representation of the property
931 '''
932 if self._value is None:
933 return ''
934 return self._value and "Yes" or "No"
936 def field(self):
937 ''' Render a form edit field for the property
938 '''
939 checked = self._value and "checked" or ""
940 s = '<input type="radio" name="%s" value="yes" %s>Yes'%(self._name,
941 checked)
942 if checked:
943 checked = ""
944 else:
945 checked = "checked"
946 s += '<input type="radio" name="%s" value="no" %s>No'%(self._name,
947 checked)
948 return s
950 class DateHTMLProperty(HTMLProperty):
951 def plain(self):
952 ''' Render a "plain" representation of the property
953 '''
954 if self._value is None:
955 return ''
956 return str(self._value.local(self._db.getUserTimezone()))
958 def now(self):
959 ''' Return the current time.
961 This is useful for defaulting a new value. Returns a
962 DateHTMLProperty.
963 '''
964 return DateHTMLProperty(self._client, self._nodeid, self._prop,
965 self._name, date.Date('.'))
967 def field(self, size = 30):
968 ''' Render a form edit field for the property
969 '''
970 if self._value is None:
971 value = ''
972 else:
973 value = cgi.escape(str(self._value.local(self._db.getUserTimezone())))
974 value = '"'.join(value.split('"'))
975 return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
977 def reldate(self, pretty=1):
978 ''' Render the interval between the date and now.
980 If the "pretty" flag is true, then make the display pretty.
981 '''
982 if not self._value:
983 return ''
985 # figure the interval
986 interval = date.Date('.') - self._value
987 if pretty:
988 return interval.pretty()
989 return str(interval)
991 _marker = []
992 def pretty(self, format=_marker):
993 ''' Render the date in a pretty format (eg. month names, spaces).
995 The format string is a standard python strftime format string.
996 Note that if the day is zero, and appears at the start of the
997 string, then it'll be stripped from the output. This is handy
998 for the situatin when a date only specifies a month and a year.
999 '''
1000 if format is not self._marker:
1001 return self._value.pretty(format)
1002 else:
1003 return self._value.pretty()
1005 def local(self, offset):
1006 ''' Return the date/time as a local (timezone offset) date/time.
1007 '''
1008 return DateHTMLProperty(self._client, self._nodeid, self._prop,
1009 self._name, self._value.local(offset))
1011 class IntervalHTMLProperty(HTMLProperty):
1012 def plain(self):
1013 ''' Render a "plain" representation of the property
1014 '''
1015 if self._value is None:
1016 return ''
1017 return str(self._value)
1019 def pretty(self):
1020 ''' Render the interval in a pretty format (eg. "yesterday")
1021 '''
1022 return self._value.pretty()
1024 def field(self, size = 30):
1025 ''' Render a form edit field for the property
1026 '''
1027 if self._value is None:
1028 value = ''
1029 else:
1030 value = cgi.escape(str(self._value))
1031 value = '"'.join(value.split('"'))
1032 return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
1034 class LinkHTMLProperty(HTMLProperty):
1035 ''' Link HTMLProperty
1036 Include the above as well as being able to access the class
1037 information. Stringifying the object itself results in the value
1038 from the item being displayed. Accessing attributes of this object
1039 result in the appropriate entry from the class being queried for the
1040 property accessed (so item/assignedto/name would look up the user
1041 entry identified by the assignedto property on item, and then the
1042 name property of that user)
1043 '''
1044 def __init__(self, *args, **kw):
1045 HTMLProperty.__init__(self, *args, **kw)
1046 # if we're representing a form value, then the -1 from the form really
1047 # should be a None
1048 if str(self._value) == '-1':
1049 self._value = None
1051 def __getattr__(self, attr):
1052 ''' return a new HTMLItem '''
1053 #print 'Link.getattr', (self, attr, self._value)
1054 if not self._value:
1055 raise AttributeError, "Can't access missing value"
1056 if self._prop.classname == 'user':
1057 klass = HTMLUser
1058 else:
1059 klass = HTMLItem
1060 i = klass(self._client, self._prop.classname, self._value)
1061 return getattr(i, attr)
1063 def plain(self, escape=0):
1064 ''' Render a "plain" representation of the property
1065 '''
1066 if self._value is None:
1067 return ''
1068 linkcl = self._db.classes[self._prop.classname]
1069 k = linkcl.labelprop(1)
1070 value = str(linkcl.get(self._value, k))
1071 if escape:
1072 value = cgi.escape(value)
1073 return value
1075 def field(self, showid=0, size=None):
1076 ''' Render a form edit field for the property
1077 '''
1078 linkcl = self._db.getclass(self._prop.classname)
1079 if linkcl.getprops().has_key('order'):
1080 sort_on = 'order'
1081 else:
1082 sort_on = linkcl.labelprop()
1083 options = linkcl.filter(None, {}, ('+', sort_on), (None, None))
1084 # TODO: make this a field display, not a menu one!
1085 l = ['<select name="%s">'%self._name]
1086 k = linkcl.labelprop(1)
1087 if self._value is None:
1088 s = 'selected '
1089 else:
1090 s = ''
1091 l.append(_('<option %svalue="-1">- no selection -</option>')%s)
1093 # make sure we list the current value if it's retired
1094 if self._value and self._value not in options:
1095 options.insert(0, self._value)
1097 for optionid in options:
1098 # get the option value, and if it's None use an empty string
1099 option = linkcl.get(optionid, k) or ''
1101 # figure if this option is selected
1102 s = ''
1103 if optionid == self._value:
1104 s = 'selected '
1106 # figure the label
1107 if showid:
1108 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1109 else:
1110 lab = option
1112 # truncate if it's too long
1113 if size is not None and len(lab) > size:
1114 lab = lab[:size-3] + '...'
1116 # and generate
1117 lab = cgi.escape(lab)
1118 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
1119 l.append('</select>')
1120 return '\n'.join(l)
1122 def menu(self, size=None, height=None, showid=0, additional=[],
1123 **conditions):
1124 ''' Render a form select list for this property
1125 '''
1126 value = self._value
1128 # sort function
1129 sortfunc = make_sort_function(self._db, self._prop.classname)
1131 linkcl = self._db.getclass(self._prop.classname)
1132 l = ['<select name="%s">'%self._name]
1133 k = linkcl.labelprop(1)
1134 s = ''
1135 if value is None:
1136 s = 'selected '
1137 l.append(_('<option %svalue="-1">- no selection -</option>')%s)
1138 if linkcl.getprops().has_key('order'):
1139 sort_on = ('+', 'order')
1140 else:
1141 sort_on = ('+', linkcl.labelprop())
1142 options = linkcl.filter(None, conditions, sort_on, (None, None))
1144 # make sure we list the current value if it's retired
1145 if self._value and self._value not in options:
1146 options.insert(0, self._value)
1148 for optionid in options:
1149 # get the option value, and if it's None use an empty string
1150 option = linkcl.get(optionid, k) or ''
1152 # figure if this option is selected
1153 s = ''
1154 if value in [optionid, option]:
1155 s = 'selected '
1157 # figure the label
1158 if showid:
1159 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1160 else:
1161 lab = option
1163 # truncate if it's too long
1164 if size is not None and len(lab) > size:
1165 lab = lab[:size-3] + '...'
1166 if additional:
1167 m = []
1168 for propname in additional:
1169 m.append(linkcl.get(optionid, propname))
1170 lab = lab + ' (%s)'%', '.join(map(str, m))
1172 # and generate
1173 lab = cgi.escape(lab)
1174 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
1175 l.append('</select>')
1176 return '\n'.join(l)
1177 # def checklist(self, ...)
1179 class MultilinkHTMLProperty(HTMLProperty):
1180 ''' Multilink HTMLProperty
1182 Also be iterable, returning a wrapper object like the Link case for
1183 each entry in the multilink.
1184 '''
1185 def __len__(self):
1186 ''' length of the multilink '''
1187 return len(self._value)
1189 def __getattr__(self, attr):
1190 ''' no extended attribute accesses make sense here '''
1191 raise AttributeError, attr
1193 def __getitem__(self, num):
1194 ''' iterate and return a new HTMLItem
1195 '''
1196 #print 'Multi.getitem', (self, num)
1197 value = self._value[num]
1198 if self._prop.classname == 'user':
1199 klass = HTMLUser
1200 else:
1201 klass = HTMLItem
1202 return klass(self._client, self._prop.classname, value)
1204 def __contains__(self, value):
1205 ''' Support the "in" operator. We have to make sure the passed-in
1206 value is a string first, not a *HTMLProperty.
1207 '''
1208 return str(value) in self._value
1210 def reverse(self):
1211 ''' return the list in reverse order
1212 '''
1213 l = self._value[:]
1214 l.reverse()
1215 if self._prop.classname == 'user':
1216 klass = HTMLUser
1217 else:
1218 klass = HTMLItem
1219 return [klass(self._client, self._prop.classname, value) for value in l]
1221 def plain(self, escape=0):
1222 ''' Render a "plain" representation of the property
1223 '''
1224 linkcl = self._db.classes[self._prop.classname]
1225 k = linkcl.labelprop(1)
1226 labels = []
1227 for v in self._value:
1228 labels.append(linkcl.get(v, k))
1229 value = ', '.join(labels)
1230 if escape:
1231 value = cgi.escape(value)
1232 return value
1234 def field(self, size=30, showid=0):
1235 ''' Render a form edit field for the property
1236 '''
1237 sortfunc = make_sort_function(self._db, self._prop.classname)
1238 linkcl = self._db.getclass(self._prop.classname)
1239 value = self._value[:]
1240 if value:
1241 value.sort(sortfunc)
1242 # map the id to the label property
1243 if not linkcl.getkey():
1244 showid=1
1245 if not showid:
1246 k = linkcl.labelprop(1)
1247 value = [linkcl.get(v, k) for v in value]
1248 value = cgi.escape(','.join(value))
1249 return '<input name="%s" size="%s" value="%s">'%(self._name, size, value)
1251 def menu(self, size=None, height=None, showid=0, additional=[],
1252 **conditions):
1253 ''' Render a form select list for this property
1254 '''
1255 value = self._value
1257 # sort function
1258 sortfunc = make_sort_function(self._db, self._prop.classname)
1260 linkcl = self._db.getclass(self._prop.classname)
1261 if linkcl.getprops().has_key('order'):
1262 sort_on = ('+', 'order')
1263 else:
1264 sort_on = ('+', linkcl.labelprop())
1265 options = linkcl.filter(None, conditions, sort_on, (None,None))
1266 height = height or min(len(options), 7)
1267 l = ['<select multiple name="%s" size="%s">'%(self._name, height)]
1268 k = linkcl.labelprop(1)
1270 # make sure we list the current values if they're retired
1271 for val in value:
1272 if val not in options:
1273 options.insert(0, val)
1275 for optionid in options:
1276 # get the option value, and if it's None use an empty string
1277 option = linkcl.get(optionid, k) or ''
1279 # figure if this option is selected
1280 s = ''
1281 if optionid in value or option in value:
1282 s = 'selected '
1284 # figure the label
1285 if showid:
1286 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1287 else:
1288 lab = option
1289 # truncate if it's too long
1290 if size is not None and len(lab) > size:
1291 lab = lab[:size-3] + '...'
1292 if additional:
1293 m = []
1294 for propname in additional:
1295 m.append(linkcl.get(optionid, propname))
1296 lab = lab + ' (%s)'%', '.join(m)
1298 # and generate
1299 lab = cgi.escape(lab)
1300 l.append('<option %svalue="%s">%s</option>'%(s, optionid,
1301 lab))
1302 l.append('</select>')
1303 return '\n'.join(l)
1305 # set the propclasses for HTMLItem
1306 propclasses = (
1307 (hyperdb.String, StringHTMLProperty),
1308 (hyperdb.Number, NumberHTMLProperty),
1309 (hyperdb.Boolean, BooleanHTMLProperty),
1310 (hyperdb.Date, DateHTMLProperty),
1311 (hyperdb.Interval, IntervalHTMLProperty),
1312 (hyperdb.Password, PasswordHTMLProperty),
1313 (hyperdb.Link, LinkHTMLProperty),
1314 (hyperdb.Multilink, MultilinkHTMLProperty),
1315 )
1317 def make_sort_function(db, classname):
1318 '''Make a sort function for a given class
1319 '''
1320 linkcl = db.getclass(classname)
1321 if linkcl.getprops().has_key('order'):
1322 sort_on = 'order'
1323 else:
1324 sort_on = linkcl.labelprop()
1325 def sortfunc(a, b, linkcl=linkcl, sort_on=sort_on):
1326 return cmp(linkcl.get(a, sort_on), linkcl.get(b, sort_on))
1327 return sortfunc
1329 def handleListCGIValue(value):
1330 ''' Value is either a single item or a list of items. Each item has a
1331 .value that we're actually interested in.
1332 '''
1333 if isinstance(value, type([])):
1334 return [value.value for value in value]
1335 else:
1336 value = value.value.strip()
1337 if not value:
1338 return []
1339 return value.split(',')
1341 class ShowDict:
1342 ''' A convenience access to the :columns index parameters
1343 '''
1344 def __init__(self, columns):
1345 self.columns = {}
1346 for col in columns:
1347 self.columns[col] = 1
1348 def __getitem__(self, name):
1349 return self.columns.has_key(name)
1351 class HTMLRequest:
1352 ''' The *request*, holding the CGI form and environment.
1354 "form" the CGI form as a cgi.FieldStorage
1355 "env" the CGI environment variables
1356 "base" the base URL for this instance
1357 "user" a HTMLUser instance for this user
1358 "classname" the current classname (possibly None)
1359 "template" the current template (suffix, also possibly None)
1361 Index args:
1362 "columns" dictionary of the columns to display in an index page
1363 "show" a convenience access to columns - request/show/colname will
1364 be true if the columns should be displayed, false otherwise
1365 "sort" index sort column (direction, column name)
1366 "group" index grouping property (direction, column name)
1367 "filter" properties to filter the index on
1368 "filterspec" values to filter the index on
1369 "search_text" text to perform a full-text search on for an index
1371 '''
1372 def __init__(self, client):
1373 self.client = client
1375 # easier access vars
1376 self.form = client.form
1377 self.env = client.env
1378 self.base = client.base
1379 self.user = HTMLUser(client, 'user', client.userid)
1381 # store the current class name and action
1382 self.classname = client.classname
1383 self.template = client.template
1385 # the special char to use for special vars
1386 self.special_char = '@'
1388 self._post_init()
1390 def _post_init(self):
1391 ''' Set attributes based on self.form
1392 '''
1393 # extract the index display information from the form
1394 self.columns = []
1395 for name in ':columns @columns'.split():
1396 if self.form.has_key(name):
1397 self.special_char = name[0]
1398 self.columns = handleListCGIValue(self.form[name])
1399 break
1400 self.show = ShowDict(self.columns)
1402 # sorting
1403 self.sort = (None, None)
1404 for name in ':sort @sort'.split():
1405 if self.form.has_key(name):
1406 self.special_char = name[0]
1407 sort = self.form[name].value
1408 if sort.startswith('-'):
1409 self.sort = ('-', sort[1:])
1410 else:
1411 self.sort = ('+', sort)
1412 if self.form.has_key(self.special_char+'sortdir'):
1413 self.sort = ('-', self.sort[1])
1415 # grouping
1416 self.group = (None, None)
1417 for name in ':group @group'.split():
1418 if self.form.has_key(name):
1419 self.special_char = name[0]
1420 group = self.form[name].value
1421 if group.startswith('-'):
1422 self.group = ('-', group[1:])
1423 else:
1424 self.group = ('+', group)
1425 if self.form.has_key(self.special_char+'groupdir'):
1426 self.group = ('-', self.group[1])
1428 # filtering
1429 self.filter = []
1430 for name in ':filter @filter'.split():
1431 if self.form.has_key(name):
1432 self.special_char = name[0]
1433 self.filter = handleListCGIValue(self.form[name])
1435 self.filterspec = {}
1436 db = self.client.db
1437 if self.classname is not None:
1438 props = db.getclass(self.classname).getprops()
1439 for name in self.filter:
1440 if self.form.has_key(name):
1441 prop = props[name]
1442 fv = self.form[name]
1443 if (isinstance(prop, hyperdb.Link) or
1444 isinstance(prop, hyperdb.Multilink)):
1445 self.filterspec[name] = lookupIds(db, prop,
1446 handleListCGIValue(fv))
1447 else:
1448 self.filterspec[name] = fv.value
1450 # full-text search argument
1451 self.search_text = None
1452 for name in ':search_text @search_text'.split():
1453 if self.form.has_key(name):
1454 self.special_char = name[0]
1455 self.search_text = self.form[name].value
1457 # pagination - size and start index
1458 # figure batch args
1459 self.pagesize = 50
1460 for name in ':pagesize @pagesize'.split():
1461 if self.form.has_key(name):
1462 self.special_char = name[0]
1463 self.pagesize = int(self.form[name].value)
1465 self.startwith = 0
1466 for name in ':startwith @startwith'.split():
1467 if self.form.has_key(name):
1468 self.special_char = name[0]
1469 self.startwith = int(self.form[name].value)
1471 def updateFromURL(self, url):
1472 ''' Parse the URL for query args, and update my attributes using the
1473 values.
1474 '''
1475 self.form = {}
1476 for name, value in cgi.parse_qsl(url):
1477 if self.form.has_key(name):
1478 if isinstance(self.form[name], type([])):
1479 self.form[name].append(cgi.MiniFieldStorage(name, value))
1480 else:
1481 self.form[name] = [self.form[name],
1482 cgi.MiniFieldStorage(name, value)]
1483 else:
1484 self.form[name] = cgi.MiniFieldStorage(name, value)
1485 self._post_init()
1487 def update(self, kwargs):
1488 ''' Update my attributes using the keyword args
1489 '''
1490 self.__dict__.update(kwargs)
1491 if kwargs.has_key('columns'):
1492 self.show = ShowDict(self.columns)
1494 def description(self):
1495 ''' Return a description of the request - handle for the page title.
1496 '''
1497 s = [self.client.db.config.TRACKER_NAME]
1498 if self.classname:
1499 if self.client.nodeid:
1500 s.append('- %s%s'%(self.classname, self.client.nodeid))
1501 else:
1502 if self.template == 'item':
1503 s.append('- new %s'%self.classname)
1504 elif self.template == 'index':
1505 s.append('- %s index'%self.classname)
1506 else:
1507 s.append('- %s %s'%(self.classname, self.template))
1508 else:
1509 s.append('- home')
1510 return ' '.join(s)
1512 def __str__(self):
1513 d = {}
1514 d.update(self.__dict__)
1515 f = ''
1516 for k in self.form.keys():
1517 f += '\n %r=%r'%(k,handleListCGIValue(self.form[k]))
1518 d['form'] = f
1519 e = ''
1520 for k,v in self.env.items():
1521 e += '\n %r=%r'%(k, v)
1522 d['env'] = e
1523 return '''
1524 form: %(form)s
1525 base: %(base)r
1526 classname: %(classname)r
1527 template: %(template)r
1528 columns: %(columns)r
1529 sort: %(sort)r
1530 group: %(group)r
1531 filter: %(filter)r
1532 search_text: %(search_text)r
1533 pagesize: %(pagesize)r
1534 startwith: %(startwith)r
1535 env: %(env)s
1536 '''%d
1538 def indexargs_form(self, columns=1, sort=1, group=1, filter=1,
1539 filterspec=1):
1540 ''' return the current index args as form elements '''
1541 l = []
1542 sc = self.special_char
1543 s = '<input type="hidden" name="%s" value="%s">'
1544 if columns and self.columns:
1545 l.append(s%(sc+'columns', ','.join(self.columns)))
1546 if sort and self.sort[1] is not None:
1547 if self.sort[0] == '-':
1548 val = '-'+self.sort[1]
1549 else:
1550 val = self.sort[1]
1551 l.append(s%(sc+'sort', val))
1552 if group and self.group[1] is not None:
1553 if self.group[0] == '-':
1554 val = '-'+self.group[1]
1555 else:
1556 val = self.group[1]
1557 l.append(s%(sc+'group', val))
1558 if filter and self.filter:
1559 l.append(s%(sc+'filter', ','.join(self.filter)))
1560 if filterspec:
1561 for k,v in self.filterspec.items():
1562 if type(v) == type([]):
1563 l.append(s%(k, ','.join(v)))
1564 else:
1565 l.append(s%(k, v))
1566 if self.search_text:
1567 l.append(s%(sc+'search_text', self.search_text))
1568 l.append(s%(sc+'pagesize', self.pagesize))
1569 l.append(s%(sc+'startwith', self.startwith))
1570 return '\n'.join(l)
1572 def indexargs_url(self, url, args):
1573 ''' embed the current index args in a URL '''
1574 sc = self.special_char
1575 l = ['%s=%s'%(k,v) for k,v in args.items()]
1576 if self.columns and not args.has_key(':columns'):
1577 l.append(sc+'columns=%s'%(','.join(self.columns)))
1578 if self.sort[1] is not None and not args.has_key(':sort'):
1579 if self.sort[0] == '-':
1580 val = '-'+self.sort[1]
1581 else:
1582 val = self.sort[1]
1583 l.append(sc+'sort=%s'%val)
1584 if self.group[1] is not None and not args.has_key(':group'):
1585 if self.group[0] == '-':
1586 val = '-'+self.group[1]
1587 else:
1588 val = self.group[1]
1589 l.append(sc+'group=%s'%val)
1590 if self.filter and not args.has_key(':filter'):
1591 l.append(sc+'filter=%s'%(','.join(self.filter)))
1592 for k,v in self.filterspec.items():
1593 if not args.has_key(k):
1594 if type(v) == type([]):
1595 l.append('%s=%s'%(k, ','.join(v)))
1596 else:
1597 l.append('%s=%s'%(k, v))
1598 if self.search_text and not args.has_key(':search_text'):
1599 l.append(sc+'search_text=%s'%self.search_text)
1600 if not args.has_key(':pagesize'):
1601 l.append(sc+'pagesize=%s'%self.pagesize)
1602 if not args.has_key(':startwith'):
1603 l.append(sc+'startwith=%s'%self.startwith)
1604 return '%s?%s'%(url, '&'.join(l))
1605 indexargs_href = indexargs_url
1607 def base_javascript(self):
1608 return '''
1609 <script language="javascript">
1610 submitted = false;
1611 function submit_once() {
1612 if (submitted) {
1613 alert("Your request is being processed.\\nPlease be patient.");
1614 return 0;
1615 }
1616 submitted = true;
1617 return 1;
1618 }
1620 function help_window(helpurl, width, height) {
1621 HelpWin = window.open('%s' + helpurl, 'RoundupHelpWindow', 'scrollbars=yes,resizable=yes,toolbar=no,height='+height+',width='+width);
1622 }
1623 </script>
1624 '''%self.base
1626 def batch(self):
1627 ''' Return a batch object for results from the "current search"
1628 '''
1629 filterspec = self.filterspec
1630 sort = self.sort
1631 group = self.group
1633 # get the list of ids we're batching over
1634 klass = self.client.db.getclass(self.classname)
1635 if self.search_text:
1636 matches = self.client.db.indexer.search(
1637 re.findall(r'\b\w{2,25}\b', self.search_text), klass)
1638 else:
1639 matches = None
1640 l = klass.filter(matches, filterspec, sort, group)
1642 # return the batch object, using IDs only
1643 return Batch(self.client, l, self.pagesize, self.startwith,
1644 classname=self.classname)
1646 # extend the standard ZTUtils Batch object to remove dependency on
1647 # Acquisition and add a couple of useful methods
1648 class Batch(ZTUtils.Batch):
1649 ''' Use me to turn a list of items, or item ids of a given class, into a
1650 series of batches.
1652 ========= ========================================================
1653 Parameter Usage
1654 ========= ========================================================
1655 sequence a list of HTMLItems or item ids
1656 classname if sequence is a list of ids, this is the class of item
1657 size how big to make the sequence.
1658 start where to start (0-indexed) in the sequence.
1659 end where to end (0-indexed) in the sequence.
1660 orphan if the next batch would contain less items than this
1661 value, then it is combined with this batch
1662 overlap the number of items shared between adjacent batches
1663 ========= ========================================================
1665 Attributes: Note that the "start" attribute, unlike the
1666 argument, is a 1-based index (I know, lame). "first" is the
1667 0-based index. "length" is the actual number of elements in
1668 the batch.
1670 "sequence_length" is the length of the original, unbatched, sequence.
1671 '''
1672 def __init__(self, client, sequence, size, start, end=0, orphan=0,
1673 overlap=0, classname=None):
1674 self.client = client
1675 self.last_index = self.last_item = None
1676 self.current_item = None
1677 self.classname = classname
1678 self.sequence_length = len(sequence)
1679 ZTUtils.Batch.__init__(self, sequence, size, start, end, orphan,
1680 overlap)
1682 # overwrite so we can late-instantiate the HTMLItem instance
1683 def __getitem__(self, index):
1684 if index < 0:
1685 if index + self.end < self.first: raise IndexError, index
1686 return self._sequence[index + self.end]
1688 if index >= self.length:
1689 raise IndexError, index
1691 # move the last_item along - but only if the fetched index changes
1692 # (for some reason, index 0 is fetched twice)
1693 if index != self.last_index:
1694 self.last_item = self.current_item
1695 self.last_index = index
1697 item = self._sequence[index + self.first]
1698 if self.classname:
1699 # map the item ids to instances
1700 if self.classname == 'user':
1701 item = HTMLUser(self.client, self.classname, item)
1702 else:
1703 item = HTMLItem(self.client, self.classname, item)
1704 self.current_item = item
1705 return item
1707 def propchanged(self, property):
1708 ''' Detect if the property marked as being the group property
1709 changed in the last iteration fetch
1710 '''
1711 if (self.last_item is None or
1712 self.last_item[property] != self.current_item[property]):
1713 return 1
1714 return 0
1716 # override these 'cos we don't have access to acquisition
1717 def previous(self):
1718 if self.start == 1:
1719 return None
1720 return Batch(self.client, self._sequence, self._size,
1721 self.first - self._size + self.overlap, 0, self.orphan,
1722 self.overlap)
1724 def next(self):
1725 try:
1726 self._sequence[self.end]
1727 except IndexError:
1728 return None
1729 return Batch(self.client, self._sequence, self._size,
1730 self.end - self.overlap, 0, self.orphan, self.overlap)
1732 class TemplatingUtils:
1733 ''' Utilities for templating
1734 '''
1735 def __init__(self, client):
1736 self.client = client
1737 def Batch(self, sequence, size, start, end=0, orphan=0, overlap=0):
1738 return Batch(self.client, sequence, size, start, end, orphan,
1739 overlap)