Code

fixed various URL / base URL issues
[roundup.git] / roundup / cgi / templating.py
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 # XXX WAH pagetemplates aren't pickleable :(
26 #def getTemplate(dir, name, classname=None, request=None):
27 #    ''' Interface to get a template, possibly loading a compiled template.
28 #    '''
29 #    # source
30 #    src = os.path.join(dir, name)
31 #
32 #    # see if we can get a compile from the template"c" directory (most
33 #    # likely is "htmlc"
34 #    split = list(os.path.split(dir))
35 #    split[-1] = split[-1] + 'c'
36 #    cdir = os.path.join(*split)
37 #    split.append(name)
38 #    cpl = os.path.join(*split)
39 #
40 #    # ok, now see if the source is newer than the compiled (or if the
41 #    # compiled even exists)
42 #    MTIME = os.path.stat.ST_MTIME
43 #    if (not os.path.exists(cpl) or os.stat(cpl)[MTIME] < os.stat(src)[MTIME]):
44 #        # nope, we need to compile
45 #        pt = RoundupPageTemplate()
46 #        pt.write(open(src).read())
47 #        pt.id = name
48 #
49 #        # save off the compiled template
50 #        if not os.path.exists(cdir):
51 #            os.makedirs(cdir)
52 #        f = open(cpl, 'wb')
53 #        pickle.dump(pt, f)
54 #        f.close()
55 #    else:
56 #        # yay, use the compiled template
57 #        f = open(cpl, 'rb')
58 #        pt = pickle.load(f)
59 #    return pt
61 templates = {}
63 class NoTemplate(Exception):
64     pass
66 def getTemplate(dir, name, extension, classname=None, request=None):
67     ''' Interface to get a template, possibly loading a compiled template.
69         "name" and "extension" indicate the template we're after, which in
70         most cases will be "name.extension". If "extension" is None, then
71         we look for a template just called "name" with no extension.
73         If the file "name.extension" doesn't exist, we look for
74         "_generic.extension" as a fallback.
75     '''
76     # default the name to "home"
77     if name is None:
78         name = 'home'
80     # find the source, figure the time it was last modified
81     if extension:
82         filename = '%s.%s'%(name, extension)
83     else:
84         filename = name
85     src = os.path.join(dir, filename)
86     try:
87         stime = os.stat(src)[os.path.stat.ST_MTIME]
88     except os.error, error:
89         if error.errno != errno.ENOENT:
90             raise
91         if not extension:
92             raise NoTemplate, 'Template file "%s" doesn\'t exist'%name
94         # try for a generic template
95         generic = '_generic.%s'%extension
96         src = os.path.join(dir, generic)
97         try:
98             stime = os.stat(src)[os.path.stat.ST_MTIME]
99         except os.error, error:
100             if error.errno != errno.ENOENT:
101                 raise
102             # nicer error
103             raise NoTemplate, 'No template file exists for templating '\
104                 '"%s" with template "%s" (neither "%s" nor "%s")'%(name,
105                 extension, filename, generic)
106         filename = generic
108     key = (dir, filename)
109     if templates.has_key(key) and stime < templates[key].mtime:
110         # compiled template is up to date
111         return templates[key]
113     # compile the template
114     templates[key] = pt = RoundupPageTemplate()
115     pt.write(open(src).read())
116     pt.id = filename
117     pt.mtime = time.time()
118     return pt
120 class RoundupPageTemplate(PageTemplate.PageTemplate):
121     ''' A Roundup-specific PageTemplate.
123         Interrogate the client to set up the various template variables to
124         be available:
126         *context*
127          this is one of three things:
128          1. None - we're viewing a "home" page
129          2. The current class of item being displayed. This is an HTMLClass
130             instance.
131          3. The current item from the database, if we're viewing a specific
132             item, as an HTMLItem instance.
133         *request*
134           Includes information about the current request, including:
135            - the url
136            - the current index information (``filterspec``, ``filter`` args,
137              ``properties``, etc) parsed out of the form. 
138            - methods for easy filterspec link generation
139            - *user*, the current user node as an HTMLItem instance
140            - *form*, the current CGI form information as a FieldStorage
141         *instance*
142           The current instance
143         *db*
144           The current database, through which db.config may be reached.
145     '''
146     def getContext(self, client, classname, request):
147         c = {
148              'options': {},
149              'nothing': None,
150              'request': request,
151              'content': client.content,
152              'db': HTMLDatabase(client),
153              'instance': client.instance,
154              'utils': TemplatingUtils(client),
155         }
156         # add in the item if there is one
157         if client.nodeid:
158             if classname == 'user':
159                 c['context'] = HTMLUser(client, classname, client.nodeid)
160             else:
161                 c['context'] = HTMLItem(client, classname, client.nodeid)
162         else:
163             c['context'] = HTMLClass(client, classname)
164         return c
166     def render(self, client, classname, request, **options):
167         """Render this Page Template"""
169         if not self._v_cooked:
170             self._cook()
172         __traceback_supplement__ = (PageTemplate.PageTemplateTracebackSupplement, self)
174         if self._v_errors:
175             raise PageTemplate.PTRuntimeError, \
176                 'Page Template %s has errors.'%self.id
178         # figure the context
179         classname = classname or client.classname
180         request = request or HTMLRequest(client)
181         c = self.getContext(client, classname, request)
182         c.update({'options': options})
184         # and go
185         output = StringIO.StringIO()
186         TALInterpreter(self._v_program, self._v_macros,
187             getEngine().getContext(c), output, tal=1, strictinsert=0)()
188         return output.getvalue()
190 class HTMLDatabase:
191     ''' Return HTMLClasses for valid class fetches
192     '''
193     def __init__(self, client):
194         self._client = client
196         # we want config to be exposed
197         self.config = client.db.config
199     def __getitem__(self, item):
200         self._client.db.getclass(item)
201         return HTMLClass(self._client, item)
203     def __getattr__(self, attr):
204         try:
205             return self[attr]
206         except KeyError:
207             raise AttributeError, attr
209     def classes(self):
210         l = self._client.db.classes.keys()
211         l.sort()
212         return [HTMLClass(self._client, cn) for cn in l]
214 def lookupIds(db, prop, ids, num_re=re.compile('-?\d+')):
215     cl = db.getclass(prop.classname)
216     l = []
217     for entry in ids:
218         if num_re.match(entry):
219             l.append(entry)
220         else:
221             l.append(cl.lookup(entry))
222     return l
224 class HTMLPermissions:
225     ''' Helpers that provide answers to commonly asked Permission questions.
226     '''
227     def is_edit_ok(self):
228         ''' Is the user allowed to Edit the current class?
229         '''
230         return self._db.security.hasPermission('Edit', self._client.userid,
231             self._classname)
232     def is_view_ok(self):
233         ''' Is the user allowed to View the current class?
234         '''
235         return self._db.security.hasPermission('View', self._client.userid,
236             self._classname)
237     def is_only_view_ok(self):
238         ''' Is the user only allowed to View (ie. not Edit) the current class?
239         '''
240         return self.is_view_ok() and not self.is_edit_ok()
242 class HTMLClass(HTMLPermissions):
243     ''' Accesses through a class (either through *class* or *db.<classname>*)
244     '''
245     def __init__(self, client, classname):
246         self._client = client
247         self._db = client.db
249         # we want classname to be exposed, but _classname gives a
250         # consistent API for extending Class/Item
251         self._classname = self.classname = classname
252         if classname is not None:
253             self._klass = self._db.getclass(self.classname)
254             self._props = self._klass.getprops()
256     def __repr__(self):
257         return '<HTMLClass(0x%x) %s>'%(id(self), self.classname)
259     def __getitem__(self, item):
260         ''' return an HTMLProperty instance
261         '''
262        #print 'HTMLClass.getitem', (self, item)
264         # we don't exist
265         if item == 'id':
266             return None
268         # get the property
269         prop = self._props[item]
271         # look up the correct HTMLProperty class
272         form = self._client.form
273         for klass, htmlklass in propclasses:
274             if not isinstance(prop, klass):
275                 continue
276             if form.has_key(item):
277                 if isinstance(prop, hyperdb.Multilink):
278                     value = lookupIds(self._db, prop,
279                         handleListCGIValue(form[item]))
280                 elif isinstance(prop, hyperdb.Link):
281                     value = form[item].value.strip()
282                     if value:
283                         value = lookupIds(self._db, prop, [value])[0]
284                     else:
285                         value = None
286                 else:
287                     value = form[item].value.strip() or None
288             else:
289                 if isinstance(prop, hyperdb.Multilink):
290                     value = []
291                 else:
292                     value = None
293             return htmlklass(self._client, '', prop, item, value)
295         # no good
296         raise KeyError, item
298     def __getattr__(self, attr):
299         ''' convenience access '''
300         try:
301             return self[attr]
302         except KeyError:
303             raise AttributeError, attr
305     def properties(self):
306         ''' Return HTMLProperty for all of this class' properties.
307         '''
308         l = []
309         for name, prop in self._props.items():
310             for klass, htmlklass in propclasses:
311                 if isinstance(prop, hyperdb.Multilink):
312                     value = []
313                 else:
314                     value = None
315                 if isinstance(prop, klass):
316                     l.append(htmlklass(self._client, '', prop, name, value))
317         return l
319     def list(self):
320         ''' List all items in this class.
321         '''
322         if self.classname == 'user':
323             klass = HTMLUser
324         else:
325             klass = HTMLItem
327         # get the list and sort it nicely
328         l = self._klass.list()
329         sortfunc = make_sort_function(self._db, self._prop.classname)
330         l.sort(sortfunc)
332         l = [klass(self._client, self.classname, x) for x in l]
333         return l
335     def csv(self):
336         ''' Return the items of this class as a chunk of CSV text.
337         '''
338         # get the CSV module
339         try:
340             import csv
341         except ImportError:
342             return 'Sorry, you need the csv module to use this function.\n'\
343                 'Get it from: http://www.object-craft.com.au/projects/csv/'
345         props = self.propnames()
346         p = csv.parser()
347         s = StringIO.StringIO()
348         s.write(p.join(props) + '\n')
349         for nodeid in self._klass.list():
350             l = []
351             for name in props:
352                 value = self._klass.get(nodeid, name)
353                 if value is None:
354                     l.append('')
355                 elif isinstance(value, type([])):
356                     l.append(':'.join(map(str, value)))
357                 else:
358                     l.append(str(self._klass.get(nodeid, name)))
359             s.write(p.join(l) + '\n')
360         return s.getvalue()
362     def propnames(self):
363         ''' Return the list of the names of the properties of this class.
364         '''
365         idlessprops = self._klass.getprops(protected=0).keys()
366         idlessprops.sort()
367         return ['id'] + idlessprops
369     def filter(self, request=None):
370         ''' Return a list of items from this class, filtered and sorted
371             by the current requested filterspec/filter/sort/group args
372         '''
373         if request is not None:
374             filterspec = request.filterspec
375             sort = request.sort
376             group = request.group
377         if self.classname == 'user':
378             klass = HTMLUser
379         else:
380             klass = HTMLItem
381         l = [klass(self._client, self.classname, x)
382              for x in self._klass.filter(None, filterspec, sort, group)]
383         return l
385     def classhelp(self, properties=None, label='list', width='500',
386             height='400'):
387         ''' Pop up a javascript window with class help
389             This generates a link to a popup window which displays the 
390             properties indicated by "properties" of the class named by
391             "classname". The "properties" should be a comma-separated list
392             (eg. 'id,name,description'). Properties defaults to all the
393             properties of a class (excluding id, creator, created and
394             activity).
396             You may optionally override the label displayed, the width and
397             height. The popup window will be resizable and scrollable.
398         '''
399         if properties is None:
400             properties = self._klass.getprops(protected=0).keys()
401             properties.sort()
402             properties = ','.join(properties)
403         return '<a href="javascript:help_window(\'%s?:template=help&' \
404             ':contentonly=1&properties=%s\', \'%s\', \'%s\')"><b>'\
405             '(%s)</b></a>'%(self.classname, properties, width, height, label)
407     def submit(self, label="Submit New Entry"):
408         ''' Generate a submit button (and action hidden element)
409         '''
410         return '  <input type="hidden" name=":action" value="new">\n'\
411         '  <input type="submit" name="submit" value="%s">'%label
413     def history(self):
414         return 'New node - no history'
416     def renderWith(self, name, **kwargs):
417         ''' Render this class with the given template.
418         '''
419         # create a new request and override the specified args
420         req = HTMLRequest(self._client)
421         req.classname = self.classname
422         req.update(kwargs)
424         # new template, using the specified classname and request
425         pt = getTemplate(self._db.config.TEMPLATES, self.classname, name)
427         # use our fabricated request
428         return pt.render(self._client, self.classname, req)
430 class HTMLItem(HTMLPermissions):
431     ''' Accesses through an *item*
432     '''
433     def __init__(self, client, classname, nodeid):
434         self._client = client
435         self._db = client.db
436         self._classname = classname
437         self._nodeid = nodeid
438         self._klass = self._db.getclass(classname)
439         self._props = self._klass.getprops()
441     def __repr__(self):
442         return '<HTMLItem(0x%x) %s %s>'%(id(self), self._classname,
443             self._nodeid)
445     def __getitem__(self, item):
446         ''' return an HTMLProperty instance
447         '''
448        #print 'HTMLItem.getitem', (self, item)
449         if item == 'id':
450             return self._nodeid
452         # get the property
453         prop = self._props[item]
455         # get the value, handling missing values
456         value = self._klass.get(self._nodeid, item, None)
457         if value is None:
458             if isinstance(self._props[item], hyperdb.Multilink):
459                 value = []
461         # look up the correct HTMLProperty class
462         for klass, htmlklass in propclasses:
463             if isinstance(prop, klass):
464                 return htmlklass(self._client, self._nodeid, prop, item, value)
466         raise KeyErorr, item
468     def __getattr__(self, attr):
469         ''' convenience access to properties '''
470         try:
471             return self[attr]
472         except KeyError:
473             raise AttributeError, attr
474     
475     def submit(self, label="Submit Changes"):
476         ''' Generate a submit button (and action hidden element)
477         '''
478         return '  <input type="hidden" name=":action" value="edit">\n'\
479         '  <input type="submit" name="submit" value="%s">'%label
481     def journal(self, direction='descending'):
482         ''' Return a list of HTMLJournalEntry instances.
483         '''
484         # XXX do this
485         return []
487     def history(self, direction='descending'):
488         l = ['<table class="history">'
489              '<tr><th colspan="4" class="header">',
490              _('History'),
491              '</th></tr><tr>',
492              _('<th>Date</th>'),
493              _('<th>User</th>'),
494              _('<th>Action</th>'),
495              _('<th>Args</th>'),
496             '</tr>']
497         comments = {}
498         history = self._klass.history(self._nodeid)
499         history.sort()
500         if direction == 'descending':
501             history.reverse()
502         for id, evt_date, user, action, args in history:
503             date_s = str(evt_date).replace("."," ")
504             arg_s = ''
505             if action == 'link' and type(args) == type(()):
506                 if len(args) == 3:
507                     linkcl, linkid, key = args
508                     arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
509                         linkcl, linkid, key)
510                 else:
511                     arg_s = str(args)
513             elif action == 'unlink' and type(args) == type(()):
514                 if len(args) == 3:
515                     linkcl, linkid, key = args
516                     arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
517                         linkcl, linkid, key)
518                 else:
519                     arg_s = str(args)
521             elif type(args) == type({}):
522                 cell = []
523                 for k in args.keys():
524                     # try to get the relevant property and treat it
525                     # specially
526                     try:
527                         prop = self._props[k]
528                     except KeyError:
529                         prop = None
530                     if prop is not None:
531                         if args[k] and (isinstance(prop, hyperdb.Multilink) or
532                                 isinstance(prop, hyperdb.Link)):
533                             # figure what the link class is
534                             classname = prop.classname
535                             try:
536                                 linkcl = self._db.getclass(classname)
537                             except KeyError:
538                                 labelprop = None
539                                 comments[classname] = _('''The linked class
540                                     %(classname)s no longer exists''')%locals()
541                             labelprop = linkcl.labelprop(1)
542                             hrefable = os.path.exists(
543                                 os.path.join(self._db.config.TEMPLATES,
544                                 classname+'.item'))
546                         if isinstance(prop, hyperdb.Multilink) and \
547                                 len(args[k]) > 0:
548                             ml = []
549                             for linkid in args[k]:
550                                 if isinstance(linkid, type(())):
551                                     sublabel = linkid[0] + ' '
552                                     linkids = linkid[1]
553                                 else:
554                                     sublabel = ''
555                                     linkids = [linkid]
556                                 subml = []
557                                 for linkid in linkids:
558                                     label = classname + linkid
559                                     # if we have a label property, try to use it
560                                     # TODO: test for node existence even when
561                                     # there's no labelprop!
562                                     try:
563                                         if labelprop is not None:
564                                             label = linkcl.get(linkid, labelprop)
565                                     except IndexError:
566                                         comments['no_link'] = _('''<strike>The
567                                             linked node no longer
568                                             exists</strike>''')
569                                         subml.append('<strike>%s</strike>'%label)
570                                     else:
571                                         if hrefable:
572                                             subml.append('<a href="%s%s">%s</a>'%(
573                                                 classname, linkid, label))
574                                 ml.append(sublabel + ', '.join(subml))
575                             cell.append('%s:\n  %s'%(k, ', '.join(ml)))
576                         elif isinstance(prop, hyperdb.Link) and args[k]:
577                             label = classname + args[k]
578                             # if we have a label property, try to use it
579                             # TODO: test for node existence even when
580                             # there's no labelprop!
581                             if labelprop is not None:
582                                 try:
583                                     label = linkcl.get(args[k], labelprop)
584                                 except IndexError:
585                                     comments['no_link'] = _('''<strike>The
586                                         linked node no longer
587                                         exists</strike>''')
588                                     cell.append(' <strike>%s</strike>,\n'%label)
589                                     # "flag" this is done .... euwww
590                                     label = None
591                             if label is not None:
592                                 if hrefable:
593                                     cell.append('%s: <a href="%s%s">%s</a>\n'%(k,
594                                         classname, args[k], label))
595                                 else:
596                                     cell.append('%s: %s' % (k,label))
598                         elif isinstance(prop, hyperdb.Date) and args[k]:
599                             d = date.Date(args[k])
600                             cell.append('%s: %s'%(k, str(d)))
602                         elif isinstance(prop, hyperdb.Interval) and args[k]:
603                             d = date.Interval(args[k])
604                             cell.append('%s: %s'%(k, str(d)))
606                         elif isinstance(prop, hyperdb.String) and args[k]:
607                             cell.append('%s: %s'%(k, cgi.escape(args[k])))
609                         elif not args[k]:
610                             cell.append('%s: (no value)\n'%k)
612                         else:
613                             cell.append('%s: %s\n'%(k, str(args[k])))
614                     else:
615                         # property no longer exists
616                         comments['no_exist'] = _('''<em>The indicated property
617                             no longer exists</em>''')
618                         cell.append('<em>%s: %s</em>\n'%(k, str(args[k])))
619                 arg_s = '<br />'.join(cell)
620             else:
621                 # unkown event!!
622                 comments['unknown'] = _('''<strong><em>This event is not
623                     handled by the history display!</em></strong>''')
624                 arg_s = '<strong><em>' + str(args) + '</em></strong>'
625             date_s = date_s.replace(' ', '&nbsp;')
626             l.append('<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>'%(
627                 date_s, user, action, arg_s))
628         if comments:
629             l.append(_('<tr><td colspan=4><strong>Note:</strong></td></tr>'))
630         for entry in comments.values():
631             l.append('<tr><td colspan=4>%s</td></tr>'%entry)
632         l.append('</table>')
633         return '\n'.join(l)
635     def renderQueryForm(self):
636         ''' Render this item, which is a query, as a search form.
637         '''
638         # create a new request and override the specified args
639         req = HTMLRequest(self._client)
640         req.classname = self._klass.get(self._nodeid, 'klass')
641         req.updateFromURL(self._klass.get(self._nodeid, 'url'))
643         # new template, using the specified classname and request
644         pt = getTemplate(self._db.config.TEMPLATES, req.classname, 'search')
646         # use our fabricated request
647         return pt.render(self._client, req.classname, req)
649 class HTMLUser(HTMLItem):
650     ''' Accesses through the *user* (a special case of item)
651     '''
652     def __init__(self, client, classname, nodeid):
653         HTMLItem.__init__(self, client, 'user', nodeid)
654         self._default_classname = client.classname
656         # used for security checks
657         self._security = client.db.security
659     _marker = []
660     def hasPermission(self, role, classname=_marker):
661         ''' Determine if the user has the Role.
663             The class being tested defaults to the template's class, but may
664             be overidden for this test by suppling an alternate classname.
665         '''
666         if classname is self._marker:
667             classname = self._default_classname
668         return self._security.hasPermission(role, self._nodeid, classname)
670     def is_edit_ok(self):
671         ''' Is the user allowed to Edit the current class?
672             Also check whether this is the current user's info.
673         '''
674         return self._db.security.hasPermission('Edit', self._client.userid,
675             self._classname) or self._nodeid == self._client.userid
677     def is_view_ok(self):
678         ''' Is the user allowed to View the current class?
679             Also check whether this is the current user's info.
680         '''
681         return self._db.security.hasPermission('Edit', self._client.userid,
682             self._classname) or self._nodeid == self._client.userid
684 class HTMLProperty:
685     ''' String, Number, Date, Interval HTMLProperty
687         Has useful attributes:
689          _name  the name of the property
690          _value the value of the property if any
692         A wrapper object which may be stringified for the plain() behaviour.
693     '''
694     def __init__(self, client, nodeid, prop, name, value):
695         self._client = client
696         self._db = client.db
697         self._nodeid = nodeid
698         self._prop = prop
699         self._name = name
700         self._value = value
701     def __repr__(self):
702         return '<HTMLProperty(0x%x) %s %r %r>'%(id(self), self._name, self._prop, self._value)
703     def __str__(self):
704         return self.plain()
705     def __cmp__(self, other):
706         if isinstance(other, HTMLProperty):
707             return cmp(self._value, other._value)
708         return cmp(self._value, other)
710 class StringHTMLProperty(HTMLProperty):
711     def plain(self, escape=0):
712         ''' Render a "plain" representation of the property
713         '''
714         if self._value is None:
715             return ''
716         if escape:
717             return cgi.escape(str(self._value))
718         return str(self._value)
720     def stext(self, escape=0):
721         ''' Render the value of the property as StructuredText.
723             This requires the StructureText module to be installed separately.
724         '''
725         s = self.plain(escape=escape)
726         if not StructuredText:
727             return s
728         return StructuredText(s,level=1,header=0)
730     def field(self, size = 30):
731         ''' Render a form edit field for the property
732         '''
733         if self._value is None:
734             value = ''
735         else:
736             value = cgi.escape(str(self._value))
737             value = '&quot;'.join(value.split('"'))
738         return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
740     def multiline(self, escape=0, rows=5, cols=40):
741         ''' Render a multiline form edit field for the property
742         '''
743         if self._value is None:
744             value = ''
745         else:
746             value = cgi.escape(str(self._value))
747             value = '&quot;'.join(value.split('"'))
748         return '<textarea name="%s" rows="%s" cols="%s">%s</textarea>'%(
749             self._name, rows, cols, value)
751     def email(self, escape=1):
752         ''' Render the value of the property as an obscured email address
753         '''
754         if self._value is None: value = ''
755         else: value = str(self._value)
756         if value.find('@') != -1:
757             name, domain = value.split('@')
758             domain = ' '.join(domain.split('.')[:-1])
759             name = name.replace('.', ' ')
760             value = '%s at %s ...'%(name, domain)
761         else:
762             value = value.replace('.', ' ')
763         if escape:
764             value = cgi.escape(value)
765         return value
767 class PasswordHTMLProperty(HTMLProperty):
768     def plain(self):
769         ''' Render a "plain" representation of the property
770         '''
771         if self._value is None:
772             return ''
773         return _('*encrypted*')
775     def field(self, size = 30):
776         ''' Render a form edit field for the property.
777         '''
778         return '<input type="password" name="%s" size="%s">'%(self._name, size)
780     def confirm(self, size = 30):
781         ''' Render a second form edit field for the property, used for 
782             confirmation that the user typed the password correctly. Generates
783             a field with name "name:confirm".
784         '''
785         return '<input type="password" name="%s:confirm" size="%s">'%(
786             self._name, size)
788 class NumberHTMLProperty(HTMLProperty):
789     def plain(self):
790         ''' Render a "plain" representation of the property
791         '''
792         return str(self._value)
794     def field(self, size = 30):
795         ''' Render a form edit field for the property
796         '''
797         if self._value is None:
798             value = ''
799         else:
800             value = cgi.escape(str(self._value))
801             value = '&quot;'.join(value.split('"'))
802         return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
804 class BooleanHTMLProperty(HTMLProperty):
805     def plain(self):
806         ''' Render a "plain" representation of the property
807         '''
808         if self.value is None:
809             return ''
810         return self._value and "Yes" or "No"
812     def field(self):
813         ''' Render a form edit field for the property
814         '''
815         checked = self._value and "checked" or ""
816         s = '<input type="radio" name="%s" value="yes" %s>Yes'%(self._name,
817             checked)
818         if checked:
819             checked = ""
820         else:
821             checked = "checked"
822         s += '<input type="radio" name="%s" value="no" %s>No'%(self._name,
823             checked)
824         return s
826 class DateHTMLProperty(HTMLProperty):
827     def plain(self):
828         ''' Render a "plain" representation of the property
829         '''
830         if self._value is None:
831             return ''
832         return str(self._value)
834     def field(self, size = 30):
835         ''' Render a form edit field for the property
836         '''
837         if self._value is None:
838             value = ''
839         else:
840             value = cgi.escape(str(self._value))
841             value = '&quot;'.join(value.split('"'))
842         return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
844     def reldate(self, pretty=1):
845         ''' Render the interval between the date and now.
847             If the "pretty" flag is true, then make the display pretty.
848         '''
849         if not self._value:
850             return ''
852         # figure the interval
853         interval = date.Date('.') - self._value
854         if pretty:
855             return interval.pretty()
856         return str(interval)
858 class IntervalHTMLProperty(HTMLProperty):
859     def plain(self):
860         ''' Render a "plain" representation of the property
861         '''
862         if self._value is None:
863             return ''
864         return str(self._value)
866     def pretty(self):
867         ''' Render the interval in a pretty format (eg. "yesterday")
868         '''
869         return self._value.pretty()
871     def field(self, size = 30):
872         ''' Render a form edit field for the property
873         '''
874         if self._value is None:
875             value = ''
876         else:
877             value = cgi.escape(str(self._value))
878             value = '&quot;'.join(value.split('"'))
879         return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
881 class LinkHTMLProperty(HTMLProperty):
882     ''' Link HTMLProperty
883         Include the above as well as being able to access the class
884         information. Stringifying the object itself results in the value
885         from the item being displayed. Accessing attributes of this object
886         result in the appropriate entry from the class being queried for the
887         property accessed (so item/assignedto/name would look up the user
888         entry identified by the assignedto property on item, and then the
889         name property of that user)
890     '''
891     def __getattr__(self, attr):
892         ''' return a new HTMLItem '''
893        #print 'Link.getattr', (self, attr, self._value)
894         if not self._value:
895             raise AttributeError, "Can't access missing value"
896         if self._prop.classname == 'user':
897             klass = HTMLUser
898         else:
899             klass = HTMLItem
900         i = klass(self._client, self._prop.classname, self._value)
901         return getattr(i, attr)
903     def plain(self, escape=0):
904         ''' Render a "plain" representation of the property
905         '''
906         if self._value is None:
907             return ''
908         linkcl = self._db.classes[self._prop.classname]
909         k = linkcl.labelprop(1)
910         value = str(linkcl.get(self._value, k))
911         if escape:
912             value = cgi.escape(value)
913         return value
915     def field(self, showid=0, size=None):
916         ''' Render a form edit field for the property
917         '''
918         linkcl = self._db.getclass(self._prop.classname)
919         if linkcl.getprops().has_key('order'):  
920             sort_on = 'order'  
921         else:  
922             sort_on = linkcl.labelprop()  
923         options = linkcl.filter(None, {}, ('+', sort_on), (None, None))
924         # TODO: make this a field display, not a menu one!
925         l = ['<select name="%s">'%self._name]
926         k = linkcl.labelprop(1)
927         if self._value is None:
928             s = 'selected '
929         else:
930             s = ''
931         l.append(_('<option %svalue="-1">- no selection -</option>')%s)
932         for optionid in options:
933             option = linkcl.get(optionid, k)
934             s = ''
935             if optionid == self._value:
936                 s = 'selected '
937             if showid:
938                 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
939             else:
940                 lab = option
941             if size is not None and len(lab) > size:
942                 lab = lab[:size-3] + '...'
943             lab = cgi.escape(lab)
944             l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
945         l.append('</select>')
946         return '\n'.join(l)
948     def menu(self, size=None, height=None, showid=0, additional=[],
949             **conditions):
950         ''' Render a form select list for this property
951         '''
952         value = self._value
954         # sort function
955         sortfunc = make_sort_function(self._db, self._prop.classname)
957         # force the value to be a single choice
958         if isinstance(value, type('')):
959             value = value[0]
960         linkcl = self._db.getclass(self._prop.classname)
961         l = ['<select name="%s">'%self._name]
962         k = linkcl.labelprop(1)
963         s = ''
964         if value is None:
965             s = 'selected '
966         l.append(_('<option %svalue="-1">- no selection -</option>')%s)
967         if linkcl.getprops().has_key('order'):  
968             sort_on = ('+', 'order')
969         else:  
970             sort_on = ('+', linkcl.labelprop())
971         options = linkcl.filter(None, conditions, sort_on, (None, None))
972         for optionid in options:
973             option = linkcl.get(optionid, k)
974             s = ''
975             if value in [optionid, option]:
976                 s = 'selected '
977             if showid:
978                 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
979             else:
980                 lab = option
981             if size is not None and len(lab) > size:
982                 lab = lab[:size-3] + '...'
983             if additional:
984                 m = []
985                 for propname in additional:
986                     m.append(linkcl.get(optionid, propname))
987                 lab = lab + ' (%s)'%', '.join(map(str, m))
988             lab = cgi.escape(lab)
989             l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
990         l.append('</select>')
991         return '\n'.join(l)
992 #    def checklist(self, ...)
994 class MultilinkHTMLProperty(HTMLProperty):
995     ''' Multilink HTMLProperty
997         Also be iterable, returning a wrapper object like the Link case for
998         each entry in the multilink.
999     '''
1000     def __len__(self):
1001         ''' length of the multilink '''
1002         return len(self._value)
1004     def __getattr__(self, attr):
1005         ''' no extended attribute accesses make sense here '''
1006         raise AttributeError, attr
1008     def __getitem__(self, num):
1009         ''' iterate and return a new HTMLItem
1010         '''
1011        #print 'Multi.getitem', (self, num)
1012         value = self._value[num]
1013         if self._prop.classname == 'user':
1014             klass = HTMLUser
1015         else:
1016             klass = HTMLItem
1017         return klass(self._client, self._prop.classname, value)
1019     def __contains__(self, value):
1020         ''' Support the "in" operator
1021         '''
1022         return value in self._value
1024     def reverse(self):
1025         ''' return the list in reverse order
1026         '''
1027         l = self._value[:]
1028         l.reverse()
1029         if self._prop.classname == 'user':
1030             klass = HTMLUser
1031         else:
1032             klass = HTMLItem
1033         return [klass(self._client, self._prop.classname, value) for value in l]
1035     def plain(self, escape=0):
1036         ''' Render a "plain" representation of the property
1037         '''
1038         linkcl = self._db.classes[self._prop.classname]
1039         k = linkcl.labelprop(1)
1040         labels = []
1041         for v in self._value:
1042             labels.append(linkcl.get(v, k))
1043         value = ', '.join(labels)
1044         if escape:
1045             value = cgi.escape(value)
1046         return value
1048     def field(self, size=30, showid=0):
1049         ''' Render a form edit field for the property
1050         '''
1051         sortfunc = make_sort_function(self._db, self._prop.classname)
1052         linkcl = self._db.getclass(self._prop.classname)
1053         value = self._value[:]
1054         if value:
1055             value.sort(sortfunc)
1056         # map the id to the label property
1057         if not linkcl.getkey():
1058             showid=1
1059         if not showid:
1060             k = linkcl.labelprop(1)
1061             value = [linkcl.get(v, k) for v in value]
1062         value = cgi.escape(','.join(value))
1063         return '<input name="%s" size="%s" value="%s">'%(self._name, size, value)
1065     def menu(self, size=None, height=None, showid=0, additional=[],
1066             **conditions):
1067         ''' Render a form select list for this property
1068         '''
1069         value = self._value
1071         # sort function
1072         sortfunc = make_sort_function(self._db, self._prop.classname)
1074         linkcl = self._db.getclass(self._prop.classname)
1075         if linkcl.getprops().has_key('order'):  
1076             sort_on = ('+', 'order')
1077         else:  
1078             sort_on = ('+', linkcl.labelprop())
1079         options = linkcl.filter(None, conditions, sort_on, (None,None)) 
1080         height = height or min(len(options), 7)
1081         l = ['<select multiple name="%s" size="%s">'%(self._name, height)]
1082         k = linkcl.labelprop(1)
1083         for optionid in options:
1084             option = linkcl.get(optionid, k)
1085             s = ''
1086             if optionid in value or option in value:
1087                 s = 'selected '
1088             if showid:
1089                 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1090             else:
1091                 lab = option
1092             if size is not None and len(lab) > size:
1093                 lab = lab[:size-3] + '...'
1094             if additional:
1095                 m = []
1096                 for propname in additional:
1097                     m.append(linkcl.get(optionid, propname))
1098                 lab = lab + ' (%s)'%', '.join(m)
1099             lab = cgi.escape(lab)
1100             l.append('<option %svalue="%s">%s</option>'%(s, optionid,
1101                 lab))
1102         l.append('</select>')
1103         return '\n'.join(l)
1105 # set the propclasses for HTMLItem
1106 propclasses = (
1107     (hyperdb.String, StringHTMLProperty),
1108     (hyperdb.Number, NumberHTMLProperty),
1109     (hyperdb.Boolean, BooleanHTMLProperty),
1110     (hyperdb.Date, DateHTMLProperty),
1111     (hyperdb.Interval, IntervalHTMLProperty),
1112     (hyperdb.Password, PasswordHTMLProperty),
1113     (hyperdb.Link, LinkHTMLProperty),
1114     (hyperdb.Multilink, MultilinkHTMLProperty),
1117 def make_sort_function(db, classname):
1118     '''Make a sort function for a given class
1119     '''
1120     linkcl = db.getclass(classname)
1121     if linkcl.getprops().has_key('order'):
1122         sort_on = 'order'
1123     else:
1124         sort_on = linkcl.labelprop()
1125     def sortfunc(a, b, linkcl=linkcl, sort_on=sort_on):
1126         return cmp(linkcl.get(a, sort_on), linkcl.get(b, sort_on))
1127     return sortfunc
1129 def handleListCGIValue(value):
1130     ''' Value is either a single item or a list of items. Each item has a
1131         .value that we're actually interested in.
1132     '''
1133     if isinstance(value, type([])):
1134         return [value.value for value in value]
1135     else:
1136         value = value.value.strip()
1137         if not value:
1138             return []
1139         return value.split(',')
1141 class ShowDict:
1142     ''' A convenience access to the :columns index parameters
1143     '''
1144     def __init__(self, columns):
1145         self.columns = {}
1146         for col in columns:
1147             self.columns[col] = 1
1148     def __getitem__(self, name):
1149         return self.columns.has_key(name)
1151 class HTMLRequest:
1152     ''' The *request*, holding the CGI form and environment.
1154         "form" the CGI form as a cgi.FieldStorage
1155         "env" the CGI environment variables
1156         "base" the base URL for this instance
1157         "user" a HTMLUser instance for this user
1158         "classname" the current classname (possibly None)
1159         "template" the current template (suffix, also possibly None)
1161         Index args:
1162         "columns" dictionary of the columns to display in an index page
1163         "show" a convenience access to columns - request/show/colname will
1164                be true if the columns should be displayed, false otherwise
1165         "sort" index sort column (direction, column name)
1166         "group" index grouping property (direction, column name)
1167         "filter" properties to filter the index on
1168         "filterspec" values to filter the index on
1169         "search_text" text to perform a full-text search on for an index
1171     '''
1172     def __init__(self, client):
1173         self.client = client
1175         # easier access vars
1176         self.form = client.form
1177         self.env = client.env
1178         self.base = client.base
1179         self.user = HTMLUser(client, 'user', client.userid)
1181         # store the current class name and action
1182         self.classname = client.classname
1183         self.template = client.template
1185         self._post_init()
1187     def _post_init(self):
1188         ''' Set attributes based on self.form
1189         '''
1190         # extract the index display information from the form
1191         self.columns = []
1192         if self.form.has_key(':columns'):
1193             self.columns = handleListCGIValue(self.form[':columns'])
1194         self.show = ShowDict(self.columns)
1196         # sorting
1197         self.sort = (None, None)
1198         if self.form.has_key(':sort'):
1199             sort = self.form[':sort'].value
1200             if sort.startswith('-'):
1201                 self.sort = ('-', sort[1:])
1202             else:
1203                 self.sort = ('+', sort)
1204         if self.form.has_key(':sortdir'):
1205             self.sort = ('-', self.sort[1])
1207         # grouping
1208         self.group = (None, None)
1209         if self.form.has_key(':group'):
1210             group = self.form[':group'].value
1211             if group.startswith('-'):
1212                 self.group = ('-', group[1:])
1213             else:
1214                 self.group = ('+', group)
1215         if self.form.has_key(':groupdir'):
1216             self.group = ('-', self.group[1])
1218         # filtering
1219         self.filter = []
1220         if self.form.has_key(':filter'):
1221             self.filter = handleListCGIValue(self.form[':filter'])
1222         self.filterspec = {}
1223         if self.classname is not None:
1224             props = self.client.db.getclass(self.classname).getprops()
1225             for name in self.filter:
1226                 if self.form.has_key(name):
1227                     prop = props[name]
1228                     fv = self.form[name]
1229                     if (isinstance(prop, hyperdb.Link) or
1230                             isinstance(prop, hyperdb.Multilink)):
1231                         self.filterspec[name] = handleListCGIValue(fv)
1232                     else:
1233                         self.filterspec[name] = fv.value
1235         # full-text search argument
1236         self.search_text = None
1237         if self.form.has_key(':search_text'):
1238             self.search_text = self.form[':search_text'].value
1240         # pagination - size and start index
1241         # figure batch args
1242         if self.form.has_key(':pagesize'):
1243             self.pagesize = int(self.form[':pagesize'].value)
1244         else:
1245             self.pagesize = 50
1246         if self.form.has_key(':startwith'):
1247             self.startwith = int(self.form[':startwith'].value)
1248         else:
1249             self.startwith = 0
1251     def updateFromURL(self, url):
1252         ''' Parse the URL for query args, and update my attributes using the
1253             values.
1254         ''' 
1255         self.form = {}
1256         for name, value in cgi.parse_qsl(url):
1257             if self.form.has_key(name):
1258                 if isinstance(self.form[name], type([])):
1259                     self.form[name].append(cgi.MiniFieldStorage(name, value))
1260                 else:
1261                     self.form[name] = [self.form[name],
1262                         cgi.MiniFieldStorage(name, value)]
1263             else:
1264                 self.form[name] = cgi.MiniFieldStorage(name, value)
1265         self._post_init()
1267     def update(self, kwargs):
1268         ''' Update my attributes using the keyword args
1269         '''
1270         self.__dict__.update(kwargs)
1271         if kwargs.has_key('columns'):
1272             self.show = ShowDict(self.columns)
1274     def description(self):
1275         ''' Return a description of the request - handle for the page title.
1276         '''
1277         s = [self.client.db.config.TRACKER_NAME]
1278         if self.classname:
1279             if self.client.nodeid:
1280                 s.append('- %s%s'%(self.classname, self.client.nodeid))
1281             else:
1282                 if self.template == 'item':
1283                     s.append('- new %s'%self.classname)
1284                 elif self.template == 'index':
1285                     s.append('- %s index'%self.classname)
1286                 else:
1287                     s.append('- %s %s'%(self.classname, self.template))
1288         else:
1289             s.append('- home')
1290         return ' '.join(s)
1292     def __str__(self):
1293         d = {}
1294         d.update(self.__dict__)
1295         f = ''
1296         for k in self.form.keys():
1297             f += '\n      %r=%r'%(k,handleListCGIValue(self.form[k]))
1298         d['form'] = f
1299         e = ''
1300         for k,v in self.env.items():
1301             e += '\n     %r=%r'%(k, v)
1302         d['env'] = e
1303         return '''
1304 form: %(form)s
1305 url: %(url)r
1306 base: %(base)r
1307 classname: %(classname)r
1308 template: %(template)r
1309 columns: %(columns)r
1310 sort: %(sort)r
1311 group: %(group)r
1312 filter: %(filter)r
1313 search_text: %(search_text)r
1314 pagesize: %(pagesize)r
1315 startwith: %(startwith)r
1316 env: %(env)s
1317 '''%d
1319     def indexargs_form(self, columns=1, sort=1, group=1, filter=1,
1320             filterspec=1):
1321         ''' return the current index args as form elements '''
1322         l = []
1323         s = '<input type="hidden" name="%s" value="%s">'
1324         if columns and self.columns:
1325             l.append(s%(':columns', ','.join(self.columns)))
1326         if sort and self.sort[1] is not None:
1327             if self.sort[0] == '-':
1328                 val = '-'+self.sort[1]
1329             else:
1330                 val = self.sort[1]
1331             l.append(s%(':sort', val))
1332         if group and self.group[1] is not None:
1333             if self.group[0] == '-':
1334                 val = '-'+self.group[1]
1335             else:
1336                 val = self.group[1]
1337             l.append(s%(':group', val))
1338         if filter and self.filter:
1339             l.append(s%(':filter', ','.join(self.filter)))
1340         if filterspec:
1341             for k,v in self.filterspec.items():
1342                 l.append(s%(k, ','.join(v)))
1343         if self.search_text:
1344             l.append(s%(':search_text', self.search_text))
1345         l.append(s%(':pagesize', self.pagesize))
1346         l.append(s%(':startwith', self.startwith))
1347         return '\n'.join(l)
1349     def indexargs_url(self, url, args):
1350         ''' embed the current index args in a URL '''
1351         l = ['%s=%s'%(k,v) for k,v in args.items()]
1352         if self.columns and not args.has_key(':columns'):
1353             l.append(':columns=%s'%(','.join(self.columns)))
1354         if self.sort[1] is not None and not args.has_key(':sort'):
1355             if self.sort[0] == '-':
1356                 val = '-'+self.sort[1]
1357             else:
1358                 val = self.sort[1]
1359             l.append(':sort=%s'%val)
1360         if self.group[1] is not None and not args.has_key(':group'):
1361             if self.group[0] == '-':
1362                 val = '-'+self.group[1]
1363             else:
1364                 val = self.group[1]
1365             l.append(':group=%s'%val)
1366         if self.filter and not args.has_key(':columns'):
1367             l.append(':filter=%s'%(','.join(self.filter)))
1368         for k,v in self.filterspec.items():
1369             if not args.has_key(k):
1370                 l.append('%s=%s'%(k, ','.join(v)))
1371         if self.search_text and not args.has_key(':search_text'):
1372             l.append(':search_text=%s'%self.search_text)
1373         if not args.has_key(':pagesize'):
1374             l.append(':pagesize=%s'%self.pagesize)
1375         if not args.has_key(':startwith'):
1376             l.append(':startwith=%s'%self.startwith)
1377         return '%s?%s'%(url, '&'.join(l))
1378     indexargs_href = indexargs_url
1380     def base_javascript(self):
1381         return '''
1382 <script language="javascript">
1383 submitted = false;
1384 function submit_once() {
1385     if (submitted) {
1386         alert("Your request is being processed.\\nPlease be patient.");
1387         return 0;
1388     }
1389     submitted = true;
1390     return 1;
1393 function help_window(helpurl, width, height) {
1394     HelpWin = window.open('%s/' + helpurl, 'RoundupHelpWindow', 'scrollbars=yes,resizable=yes,toolbar=no,height='+height+',width='+width);
1396 </script>
1397 '''%self.base
1399     def batch(self):
1400         ''' Return a batch object for results from the "current search"
1401         '''
1402         filterspec = self.filterspec
1403         sort = self.sort
1404         group = self.group
1406         # get the list of ids we're batching over
1407         klass = self.client.db.getclass(self.classname)
1408         if self.search_text:
1409             matches = self.client.db.indexer.search(
1410                 re.findall(r'\b\w{2,25}\b', self.search_text), klass)
1411         else:
1412             matches = None
1413         l = klass.filter(matches, filterspec, sort, group)
1415         # map the item ids to instances
1416         if self.classname == 'user':
1417             klass = HTMLUser
1418         else:
1419             klass = HTMLItem
1420         l = [klass(self.client, self.classname, item) for item in l]
1422         # return the batch object
1423         return Batch(self.client, l, self.pagesize, self.startwith)
1425 # extend the standard ZTUtils Batch object to remove dependency on
1426 # Acquisition and add a couple of useful methods
1427 class Batch(ZTUtils.Batch):
1428     ''' Use me to turn a list of items, or item ids of a given class, into a
1429         series of batches.
1431         ========= ========================================================
1432         Parameter  Usage
1433         ========= ========================================================
1434         sequence  a list of HTMLItems
1435         size      how big to make the sequence.
1436         start     where to start (0-indexed) in the sequence.
1437         end       where to end (0-indexed) in the sequence.
1438         orphan    if the next batch would contain less items than this
1439                   value, then it is combined with this batch
1440         overlap   the number of items shared between adjacent batches
1441         ========= ========================================================
1443         Attributes: Note that the "start" attribute, unlike the
1444         argument, is a 1-based index (I know, lame).  "first" is the
1445         0-based index.  "length" is the actual number of elements in
1446         the batch.
1448         "sequence_length" is the length of the original, unbatched, sequence.
1449     '''
1450     def __init__(self, client, sequence, size, start, end=0, orphan=0,
1451             overlap=0):
1452         self.client = client
1453         self.last_index = self.last_item = None
1454         self.current_item = None
1455         self.sequence_length = len(sequence)
1456         ZTUtils.Batch.__init__(self, sequence, size, start, end, orphan,
1457             overlap)
1459     # overwrite so we can late-instantiate the HTMLItem instance
1460     def __getitem__(self, index):
1461         if index < 0:
1462             if index + self.end < self.first: raise IndexError, index
1463             return self._sequence[index + self.end]
1464         
1465         if index >= self.length:
1466             raise IndexError, index
1468         # move the last_item along - but only if the fetched index changes
1469         # (for some reason, index 0 is fetched twice)
1470         if index != self.last_index:
1471             self.last_item = self.current_item
1472             self.last_index = index
1474         self.current_item = self._sequence[index + self.first]
1475         return self.current_item
1477     def propchanged(self, property):
1478         ''' Detect if the property marked as being the group property
1479             changed in the last iteration fetch
1480         '''
1481         if (self.last_item is None or
1482                 self.last_item[property] != self.current_item[property]):
1483             return 1
1484         return 0
1486     # override these 'cos we don't have access to acquisition
1487     def previous(self):
1488         if self.start == 1:
1489             return None
1490         return Batch(self.client, self._sequence, self._size,
1491             self.first - self._size + self.overlap, 0, self.orphan,
1492             self.overlap)
1494     def next(self):
1495         try:
1496             self._sequence[self.end]
1497         except IndexError:
1498             return None
1499         return Batch(self.client, self._sequence, self._size,
1500             self.end - self.overlap, 0, self.orphan, self.overlap)
1502 class TemplatingUtils:
1503     ''' Utilities for templating
1504     '''
1505     def __init__(self, client):
1506         self.client = client
1507     def Batch(self, sequence, size, start, end=0, orphan=0, overlap=0):
1508         return Batch(self.client, sequence, size, start, end, orphan,
1509             overlap)