roundup-1.4.20/0000755000175000017630000000000011754432235011772 5ustar ralfprivroundup-1.4.20/README.txt0000644000175000017630000000252011741527216013467 0ustar ralfpriv======================================================= Roundup: an Issue-Tracking System for Knowledge Workers ======================================================= Copyright (c) 2003-2009 Richard Jones (richard@mechanicalcat.net) Copyright (c) 2002 eKit.com Inc (http://www.ekit.com/) Copyright (c) 2001 Bizar Software Pty Ltd (http://www.bizarsoftware.com.au/) INSTANT GRATIFICATION ===================== The impatient may try Roundup immediately by typing at the console:: python demo.py To start anew (a fresh demo instance):: python demo.py nuke Run demo.py from the *source* directory; don't try to run demo.py from the *installed* directory, it will *break*. Installation ============ For installation instructions, please see installation.txt in the "doc" directory. Upgrading ========= For upgrading instructions, please see upgrading.txt in the "doc" directory. Usage and Other Information =========================== See the index.txt file in the "doc" directory. The *.txt files in the "doc" directory are written in reStructedText. If you have rst2html installed (part of the docutils suite) you can convert these to HTML by running "make html" in the "doc" directory. For Developers ============== To get started on development work, read the developers.txt file in the "doc" directory. License ======= See COPYING.txt roundup-1.4.20/PKG-INFO0000644000175000017630000002274111754432235013075 0ustar ralfprivMetadata-Version: 1.0 Name: roundup Version: 1.4.20 Summary: A simple-to-use and -install issue-tracking system with command-line, web and e-mail interfaces. Highly customisable. Home-page: http://www.roundup-tracker.org Author: Richard Jones Author-email: richard@users.sourceforge.net License: UNKNOWN Download-URL: http://pypi.python.org/pypi/roundup Description: I'm proud to release version 1.4.20 of Roundup which can be seen as a security release. We've fixed several security issues, in particular some XSS issues. We've also dropped support for python 2.4 with this release. This release also introduces some minor features and, as usual, fixes some bugs: Features: - Experimental support for the new Chameleon templating engine. We now have two configurable templating engines, the old Zope TAL templates (called zopetal in the config) and the new Chameleon (called chameleon in the config). A new config-option "template_engine" under [main] can take these config-options, the default is zopetal. Thanks to Cheer Xiao for the idea of making this configurable *and* for the actual implementation! (Ralf) WARNING: Chameleon support is highly experimental and *not* recommended for production use. It has known performance issues and i18n is not yet functioning. It's still under active development. Only use this feature if you want to experiment with Chameleon and/or help with Roundup developement. If you found a bug in Chameleon support, please report after testing against latest Roundup source from the Mercurial repository. - issue2550678: Allow pagesize=-1 which returns all results. Suggested and implemented by John Kristensen. Tested by Satchidanand Haridas. (Bernhard) - Allow to turn off translation of generated html options in menu method of LinkHTMLProperty and MultilinkHTMLProperty -- default is translation as it used to be (Ralf) - Sending of OpenPGP encrypted mail to all users or selected users (via roles) is now working. (Ralf) - Add config-option "nosy" to messages_to_author setting in [nosy] section of config: This will send a message to the author only in the case where the author is on the nosy-list (either added earlier or via the add_author setting). Current config-options for this setting will send / not send to author without considering the nosy list. (Ralf) Fixed: - issue2550730: FAQ has broken link to Zope book. Reported and fixed by John Rouillard.(Bernhard) - issue2550728: remove buggy parentheses in TAL/DummyEngine.py. Reported and fixed by Ralf Hemmecke. (Bernhard) - issue2550715: IndexError when requesting non-existing file via http. Reported and fixed by Cedric Krier. (Bernhard) - issue2550712: exportcsvaction errors poorly when given invalid columns. Reported by Will Kahn-Greene, fixed by Cedric Krier. (Bernhard) - issue2550695: 'No sort or group' settings not retained when editing queries. Reported and fixed by John Kristensen. Tested by Satchidanand Haridas. (Bernhard) - Fix matching of incoming email addresses to the alternate_addresses field of a user -- this would match substrings, e.g. if the user has discuss-support@example.com as an alternate email and an incoming mail is addressed to support@example.com this would (wrongly) match. (Ralf) - issue2550729: Fix password history display for anydbm backend, thanks to Ralf Hemmecke for reporting. (Ralf) - OpenPGP support is again working (pyme API has changed significantly) and we now have a regression test. We now take care that bounce-messages for incoming encrypted mails or mails where the policy dictates that outgoing traffic should be encrypted is actually OpenPGP encrypted. (Ralf) - Ignore confirm set() fields by themselves in the absence of non-"confirm" values; otherwise a bare confirm field can be used to change the a password. Reported by Cam Blackwood. (Ralf) - Updated version of simplified Chinese message file by Cheer Xiao: Corrected some mistakes, added a few more items and did some formating. (Ralf) - Fix xmlrpc URL parsing so that passwords may contain a ':' character (Ralf) - Be more tolerant when parsing RFC2047 encoded mail headers. Use backported version of my proposed changes to email.header.decode_header in http://bugs.python.org/issue1079 (Ralf) - issue2550684 Fix XSS vulnerability when username contains HTML code, thanks to Thomas Arendsen Hein for reporting and patch. (Ralf) - issue2550711 Fix XSS vulnerability in @action parameter, thanks to "om" for reporting. (Ralf) - issue2550535 In some cases even when keep_quoted_text=yes is configured we would strip quoted sections. This hit the python bug-tracker especially for python interpreter examples with leading '>>>' strings. The fix is slightly different compared to the proposal as this broke keep_quoted_text=no in certain cases. We also fix a bug where keep_quoted_text=no would drop the last line of a non-quoted section if there wasn't an empty line between the next quotes. (Ralf) - issue2431638 wrong registration link in bounce mail for non-registered users reported *years* ago by anonymous (Ralf) - Fix doc/upgrading.txt which produces errors with latest docutils about wrong block structure. Fix .gitignore in doc directory. Thanks to Cheer Xiao for the patches. (Ralf) - Fix wrong execute permissions on some files, thanks to Cheer Xiao for the patch. (Ralf) - Fix override of TemplatingUtils in instance.py, thanks to Cheer Xiao for the patch. (Ralf) - Fix another XSS with the "otk" parameter, thanks to Jesse Ruderman for reporting. (Ralf) - Mark cookies HttpOnly and -- if https is used -- secure. Fixes issue2550689, but is untested if this really works in browsers. Thanks to Joseph Myers for reporting. (Ralf) - Fix another XSS with the ok- and error message, see issue2550724. We solve this differently from the proposals in the bug-report by not allowing *any* html-tags in ok/error messages anymore. Thanks to David Benjamin for the bug-report and to Ezio Melotti for several proposed fixes. (Ralf) If you're upgrading from an older version of Roundup you *must* follow the "Software Upgrade" guidelines given in the maintenance documentation. Roundup requires python 2.5 or later (but not 3+) for correct operation. To give Roundup a try, just download (see below), unpack and run:: python demo.py Release info and download page: http://pypi.python.org/pypi/roundup Source and documentation is available at the website: http://roundup-tracker.org/ Mailing lists - the place to ask questions: http://sourceforge.net/mail/?group_id=31577 About Roundup ============= Roundup is a simple-to-use and -install issue-tracking system with command-line, web and e-mail interfaces. It is based on the winning design from Ka-Ping Yee in the Software Carpentry "Track" design competition. Note: Ping is not responsible for this project. The contact for this project is richard@users.sourceforge.net. Roundup manages a number of issues (with flexible properties such as "description", "priority", and so on) and provides the ability to: (a) submit new issues, (b) find and edit existing issues, and (c) discuss issues with other participants. The system will facilitate communication among the participants by managing discussions and notifying interested parties when issues are edited. One of the major design goals for Roundup that it be simple to get going. Roundup is therefore usable "out of the box" with any python 2.5+ (but not 3+) installation. It doesn't even need to be "installed" to be operational, though an install script is provided. It comes with two issue tracker templates (a classic bug/feature tracker and a minimal skeleton) and four database back-ends (anydbm, sqlite, mysql and postgresql). Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Environment :: Console Classifier: Environment :: Web Environment Classifier: Intended Audience :: End Users/Desktop Classifier: Intended Audience :: Developers Classifier: Intended Audience :: System Administrators Classifier: License :: OSI Approved :: Python Software Foundation License Classifier: Operating System :: MacOS :: MacOS X Classifier: Operating System :: Microsoft :: Windows Classifier: Operating System :: POSIX Classifier: Programming Language :: Python Classifier: Topic :: Communications :: Email Classifier: Topic :: Office/Business Classifier: Topic :: Software Development :: Bug Tracking roundup-1.4.20/setup.py0000644000175000017630000001356711741527216013520 0ustar ralfpriv#! /usr/bin/env python # # Copyright (c) 2001 Bizar Software Pty Ltd (http://www.bizarsoftware.com.au/) # This module is free software, and you may redistribute it and/or modify # under the same terms as Python, so long as this copyright message and # disclaimer are retained in their original form. # # IN NO EVENT SHALL BIZAR SOFTWARE PTY LTD BE LIABLE TO ANY PARTY FOR # DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING # OUT OF THE USE OF THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. # # BIZAR SOFTWARE PTY LTD SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, # BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS # FOR A PARTICULAR PURPOSE. THE CODE PROVIDED HEREUNDER IS ON AN "AS IS" # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. # from roundup.dist.command.build_doc import build_doc from roundup.dist.command.build_scripts import build_scripts from roundup.dist.command.build_py import build_py from roundup.dist.command.build import build, list_message_files from roundup.dist.command.bdist_rpm import bdist_rpm from roundup.dist.command.install_lib import install_lib from distutils.core import setup import sys, os from glob import glob # patch distutils if it can't cope with the "classifiers" keyword from distutils.dist import DistributionMetadata if not hasattr(DistributionMetadata, 'classifiers'): DistributionMetadata.classifiers = None DistributionMetadata.download_url = None def include(d, e): """Generate a pair of (directory, file-list) for installation. 'd' -- A directory 'e' -- A glob pattern""" return (d, [f for f in glob('%s/%s'%(d, e)) if os.path.isfile(f)]) def scriptname(path): """ Helper for building a list of script names from a list of module files. """ script = os.path.splitext(os.path.basename(path))[0] script = script.replace('_', '-') return script def main(): # template munching packages = [ 'roundup', 'roundup.anypy', 'roundup.cgi', 'roundup.cgi.PageTemplates', 'roundup.cgi.TAL', 'roundup.cgi.ZTUtils', 'roundup.backends', 'roundup.scripts', ] py_modules = ['roundup.demo',] # build list of scripts from their implementation modules scripts = [scriptname(f) for f in glob('roundup/scripts/[!_]*.py')] data_files = [ ('share/roundup/cgi-bin', ['frontends/roundup.cgi']), ] # install man pages on POSIX platforms if os.name == 'posix': data_files.append(include('share/man/man1', '*')) # add the templates to the data files lists from roundup.init import listTemplates templates = [t['path'] for t in listTemplates('share/roundup/templates').values()] for tdir in templates: for idir in '. detectors extensions html'.split(): data_files.append(include(os.path.join(tdir, idir), '*')) # add message files for (_dist_file, _mo_file) in list_message_files(): data_files.append((os.path.dirname(_mo_file), [os.path.join("build", _mo_file)])) # add docs data_files.append(include('share/doc/roundup/html', '*')) # perform the setup action from roundup import __version__ # long_description may not contain non-ascii characters. Distutils # will produce an non-installable installer on linux *and* we can't # run the bdist_wininst on Linux if there are non-ascii characters # because the distutils installer will try to use the mbcs codec # which isn't available on non-windows platforms. See also # http://bugs.python.org/issue10945 long_description=open('doc/announcement.txt').read().decode('utf8') try: long_description = long_description.encode('ascii') except UnicodeEncodeError, cause: print >> sys.stderr, "doc/announcement.txt contains non-ascii: %s" \ % cause sys.exit(42) setup(name='roundup', version=__version__, author="Richard Jones", author_email="richard@users.sourceforge.net", description="A simple-to-use and -install issue-tracking system" " with command-line, web and e-mail interfaces. Highly" " customisable.", long_description=long_description, url='http://www.roundup-tracker.org', download_url='http://pypi.python.org/pypi/roundup', classifiers=['Development Status :: 5 - Production/Stable', 'Environment :: Console', 'Environment :: Web Environment', 'Intended Audience :: End Users/Desktop', 'Intended Audience :: Developers', 'Intended Audience :: System Administrators', 'License :: OSI Approved :: Python Software Foundation License', 'Operating System :: MacOS :: MacOS X', 'Operating System :: Microsoft :: Windows', 'Operating System :: POSIX', 'Programming Language :: Python', 'Topic :: Communications :: Email', 'Topic :: Office/Business', 'Topic :: Software Development :: Bug Tracking', ], # Override certain command classes with our own ones cmdclass= {'build_doc': build_doc, 'build_scripts': build_scripts, 'build_py': build_py, 'build': build, 'bdist_rpm': bdist_rpm, 'install_lib': install_lib, }, packages=packages, py_modules=py_modules, scripts=scripts, data_files=data_files) if __name__ == '__main__': main() # vim: set filetype=python sts=4 sw=4 et si : roundup-1.4.20/scripts/0000755000175000017630000000000011754432235013461 5ustar ralfprivroundup-1.4.20/scripts/README.txt0000644000175000017630000000203711741527216015161 0ustar ralfprivScripts in this directory: add-issue Add a single issue, as specified on the command line, to your tracker. The initial message for the issue is taken from standard input. roundup-reminder Generate an email that lists outstanding issues. Send in both plain text and HTML formats. weekly-report Generate a simple report outlining the activity in one tracker for the most recent week. schema_diagram.py Generate a schema diagram for a roundup tracker. It generates a 'dot file' that is then fed into the 'dot' tool (http://www.graphviz.org) to generate a graph. server-ctl Control the roundup-server daemon from the command line with start, stop, restart, condstart (conditional start - only if server is stopped) and status commands. roundup.rc-debian An control script that may be installed in /etc/init.d on Debian systems. Offers start, stop and restart commands and integrates with the Debian init process. imapServer.py This IMAP server script that runs in the background and checks for new email from a variety of mailboxes. roundup-1.4.20/scripts/copy-user.py0000755000175000017630000000457711741527216016001 0ustar ralfpriv#!/usr/bin/env python # Copyright (C) 2003 by Intevation GmbH # Author: # Thomas Arendsen Hein # # This program is free software dual licensed under the GPL (>=v2) # and the Roundup Licensing (see COPYING.txt in the roundup distribution). """ copy-user [...] Copy one or more Roundup users from one tracker instance to another. Example: copy-user /roundup/tracker1 /roundup/tracker2 `seq 3 10` 14 16 (copies users 3, 4, 5, 6, 7, 8, 9, 10, 14 and 16) """ import sys import roundup.instance def copy_user(home1, home2, *userids): """Copy users which are listed by userids from home1 to home2""" copyattribs = ['username', 'password', 'address', 'realname', 'phone', 'organisation', 'alternate_addresses', 'roles', 'timezone'] try: instance1 = roundup.instance.open(home1) print "Opened source instance: %s" % home1 except: print "Can't open source instance: %s" % home1 sys.exit(1) try: instance2 = roundup.instance.open(home2) print "Opened target instance: %s" % home2 except: print "Can't open target instance: %s" % home2 sys.exit(1) db1 = instance1.open('admin') db2 = instance2.open('admin') userlist = db1.user.list() for userid in userids: try: userid = str(int(userid)) except ValueError, why: print "Not a numeric user id: %s Skipping ..." % (userid,) continue if userid not in userlist: print "User %s not in source instance. Skipping ..." % userid continue user = {} for attrib in copyattribs: value = db1.user.get(userid, attrib) if value: user[attrib] = value try: db2.user.lookup(user['username']) print "User %s: Username '%s' exists in target instance. Skipping ..." % (userid, user['username']) continue except KeyError, why: pass print "Copying user %s (%s) ..." % (userid, user['username']) db2.user.create(**user) db2.commit() db2.close() print "Closed target instance." db1.close() print "Closed source instance." if __name__ == "__main__": if len(sys.argv) < 4: print __doc__ sys.exit(1) else: copy_user(*sys.argv[1:]) roundup-1.4.20/scripts/roundup-reminder0000755000175000017630000001414111741527216016707 0ustar ralfpriv#! /usr/bin/env python # Copyright (c) 2002 ekit.com Inc (http://www.ekit-inc.com/) # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. ''' Simple script that emails all users of a tracker with the issues that are currently assigned to them. TODO: introduce some structure ;) TODO: possibly make this more general and configurable... ''' import sys, cStringIO, MimeWriter, smtplib from roundup import instance, date from roundup.mailer import SMTPConnection # open the instance if len(sys.argv) != 2: print 'You need to specify an instance home dir' instance_home = sys.argv[1] instance = instance.open(instance_home) db = instance.open('admin') resolved_id = db.status.lookup('resolved') def listCompare(x, y): "compare two tuples such that order is positive on [0] and negative on [1]" if x[0] < y[0]: return -1 if x[0] > y[0]: return 1 if x[1] > y[1]: return -1 if x[1] < y[1]: return 1 return 0 # loop through all the users for user_id in db.user.list(): # make sure we care aboue this user name = db.user.get(user_id, 'realname') if name is None: name = db.user.get(user_id, 'username') address = db.user.get(user_id, 'address') if address is None: continue # extract this user's issues l = [] for issue_id in db.issue.find(assignedto=user_id): if db.issue.get(issue_id, 'status') == resolved_id: continue order = db.priority.get(db.issue.get(issue_id, 'priority'), 'order') l.append((order, db.issue.get(issue_id, 'activity'), db.issue.get(issue_id, 'creation'), issue_id)) # sort the issues by timeliness and creation date l.sort(listCompare) if not l: continue # generate the email message message = cStringIO.StringIO() writer = MimeWriter.MimeWriter(message) writer.addheader('Subject', 'Your active %s issues'%db.config.TRACKER_NAME) writer.addheader('To', address) writer.addheader('From', '%s <%s>'%(db.config.TRACKER_NAME, db.config.ADMIN_EMAIL)) writer.addheader('Reply-To', '%s <%s>'%(db.config.TRACKER_NAME, db.config.ADMIN_EMAIL)) writer.addheader('MIME-Version', '1.0') writer.addheader('X-Roundup-Name', db.config.TRACKER_NAME) # start the multipart part = writer.startmultipartbody('alternative') part = writer.nextpart() body = part.startbody('text/plain') # do the plain text bit print >>body, 'Created ID Activity Title' print >>body, '='*75 # '2 months 213 immediate cc_daemon barfage old_priority = None for priority_order, activity_date, creation_date, issue_id in l: priority = db.issue.get(issue_id, 'priority') if (priority != old_priority): old_priority = priority print >>body, ' ', db.priority.get(priority,'name') # pretty creation creation = (creation_date - date.Date('.')).pretty() if creation is None: creation = creation_date.pretty() activity = (activity_date - date.Date('.')).pretty() title = db.issue.get(issue_id, 'title') if len(title) > 42: title = title[:38] + ' ...' print >>body, '%-11s %-4s %-9s %-42s'%(creation, issue_id, activity, title) # some help to finish off print >>body, ''' To view or respond to any of the issues listed above, visit the URL %s and click on "My Issues". Do NOT respond to this message. '''%db.config.TRACKER_WEB # now the HTML one part = writer.nextpart() body = part.startbody('text/html') colours = { 'immediate': ' bgcolor="#ffcdcd"', 'day': ' bgcolor="#ffdecd"', 'week': ' bgcolor="#ffeecd"', 'month': ' bgcolor="#ffffcd"', 'whenever': ' bgcolor="#ffffff"', } print >>body, ''' ''' old_priority = None for priority_order, activity_date, creation_date, issue_id in l: priority = db.issue.get(issue_id,'priority') if (priority != old_priority): old_priority = priority print >>body, ''%db.priority.get(priority,'name') creation = (date.Date('.') - creation_date).pretty() if creation is None: creation = (creation_date - date.Date('.')).pretty() title = db.issue.get(issue_id, 'title') issue_id = '%s'%(db.config.TRACKER_WEB, issue_id, issue_id) activity = (activity_date - date.Date('.')).pretty() print >>body, ''''''%(creation, issue_id, activity, title) print >>body, '
Created ID Activity Title
->->->%s
%s%s%s %s
' print >>body, '''

To view or respond to any of the issues listed above, simply click on the issue ID. Do not respond to this message.

''' # finish of the multipart writer.lastpart() # all done, send! smtp = SMTPConnection(db.config) smtp.sendmail(db.config.ADMIN_EMAIL, address, message.getvalue()) # vim: set filetype=python ts=4 sw=4 et si roundup-1.4.20/scripts/weekly-report0000755000175000017630000000316311741527216016223 0ustar ralfpriv#! /usr/bin/env python # This script generates a simple report outlining the activity in one # tracker for the most recent week. # This script is free software, you may redistribute it # and/or modify under the same terms as Python. import sys, math from roundup import instance, date # open the instance if len(sys.argv) != 2: print 'You need to specify an instance home dir' instance_home = sys.argv[1] instance = instance.open(instance_home) db = instance.open('admin') old = date.Date('-1w') created = [] summary = {} messages = [] # loop through all the recently-active issues for issue_id in db.issue.filter(None, {'activity': '-1w;'}): num = 0 for x,ts,userid,action,data in db.issue.history(issue_id): if ts < old: continue if action == 'create': created.append(issue_id) elif action == 'set' and data.has_key('messages'): num += 1 summary.setdefault(db.issue.get(issue_id, 'status'), []).append(issue_id) messages.append((num, issue_id)) #print 'STATUS SUMMARY:' #for k,v in summary.items(): # print k, len(v) print '\nCREATED:' print '\n'.join(['%s: %s'%(id, db.issue.get(id, 'title')) for id in created]) print '\nRESOLVED:' resolved_id = db.status.lookup('resolved') print '\n'.join(['%s: %s'%(id, db.issue.get(id, 'title')) for id in summary.get(resolved_id, [])]) print '\nTOP TEN MOST DISCUSSED:' messages.sort() messages.reverse() nmax = messages[0][0] fmt = '%%%dd - %%s: %%s'%(int(math.log(nmax, 10)) + 1) print '\n'.join([fmt%(num, id, db.issue.get(id, 'title')) for num, id in messages[:10]]) # vim: set filetype=python ts=4 sw=4 et si roundup-1.4.20/scripts/add-issue0000755000175000017630000000270011741527216015264 0ustar ralfpriv#! /usr/bin/env python ''' Usage: %s Create a new issue in the given tracker. Input is taken from STDIN to create the initial issue message (which may be empty). Issues will be created as the current user (%s) if they exist as a Roundup user, or "admin" otherwise. ''' import sys, os, pwd from roundup import instance, mailgw, date # open the instance username = pwd.getpwuid(os.getuid())[0] if len(sys.argv) < 3: print "Error: Not enough arguments" print __doc__.strip()%(sys.argv[0], username) sys.exit(1) tracker_home = sys.argv[1] issue_priority = sys.argv[2] issue_title = ' '.join(sys.argv[3:]) # get the message, if any message_text = sys.stdin.read().strip() # open the tracker tracker = instance.open(tracker_home) db = tracker.open('admin') uid = db.user.lookup('admin') try: # try to open the tracker as the current user uid = db.user.lookup(username) db.close() db = tracker.open(username) except KeyError: pass try: # handle the message messages = [] if message_text: summary, x = mailgw.parseContent(message_text, 0, 0) msg = db.msg.create(content=message_text, summary=summary, author=uid, date=date.Date()) messages = [msg] # now create the issue db.issue.create(title=issue_title, priority=issue_priority, messages=messages) db.commit() finally: db.close() # vim: set filetype=python ts=4 sw=4 et si roundup-1.4.20/scripts/import_sf.py0000644000175000017630000003431711741527216016045 0ustar ralfpriv""" Import tracker data from Sourceforge.NET This script needs four steps to work: 1. Export the project XML data using the admin web interface at sf.net 2. Run the file fetching (these are not included in the XML): import_sf.py files this will place all the downloaded files in the files dir by file id. 3. Convert the sf.net XML to Roundup "export" format: import_sf.py import this will generate a directory "/tmp/imported" which contains the data to be imported into a Roundup tracker. 4. Import the data: roundup-admin -i import /tmp/imported And you're done! """ import sys, os, csv, time, urllib2, httplib, mimetypes, urlparse # Python 2.3 ... 2.6 compatibility: from roundup.anypy.sets_ import set try: import cElementTree as ElementTree except ImportError: from elementtree import ElementTree from roundup import instance, hyperdb, date, support, password today = date.Date('.') DL_URL = 'http://sourceforge.net/tracker/download.php?group_id=%(group_id)s&atid=%(atid)s&aid=%(aid)s' def get_url(aid): """ so basically we have to jump through hoops, given an artifact id, to figure what the URL should be to access that artifact, and hence any attached files.""" # first we hit this URL... conn = httplib.HTTPConnection("sourceforge.net") conn.request("GET", "/support/tracker.php?aid=%s"%aid) response = conn.getresponse() # which should respond with a redirect to the correct url which has the # magic "group_id" and "atid" values in it that we need assert response.status == 302, 'response code was %s'%response.status location = response.getheader('location') query = urlparse.urlparse(response.getheader('location'))[-2] info = dict([param.split('=') for param in query.split('&')]) return DL_URL%info def fetch_files(xml_file, file_dir): """ Fetch files referenced in the xml_file into the dir file_dir. """ root = ElementTree.parse(xml_file).getroot() to_fetch = set() deleted = set() for artifact in root.find('artifacts'): for field in artifact.findall('field'): if field.get('name') == 'artifact_id': aid = field.text for field in artifact.findall('field'): if field.get('name') != 'artifact_history': continue for event in field.findall('history'): d = {} for field in event.findall('field'): d[field.get('name')] = field.text if d['field_name'] == 'File Added': fid = d['old_value'].split(':')[0] to_fetch.add((aid, fid)) if d['field_name'] == 'File Deleted': fid = d['old_value'].split(':')[0] deleted.add((aid, fid)) to_fetch = to_fetch - deleted got = set(os.listdir(file_dir)) to_fetch = to_fetch - got # load cached urls (sigh) urls = {} if os.path.exists(os.path.join(file_dir, 'urls.txt')): for line in open(os.path.join(file_dir, 'urls.txt')): aid, url = line.strip().split() urls[aid] = url for aid, fid in support.Progress('Fetching files', list(to_fetch)): if fid in got: continue if not urls.has_key(aid): urls[aid] = get_url(aid) f = open(os.path.join(file_dir, 'urls.txt'), 'a') f.write('%s %s\n'%(aid, urls[aid])) f.close() url = urls[aid] + '&file_id=' + fid f = urllib2.urlopen(url) data = f.read() n = open(os.path.join(file_dir, fid), 'w') n.write(data) f.close() n.close() def import_xml(tracker_home, xml_file, file_dir): """ Generate Roundup tracker import files based on the tracker schema, sf.net xml export and downloaded files from sf.net. """ tracker = instance.open(tracker_home) db = tracker.open('admin') resolved = db.status.lookup('resolved') unread = db.status.lookup('unread') chatting = db.status.lookup('unread') critical = db.priority.lookup('critical') urgent = db.priority.lookup('urgent') bug = db.priority.lookup('bug') feature = db.priority.lookup('feature') wish = db.priority.lookup('wish') adminuid = db.user.lookup('admin') anonuid = db.user.lookup('anonymous') root = ElementTree.parse(xml_file).getroot() def to_date(ts): return date.Date(time.gmtime(float(ts))) # parse out the XML artifacts = [] categories = set() users = set() add_files = set() remove_files = set() for artifact in root.find('artifacts'): d = {} op = {} artifacts.append(d) for field in artifact.findall('field'): name = field.get('name') if name == 'artifact_messages': for message in field.findall('message'): l = d.setdefault('messages', []) m = {} l.append(m) for field in message.findall('field'): name = field.get('name') if name == 'adddate': m[name] = to_date(field.text) else: m[name] = field.text if name == 'user_name': users.add(field.text) elif name == 'artifact_history': for event in field.findall('history'): l = d.setdefault('history', []) e = {} l.append(e) for field in event.findall('field'): name = field.get('name') if name == 'entrydate': e[name] = to_date(field.text) else: e[name] = field.text if name == 'mod_by': users.add(field.text) if e['field_name'] == 'File Added': add_files.add(e['old_value'].split(':')[0]) elif e['field_name'] == 'File Deleted': remove_files.add(e['old_value'].split(':')[0]) elif name == 'details': op['body'] = field.text elif name == 'submitted_by': op['user_name'] = field.text d[name] = field.text users.add(field.text) elif name == 'open_date': thedate = to_date(field.text) op['adddate'] = thedate d[name] = thedate else: d[name] = field.text categories.add(d['category']) if op.has_key('body'): l = d.setdefault('messages', []) l.insert(0, op) add_files -= remove_files # create users userd = {'nobody': '2'} users.remove('nobody') data = [ {'id': '1', 'username': 'admin', 'password': password.Password('admin'), 'roles': 'Admin', 'address': 'richard@python.org'}, {'id': '2', 'username': 'anonymous', 'roles': 'Anonymous'}, ] for n, user in enumerate(list(users)): userd[user] = n+3 data.append({'id': str(n+3), 'username': user, 'roles': 'User', 'address': '%s@users.sourceforge.net'%user}) write_csv(db.user, data) users=userd # create categories categoryd = {'None': None} categories.remove('None') data = [] for n, category in enumerate(list(categories)): categoryd[category] = n data.append({'id': str(n), 'name': category}) write_csv(db.keyword, data) categories = categoryd # create issues issue_data = [] file_data = [] message_data = [] issue_journal = [] message_id = 0 for artifact in artifacts: d = {} d['id'] = artifact['artifact_id'] d['title'] = artifact['summary'] d['assignedto'] = users[artifact['assigned_to']] if d['assignedto'] == '2': d['assignedto'] = None d['creation'] = artifact['open_date'] activity = artifact['open_date'] d['creator'] = users[artifact['submitted_by']] actor = d['creator'] if categories[artifact['category']]: d['keyword'] = [categories[artifact['category']]] issue_journal.append(( d['id'], d['creation'].get_tuple(), d['creator'], "'create'", {} )) p = int(artifact['priority']) if artifact['artifact_type'] == 'Feature Requests': if p > 3: d['priority'] = feature else: d['priority'] = wish else: if p > 7: d['priority'] = critical elif p > 5: d['priority'] = urgent elif p > 3: d['priority'] = bug else: d['priority'] = feature s = artifact['status'] if s == 'Closed': d['status'] = resolved elif s == 'Deleted': d['status'] = resolved d['is retired'] = True else: d['status'] = unread nosy = set() for message in artifact.get('messages', []): authid = users[message['user_name']] if not message['body']: continue body = convert_message(message['body'], message_id) if not body: continue m = {'content': body, 'author': authid, 'date': message['adddate'], 'creation': message['adddate'], } message_data.append(m) if authid not in (None, '2'): nosy.add(authid) activity = message['adddate'] actor = authid if d['status'] == unread: d['status'] = chatting # add import message m = {'content': 'IMPORT FROM SOURCEFORGE', 'author': '1', 'date': today, 'creation': today} message_data.append(m) # sort messages and assign ids d['messages'] = [] message_data.sort(lambda a,b:cmp(a['date'],b['date'])) for message in message_data: message_id += 1 message['id'] = str(message_id) d['messages'].append(message_id) d['nosy'] = list(nosy) files = [] for event in artifact.get('history', []): if event['field_name'] == 'File Added': fid, name = event['old_value'].split(':', 1) if fid in add_files: files.append(fid) name = name.strip() try: f = open(os.path.join(file_dir, fid)) content = f.read() f.close() except: content = 'content missing' file_data.append({ 'id': fid, 'creation': event['entrydate'], 'creator': users[event['mod_by']], 'name': name, 'type': mimetypes.guess_type(name)[0], 'content': content, }) continue elif event['field_name'] == 'close_date': action = "'set'" info = { 'status': unread } elif event['field_name'] == 'summary': action = "'set'" info = { 'title': event['old_value'] } else: # not an interesting / translatable event continue row = [ d['id'], event['entrydate'].get_tuple(), users[event['mod_by']], action, info ] if event['entrydate'] > activity: activity = event['entrydate'] issue_journal.append(row) d['files'] = files d['activity'] = activity d['actor'] = actor issue_data.append(d) write_csv(db.issue, issue_data) write_csv(db.msg, message_data) write_csv(db.file, file_data) f = open('/tmp/imported/issue-journals.csv', 'w') writer = csv.writer(f, colon_separated) writer.writerows(issue_journal) f.close() def convert_message(content, id): """ Strip off the useless sf message header crap """ if content[:14] == 'Logged In: YES': return '\n'.join(content.splitlines()[3:]).strip() return content class colon_separated(csv.excel): delimiter = ':' def write_csv(klass, data): props = klass.getprops() if not os.path.exists('/tmp/imported'): os.mkdir('/tmp/imported') f = open('/tmp/imported/%s.csv'%klass.classname, 'w') writer = csv.writer(f, colon_separated) propnames = klass.export_propnames() propnames.append('is retired') writer.writerow(propnames) for entry in data: row = [] for name in propnames: if name == 'is retired': continue prop = props[name] if entry.has_key(name): if isinstance(prop, hyperdb.Date) or \ isinstance(prop, hyperdb.Interval): row.append(repr(entry[name].get_tuple())) elif isinstance(prop, hyperdb.Password): row.append(repr(str(entry[name]))) else: row.append(repr(entry[name])) elif isinstance(prop, hyperdb.Multilink): row.append('[]') elif name in ('creator', 'actor'): row.append("'1'") elif name in ('created', 'activity'): row.append(repr(today.get_tuple())) else: row.append('None') row.append(entry.get('is retired', False)) writer.writerow(row) if isinstance(klass, hyperdb.FileClass) and entry.get('content'): fname = klass.exportFilename('/tmp/imported/', entry['id']) support.ensureParentsExist(fname) c = open(fname, 'w') if isinstance(entry['content'], unicode): c.write(entry['content'].encode('utf8')) else: c.write(entry['content']) c.close() f.close() f = open('/tmp/imported/%s-journals.csv'%klass.classname, 'w') f.close() if __name__ == '__main__': if sys.argv[1] == 'import': import_xml(*sys.argv[2:]) elif sys.argv[1] == 'files': fetch_files(*sys.argv[2:]) roundup-1.4.20/scripts/server-ctl0000755000175000017630000000352211741527216015477 0ustar ralfpriv#!/bin/sh # # Configuration # CONFFILE="/var/roundup/server-config.ini" # this will end up with extra space, but it should be ignored in the script PIDFILE=`grep '^pidfile' ${CONFFILE} | awk -F = '{print $2}' ` SERVER="/usr/local/bin/roundup-server -C ${CONFFILE}" ERROR=0 ARGV="$@" if [ "x$ARGV" = "x" ] ; then ARGS="help" fi if [ -z "${PIDFILE}" ] ; then echo "pidfile option must be set in configuration file" exit 1 fi for ARG in $@ $ARGS do # check for pidfile if [ -f $PIDFILE ] ; then PID=`cat $PIDFILE` if [ "x$PID" != "x" ] && kill -0 $PID 2>/dev/null ; then STATUS="roundup-server (pid $PID) running" RUNNING=1 else STATUS="roundup-server (pid $PID?) not running" RUNNING=0 fi else STATUS="roundup-server (no pid file) not running" RUNNING=0 fi case $ARG in start) if [ $RUNNING -eq 1 ] ; then echo "$0 $ARG: roundup-server (pid $PID) already running" continue fi if $SERVER ; then echo "$0 $ARG: roundup-server started" else echo "$0 $ARG: roundup-server could not be started" ERROR=1 fi ;; condstart) if [ $RUNNING -eq 1 ] ; then continue fi if $SERVER ; then echo "$0 $ARG: roundup-server started" else echo "$0 $ARG: roundup-server could not be started" ERROR=1 fi ;; stop) if [ $RUNNING -eq 0 ] ; then echo "$0 $ARG: $STATUS" continue fi if kill $PID ; then echo "$0 $ARG: roundup-server stopped" else echo "$0 $ARG: roundup-server could not be stopped" ERROR=2 fi ;; status) echo $STATUS ;; *) echo "usage: $0 (start|condstart|stop|status)" cat <&2 exit 1 ;; esac } start_stop "$@" exit 0 roundup-1.4.20/scripts/schema_diagram.py0000644000175000017630000000300511741527216016755 0ustar ralfpriv#! /usr/bin/python # # Schema diagram generator contributed by Stefan Seefeld of the fresco # project http://www.fresco.org/. # # It generates a 'dot file' that is then fed into the 'dot' # tool (http://www.graphviz.org) to generate a graph: # # %> ./schema.py # %> dot -Tps schema.dot -o schema.ps # %> gv schema.ps # import sys import roundup.instance # open the instance instance = roundup.instance.open(sys.argv[1]) db = instance.open() # diagram preamble print 'digraph schema {' print 'size="8,6"' print 'node [shape="record" bgcolor="#ffe4c4" style=filled]' print 'edge [taillabel="1" headlabel="1" dir=back arrowtail=ediamond]' # get all the classes types = db.classes.keys() # one record node per class for i in range(len(types)): print 'node%d [label=\"{%s|}"]'%(i, types[i]) # now draw in the relations for name in db.classes.keys(): type = db.classes[name] attributes = type.getprops() for a in attributes.keys(): attribute = attributes[a] if isinstance(attribute, roundup.hyperdb.Link): print 'node%d -> node%d [label=%s]'%(types.index(name), types.index(attribute.classname), a) elif isinstance(attribute, roundup.hyperdb.Multilink): print 'node%d -> node%d [taillabel="*" label=%s]'%(types.index(name), types.index(attribute.classname), a) # all done print '}' roundup-1.4.20/scripts/imapServer.py0000644000175000017630000003352711741527216016162 0ustar ralfpriv#!/usr/bin/env python """\ This script is a wrapper around the mailgw.py script that exists in roundup. It runs as service instead of running as a one-time shot. It also connects to a secure IMAP server. The main reasons for this script are: 1) The roundup-mailgw script isn't designed to run as a server. It expects that you either run it by hand, and enter the password each time, or you supply the password on the command line. I prefer to run a server that I initialize with the password, and then it just runs. I don't want to have to pass it on the command line, so running through crontab isn't a possibility. (This wouldn't be a problem on a local machine running through a mailspool.) 2) mailgw.py somehow screws up SSL support so IMAP4_SSL doesn't work. So hopefully running that work outside of the mailgw will allow it to work. 3) I wanted to be able to check multiple projects at the same time. roundup-mailgw is only for 1 mailbox and 1 project. *TODO*: For the first round, the program spawns a new roundup-mailgw for each imap message that it finds and pipes the result in. In the future it might be more practical to actually include the roundup files and run the appropriate commands using python. *TODO*: Look into supporting a logfile instead of using 2>/logfile *TODO*: Add an option for changing the uid/gid of the running process. """ import getpass import logging import imaplib import optparse import os import re import time logging.basicConfig() log = logging.getLogger('roundup.IMAPServer') version = '0.1.2' class RoundupMailbox: """This contains all the info about each mailbox. Username, Password, server, security, roundup database """ def __init__(self, dbhome='', username=None, password=None, mailbox=None , server=None, protocol='imaps'): self.username = username self.password = password self.mailbox = mailbox self.server = server self.protocol = protocol self.dbhome = dbhome try: if not self.dbhome: self.dbhome = raw_input('Tracker home: ') if not os.path.exists(self.dbhome): raise ValueError, 'Invalid home address: ' \ 'directory "%s" does not exist.' % self.dbhome if not self.server: self.server = raw_input('Server: ') if not self.server: raise ValueError, 'No Servername supplied' protocol = raw_input('protocol [imaps]? ') self.protocol = protocol if not self.username: self.username = raw_input('Username: ') if not self.username: raise ValueError, 'Invalid Username' if not self.password: print 'For server %s, user %s' % (self.server, self.username) self.password = getpass.getpass() # password can be empty because it could be superceeded # by a later entry #if self.mailbox is None: # self.mailbox = raw_input('Mailbox [INBOX]: ') # # We allow an empty mailbox because that will # # select the INBOX, whatever it is called except (KeyboardInterrupt, EOFError): raise ValueError, 'Canceled by User' def __str__(self): return 'Mailbox{ server:%(server)s, protocol:%(protocol)s, ' \ 'username:%(username)s, mailbox:%(mailbox)s, ' \ 'dbhome:%(dbhome)s }' % self.__dict__ # [als] class name is misleading. this is imap client, not imap server class IMAPServer: """IMAP mail gatherer. This class runs as a server process. It is configured with a list of mailboxes to connect to, along with the roundup database directories that correspond with each email address. It then connects to each mailbox at a specified interval, and if there are new messages it reads them, and sends the result to the roundup.mailgw. *TODO*: Try to be smart about how you access the mailboxes so that you can connect once, and access multiple mailboxes and possibly multiple usernames. *NOTE*: This assumes that if you are using the same user on the same server, you are using the same password. (the last one supplied is used.) Empty passwords are ignored. Only the last protocol supplied is used. """ def __init__(self, pidfile=None, delay=5, daemon=False): #This is sorted by servername, then username, then mailboxes self.mailboxes = {} self.delay = float(delay) self.pidfile = pidfile self.daemon = daemon def setDelay(self, delay): self.delay = delay def addMailbox(self, mailbox): """ The linkage is as follows: servers -- users - mailbox:dbhome So there can be multiple servers, each with multiple users. Each username can be associated with multiple mailboxes. each mailbox is associated with 1 database home """ log.info('Adding mailbox %s', mailbox) if not self.mailboxes.has_key(mailbox.server): self.mailboxes[mailbox.server] = {'protocol':'imaps', 'users':{}} server = self.mailboxes[mailbox.server] if mailbox.protocol: server['protocol'] = mailbox.protocol if not server['users'].has_key(mailbox.username): server['users'][mailbox.username] = {'password':'', 'mailboxes':{}} user = server['users'][mailbox.username] if mailbox.password: user['password'] = mailbox.password if user['mailboxes'].has_key(mailbox.mailbox): raise ValueError, 'Mailbox is already defined' user['mailboxes'][mailbox.mailbox] = mailbox.dbhome def _process(self, message, dbhome): """Actually process one of the email messages""" child = os.popen('roundup-mailgw %s' % dbhome, 'wb') child.write(message) child.close() #print message def _getMessages(self, serv, count, dbhome): """This assumes that you currently have a mailbox open, and want to process all messages that are inside. """ for n in range(1, count+1): (t, data) = serv.fetch(n, '(RFC822)') if t == 'OK': self._process(data[0][1], dbhome) serv.store(n, '+FLAGS', r'(\Deleted)') def checkBoxes(self): """This actually goes out and does all the checking. Returns False if there were any errors, otherwise returns true. """ noErrors = True for server in self.mailboxes: log.info('Connecting to server: %s', server) s_vals = self.mailboxes[server] try: for user in s_vals['users']: u_vals = s_vals['users'][user] # TODO: As near as I can tell, you can only # login with 1 username for each connection to a server. protocol = s_vals['protocol'].lower() if protocol == 'imaps': serv = imaplib.IMAP4_SSL(server) elif protocol == 'imap': serv = imaplib.IMAP4(server) else: raise ValueError, 'Unknown protocol %s' % protocol password = u_vals['password'] try: log.info('Connecting as user: %s', user) serv.login(user, password) for mbox in u_vals['mailboxes']: dbhome = u_vals['mailboxes'][mbox] log.info('Using mailbox: %s, home: %s', mbox, dbhome) #access a specific mailbox if mbox: (t, data) = serv.select(mbox) else: # Select the default mailbox (INBOX) (t, data) = serv.select() try: nMessages = int(data[0]) except ValueError: nMessages = 0 log.info('Found %s messages', nMessages) if nMessages: self._getMessages(serv, nMessages, dbhome) serv.expunge() # We are done with this mailbox serv.close() except: log.exception('Exception with server %s user %s', server, user) noErrors = False serv.logout() serv.shutdown() del serv except: log.exception('Exception while connecting to %s', server) noErrors = False return noErrors def makeDaemon(self): """Turn this process into a daemon. - make our parent PID 1 Write our new PID to the pidfile. From A.M. Kuuchling (possibly originally Greg Ward) with modification from Oren Tirosh, and finally a small mod from me. Originally taken from roundup.scripts.roundup_server.py """ log.info('Running as Daemon') # Fork once if os.fork() != 0: os._exit(0) # Create new session os.setsid() # Second fork to force PPID=1 pid = os.fork() if pid: if self.pidfile: pidfile = open(self.pidfile, 'w') pidfile.write(str(pid)) pidfile.close() os._exit(0) def run(self): """Run email gathering daemon. This spawns itself as a daemon, and then runs continually, just sleeping inbetween checks. It is recommended that you run checkBoxes once first before you select run. That way you can know if there were any failures. """ if self.daemon: self.makeDaemon() while True: time.sleep(self.delay * 60.0) log.info('Time: %s', time.strftime('%Y-%m-%d %H:%M:%S')) self.checkBoxes() def getItems(s): """Parse a string looking for userame@server""" myRE = re.compile( r'((?P[^:]+)://)?'#You can supply a protocol if you like r'(' #The username part is optional r'(?P[^:]+)' #You can supply the password as r'(:(?P.+))?' #username:password@server r'@)?' r'(?P[^/]+)' r'(/(?P.+))?$' ) m = myRE.match(s) if m: return m.groupdict() else: return None def main(): """This is what is called if run at the prompt""" parser = optparse.OptionParser( version=('%prog ' + version), usage="""usage: %prog [options] (home server)... So each entry has a home, and then the server configuration. Home is just a path to the roundup issue tracker. The server is something of the form: imaps://user:password@server/mailbox If you don't supply the protocol, imaps is assumed. Without user or password, you will be prompted for them. The server must be supplied. Without mailbox the INBOX is used. Examples: %prog /home/roundup/trackers/test imaps://test@imap.example.com/test %prog /home/roundup/trackers/test imap.example.com \ /home/roundup/trackers/test2 imap.example.com/test2 """ ) parser.add_option('-d', '--delay', dest='delay', type='float', metavar='', default=5, help="Set the delay between checks in minutes. (default 5)" ) parser.add_option('-p', '--pid-file', dest='pidfile', metavar='', default=None, help="The pid of the server process will be written to " ) parser.add_option('-n', '--no-daemon', dest='daemon', action='store_false', default=True, help="Do not fork into the background after running the first check." ) parser.add_option('-v', '--verbose', dest='verbose', action='store_const', const=logging.INFO, help="Be more verbose in letting you know what is going on." " Enables informational messages." ) parser.add_option('-V', '--very-verbose', dest='verbose', action='store_const', const=logging.DEBUG, help="Be very verbose in letting you know what is going on." " Enables debugging messages." ) parser.add_option('-q', '--quiet', dest='verbose', action='store_const', const=logging.ERROR, help="Be less verbose. Ignores warnings, only prints errors." ) parser.add_option('-Q', '--very-quiet', dest='verbose', action='store_const', const=logging.CRITICAL, help="Be much less verbose. Ignores warnings and errors." " Only print CRITICAL messages." ) (opts, args) = parser.parse_args() if (len(args) == 0) or (len(args) % 2 == 1): parser.error('Invalid number of arguments. ' 'Each site needs a home and a server.') log.setLevel(opts.verbose) myServer = IMAPServer(delay=opts.delay, pidfile=opts.pidfile, daemon=opts.daemon) for i in range(0,len(args),2): home = args[i] server = args[i+1] if not os.path.exists(home): parser.error('Home: "%s" does not exist' % home) info = getItems(server) if not info: parser.error('Invalid server string: "%s"' % server) myServer.addMailbox( RoundupMailbox(dbhome=home, mailbox=info['mailbox'] , username=info['username'], password=info['password'] , server=info['server'], protocol=info['protocol'] ) ) if myServer.checkBoxes(): myServer.run() if __name__ == '__main__': main() # vim: et ft=python si sts=4 sw=4 roundup-1.4.20/doc/0000755000175000017630000000000011754432235012537 5ustar ralfprivroundup-1.4.20/doc/overview.txt0000644000175000017630000005674011741527216015162 0ustar ralfpriv======================================================= Roundup: an Issue-Tracking System for Knowledge Workers ======================================================= :Authors: Ka-Ping Yee (original_), Richard Jones (implementation) .. _original: original_overview.html .. contents:: Introduction ============ Roundup is an issue-tracking system called which will manage a number of issues (with properties such as "description", "priority", and so on) and provides the ability to: (a) submit new issues, (b) find and edit existing issues, and (c) discuss issues with other participants. Roundup facilitates communication among the participants by managing discussions and notifying interested parties when issues are edited. Background ---------- A typical software project requires the management of many tasks, usually distributed among several collaborators. In fact, any project team could use a tool for sorting out and discussing all the relevant issues. A common approach is to set up some kind of "to-do" list that people can share. However, to address the overall problem we need much more than just a shared to-do list; we need to manage a growing body of knowledge and experience to help a team collaborate effectively on a project. The issue-tracking tool becomes a nexus for communication: the Grand Central Station of the group intelligence. The primary focus of this design is to help developers work together well, not to provide a customer service interface to the developers. This is not to say that the design is to be made unsuitable for customers to use. Rather, it is assumed that many of the same qualities that are good for supporting development (see below) are also good for non-developers using the system. Additional niceties for providing a safe or simplified interface to clients are intentionally deferred for later consideration. A good issue-tracking system should have at least the following properties: **Low barrier to participation** The usefulness of the tool depends entirely on the information people contribute to it. It must be made as easy as possible to submit new issues and contribute information about existing issues. **Straightforward navigation** It should be easy for users to extract information they need from the system to direct their decisions and tasks. They should be able to get a decent overview of things as well as finding specific information when they know what they're after. **Controlled information flow** The users must have control over how much information and what information they get. A common flaw of some issue-tracking systems is that they inundate users with so much useless e-mail that people avoid the system altogether. With a nod to the time-honoured computer science tradition of "filling in the fourth quadrant", we note that there are really four kinds of information flow going on here. The three mentioned qualities really address the first three quadrants of this 2-by-2 matrix, respectively: 1. User push: a user submits information to the system. 2. User pull: a user queries for information from the system. 3. System push: the system sends information out to users. 4. System pull: the system solicits information from users. An example of the fourth kind of flow is voting. Voting isn't described in this design, but it should be noted as a potential enhancement. Guiding Principles ------------------ **Simplicity** It is a strong requirement that the tool be accessible and understandable. It should be fairly obvious what different parts of the interface do, and the inner mechanisms should operate in ways that most users can easily predict. **Efficiency** We aim to optimize for minimum effort to do the most common operations, and best use of resources like screen real estate to maximize the amount of information that we summarize and present. **Generality** We try to avoid making unnecessary assumptions that would restrict the applicability of the tool. For example, there is no reason why one might not also want to use this tool to manage a design process, non-software projects, or organizational decisions. **Persistence** We prefer hiding or reclassifying information to deleting it. This helps support the collection of statistics later. If records are never destroyed, there is little danger in providing access to a larger community, and logging yields accountability, which may encourage better behaviour. Data Model ========== Roundup stores a number of *items*, each of which can have several properties and an associated discussion. The properties can be used to classify or search for items. The discussion is a sequence of e-mail messages. Each item is identified by a unique number, and has an activity log which records the time and content of edits made on its properties. The log stays fairly small since the design intentionally provides only small data types as item properties, and encourages anything large to be attached to e-mail where it becomes part of the discussion. The next section explains how items are organized. The Hyperdatabase ----------------- Often when classifying information we are asked to select exactly one of a number of categories or to fit it into a rigid hierarchy. Yet things only sometimes fall into one category; often, a piece of information may be related to several concepts. For example, forcing each item into a single keyword category is not just suboptimal but counterproductive: seekers of that item may expect to find it in a different category and conclude that the item is not present in the database -- which has them *worse* off than if the items were not categorized at all. Some systems try to alleviate this problem by allowing items to appear at multiple locations in a tree, as with "aliases" or "symbolic links" in a filesystem, for example. This does help somewhat, but we want to be even more flexible by allowing the organization of items into sets that may freely intersect. Rather than putting each item at exactly one place in an overall "grand scheme", a item can belong to as many sets as are appropriate. If we choose to represent the sets themselves as items and set membership as a link between items, we're now ready to present the definition of a hyperdatabase. A *hyperdatabase* is a collection of *items* that may be hyperlinked to each other (hence the name "hyperdatabase"). Each item carries a collection of key-value pairs, where some of the values may be links to other items. Any item may have an arbitrary number of outgoing and incoming links. Hyperdatabases are able to efficiently answer queries such as "what items link to this item?" and "what items does this item link to?" Rationale --------- There are several reasons for building our own kind of database for Roundup rather than using an existing one. Requiring the installation of a full-blown third-party SQL database system would probably deter many potential users from attempting to set up Roundup; yet a real relational database would be too complicated to implement on our own. On the other hand, a hyperdatabase can be implemented fairly easily using one of the Python DBM modules, so we can take the "batteries-included" approach and provide it as part of the system. It's easier to build and understand than a true relational database (in accordance with our guiding principle of *simplicity*), but provides most of the query functionality we want. A hyperdatabase is well suited for finding the intersection of a number of sets in which items belong. We expect that most of the queries people want to do will be of this form, rather than complicated SQL queries. For example, a typical request might be "show me all critical items related to security". The ability to store arbitrary key-value pairs and links on items gives it more flexibility than an RDBMS. Users are not going to be making thousands of queries per second, so it makes sense to optimize for simplicity and flexibility rather than performance. .. img: images/hyperdb.png Roundup's Hyperdatabase ----------------------- For our application, we store each item as a item in a hyperdatabase. The item's properties are stored as key-value pairs on its item. Several types of properties are allowed: *string*, *number*, *boolean*, *date*, *interval, *link*, and *multlink*. Another type, *password*, is a special type of string and it's only used internally to Roundup. The *string* type is for short, free-form strings. String properties are not intended to contain large amounts of text, and it is recommended that they be presented as one-line fields to encourage brevity. A *number* is a special type of string that represents a numeric value. A *boolean* is further constrained to be a *true* or *false* value. The *date* type is for calendar dates and times. An *interval* is the time between two dates. The *link* type denotes a single selection from a number of options. A *link* property entails a link from the item possessing the property to the item representing the chosen option. The *multilink* type is for a list of links to any number of other items in the in the database. A *multilink* property, for example, can be used to refer to related items or keyword categories relevant to an item. For Roundup, all items have four properties that are not customizable: 1. a *date* property named **creation** 2. a *link* property named **creator** 3. a *date* property named **activity** These properties represent the date of the creation of the item, who created it, and when the last change was made. Further, all *issue* items have an additional four properties: 1. a *string* property named **title** 2. a *multilink* property named **nosy** 3. a *multilink* property named **messages** 4. a *multilink* property named **files** 5. a *multilink* property named **superseder** The **title** property is a short one-line description of the item. The detailed description can go in the first e-mail message of the item's messages spool. The **nosy** property contains a list of the people who are interested in an item. This mechanism is explained in the section on `Submission and Discussion`_. Each message added to the item goes in the **messages** spool - any attached files go in the **files** spool. The **superseder** property is used to support the splitting, joining, or replacing of items. When several items need to be joined into a single item, all the old items link to the new item in their **superseder** property. When an item needs to be split apart, the item references all the new items in its **superseder** propety. We can easily list all active items just by checking for an empty **superseder** property, and trace the path of an item's origins by querying the hyperdatabase for links. Users of the system are also represented by items in the hyperdatabase, containing properties like the user's e-mail address, login name, and password. The Default Schema ------------------ It is hoped that the hyperdatabase together with the specializations mentioned above for Roundup will be applicable in a variety of situations (in accordance with our guiding principle of *generality*). To address the problem at hand, we need a specific schema for items applied particularly to software development. Again, we are trying to keep the schema simple: too many options make it tougher for someone to make a good choice:: # IssueClass automatically gets these properties: # title = String() # messages = Multilink("msg") # files = Multilink("file") # nosy = Multilink("user") # superseder = Multilink("issue") # (it also gets the Class properties creation, activity and creator) issue = IssueClass(db, "issue", assignedto=Link("user"), keyword=Multilink("keyword"), priority=Link("priority"), status=Link("status")) The **assignedto** property assigns responsibility for an item to a person or a list of people. The **keyword** property places the item in an arbitrary number of relevant keyword sets (see the section on `Browsing and Searching`_). The **prority** and **status** values are initially: =========== ===================================== Priority Description =========== ===================================== "critical" panic: work is stopped! "urgent" important, but not deadly "bug" lost work or incorrect results "feature" want missing functionality "wish" avoidable bugs, missing conveniences =========== ===================================== ============= ===================================== Status Description ============= ===================================== "unread" submitted but no action yet "deferred" intentionally set aside "chatting" under review or seeking clarification "need-eg" need a reproducible example of a bug "in-progress" understood; development in progress "testing" we think it's done; others, please test "done-cbb" okay for now, but could be better "resolved" fix has been released ============= ===================================== As previously mentioned, each item gets an activity log. Whenever a property on an item is changed, the log records the time of the change, the user making the change, and the old and new values of the property. This permits the later gathering of statistics (for example, the average time from submission to resolution). We do not specify or enforce a state transition graph, since making the system rigid in that fashion is probably more trouble than it's worth. Experience has shown that there are probably two convenient automatic state transitions: 1. from **unread** to **chatting** when e-mail is written about an item 2. from **testing** to **resolved** when a new release of the software is made Beyond these, in accordance with our principle of *generality*, we allow access to the hyperdatabase API so that scripts can automate transitions themselves or be triggered by changes in the database. User Interface ============== Roundup provides its services through two main interfaces: e-mail and the Web. This division is chosen to optimize the most common tasks. E-mail is best suited for the submission of new items since most people are most comfortable with composing long messages in their own favourite e-mail client. E-mail also permits them to mention URLs or attach files relevant to their submission. Indeed, in many cases people are already used to making requests by sending e-mail to a mailing list of people; they can do exactly the same thing to use Roundup without even thinking about it. Similarly, people are already familiar with holding discussions in e-mail, and plenty of valuable usage conventions and software tools already exist for that medium. The Web, on the other hand, is best suited for summarizing and seeking information, because it can present an interactive overview of items. Since the Web has forms, it's also the best place to edit items. Submission and Discussion ------------------------- The system needs an address for receiving mail and an address that forwards mail to all participants. Each item has its own list of interested parties, known as its *nosy list*. Here's how nosy lists work: 1. New items are always submitted by sending an e-mail message to Roundup. The "Subject:" field becomes the description of the new item. The message is saved in the mail spool of the new item, and copied to the list of all participants so everyone knows that a new item has been added. The new item's nosy list initially contains the submitter. 2. All e-mail messages sent by Roundup have their "Reply-To:" field set to Roundup's address, and have the item's number in the "Subject:" field. Thus, any replies to the initial announcement and subsequent threads are all received by Roundup. Roundup notes the item number in the "Subject:" field of each incoming message and appends the message to the appropriate spool. 3. Any incoming e-mail tagged with an item number is copied to all the people on the item's nosy list, and any users found in the "From:", "To:", or "Cc:" fields are automatically added to the nosy list. Whenever a user edits an item's properties in the Web interface, they are also added to the nosy list. The effect is like each item having its own little mailing list, except that no one ever has to worry about subscribing to anything. Indicating interest in an issue is sufficient, and if you want to bring someone new into the conversation, all you need to do is "Cc:" a message to them. It turns out that no one ever has to worry about unsubscribing, either: the nosy lists are so specific in scope that the conversation tends to die down by itself when the issue is resolved or people no longer find it sufficiently important. Each nosy list is like an asynchronous chat room, lasting only a short time (typically five or ten messages) and involving a small group of people. However, that group is the *right* group of people: only those who express interest in an item in some way ever end up on the list, so no one gets spammed with mail they don't care about, and no one who *wants* to see mail about a particular item needs to be left out, for they can easily join in, and just as easily look at the mail spool on an item to catch up on any messages they might have missed. We can take this a step further and permit users to monitor particular keywords or classifications of items by allowing other kinds of items to also have their own nosy lists. For example, a manager could be on the nosy list of the priority value item for "critical", or a developer could be on the nosy list of the keyword value item for "security". The recipients are then determined by the union of the nosy lists on the item and all the items it links to. Using many small, specific mailing lists results in much more effective communication than one big list. Taking away the effort of subscribing and unsubscribing gives these lists the "feel" of being cheap and disposable. The transparent capture of the mail spool attached to each issue also yields a nice knowledge repository over time. Editing ------- Since Roundup is intended to support arbitrary user-defined schema for item properties, the editing interface must be automatically generated from the schema. The configuration for Roundup will include a template describing how to lay out the properties to present a UI for inspecting and editing items. For example:: Priority priority Status status To display the editing form for an item, Roundup inserts an HTML form widget where it encounters an expression like ``tal:content="structure context/priority/menu"``. Each type has its own appropriate editing widget: - *string* and *number* properties appear as text fields - *boolean* properties appear as a yes/no selection - *date* and *interval* properties appear as text fields - *link* properties appear as selection lists - *multilink* properties appear as multiple-selection lists or text fields with pop-up widgets for larger selections. We foresee the use of custom date fields for things like deadlines, so input fields for *date* properties support a simple way of specifying relative dates (such as "3w" for "three weeks from now"). The **superseder** property is a special case: although it is more efficient to store a **superseder** property in the superseded item, it makes more sense to provide a "supersedes" edit field on the superseding item. We use a special widget on items for this purpose (a text field containing a comma-separated list of items). Links in the **superseder** property appear on both the superseding and superseded items to facilitate navigating an item's pedigree. After the editing widgets, the item inspection page shows a "note" text box and then a display of the messages in the discussion spool. This field lets you enter a note explaining your change when you edit the item, and the note is included in the notification message that goes out to tell the interested parties on the nosy list of your edits. Browsing and Searching ---------------------- The ideal we would like to achieve is to make searching as much like browsing as possible: the user simply clicks about on things that seem interesting, and the information narrows down comfortably until the goal is in sight. This is preferable to trying to digest a screen filled with widgets and buttons or entering a search expression in some arcane algebraic syntax. While a one-shot search may be appropriate when you're looking for a single item and you know exactly what you want, it's not very helpful when you want an overview of things ("Gee, there are a lot more high-priority items than there were last week!") or trying to do comparisons ("I have some time today, so who is busiest and could most use some help?") The browsing interface presents filtering functionality for each of the properties in the schema. As with editing, the interface is generated from a template describing how to lay out the properties. Each type of property has its own appropriate filtering widget: - *string* properties appear as text fields supporting case-insensitive substring match - *date* properties appear as a text field which accepts a date range with start, end or both - *link* properties appear as a group of selectable options (the filter selects the *union* of the sets of items associated with the active options) - *multilink* properties appear as a group of selectable options (the filter selects the *intersection* of the sets of items associated with the active options) For a *multilink* property like **keyword**, one possibility is to show, as hyperlinks, the keywords whose sets have non-empty intersections with the currently displayed set of items. Sorting the keywords by popularity seems reasonable. Clicking on a keyword then narrows both the list of items and the list of keywords. This gives some of the feel of walking around a directory tree -- but without the restriction of having to select keywords in a particular hierarchical order, and without the need to travel all the way to the leaves of the tree before any items are visible. Below the filtering form is a listing of items, with their properties displayed in a table. Rows in the table are generated from a template, as with the editing interface. This listing is the central overview of the system, and it should aim to maximize the density of useful information in accordance with our guiding principle of *efficiency*. Colour may be used to indicate the status of each item to help the eye sift through the index quickly. Roundup sorts items in groups by priority, and then within groups by the date of last activity. This reveals at a glance where discussion is most active, and provides an easy way for anyone to move an issue up in the list. The page produced by a given set of browsing options constitutes an *index*. The options should all be part of the query parameters in the URL so that views may be bookmarked. An index specifies: - search strings for string properties - date ranges for date properties - acceptable values for choice properties - required values for reference properties - a sorting key - a grouping key - a list of properties for which to display filtering widgets Our default index is: - all **status** values except "resolved" - show **priority** and **fixer** - grouping by **priority** in sections - sorting by decreasing **activity** date The starting URL for Roundup immediately presents the listing of items generated by this default index, with no preceding query screen. roundup-1.4.20/doc/default.css0000644000175000017630000000753111741527216014703 0ustar ralfpriv/* :Author: David Goodger :Contact: goodger@users.sourceforge.net :date: 2004-06-09 00:25:32 :copyright: This stylesheet has been placed in the public domain. Default cascading style sheet for the HTML output of Docutils. */ a.target { color: blue } a.toc-backref { text-decoration: none ; color: black } dd { margin-bottom: 0.5em } div.abstract { margin: 2em 5em } div.abstract p.topic-title { font-weight: bold ; text-align: center } div.attention, div.caution, div.danger, div.error, div.important, div.tip, div.warning { margin: 2em ; border: medium outset ; padding: 1em } div.hint, div.note { font-size: 80%; float: right; width: 15em; margin: 0.5em; margin-left: 1em ; border: solid #aaa; background: #eee; padding: 1em; } div.attention p.admonition-title, div.caution p.admonition-title, div.danger p.admonition-title, div.error p.admonition-title, div.warning p.admonition-title { color: red ; font-weight: bold ; font-family: sans-serif } div.hint p.admonition-title, div.important p.admonition-title, div.note p.admonition-title, div.tip p.admonition-title { font-weight: bold ; font-family: sans-serif } div.dedication { margin: 2em 5em ; text-align: center ; font-style: italic } div.dedication p.topic-title { font-weight: bold ; font-style: normal } div.figure { margin-left: 2em } div.footer, div.header { font-size: smaller } div.system-messages { margin: 5em } div.system-messages h1 { color: red } div.system-message { border: medium outset ; padding: 1em } div.system-message p.system-message-title { color: red ; font-weight: bold } div.topic { margin: 2em } h1 { margin-top: 2em; text-decoration: underline; } h1.title { text-align: center; margin-top: .5em; } h2.subtitle { text-align: center } hr { width: 75% } ol.simple, ul.simple { margin-top: 0; margin-bottom: 1em } ol.arabic { list-style: decimal } ol.loweralpha { list-style: lower-alpha } ol.upperalpha { list-style: upper-alpha } ol.lowerroman { list-style: lower-roman } ol.upperroman { list-style: upper-roman } p.caption { font-style: italic } p.credits { font-style: italic ; font-size: smaller } p.first { margin-top: 0 } p.label { white-space: nowrap } p.topic-title { font-weight: bold } pre.address { margin-bottom: 0 ; margin-top: 0 ; font-family: serif ; font-size: 100% } pre.line-block { font-family: serif ; font-size: 100% } pre.literal-block, pre.doctest-block { margin-left: 2em ; margin-right: 2em ; background-color: #eeeeee } span.classifier { font-family: sans-serif ; font-style: oblique } span.classifier-delimiter { font-family: sans-serif ; font-weight: bold } span.field-argument { font-style: italic } span.interpreted { font-family: sans-serif } span.option-argument { font-style: italic } span.pre { white-space: pre } span.problematic { color: red } table { margin-top: 0.5em ; margin-bottom: 0.5em ; } table.citation { border-top: 0; border-bottom: 0; border-right: 0; border-left: solid thin gray ; padding-left: 0.5ex } table.docinfo { margin: 2em 4em } table.footnote { border-left: solid thin black ; padding-left: 0.5ex } td, th { padding-left: 0.5em ; padding-right: 0.5em ; vertical-align: baseline; } table.table { border-spacing: 0px; border-collapse: separate; } table.table td { text-align: left; border: solid thin gray; } table.table th { text-align: left; border: solid thin gray; } td > p:first-child, th > p:first-child { margin-top: 0em } th.docinfo-name { font-weight: bold ; text-align: right } th.field-name { font-weight: bold ; text-align: right } h1 tt, h2 tt, h3 tt, h4 tt, h5 tt, h6 tt { font-size: 100% } tt { background-color: #eeeeee } tt.literal span.pre { background-color: #eeeeee } ul.auto-toc { list-style-type: none } roundup-1.4.20/doc/images/0000755000175000017630000000000011754432235014004 5ustar ralfprivroundup-1.4.20/doc/images/new_issue.png0000644000175000017630000005354711741527216016531 0ustar ralfprivPNG  IHDR`q\ IDATxwXJS lH F% S=* *Xc +Ć"EA@G+?֬w5ฃٝ}o {Mf):h[ $ "pA\ $ "pA\e@;$(Zͅ "@3QBEE=L r0Y~5f#$$&.XXU]-$eiCg'{YEsD6fe4 "@@ennn?-$tDﲷ499Ydy;h 3gnmSiA 8?_ryd%ݻ-hR֝A?i$' rp`VSiS?ӄ TUU%-4r5&rbOko vrvv3oK+^KĢ?s̙ڈfgHU@^L|YillR^.!>~ߞPggV x $Jxx)SԄWyfFFFEEçOIW8qb~~>|B*++=<< !?077'}`tr#GѣGS5V1=|ѣl6jƌcƌTq/y<7n8ydzzB޽,XǏ=bzIs +"QCCҥXqcQMLfhOUW5sGNMM-BHnN-1 NN.BB,-L׮x#r FpP32%Kuz"" {޵{O}D~IY__RVV)WfΜTVV`0RRR80š &g~3j͛TvH-li%{͝;ҥKuuuL&4))),,ۻ՚A")QQQ/NJJz}uuӧO׭[w^H\zǏkjj>|tҤ$^|yѢE ߿mQ;{5޿tR^x%#5;ᛔ8cK+m[7ih'Qf I+7o{&wg=N[7A/jorss !AAA;y򤩩)!$??ٲeTeGGG!_t! ЅwA:}ݻwOlZ~,g?<sqqY@mԲɐ ʂ 'Nus7.)ei=pvuY3AM*'N mee寿ʷg.5ydue''tjٳge3gFDDݼySWW߼CBرceccӧPy8 6CIނOѤ~/KS׮]gs А2M81klG.];vd@[g3?_n MLL233~8tpރZU9!!; }$2eږ-:~5i#HrL/[[[UUUⳋ/2LYEww .X+WPUUU Լȕ.]_effz*..ktRqoj!Чy4aNN\TTdllL-StAFFF_9Y23NWTZ+#Fz>*羔nX-OUAD3eeeJDJ/_~]d:t0`Le43zZnРA-|NtyUUUSQPᕕQqqq|rSwޥE(LZ{V#H\f|('ǛӹsgjyQQQUUU߿3f4ṬA]]ƳٹaǏiyy9zk}iR5B |ڧO|ތB} ?޿aaaMk׮qΘJ̙3?,,,99!!!ȑ#;q: %S̄%% lٲVEEE}w}'C''Cѫ}ܹso~UeeŠA SO>;vy6ݝ~ڳv^ӧccc=zիijj8c =1W#888!1It=hNnn.!AW\yA^^^MM^߾}G=tPq^i"!A.q.H D A.H D A.H D A.H D A.H D A.Hѣe! l6[1@SES111 ʑ"+y{{K!h p A.ʲ"..>!1IQȒѐ J:;:Y'#Gh>$k׮6mlWTv&@d-$tDC_lذaJJJ H:탅lMN*$'s֧Wȇ GADww{GD/x] sZ>-dѣ#G={`0--u=A466.((a(m&'sU]]gO>#UrKvJ7o^ol6f7/ PGOÈ b˩^1̦L8p xBODjg "JX,˗E[^YBUUԩd0l6b:vr$BH 9;[ٜ>MΙs<-`i7^^}gO~m~oyHLs$pō 픶ړ'o\\,ozLGG]:b#UVn>ǿ}[W{,ȪUg1QUUˢDIK`<',ᅨlBzcws|[9bFjB-1gGq&B76fL{o$$dWiUVݽ'}o2 Mf@" x»w3amme8MAM._ZZcʔëW{NÇ@1 ̹I a<]I>>q(m2~t[ ھxQHңVt8Ay,ʕTB/vvf9fĉnVfVA\rJit~UUw"O}8cT]A7n޹~#6'uHmW,_)O'KIyujvlFHx=_xu\**/UO'qbanc7r@YY񫯆&%Ta*aa3]쟐7BV{f ?v,8 ?ODj bKm@pB1lI]\zzUrˇs:tP5ʚ-UBm; .NMM/D[aN׮]u7nrܙm  ##OmU׮]w$™3lڼШSN6|ӊgUPx>N3ƌ\Sva2jmm5jYA»Ǐ-nfEUUAqe55:zֻ9N;vL:qb^ K.tSTTػfIIUYo)**CPN޶ Mݭ+8MVݨÖvgjYMMdP˥%%ꛙ4n{މ4kTaAAlKKK9Ga|V'y4hFfg|Z(2ɬig<~pOS[;>xgOc+*j6]75}߾0O߬_Eo;S[P]]mlA ?ODj74=zoҤ !{,ҢEC|Qq/!FF޾ /FWC̦X\R]ݜk]]mCQBHl쭀UFyB~|ٳ|ee%[[]pFDPsG@v |H <2Bq{{SGQQaʔAvڣq'_qԤVر50p'޶uweݘL&*|[433#쇃{p,)ӶlSSSСk !:igffa)~x|?NS yƅ L 0u̘cp=[.?dܸ~*hɕO?puw$`<=&ODig bU]]Y>!dѢ!|N7|"/[mfc/]qÿ\ //¢GP?ΛyQQa_~ wqAz];&Mff鿕M6:y/A)!cO}:1a<ILd%oo)DN}ם;/^2b$YG͇sLv`<#A( hd/@1a<6#A( h&@BBaxmFH#kY,vdy~qwk„6oIt.)>o-®T sRJw+11#&fݻ+0$t@QVV~.($ȊN1_DCCZjau ;z…:8tECֱBHrCOJ ړ'o\\,n({kK۷|}]'LOW U:q֭, IDATAVVI]#)iuVV=7<ɫgZZΙGx'GViϞ}wc`kppLP$ΜyXPPad=wr>>Ç{W3usw&N<@ǷjDD6 $1 `̆ Q=zdjnuSbHܶmB^Eiiu1šy\]{dgV6`~'})++BV:=xǎ/_ED$R Nn|yX_(\Rv mիi7nًg˩1|pjj*|җ@83ga-fvȉo<H2!C,/^_,,+{/N5k<3VQQ#""*ߴk𞪪JЭ6o0Nˇ響AEE(8xN""=llTT7^ƓI={ȸqxEXTڵc]^^Soc 0OL6@\]{4xQHa2YyWFHzQSS6'5`e˕{lhkۍ^<˗ErZZʕ\$**~߭woCMӧ(::-66J#+.:u̙|xFvJJAwa2Y%eeQSS7eer|(^~0ާ}rG{̙cǤ';/kk}]M5ȷsԩ|(ϟ//M:t vvԩ[>>?!)((lr.30J^;(((̞ -bHf$q0Y!))%g>~NfMMC}=㼀~:}߾0O߬_688&5-JM}CMxн{o<}YSӐﴠN''6TW'&fkǑgAAtoM5Ƚ SqHn.$?@=~hC !u6mPhǧΜiOϚaFPB3OBB?'L:HI#;>x5픔N1;sӝ;:tPԝ>vҤ3=qbXC¢9N9úu޾00J{oġCoY,N1Ϟ+++ښ5Ed'F 8_XXil=oךk{Ă>V4tD!o$;[с@>>EЫ^^6556DQ?߅@!JJ&  f}5~5ɂ_lƍ+l<+BYYJ111R1xCH>;U[¯q2x4}a5bW2mqt$#G33ޚX,ҡihhر-[KgwOm)g(ؑTTJ\!2h V7ih==u_:6 #d;;Z‚zEtt< L!YY_?n!~?yXF1vim4#d;g2=r Ƒ7Iu5"qt35%nCs8v"9ʕo,e@ۅj "@7y29sFX%KwblLΝp!$8̙C"GFx D ɓ]zaXr%%ߗϜIrs Ç$/)ZFv "@K)++ڵjzbQR@WmFv 7Hdr ӌוq!# JcBb ((0@[R`Q\!!d4#{-'a?mM* e8 \pYJdwxm \QŋdHaa؏ɿWkjx۶ Ud٥*66#,NVVĉ8RWXXAuؼ{㈈Ă ##s&Oʆ bCfW$`c}A7涖oov_~Iz MqH\ffĉ@:Z7Ap!52e O.NZu{UU_,H?A崿zqx_,Z֬ר[WwJڼƍ^=Kik֓rcohmE!Ob|95<<>0pb>/^]{^MMO "iVe '^-j5m/$o?""q۶ z[akwfEo5hPÇQHЪZA,^ cb^Xu/>f%i'N_ƈf'' hLL̦7m>~`;ԦDy:o%0IlVCZQ=45k<3VQQ#""ccUUtm@ ڑ={ȸqxEXT \+&ϸsgujsg~_QӧRPPATN{h SzsbnC/wVS@-\y>3'"3ֶjn23K3;;b lw7:q⸛8/ӨZlX X]WW׸͛==[Y:`׻ŒoWUUp+44oݲf@^GQB̻w7d G3|{=FzziDH^0$$䆍3@Bb= Z R9}pt R+VܿX޽Wf;/\L XOR|b=E{'GS=<^ATXPp7bsr^lve~[,>y*ྔQ&AMmmWŋQQWoݎeX!2_t"(|˷p >̵f;EٺmũTs7lڵM[Ο;ӸZaaAqqqTad 300\*ڵW8sMuiÆxZ OiØڄ-<{2HN۳8wLʧyxymmҡ#N}ܪiʕ4ww+--'cwaP˫W!~~sjk:iV` ʼng{xM}=!)鵟ijcppLj[Y~PX9>((:%%d,a:VݨivgjYMMdP˥%%ꛙ4n{މ4kTaAAlKKK9-aei|V'y4hFfg|Z}zlIM}! E߿ƍr#!@{q>quVaT2n%ȟ"6t(I佔ZEk|+F5.4i`h3f̰;|gJvM**yz[\aa̙{cw?Ŧe66Fg;QFUUUpp<ۯ5zyoE=b|[Iܤ̦X΄R]ݜk]]mCQBHl쭀UFyB~mb^b'4 dJǎ"RPr WAH`UU"M2cbb._N{yy&O8bӟ$E!g|VYQ%@ڱbco͘YxΝ+Wv`ҋCB9;ٻIg w<>L4WA5,үyB ɢE$9!H\5xyxy:i|4 ^^ @qp }L7|-qr",믏,[\9;ɩuJ< ?e3@[0y29sFX%KwblLΝp3!$8̙CH2v912lw1 xxEȓ'd@B8M/KƎpL2s&WÇ$/zGH֒u9RV&+VkɥK-jzbQRDX D)^"(1$dr s:1!Ag'dzgprru ޛ7bY 9䘐M ݇!W A77BHB"ަRbCg'G}# ]>Zs\pYJdB @( Tv$AEM] JCh=޽Y-$t_'Yٳ:mmMOur2o7ַ}=wڸ F 5JdׯB$[b79t(vt3gŭܻw7.[Rtf4x0-Ib1L&`0\\\O4Jf1!!+66#<|v:08x?'HVz…:8tECֱI+s^M嗤%''O:'++{H܅ )#FX/_>R]]bomAΝ{rX|QQَ۷_ f*u8qut4,p  ķ™3#" *uy^ŋBq† ^!!+*jBKK݇sՇsOl6g/@k)dDWCX.Xk\\fUU]}=#5m@n oسcuu،M.?w NK+`0Xiiŋ{7̚~~̬˴iBC%3gE3g򃂢gUUUr^˩={?DkAp "HA+-QۻucrppeVi@Z~ 8GG(GFFDA s A^>5 aH D A.H D A.H D A.HɨH?H䁙Y||l)fy`ee%>^dM333 H1!1MցܹCB9;9݄Q\!!dNTD)qss;hSp3p '$&: tݻwISRRs6;wnAt yHĽhM6pf|aB薄Qw1@;q`af$9ٟD- Ml6fIA( ? %$́"2Aeމ/}S=DKSq֌ ig !f81…ҏ @Be[b;gƀ ֑'>0(Y7_ڸյGvv9mmaz:.E'ygtg)_i'1SQWSgijiّa 3S(rxxM^TFhm8a+H@ bvٺ=tp\ތOLlt&UUL"]M|Q@b #ii+W,QTTh:AR']:+V׳I}g.OǎJuu ,zu7dQ ] D dX:Ad0 M  pnAmЁ*RܧY cܜgo5'H*$ѧwu R7c3"\🊆l ~wy;%kU&'~hM]̒xн{o<}YSӐﴬhuL6[YzR]R묢c۱^KPIS̤evJA)ѩ;.f 3oġCoY,N I~WE svc:ݍ݇ 7Ew! / "1$Rq_uo& uvvߖT~tщ8>g7MVzyD_x՞[,>>ZSn]wijSsΚ*!$ZjE]]]/LJ u|Ą35bh<_xOsBHsлnK/?gdYH-0r钯*la/ө g_ ڸsɯ'"9Y[u?ni|anrȏ3>uC~gGۊ [, 5!A$";{5޿j]%$%&\s½{vqwޢ$ևQ-9t舙Y͛68rmKeE凭< 6~UGW ߝ?w.ldh>ou[S ,zҸBm#OmظHSCkժ׮]!l>txRB;W ^&`TVVq ~__QQў={|>ϷZ`1ٟp8SN5UͱTSˋj## & n~~>չÉ' X윔b'__"ҿy<^IIH$rppHMM򫺪ر_oh4..S ґiai>)?'P|CQeϬUsH@pO.d2-iL7액/A|>_zG;;;^D L&311144TR#GNɿ ŋMJ" UIѱzh-=b899%$$ڵKu֭[$I]$[ZZ*r[X[?Ӹ9P A R{7 pww6Θּ~]LD||ݻNGF0߼yۛΙ3nƲ쐐OOO___'''H҃^tSPh᡺> , dι Jd\BF kk+aeeO 357/[l ),,d)))ڻm߾ͭir< ުF;Rwvӿ(*;;{޽ ɬuuunnngϞ} /LouRݦqXә ;zkiP]aC.Y=%#11qǎ+++: ի7ntqqzdʕ\.ڵkr\ p܄j+WT3u늊kkk322&|obrr A$uvx)Sl 葴=7͎AEGS_Nyw~OAe|PHGzSFFQ__˗MswwnLrrrF$#s~~~ߗ卍YYYc7xeG6LwܬY&,,ڴqƏ?X"(J5"~~~ s^2>377Bx?O]Ej];ˏO^s[|&-==2{{;tY>HkCӧOY&o5n\ |ӦMKNNˣ6X`x{{%//Rw̙z2h> zsYYY\.7::ǯjbb!Ѹ-+tuAD\3gΘa5*]숫ssM09_ GOu\wAAY RRaMGO%K^߽??pe ÚfGY @ "A@5 DPj@ "A@5 DPja|&:IENDB`roundup-1.4.20/doc/images/registration.png0000644000175000017630000005161511741527216017234 0ustar ralfprivPNG  IHDRw w4 IDATxwXW೔ " AT&E*آT"/vŒR RlED"EI}Xe(ʕk朙3waΰc:N Y@VedY@VedY@VedY@Vco Y@zT4X, =*rk;vpvHHH`0Ithœz_:S'B{O|e?all}ɓ',l666-M8177>rȧ~J)//;vlMM !FFF{-ZZwW\ip_$Ra(]x1222++۷jjjFFF#G:u$?SSS خ0ٳ,bmK6y5ki淊&?9rʔ)"[aaaAAA=x555ybYhQ~~>5bŊBHmff5k^|ITTT$'''''_pĄq֭[|>5Ç>zYDGG^JÇ7Ԭ޽ٳ r~~bbV%K.Q)))T -44t޼yTZRTc$fQLMM]fMQQ]RVVFޟw^ +B _耥GO:E߽{w5(!֊GL5xDϜ9Sx/ u֥ /˗/_^tILodgg/^"''aÆɓ'SUmRh',feeC*++[l!PQQ>}t>}!+V(//~۩䡦}:H~ΝӧO^̌*OHHX'"x/_޸qcXXX xĉiӦQ%|>Ktu]JM$''7ITD;\b>nnnT֭ۡC}}}uF),,tssxdX vwwRln|}}`^dRsUjWj{vvvsHrI0I%&EjG!C6mt;w]tGWݒ%K|ݻw9?pB****zxx)/(B@ ݷo5fEʩS;WW1cưlSSSYAA3g3gTWWSӋ->|8Ͷ[pajY,֬YԔMMMԬE,^RWWoV䄐e˖}'***=]FOV/Y[[hT-++'Q#Tt,AM$(y^h Ͷ{M)ZuiDkkk`Đ"zMTWW7mG${גSj ǎr8++'R 9rd˄Ÿj|RdXpȑ NK&N8N_\!xvvvDwӭKtAAAAAAXXuA'м%*++m&|we^k3hx.((ի5ܿ/_xⴴ̨(6˗KNo3KaÆlkjkkɿ##Gz*Ͽy&0]iGQ_GPoY7t; 1R&HxIq$Jw>=zjb })O>tFF%ل u;77?ѣGoN ,]T>(ޞ}vO鈈)RTT2dȔ)S~oۊ zZ^^rffZ_Qfmmml6::t(U".51E f_^Kӧ, (pboI|p||޽Gjii ߖ"Y?|_m8gΜHMݽ{ݻnlC{e)! 1ɏf]k- _(=qDuuwqqn޼m۶/^֖FFF66blBzׯ{X {ӧO'BoKKKo!܎(~-}0BSWW577f9n馚|:j{s̡gׯ_?uTjŋ;wN:%bkKxO( Cxzz ߽{wٲe"|.>}s\+77~j#b6;jԝ*tvv޼y3uܺ_P 5O/o_tȑ?fnn~ܹ˗/gff:>ng„ ǎ8cƌ>ܹs<,**QSSӧ_G/`7o|~S ʹJ/QAt]x jȑ455E~Ә&T| t .DFF>BMM~ԩ"?R:xc=zMMM]\\[bҤI{>~xJJ@ ǍI[ٟ35hРyYYYIGbsU~V|Q__d@`ddW_M2E8 իor֭G6쫯!ꗲϧjiiV?5\iiiC;}B/0~70@;Ծ9sΝ;YUUÇLwwwᮆOK퉭ul\#F̝KMMygmРA]z r!^ ֶ벏=y{rrr*++uuunjcooPK$\h'WLp +Ȳ, +Ȳ, +Ȳ, +Ȳ, +Ȳ, +Ȳ, +Ȳ, +Ȳ, +Ȳ, +Ȳ, +Ȳ3f 1tN,@t FdQ8{Hь3 1 Ȋظxckc1tȲ0*pg-,0J\.@p\O/odYh9\8]?pSB䃂1@,ۥ];NI;p]KV?]\{{hիɪlUUT !ήm۾%YԒ'Nl2aԨlގ·GѾ0 #@}՞=ow.<UTT !rr,ظ=T5f,@', 2mР#M >_`aa0{M Ўx-, rFdظxch-Ȳ0*pg-,0Jx'+\.YZYV8]OԊ<9rY Е(#8p.!SGG?z؁}hVHP,=Q!̬+֘y3N^^96[\'W^jzɧN}8_^5dHkvolk׎]Ν~AA6lJa5ǎ5?8-OR[ܸq܉1ݗ-,%%700䝩ަMMMcEř39[,tH55#ѫ)98X$=^Nm-ڱ={vSUe9B@c<~O11ѥf :;R3jfS|kt;**>_ ~yu77ڷ/d˖ Uqqϋ’w"-hBEc D zZZ!d䔈s@m?ȋ,+I&M|fZ``_ ._Jbb…#fhh(k݂zщlQ|aa+Օ!@GcPgk33'O Z=i8%%;.OM}M;FCCےXXBْF:(dYƸrXUxo}|X  jε#8+*E }}Bnv6O\r܃g mْM,Yb(ӧ7R,Eqt19w/¨_L4ɏbfg:K{~۶}wg„C{k͙c#Űp\\?yOOڳ疟ߝCz̟?|&qw,7thc\^**U-- .w,'R4cƌ`FdgL9ڷ L<ch<׬vc: 0b~㥴~nn=!gt$1nƏ7wwٳ%%33{՟e/GcYAdYYAdY`ul\QO/o[kø\;B7ӁԱ>-, rFdظxch-Ȳ0*pg-,0Jx'+\.YZKgYͯ^I"/UQQ_oLK'dMβ|ӧ۶ܰjEEՔ)C DBO-?Wcܺ BH@@\km =?z4ѣ!D&HG͚ݎ"!YXYYD_0*),{^\O&LM=[awKKٳ4WYYKJ!DF%t'II嬬J'cc&@\yI__%11gfq44|nOHHx{ӂ9Օ,, >|[5y>_]RTT1lXUUv#4'S #ƍNO g ! ffQOMT !shh(BRRrkڹsbP۷;a"ĕ=ziiiɓqD,iO*l eʕcZTT c-X0;׎r@8;m27';ٳmn .[gDDziibnʄ &ȿ7@]Գ'QT/ʒATGxoQG1}wgI~3>>;{~۶}wg„C{k͙cwСp#ߴv={۶ZZǛ/_>*BȠA44˫L;@SS̬TU|49R IDATH|{֚7.!!5{cx<3f3MǕ͛2h߾.2KC;fQ@c㥴~nn=!g@Koxsw=+PRR03߻wʨQ dY9:ptt~a@VedY@VedYfkctƺ{a\!˛@XSLBqvFN #sY`^TTLl\x㽕c֯Z[ )((y,2ZjZD^vؿ̙NNeŶ'>h?:|=}:a#80oee?U߳Vq۳gN4(!*O maՊ)Se r^w{{NOWAAN[[m:U/.~KN ?I̬֭_BfЊ:Ȝ9rrƖ{~hԣGy3\[[cܽ{Sw?Ηg gڱ݉g5WZiie|Z|q'NVv_쳔䒒wz675Փp{B蒠G6\%cǚ͟?͖'8v^7ZZVVӦ 6#U&z?Г:rB..u" ~ܼw'/P{ w" ܫbc/?+//wʢŊ+V^6oՕρT9}*…ĩS(g۾9#h 99oΟ_cĴ[KYYń}ntIFFշoM6رCu?޴i;kNkoo믑MVB^B'%JJzUVKBG%OknX3^M|:eKKB44[(>_栣b/^fqFF=l?%Pgikdԃ=^t҈%KԔLY,^(I|ɓ|*%=Ru?VQQTQQ9Cu1UI>!DU(>n9uJ*J9L>_P[˧r'UhgO?E:b!+V8hk ɾt{ĸ[7咒w55U\|B.5KMQVEE q7FhB޽!rr,MlVVV41/'LJ +|}#S%% SSG/>hhplk== UVV}咒r!}h $22i`R+yy9+TH4 @ѱ!ž=|koO5HobJEʛ1_ILYpĬY %>_`m|}#^x>-x ))' `*uu% _dM<&ggU GUH8ߟI1[+6vjf֓^@>{VH8P"lu !shh(BRRrkڹsbP۷;wY|w8 MKK+O#BeT*lKrΡcga>ZjtRҫYTS/)ytɒpuɱ-*ɱ,l-anO9wAUUͳg۶]ψJ%%nݔ !MVo|9gO ssO>饣NݩD_%RܹvPmq[cB3xpӧoXݻlm}|GOG133]qwg-?;?MtϞmJ--͗/d!dР^JUԍKϝ{bfVw*E۶K?;&5o]BBvk@;x"E3ff$<ch<׬vc: :1@{, +Ȳ, +Ȳ, +Ȳ, K`: mkcrM~˵#xzy3@[k B1c/ųZN@:8EE3@CkQ8{V4Θ1PkR @P!B'`bNhW\7,l&M4x\[99UtȲ]JkkWMM…# SS):yy9 [r!ӱt68Bc_<U(occ~STŋqyyesL<*pvmZGfe/X0|m@B,Qg=v:'ssvk)7ROٱcopEYYq8skdmz} ÇyO?]T)@[ˆqWW[OK{uugO;' GQQ^[[uJǸT3 >xpoEEO>n MuD̖-Ff˛|pd!sp.uQɱ9;u?Z02ҦuS@MgdYZU闛\+--o͚›nfe.1?aɂ ""VhCÈ14C~:¦gibZ|8_qSȲ fq<Uo_HRRNuum~:}g4PL0=DL5"|v{bb@@\^^96'йlIM}/pgOѡ+{swzv瀀8u]\~v]vLxKwKd?xiĈ~7n,X~o?,M~iƍ'b~տc&y0l)/R\KKݺu9uuN9 {/f+ꪯ];V8S9iȐ>ڪ+W:=nnnQԔw,I9s&a('֭s:}M)aĸhh()--Pgk>-nOx **99]kdMOw\YAdӦ sqqU_}n4ۘ"K˺3uKK˒)!˶ssOZBB=+IJlpDYlBM v]b5Վ&X?ܷ/lV:M)aĸL2?9=mڧտnRR=NLYbaw6[r^vsC23w6N{xLͲXD 4T~:¦gibx:dٶ`:xpoWqq++?TV~{c(g<~kWSOK۵+_񘲲UU5[\oVKI L.)ygjixSS=zG6\%cǚ͟?͖'!T?;yyes؈+ :źVZZˣGSRrkjjӝ3f3j~RKKpڴaÆ}$ sCme /Μ.^}i ٭[>=^2#g:l.]J )/vfmNL#?|1<~E T2\8uիY,!޽K2s54==o[wAu뮤jkk\VV믑G|# sC=Pw$R.4bD?jv{55%S d===s&+֭ͲbBȊj77^X|-ںQzNm-ڱ={vVrc1T}$VTQ7|8sjE1U8R\50$()ӱ{kBXbqqϋ’wږF?'R$ccٴhhplk== U4d_99YWs.1kGCCX[iFh0J'F$$=}ZiARRN@Učׄsm44!))ވYOBꆩEƫ6€rkڹsbP۷;B=+l sCe>!ܹUU5Ϟnv;׎-.~*ym,WT߿?ӂZ~AA A.[gDDziibn߬, sÈ8rwСp#ߴ)umۗ~~w&L[k-, rFdظxNKȲ0*p:-xF-,0Jx'H͛7Mbhhhbbr=;X}0!$0p]jo%BƶH*&PbG䒷o&p8Ie@@Ր 0"NNN555B_dbgg`)SN2400R:7)e?/.+{oiiHy ^߾ P,N""*U_`ĉm̙{YYEUU5k9]۷;<U(occ~S<ۣG^MVUe;8Z娢܍Jh22X,֠Ak֌;Q!hq%y7.!$00?:?@syNMcUYLdeS]]ۯw89 S.ES'o̘1x{<GMU"LOj<ɟ%3gKL|Y]]S\f&WU}(((߲ڮ]ARlTDuci1cUU5o޼۱/f.ᇋG+fӍddꚇ_9;J7"~?韩S罊5))k^_.ES`;wH… %%%<^pA HdZ%%ْwοY1 ;s1z\--=23#<*'RlTG[2#-+{_YJם;7 &&ʠSUa4V.ESdYh,!fYiF3 ꥥBji +&&'~Ŵ5k. ɱi33}卌FuScc*|H;ڨ1,cl܃.P_"]"ĬiaK~iݴ*Rϴi\\~st`aa`eW_r)h=z 2&^$(pam-, ""V{Xц WLLtf+mmHQ Ͼckh[qƈYqљXr)h9*Rtuu  FDliie|󰰕u-/rvRSS.d@ =|8_J 9>>wɲۨDےuMMz 6&&fʔ)^kV}x,<񔗗3De144|a#FdHrY۷;e"///=ƌcYڵsO{{͛wf\ I:z_:N=bwrr壆CIL9p ,5ڵc #gg ~ !. 22޾Ah(:e/\H\+5UUSYٵ}ɓYYE66;uJ-~pdVV'NضxGF]vp0]QEE L/30М=Zxƪ8]۶}y̽b*޷/$))_?ᄈqr(\kÆOgn  wvml F gh[:eSS_o:~9wϞ[Ǯ[dnnP^^u`؎;u͛' ?tYSS7-ߗ.jciCCC?sDSSǏ7nJ"rDM ㏗f}"iA@@M+/ nvz>yqci= $Z:O ߘ1cծ55ֻiKK"rOjtUjj̙$۷UNNsbb2骨 KK&,-=RSsa4V.C$sh7[BΝ;M… %%%<7^pA H\VCCHyii->a-6/0SE13̴i\\~st`aa`eW_Dx@P-jNgYssI-, Y/t>_Jn6lbb{,}} 6[ok%ziFL0W;N`<::s߾￷S.9= E"lyWt2S ~paei>mr{>~/ɆsVeh؝V ܻMW=x𒞽$U ̬wٸO:uj1M TO6@{Ӂ]]O=PY!.$?Y~_UWVV~v%;~<}UUMDD-*]SS_SRr=<$jV0nn߿]u\^4ŗGz׮`ng{lCM1&l4d_߈"BY/$ɺ}|}#RS_ ٳm\rΉA"i/_hGPQQ~^4εݹ&_[9r'%%WAA~ذ>{|%>~]ֳƂ4Z6ްxÇ>|f0TdYYAdYYA@GG, ^=!L"JaƌL =*Rtuu  FBpp01HI[[[DOO$Zc8E B@ 'ٳ cw? ,m2e岔۷o3~YdYxkjj +{YcAdYYuY-~sYxlnoTTS啕kVhbzcT9=򹓃I_{͙ӧ*BFekcGr0x肐eyͲeee~}-YCR^v3ee];r/wb #B'Ěnn+HL>Z'{B~q×_/; !ߟkiB6nrn+BQVV./*,իΝEj)5NYasw-mI"G貐ey=K/_f_cZZJ޽ !yyy;9˰X,B^4$j!CWVVyǎ\X]]] ƹ,@, ̓|!$"Ȳ<)@ 1}s'\][Zr|O֬v4ZmφСD3E 2l={Q ,:y5X㒇-K.5f̘m tM^3f|VoX[2Jc޿};xj7F R@B ۗ.[Ç=ݩPYlI1p0OvW. N?ѣ.[Y,0OvYvogVxCJe,dY`l;ѹ@!@g_ ea6֗.9rӁPDD5QXEE3 XsvLG @\dYYAdYYAdYYAdYY&r8~#E08]JNk٬ZBPP_234HsY )VėNۿݻkmk׮uuu޽{Ժ~mTaa!Ci$Z UHeϟk.xb@@@^^9s&OL=Og/Çgee.X`ĉt;Gﻐ'N74d_x&Ut5SË]%9͢{D H)MfI4 bpD3(./6*9bSՖ6 R˽쓹ji3=̤tAaai_?B.67/_nCj.obA`xrH>gR#kW[턄BN㯧e^j67nܘKL~)H EJJʢl oߎJNN>vڵkUVV޼yd29V8,j5wouu 0 (..;wqz>--c399p d6KOeVf2Y׵k]\?ʵ5a>>>mL?Jcc/ff_}a؟~OWTĄj9wŖnXvOKKL&3 uT*{4Bbc7p]4!$+ٳр}xx8>>voNr|Ϟ=qqq "h,6ŋ]]]Vv{dJu'ӛ,!$##޽{f݁Ep&***iZ5|{8lkrُ?j;;;kjjryAAnND.kΝtԣ.kJ%~kY\"aaaΝ%IOO=>˶m6884}+16Bzz~nddFY[.BRB8{&XArJ,"##id2 B 'X~y%;;eG\.lNN7aÆn~-**NsnnNժT*:*Io6kZ֭%Ltѣ{oumm+2}t.l33.~ #˫F#tHRt/_>|0::z,SSSCTTT9quuu.Kח eqc\.W*:77W^^νɓo>n ܛ*ҥKFv î]ʴ˗SSiӺ}}%#wfUۛ<|;nw_0/ҦMtܟe߻wn?ylV*j:tЕ+W Cxxx|||uuYj5˲߼ysqqB̝XgQVVƲl٢P()6t<}j⋆>ZOosLXq~ ~Eu? !IoaLx7wٖzزfMT*ӧƾ1AP. oLhUY7)IBA ,PPe* TYU@(BA ,PPe* TYU@(s>⢣ӕIENDB`roundup-1.4.20/doc/images/logo-acl-medium.png0000644000175000017630000001176511741527216017477 0ustar ralfprivPNG  IHDRJ6\ΝPLTE19B9ZB祌JJRkc{c!ZsRޔJ99BsZ1){{Rc99BcckB!{﵄ޜ!J)R99c)Z{c9)k{)!ZJ1kc{!ZJJ1k!Rs)B{kZ){J)Bs!Jk{ZJZ)1{scsk))ֵk!ZJZs{{11BZRRRZk!ֵ)!)1)k{J)R9ZZZBJ1ZZJ{BR!ZRcJZZZZsc1ZZJZZs֔1cB)Z!R!1sks1RJcRk!֔έ!{skcB{{Rs{ks11B1R)9JcR!!9Z!cRs))!)JBZs!RR9{B)J!)kcs9ƽBcZs1!B!kck֭Z!J!cRBB11)c{)199֭R)11Rs9k{s!{s99k1Z)s9J)c{sZ9{Jkcc)kcRsޔ֌z5tIDATx[T׵.ڐ8¥ RD$XH#hXSĔ)0ī6R|T QUx)kO~e̙}ޟ}HAS?r:N1j:'qܵ㜆S';Dn#:EfWŧ8Wקd /i9g?|51 :"(/$I {0)hL ؍KmXiΤ}}BX4MSIÜZ#"Zzͫ&<=ϙsᄄn7Fv, 0s:!tuz)<)O`bASHn y|>ʪcΈuxqB>~E)3!3uz“l%8r,Q𣌑4б/\zG ` %ǎ8iiAZWFÇ粪Z4J0! -3Goq{ *?D3Yo5' ]4Oh_L`;0f盏yG }P579;x8r0a0̥~_U<}jܫï/_ΰn+{PEA"`_ !x_W^xxIZˊCr aRzs/&LE*/^re!?Z.kW]J)+\[{پ=\lef<8~ժpf[E^F12kktA匸wV>>ߟ*(45*ҳ&+}k!vE7q2Dn==1 ϕ [5xL5}y!l;dn&ǸL9Ύ򹀉fídӯ)HqN8|v&fB(%G/W2M?SXq~l7Ie |_ |j xŽ9~r`׶FvBW;pvwS #JMɖ14E3V* &D^VmP^H^vX)TOEӹGrk/ևvtyF5򀪰5Ck֬Yzhꪪը5xc[23/wՑP!&*f.'*ÂKk&7ɡ_(d#=J5'kn];lv]>\ I]-LQw&) B$ii0Keܕrkݻ[`-kE w= 6lnj\Ӳ whW_=m ;eb0e$aRCL lF˲_ \ \(_3PFY?:Mv'>[-w<#JEu-3#Ž藥s宅 ԙ ?BxKD!ꝎjY=Asً~?Xo^ >_3^ A``QQc6[W)7nNfj:Ob9G;1+Nou _y} WŠַПܩ+V:|2<}r2wj^[O`(@>^_HGw<ղu#eF|F`،醅;x{ـDVFEF#3=zbsp 1j,J*\_r/[4ęn4C[vknKFa-1}2S{L /Iw_ra1 E4E-&I_][58JӷjH6lT\_cs}fR{"/#R߻FnC$D$ I]x>IgTz3}2O6I$1Q]]><_5YFH""BBBҢL^%! *uu}QE{-zԽ\'!/-le {K/[#?Ծ,)2w6z`y$MsAVp[ +Af=#xupѶ0m Y Ɨi[!. OGJkCSim}zwPm),_|YVh 7xQ>$+sFy *SS8=)uυ77 ?>ε<;83rrw|\zS倍7J3DL͌'tv Sp' ;5Ojx*'C zƇ2Nr'N#_>Oz5.d`ξ'UP:P ('`+ΉCxT~s\`>GU؝ȱSfrJ!g+ɓÿH%z]}|oGF}7fA̞޳cX'L4JXxH*7/WvShףB:Qa(L{rU؞lؾ-cUz13vwwW؎j8а6!uIi <͛%}Mk!2irl% =}a:JQ̎C6|H߽pU1|lzssAAED]Fұ1`5cd@=G;+/t:]L-Zi4mutslOO_sc~ӬnK߭3l1'p疣G)бƱ&oR•jL$8e7@))&S+vQMc#=#(U۱B׍=[*(`MJ$njj،|;G5 >) R.n؀#(la` Fc6l麻yx GF.^1zd:s S3mXnzџu"ҏ-ޛ SsMz!t%Gb7@DvaA[V̜93fm) 4=S:1 .&;'"a!s"g&ookkwpπfJc#43F%A%%"I)@tϟ?-UR.8T2yڙ!.J !d.!1HANJ0CG_RR;Ry !< +Xmlbw* "A">X2p;;FR& ;R1kV322CǙa1DR&x9$ʰJbRL_1[gHaÅ( ) gP ?> c2 &-a.äm?&xPS'wG so]NO\"B4Ѵ?kuPM-JIENDB`roundup-1.4.20/doc/images/logo-codesourcery-medium.png0000644000175000017630000000531111741527216021434 0ustar ralfprivPNG  IHDRJ6\ΝPLTEckk筭便ccZ199{{{119sss))1kkk!!)֭ccc{{999111ޔZccssk)))BJR֌!!!΄RZZ911!!JRRJJRBBJBB9RZcZZZRRRks{!JRJ{JJJ猌BBBZZcZZRs{{)19RRJksskksާmIDATx횏WVJTlb-6چYKkŵ㾙yۘ ovwX`"{]~Wߕw%{]~W __BL{a{17`r!2JCQ,xIpم;?|gvWPCZV( h3}MQ ǿOn` yJw}dm\lp &h"qbW_ilh0q)C7q}&7gO D@yoTN'$>ݸ) n‹و _].ۓ2g%/pc(Kfb1iU]₵[*5n.ɔ<&Z@[#%# ~$ "dgYם)H!xL"> mAuŇT9Ci[U8 <W LˁcBFj_/d(iWr BWaL "a*YvTR\Dݦ p"baxgjY 53 N] /fAJ54}28RV+[#Z_( Fvɳ)x7m6`uO}/#x[ 8Z;/$ ޥ~٧lx,iE[:can.]WDĥ 4O4xw (T͟!ߋǸY煛:f7mpQ|hgxq5x6ތޚ;~ jP4 My1Vm)`^jw Hͳ/a~zS^AMR3+n0ak:|YmS,78jvhM dMAsw!|d`d6xFOըlF?#oBh dx!tF5 fz K;;'ا0MrWѧK2<>cژ9KG$QRR^%X5O UpB"D,HB^+b)®aPJPfCѐ]Ga_1iݲtv7 162ufkM%O{w(O甫=Ȼ-eՕUf|m*]KΥK*˞kY+յZI|y\q7 "aiqyoTf` ߌ'yi?~i&|ggzI΁X̟ ˫f_L&GSK ½6?$~7< ~e[Sf: [??߇d|_`жö:sc t\=LӍY|,^x~1K&a%Ae$*[W/b>:rվ[hGc ?_V>'ߕw%{]~Wߕw%2<#IENDB`roundup-1.4.20/doc/images/logo-software-carpentry-standard.png0000644000175000017630000003147411741527216023116 0ustar ralfprivPNG  IHDR,JrPLTEͲ򜠤O<ӆ߿wggeDCB./,##!Prsq{}}VWUIVXG/U IDATx]{b}RiUJZ-.,A@{N#$Ïnǥk5D'@5J`5GXhqt*x^È7U"HᠩB[XV1.ͳq*u] BHh+c@ l1Il]q(8-U9$@ U*FbLūM&GߏUEHk@ !`g9.Pզ$!.ĸ5jKHVj]P/F(뢵ʩtg! A!MMa=Th4u3tA†X5*S$qynq/" X] ?J,9:k zE4u5v`5b| $˃EmK]L $Th[rhj4ckʫf,)X,E4ދWIYb01rU1̳|3ӡ.φBg1g("|D}j?e9#C7Y^ב#_+z[Xh`j+9-ߗwNvKz9XuI 4ꧮ$禑(庖9oM%bHD(/I3"R_})au0QUtjy\ݭV>#쬝2XNu8gV{zal6;ZeQǪ>uIwgpc ˋ V>`%ץ㸫%`|?cG:y]5nB(J,uq%b{{P"=tjOmpH|-Yn M96 CֹXJŒ%XჷFf ;|_BiGš\ %5T$ոX́*ilMy4<Z#gL0tz݊uGB<ëbZ&[AeU1Vm#0\Xo_qq3 wW7W`VofԈJu enhMwۃ `Cd,^Yi5m# !‚X X66ζ.)*U=R`px˫Fym]/.[zUlQjCo +I)ǿ% J#3Ib') %F`VS,YX x2^AΊ0Io~/NՋC$Ya^VZԇm.+xcѦEFdZЧa[L$3J |>58N\M%+hmz_VCʂHbt6"tᡤaXK(DOm/LοY_nڷ.)'O^Xރ+1ܔeq<ՉJ`X>Q`ֹB}!Z]RIԪA.Ɣ5n@ݠ+dU(^!$zAӝ3Z !U t ڄ$hĪmdx ٧AoMR'>*'PV$jXdғRӧ קϔ@G)rrmQ0 ͋ે;p>)?SEh8X#fU3$ 8Ø$l^r`{>YQ֣)rk57k90_Zm=H+U1fV>>m$F$"gh WG0-C]b_F \M* SH싓0pf~ MO.Q3Zoě쎣"A[Ug|f"J8nMEa,xrv]]کh=>ulLGaJ  T֨x0:zYz _Sp~ r>ekYr q4 iގNJBj\*ab9ZHFk\B֯T{=gxl-xB1ɼHū"tH;D)DVWי)k䞋e֝;[`nmc'\hY(Rt{k~(WM>zXGX=$EۻE +{^^~r +jcMR<ãtUjg5Ӱ8_M[YemgmXrT ֮,H-@\f-IY'̍~軧;KΒ`s~zKԖ[`QLi,KI=2slm>g%,M bQVk>94M8= ^ >8eFscj39 ݈lz`,eRGaxhӺ<|-!qD)t5P,'1L6ގ/,"jY3Ȍ g-.: Xb>͉g?fqQ&I=`LrwtVC(Cxzw`^HvGr1Kl3ŭ_VxfU M(҂GF31|dZ }[`EHX*FwJibw`wXxi%H^ wy4JAL+cVdgr;/4"Mc2P)=X>Xdž;I1hٿ`iH_("xA׊2=ʶ !`ʹOЏc)3e15-."L}Tخh7^{iKΚ&`2A +"]~1a7!Uh3BOцXH|0++*u0MVse;*6.\`yŨqOX>M}2~*yקk~/;_묎D\ M,yVk +/Jt\Z>8&Np%Zִ{$9kTf?w_g^)ʛaX^io h'qm;X. ? N> ,(W`_qkғseHO e( lEKu j BYz}2 ch[~lrNS/ZFX&Qƞ Kg ,d.0JJ9 w rZ~Agߴ|N O7ʿ冱i$:zz֣bx7qOg:k.`}֒vPjKS :dVWg;Cj<~C V/XX-GgyJgC[PIU:Xǰ{=HY"g:U6[&Cfr^ߐ'RÆ\ud>՜ zyv'%ٞ'v,-7ˤX\3J%gboY ` HZwSh,G#LyOw3tV=[(J+twqT|GvXViuuշۆG\E͇%Ro]:?o7FaY!ꆙcLhËyFXv T͆g<9i0X .ga|N)UQR?L+t/b5!O2lYgrF ] +(G.50>R,\h.&DW8+_v3Y.f5DwfTP weoDzvwYWOfظk*İh\.bձJo';lM#4d!Ialg*zuU MGZp_xA﷯)a~?i>Ul/Qm7݋i,R1IXsխ8 ˔hpWIasޔv1K9Xs4k4G7@oeͮ+#G;HivHxVT,nd }.Pcquuv:X<53Lur^UX@ YLQm1bgd]m7BjDHqY0{~z,WMRDAֻ2Z]u 520dsvdfyé: y+ "Xf[?U;e0:P@ZVUc,O9fˮ|Ȕc d城,rC\,U$:gӧG,]F F/ *Q?@q֕C]w19Hā'-WZ O5'Y΃ bx0_-hS[tAN,fMKjզU!\ ыgˀ)O{0:4odvV3%AɰzEpu=+IDATLؙVj'඿Y)gE7|$y'Jq@I`R c Q+YRhK"ܬ\+&ܸ fqUkHv_c.5ͪ*F% UE&UU%llUe{J{=GeYpS[|a"okq ac.r| #UiK4L"Srj6VEohy@"R* Tސ/RTh$n'Wa|QRYwI<ᝌ5cOrO&gaSR1!NeؗkYUI X9qP$;A,MgY]1<< :K^[:bWۥク4Y0<ï} 9?R鬦 L%:Oڍ8Uq 5Sn6db+(ΆY>ĪvNcfU]5*YX;OerΒ߷2g/}UAE*}Wp' 6ejZ}V=;vK,q q $0P>U^ I֝VΒ$ic١G( |M(H&Rd $߰)T#a@!/Spoi9a1"C'//ܧ.&E ,ыgU<7+6:KRȏ#f~\KQ,J 5)URLgUUۭj=;6 .8qۢtDNJTHUgULVeYdRu͓P?sϪ8nqsna+_?`X?(A)3s4/|z73?(cMtWc,o~xsSYMU/`aI Ì7r<(my(z~JnDyv| 7o0?܌Ǘ0V,س֞H)Ϗ.Xmx?I_, GqGz^,r{}h\G x* }7$ӾK~~BĿL XqV0-Cܙz@ ES+³-stL3&E· |~4ԌRkL\3Im#<~7 n5ƴf! rL42S +WGx$L5pP?ݶM3=BoSc Iq" ρNo`4>K\.8\P7a.?>?Ga>h!*Rj5lvx7MM=_hC$Rryhe Ob`%=?еMܞӼm2-$;ih8v'`GB e)bcۣ+aΈØ-ǞV{=EAj-f 3W$i0z=`n:Z78ajqA,<߉:{sMcT,7BK$=z,P@i!&5R3x<ʉGc؁LHR tGw `X偬$@5w 1X Gf"E ԏ} 6!wu#`$~ < Mpcx@~HϷ 3AOSt}u1FQPL/\&R>"ͱ0oH]7D\7??x`iڜjBV6> B7D4|#'Z{(f\oɝo|XUf;V8NC4Ԑ=~@r']S*=_@^ě晟g9n.*vs~nnnI9l$1H|z¯yIA~$\œ㯖Q?< *:SpغǛǥK' C/>m}ǛbRni)aˈ  M Ժy'I^Fvvi~ t`D{_<'d!nCXx^n*aPX'5m-J:ζ}'hlhU.t6bMHnMAbpxTt`+ Nv LmbkK7v,U9#%˃]"w|{y@sl^$07v5 {sY$! 0:2XOEX8g?a>R?)!^7ݲo zms(=,,tم[_)A~P!k6<IENDB`roundup-1.4.20/doc/images/roundup-1.png0000644000175000017630000006340111741527216016350 0ustar ralfprivPNG  IHDR}0PLTE7S ){BF " Df4B$@#q:+v@1I}}}d{{{/Gwww7RJks{s izzzU >\ppplll Cd0Ab*ZZZLLL/FJ)BBB@@@>>>d4N4N/~;X 000@`...>^'&&&G'!Y1'k:%g8.}D BBk*u@cskF IDATxeGyXvi*AC؉O 0#;Qh&Ѓр^kڑmtQqZAŃҺ@h?QUߣΩsN{My㫪6B1^xB(Tžb7?HSʬ(" M:_k_<Rl(tjyվ+S9[$H/=O.:о Eاڧȉ~ϒ0=&C}>Kīl)y}k&k'ԫkHE)D7>YCy<$a-= !Y&"i]R6.ka RR>  Ywi)i`>VF6$  t$̢ !Uo@`h'`T8/b5m@؋9rAJX[.j`hy<}e,5'wGdQ-ҾOE)gQ c"iם| ¾=c+EF>t跿o\-¡ E](Z(Kxj:)>^gǥ߫ Ų`s }:U3s4mSRdU+"hhkD{lfa(bu"ݶ:χ4}D N;Wld_cϖJ#`&Z;<)c"ܶ:?~K0`}<ɮmd2&Xn3EZZ.=.yup/ޥ^*T!e_9a4QRߔxxBu}X{džOf{:OQ;r3Q싐Quג}6̾q}-Ob!>M$evrAHV5BDב}C\FX vkWI\"Vx)0Vg;^}ݶ*Ϛp:BXғ]qU}rطaI,F>>JAO1TAO1TAO1T!}g׾kx7+M]!AOC4-E%ĦL&,Zj_KaiM^Ίr0u/}هEZΊro4Ju=ja>վ<4寲omѝ;G9nU˨OT/^AO?} X+G5rܧ/=+&վ6:z|~pppg踈)m46PsV @!WjCgΕ飗 );a$bq)b"\ ~T=oH:hy\[ɲ["rVqY';:8qT˂|Fs%y 7zriq"Qe; ?[ߺU(_~R n}q+ Px~+̾Frk~pRЊjC![m>?<=syw̵g}+¾|u~zt&_A#+~h訦B|q{ރRʞ7}w]nu:~-|~D/WQo54=Y=\e;=w4qKQ띂s?SA3C jn֭;VMG PE^Ю`Pr*] }S7}Ƀ+38W(vN/Яо[Z4oQŴ2}i_aW >NwfAŠW >USM<%X&ǞYWKg|N_ȜAʸcȾ)wT}w*|#ŀ׾z$dUσLE1bɚR.4Gnv K~lrTה,ן}9G_~hK|b_^(_y,7]~fGg_52VA-yW85sǪN ("߁`ŸoU+jpП}/Hr/Cw6FA6ƶ7i_Ic3.#$vOfd-sr3kJUN!k5?䆳 ӾB؏}\(GdtŹr2oaQ>R#xeGVi9+/v*璼p`^*Uϯ4fM1Ŕ9nA#s,ŔXw~uӾ+l w*}~s}>r sp8w-b_t7Ek׆i_ŋFYاя}W w~|'XI>4Tnj)>-b۞s5]ǭ$]1O'J)'+E}\<h>wI>7-bs}%/x]dr¾w=*&>B>Oʉܾ{Wjݿ!ڇn9޾J(ՓU)h %+2cԾc>[Lu$WZ^S҉~ ~OwEsc5nGmg 7e-^9>o :/͚2b9Cξ{ĒKDu,ț6AȫmԾikay߆o _}tԾW fM!ľfd⍷!}qM2f9+R C}W]vM7+{3.?~IڧPD̆f\woA&$S("Wm+~`PΪ}!fW߮^Bƙqzڧɾ׮(T=մvf)\=>>E7HC gPyL>>E7ݬ9Vuż꣕]-d<>>E7+|p\f1wњ^.KѾ4xw ]Koe[/@%hSß5cB|ze.KҾ/(6Ⱦw-ynدqYo:(pMv"WnvykH*\lgnD٧ڧhF3ʯ̸Z/wߨ.'pU}̾G9n)s`X/+_-T]6zކ߅ja}rOO{t޾߸vUz)l >lKЌ,$*ha=7+oE撑TAza^e\Cc!?r":Q[pv}یjiťI}[?ݦ>.rFGξeǵ#TqZ }SutW rҲ>$vhC0ri\mCWLzgRh 8,omD nK,7WoVKՋ0f-pv =}qc<_8< ܑh9h ӾCRHAԊʾ }uG[5(ka>mC?P/t>/4/v"i^mB^jY<޵3 { b}`c?4v>Hd*p)#}o] }OUStF:6ƪ)+$3Ŧ`<}S{LOE MRa芁SSTӾPӾ ūM]%U_^U(*yAO1B7W(FXn־kLpڧX"ΞīU>QW)TAžuڧXBSS,%*ҹ >Ųbjb9(HWqjbOO=K7jb((wVOO7mޫ:KYo^ODPO\^=US,!)>t}; E2ٗ}SGNPSL>tPSL>tPSL>tPSL>tPSL>tPSL>tPSL>tPSL>tPSL>tPSL>tP2+@ڷeÿ/V}ڷS5#~kS2_u?$92F\|%KVoQ/?DOJ,/+aD8W^yU=_W&SrFD@,KULQinGѾ ΋_w~O~쒟w\zQ"*Gx…J[\Qo8TV⏊gg #^}/X. jIL9@GE7-UHR *_~ -sť}N<z[}[ݫ}Rٗ /wE:Ԕ)OT=l˵/a߅ _|k_[(..\S "j 69*Z~:>IąI/^xSw[?~A{\ P]ﰙmPl8n6|&&3w$N#<)⃎{mokŲ׿Ɖ~KY6[?o=xwǞϸ/L)e;;:>sj5js ~ve4m{M/])FTpc9*b>"vv \w/GĉIZZ&ǎ`ӎL7}2/0]Zr{Kݻ…N˧}6s6>z5}! /NJG?w˿Zv 9;|Ÿŋww]m}Cg4Gmfsp7u7\ ].v}~wcLp5-tPSL>E_GZPS'^yO|>EoT@O\z_y3^O6ڧ艏Ƞv[T[nDzw@}vnZ0^4Єjz`}:,03*~?Ltg>:=_ 6|x~??~O Ͼ{፟' Wo]P%b_+|=Ix_q:?:|{Ղjz#W0K~å_ 7ϛc?' {3v5ڷ]HE' /U!ew{ z"y_9[tO?O@3+o#?Wo_7 ~l[) s<-udi{Ĥ{KoW_|SN>P^/qeU(9P' 1{$,MUŴԬ*Mkn)nF y÷}YݻVys=}WO 3'Q6=Mds,\L>GSg3fDF4ڔPUj )Gje *./~njgkRK$bTlPZ*KJXSKsuak\>=r.SPG:%sfk'4j_E+ VSKsuaCyǛ\/|[|:pcL6L8*L fbq>t~ňj.}+=)F]o/2)06W;kڧH¥'_P? ((wVOOш_qo~; ?}oyׯ?H_z>E.}?{z?~s~ݟx3K/(VاڧhBΟghcj+^|hZo__O4H:ײgv.Z|3JtnВ>"s*_w=bhue5}YԗE[<. v<e]%c?7~6$lp0rFwTH[*^"p${Ϳ#1Csͳ)/ `Ha_ΘF%"þ}͟<0+\Ͽt>حHB}ٌ.c嘔D&k+j3kjM55E +>??oOFY17[}Sv>\ۅ(\c?Bc#kmLRDzN #Ѭꑒ-=hB^gVbYKsD Ʒ~{F<_LY|gg_7"d>x|U.b?#O 5yNdI#r74֫"Ťc-׾ߙϿ|t!@s{8|׸L:wi r,R o)Ka4&Z.L:mj<> ; IDATo>GoC_;b}}f[zg@9@GĿ|.7\`k68^e Ia!J53|3|l:xDF&OZoc~67p1Jͱ<|sZŶH왂_~cW#@ENcGVyalcJt &I$lc-ڧXl)}ڧ E2ٗ}SGNPSL>tPSL>tPSL>tPSL>tPSL>tPSL>tPSL>tPSLP4JbKY^l)(Fk)z#ځ}ҨA{۸o#[M/K׈&Brk_ Vߧԣ׆Z]D\`s}ް|`FMn&ùFzЬIg>;%  G1hKWB]TC",> R c&^zQ6;ʾ>>fO}R={de_l|&>hk2=e%M_灸Nr🄏]jlYfu,* ~ZG`8۱<ٵo襫Cn[B{o5׫OW"rk߳ E2rk "}[nbڧ}9_cnOu몹`nuUqΝ`}G.U8>dvV>SkǾgֱ~ ݍj_N1ݜoNg}g{O{^mq~6UˏO 7szRˍK}$έLR%ys^Ų1}%b/TAO1rkߟ(ȭ}d־ۣX'־Je}oB/S65JMlѿijTÊ*ūTse; fec>3@DĜ[ .C}ᇍ,a&`PGd:˭RE^Ӫ$T& 7)mjFlhŀ܍5r `kVкVH߰ OkZJq֧Q sن}=?H_61ԚLH^&YFeQAP .|*"4D2K\o~/.^nYH?GA[K[G O2G|и+U#e5 +ʼnRS!]a̟Ag +G*dg~mC?ks]O0i%>QW+>_ˍX1s >`!4#P88fA.t'>|y'c d$}Kľ rHt8Xݸ&e ^DWu=/ G18e"'g#qe$1:Lѵ֋^ @\v -0PA]Q<&>O"{(Isrku,Wfz^lkI"3 D61F Lό CyHqe  )e&`p4âHYjXgzLV4eǙڷʀ$kW\k!OՔ7`)9KnkX.SZ;Pd /j)9Vi4n-Ц^fjo̲]ڧX-l)V }(tdpMјFyjTӦlq-(6@zjn+2**y`y5v+t)|lی>w8tG9>R ѓ^^&u2Z X}L.X{C)EEh"["vk}/)r1r ޣS&} k}[=]Ee_>>ec c7$$ՠ? С >o#n+]e"iGjCpV}}Kö3e - Sc@/ 6<rkBHFn훺=uBnU(-W'-Y6W9lf3f 4R+heiO~8WvF \eX0cS%Wnɛ[ ÌeH˛Wc[ohg_ 6?(B[ {'J%kBPyFLTM46>g2thyHo6[fe "+Zs< hrLs'D‷`= }CۜhF>;`7Ku8{-ă%da K$m릪'W՚f\q2M_7+CE䉳o!9Y=*ыh"Rq g a8IWb}g`>@g)(>'2L&D^AEbSLa5sjWe:Q=wE7Pf~찚* >24ǁL\eKoл۲ʾ˟Cl3]}f]78ʾʮ| Ž<ޅ[^&F]J rkHkӉEjrٲMy3jX2`lkw J]jy8„+W( NB&c!X0/4YL+=[4`"L4#h_pnk>t(o"LMn>)0qF,7ٷXBT\Sυ RO? oCоx}i1؇lV0@-cMŸ4d X2?u&Ig`[_G";SwX-x'ԏ}ޒ aR{H}(exڣCU^.:yV37 lXq Â>h0MyY1*eB=U kAЏ Ycy3GJ+lwvEcI) 2Nt=2OϦNV X,4Q4/ªԍ 8-X)XPm{X7`jS"MejզFk_aP߀ժM>jcOAy3J܆5|e@*rƫ:ajM"O4MbooZLbMHFD{Q7*RKkoS, ί}T hH[oYIw{M`_j ٵ~_yv#y b+R&x?}L}J]6[;X`MQQ=.r5ma{b7>e>*e3惙?;* ʙ_gy1}UlwsbAC4Oih y0U} 7<;A/zѾٌw3^\c9}p' `i[qK*du~/Bicv)K4qᲘ~m%vm=ᨣw2}iL} 58GA}&c:> оC.,GO=ȣzD0(hbKR9QN'DWޕ}{…>LK`3s7suom HL}.e|ER(ɢmX!Z0F^ڌw9VqFc,݌CZnbbw ܰ OD]: 44X/KpΡ%kY0j1):b-T&r@S[[U([n(PSL7u{>tP umb#mskTX//, s|Pڗ1m/SKD3H;oTrc*}fo[GP/3 Gg[SK@c;هcgUr;vϚ~WlȬc>gת}C缊aUSL>tȭ}P$#*ȭ}SGNȮ}ׯ_-u,?@ccX@ j(b+8O7JgsȐN! }!+e8i˾L֗>/8IKƾ$aƒ^Flfߒczia%?Nm8h{=07ձ b:C}cz]M2kXk@X TV?Di®]2(* ~ #k57;EI;f5'kF\>wv9:HQ{$p KtVGuI!W,.|g6&NN%}9)mϾ؉:emξSA@dx\NSi#|Q7VBAVlӥx4]Ez^@gg9$%Ĩl %ax<9wŸ;}Yil? ~/[ۜ}}^Sy(*E<}׵Ь΁]m;|rI}LJgíS6]3˹Ҝ%aNX)1G=bgSA2w}n)6̖rs,m&>b3;f>%SwfAflB'@ȧT6 3HDrIj4āuة(USbm&ZSjȯ}&KauHxC,ٵoh9k^)CaunkH 1KnW([nb0af.`UIdPe[5i*0VckaZifʱE\gJ*?G0Ol'AZwY3vea.:4ki>gd>*e=/ynqp~q:}OmI>^ 6/ѾjcߌoR @)ϒطOr3:QJӼĭ * ciٱ>Ϭ Z؇YG']ٷF`'j j_xcs#O0}ډ}L5o}Ӹ@s)W\]vrlHlL:$)'b& YW?>Bl@|9i>d>:͸Z87 7@:X}LgjHA@9LO3۫k/iwy>8mςoա7fɂӾ=ucYhbe}ڧX}vX2mqY6R{lpoCs.3hM5ȯ}vXUƧ_NEG/,E~Êؗ\D("aƾ]"a9hC7Jх_{B*`k̘ku]& _Q%kWɯnf~n*>Nm7Sy~u`x 7Q$J7L21Ԃ՘:ѾVm##J~ur`DC'3"9"Zn?f[ډ`,K"}};ɳ0zm!G,}SEM>p}1(D G=>XX+ؗ>bMXwwXnTO&%>^>UN=ӆN!M^<7 -\N&d׾.;6LdqhөQ`6h_Ȅ%2" -[gIniYu3kY{9Vp缊DtgOr^N7H1VU:{7vaY1Ѭc]Oȭ}SGNȭ}4\淹v¶9z\f` ?KW4$(cKc_nﴅ}C-Ͼq_ŷ%p Md_wþ̘TN7;meSG0~`_ᅠ cuL|g f_`1V!+& %ݞ:5vãhy@of(}8u~y xrE4p ۑN=~@@(iWg7y>;kbSw(?b3uJsw}&)(I$u@)) :AoH>9E*ӴqG{8-G\@{1!w ]=Yxx4r-v; :RK2\=@pAfxvBϳNi;oI}dTI![ENg'n.y47pfKet)]&4 ľ%{6ҴL2͹[NO\, m)N'i)St=aIEMyOiC]I`ӱkEs䪡orAflnX"J/g7PVg[C۳DHFn훺=uBn'噽M,sC λEl7dK33!ɒZn׾oiw :'|e_Ӄ[2K2?d_긯 Q-}lG⥙2EUl.Zd$L8_ro ҝ$&vliJ0[Wbbְ5Ek=OȆ3+Y1_uaʈ8v#O'$;P 9y',)+K'HIA`QaǖƼƼ)`Ѣ@,7'F>:pso"?ʳ~s'NB-} !Kl) q"A87GbkN:Fp.TvWrbLqrg' qIpP f0?e;@^ZA8}*]g # Fоa:@ٱ/M |'D=pDGc_Rrd(WðӬ:3,{bt*>p> fK hŸwy m>άHDnٵe|ξ`l);}tLv4cbܚ?A['~u\~}ªrBkƝ\ᕬȯ} pȭ}Sg˭jcv-ڧ}!7΂bdl}ʾĺk}a}fz+',hٕV`0fҭCKDׯ'X;f/05ٓ}b;P30F16K}k> 7(yɞ5I1^C2X e`OAn[ Gn훺=uBn@ٿ4~L p5TFStBk_0t4KaB@}rk_u^FR.wSٗ`#x5xR D~wX!jݣ0O (57rĆ0%?$F *񪞞zvTh-e\H\?0_ yqho|pJ(|k}j: U) /=BDz-DH}GbN yDxMX̜-|`^~~P{++M;gJ>G'P>\rhB$N?J>)0qF-*5F`ݮ}Dځ}®`^?aAc2_ ǚ+':+Dl'O;X> X1@==/0%t =u,pw(G} IDATi}HWI62:Sb[;/TxM#Id:EK@-E_VsjK] N[ԋVAF>}3/J{o 0qRdt>vl% *jNSAlQ7C vaEjyzjSZg)o#P:ρPE*X15*w}KOi@ sJΌq>|Q^ 4˸Dmek`Lk/@  C}4H:@}#k%Pk;;Gw+}~6g>כz¡xchUtw Ci|J q wUm&2:s'M t8"ὑ]og{\Nq*4[(ȭ}SGNA9zQ̱q7P ]#V0xb1yj043rU_O:͸ȷL\6w$ I#q}% MW1Ι80EM3[dBꒋr :@I+"aVIuϯ}-/POu ݿj"qcڧ}05rs|;\pSg+yaq5yk_+Ηq{\lϐ 6V mbQLyqhաhHIl2/׺*>hKP1X0kOarDU֦JZ 2$;acqhy<`@×h26bwXC<;~…}; WGwobkw)"t2՟`Md"%!A_=`^]fw7a1Qp-pW`Z"Q[>1£׼Êy5of'4s&-fc>sB!򑴯yՌv^݇VI,v m["AN%QrF!8=Id4vDXV$$ۊ vr |IDs˼, ƪFA6ʾD~B/<a} E2rkQFоr:?3~uط;/FF-sƲ5XlAgֶʆz}ІgVww;MMt;x<\@s`J \,17,^¨WX>&Dk.i²PShaѾ}2K臭.r@}CP%fh#ؗT *&vZGO2`=cAF7q7>9c?d}> ge11Aā ܈f Fڗ!Or /<@@ k]יcYY'CK5i3ƘV\YDxڋm$:ޑf?؋g8ƚ HL}.el$gcCJ E|}lǬ/ H9G|:LT* q:y';;0{6l:|6o+о8 x>29Or!)*l} 6}ڷ$dOf}PSL>qxqҡNmվg܂4 pmվf+>\_t͔/0IYQ`/Z ,چL-dX{#8w91q},̅6}x4(>Jon"6S]oEW?z(A~Ԏ #}s1J>1e۪\rmX%C;6";s&+T׾4$}IM79C3\M־"o( ٵD'iV0aO  HT,1W'0~QBd֑} 5/wb~O ,#IВ0E$0%:h>d`* RX{T3cxCrF0gtQZ;d* 憰3!* 쎅}u#Y#Z G?_`avlJz,7-`o0-R3HZ4>3]9aVe0jCN:g1OGcӒ,.mHN{F!'[*|"Lz>}fsx>|,gs%'q'Vm؇,hN,y$DlpDW;aOue_q_ O#>VHb-OZ: ^p}L:H[yjɱj{ ءDݹqçjISqp[0I=).+,}|F,[h=KM%e>>iYtd3u4CQqq@ -2osĔraD,E omx-IڧH޶ٵO2ZM% >tPSL׺j{  |#44:C )7h }#<. >&9!'`dxUF}6AqD$>|@,Dqr>agƜUB;N>;w y!OnPB~0hfzL;:"%ʷǶf ?ӒM, ;'Ĵ)exV5v~B1)hoP$#M:!USE}%dl@Q#sy$! aFEFоz G>Cit'e[g` A(mjR}fsx0s¼G涢?`! f"/m#hHYdYr!Kivyi,HJE}nQWc}@OhlgD&8| #XXٵ*Ef:'bRKF/ālkj70TFR;`@aڧP")PSLBwCHjb:־ۣX')jh0nPnjX(|0 ._bjho`kP?T"n {^ ls.zM2j7jߨhyWj}٧ : jr@a}c]dU`kz.@O x9bynUAO1rkߟ(ȭ}{ E2rkQFоEyW̑Ę@(pUX[}7 04N؆W3 MM@X)ٗ}5} DKA}cD2Y;*iΛQ,Qx*YCWb,Օ H&&_ZՕkS΍Gl6ñdaua@ukSE̲G\6mnh w}6ak( PY_9c$0#,KXksb,>;Dž}jUg0>hojOP6}6 hk"g)g-Q%oOYtbR>Os"W\\_q?0J: gp'r,&9™O6/3kkf\g_fLR ڳZ2#T0ri\fogL?PG`3"m֛j+oվ>XՁjb:)ڗê:aC^]ܐ^z&õkp q' 8ʄڗ*XF1eb!= j }" h>ʾ叏ImH2#%X۳JeBͰ("8p<HX`Rjn|ٟP;clλ*;c6j-,a]#h*a]ֻ#6[]ZpS46[G6kMxݦ+m_1]Y>_ X\nr]t㓮tGտo5p%ʾHD۲B0VFĢ] `l&ڬz`dєwK(g_jȤk0 #k֗kxA;. u=wazGޮaEMbv.Umwd"U+ޤ]&XaϊoٷFR+y+hɽy+U. VW$$3] #;7yط'!Dc0+4p'FvĪ$,X/&X{Zf>ar/W{G'cߞ0BȶmE:扰V1nlJ}Y^;:?~Fy W}O}@}^i91mUfv׳[ѱ6&Tr!n vZ}TN+ t J^z>?-ȯcXi9]mUd k.M eMW t"˺TuR#{67DbʡU I8\Oo3y.v/b/mh74Ev8>c^ bee^)jb:} OCx F6[teF>dCrٗ0x |U>ٷd(]]s+I>+F tެTpoW`m^4H<\^lhȋUcì=˘3oJ`ʾ "}+ӹrR R& 祕F42xCavXV ?bۣ oo/Dޞ{E^9= Ie~*ˡZ2/ rb,>u,obVhD=}(9>Z@ԔQ+jﰊ+"}i C%eշg׌Ȯ}]*bqdgwуEnwE*5 I}r y]h8|I*Eĝٵj 1l 6/JSGuʷ*E,Q,E(10E~sXȯ}LժAʾVj[8]jn:V6cc;OJSFоn%phzqFYX-L2h/, B;y–07%aM+¾h h_V{}5ox!;Td[ǐ7 5H>=G`M!qh_VE}eu1 ?^$v|\WǐuyVoUK+qLaŽgo|Y0.؇DI7}x.ȋ/i;X'R>`)va&!6}6j%vX%Ke7}zGV,'}{Ԧ;VO8!/k_Vb^+ H1ov+6Qtf3ط;:޴VtQܞ @dvGq6{5:usm*ZμYg}S@ٷ wȭ} E2rkQFоr;3nu0*:虼cٌY]{$%zޱYG_ J0#Wo3v)e>a_0cS%BocD"|fcP6`5ؗ7o7@ Bvfy.m =yNT 8wsX~kvY dWX*ԈT™~i"{Z7RGAvt2þc}ϥ ?ӯs'؅]g صO&1. =84ׅe؅O K'\*>C.}f&ۥuP.1CM, 2pFI[:ұy%] `Iz"Pr`,W>?aY:}]Zp,`. dQ59}~ fk_lt>?F}샛}L@8x'3a\mEɩ sE&d׾ˮ kyh/;ǝSC]T.)a1 4G|Wq`8ftJN.1<@x4 sD]3K ԗīY/Ђ-%kIDK_mڷy"9*}1J\|IDATڗ6:l_,eW^n)FB8w劽nSt%jh/]W8Bn褖nCKoݔ<0V]I.ҵ"u<\I{; Q, G ,y "yξqe^}fF c_I+5UbXd=wԾuGdY4%][ub-Y+f ՙ V-E5]raY}]K97#X=+odQQy9C4ŦHҙ} ߚٷUEF\qC1lRK~޴>K:n[.'&+>^ TG2;7axf&`a^b>[PhB/O<W2ڧX"J+Pbb9(}}ոUbI8*KzB1mwb_@5s)VmwLJq++q;:.*3|([bq#d_P*Y_xᅃ#eX8o*ޕ;R_/dj;@76}ظR[W8{?zQ d߱U3kWR ՃȾ+~5}}vQ\7.6}dž|}|iW*+̏쐵 ɾ~GN궶i+WMy ;ʾQ+Eٞ׵eվ~y5T du6֚k>VTek}vYRT/-/UBр6 >b0$SLB4{MX"R |!1pʪ*$->pH#zz;w7@ GDR0~ʄ}||;'"2 i"(:W7_%-ov Dfm .]:|zzzIIIMMСC}}}UUT^^>y䢢">hH-Mڥ㼂& ^zhѢdw۶m 믅sDz$`J䫩 Mdc:GtrrZfM׮]sss7lؐJ};/_.ovʕ @5z6|k7byVLOW{Y^^x)D)p=vnݺBuzjҥK"ibfffTT,dy'Vi4M]娹y粲qqQ[#==[5 RЊibFFƑ#GRRR\M~|}}TI՚qFMMMttK>|X^^emm=hР'*++]Ӗu!˗/Av{ [[I&1B\ǤᥐǍAx#G222TTT̙!۷ߟ쨀1 mUib\owC144zo~Ux<^d棿ZUj6Brs7_w˭ؘbcmb+,,xr68|s{maa}w-luppltDJZ+MqFpp0˥K?={)j͐x⼼<255555?SRRǏӹinn.n=߼yǏ !z0`@d#::z􋗒rVݨ8;w۷oܹݝ.<}u|>5+߀d4mϘ؃Jiq75_g&|Ư7B\nCԎ]Q"B6-Jjʝ m[#2OuF[[g{T$~;v\{/5 "%7Ν;/((ʕ+׮];rHppCS SQQit}#k.,,ƍG <}/*ZZZ x<^vvvHHUخ]/nٳф{{۷kkk74]nnMW}vqƮ]N:ž$;IH≎>xիW'NHBBBQGGg7nرclj'!%#k>Kˮ֮vw;=ύ!++EǎzNf:ں_}Pg^[xk^Fk _nY'sWM Ԭ];ݓInkMΦ&TTTLp!SNmj5alğ;wСCf,Y_B~yBz}{^t)551xyу]ܹSOO)GYaȑ#6!dI;wwoٲ[o>W^ٳgTaZZ]_~ۛ5o޼[<^YhlޢE_-Z@ x}{>|?&>B5uvrxx轻w !tqqeíPZZZ<Kew6ΟAPP\U&._LOcSPPO'77ZbEUUݻj{GO^^;ohhHV e||<=-<Cr$`bccvĄx굄|LqzbFfέi-7hllqíH:>|NHHHHH'N@e{, c& 'ƍcOM\r嫯Ip-[ 8ֱc]v:,HNRMMMeCu!;u8 0Y¦L:uaIIqVpk6nV6pqDJZl… ?c)O>ݾ}~ۤj7Ih@`wYv ۪֭U޽^dܸqBmXJ :MW u/ ̄9WO&+//Z4w׏gLaqpа#M8iD(nσu3}v쳉{ Yp =zuu &L蒒ƭW=;wNxxzZqSN=EEEtaaB̌>\ ehc=&8j>;A} ~sӨYRSS[b#2+nDJZ}ӧ!dܹC ECȦ6u痖R^~q .2244w@:d nӑ\vFܾKi& yxxi7P c}M%AiӦ)))eee|>իWѱI!NNNkjjRRRǂP۷oxϟ?;i$aiB{bw}{ijS—3?ehʔ)4}ACJAdZlbFFFFF"]]ŋ7!d7oޤ."`޽5k֬Y#-ҥ˪Uk|>tӦMoWhc,"4رcn*))r:::&MofYs|>> ח1M$رc'OrJVVVEEZǎKݶL޽{KΟ?^\\\[[allܽ{Ç!g%"2] $&6ȮaO{_dhǫayxQD9s͛ZZZ=z:th"D`:i"0@ &i"0@ &i"0@ &i"0@ &i"0@ &i"0P:tc"( :& :;wUC9z(z ( :#!ׯ'$; ypw DrDOWy OԷedJMb  &16}||yL^/IJSKد+򅛞=sLϞ=544KFFFR<o&&&ړ'O`Jxxٳkkk%[[[;k,N:mٲnͶQQQٳgF~RRRDvYEEeݺuٳgllCwﮩ윚dlfܹ ,xj _M噛 o @Cx&++k̘1zzzZZZ#F'a?'yĽs$DPHc6GL!bpUWWϘ1n^l3JSSS]dFINΓz3f+**bcc_pݺuYYY? CCCo޼\XXخ]KԎMMMzڵk%f͚✜˗/Ӎٖ:.\vڋ/ƍp_rŋSL5j_u/^?~ܹ4WZdfffeeY}< j+Wܰaϧ_>((H___x[o uNZ 03f̢E -ZԤ_>[&#!*wF+W=IPӧ?СCmKK!C\\²J+ܺ(ry}wΜDlv5-W(uzK.۷oR tvz}jԴB!|rͶՑWhnn.|6Hl"c}qtD>߫We˖YYYJ/7L_}%///oj`,_>qĽs$DlB>8E"N6;Dl^\q9R MPoM ?~<9۷7|SUUɓ ӦMȨ\.*.\_VV*\`y=zrSSS'O̸E}ӦMiӂKJJb-D.4iRpp0AAASL\_+"]***+W [jF oazz:cZZZZZZl.!kEWA;GBTF+4#LB$W#lwh^\qɓ/^Lh}&KJCCMF&&'';;;s8.]ܹsa;;;tY.((p֭T! ڵz^~[!S"Μ9Z3gl׮͛lK\"TD ɳ׮]aG٪ٳg.Wex, cǎ5Wߴi.;ԩS666z׮]M L'EqwߙVhaSCzjڴiT|5l"P)--eMwZ""2Ga]zHK3fʔ)TLzfW~~~YYYWپGDF-j@q.^(Z߿?++kҤI򎥙 BhѢEk֬ꫯ&LФu}@&ȍѣGUUU CUUոq֭['p@qȎ/ BZ>>LQ@gذa,"l OD _QXβsz\|B'Ow/yGBWM :ƶ C"cO݄4QF>}yDn/c绽? 'B(# }UU'@XYy!p;n'[Iˡ?`CDWpvL&Ov>ܱѵdk'0?8 |o>}/+kbb}cTOZ ʗ&RdfmpfŊSw\>/(ݭ<mo9_XXtn:LL̉pDŽWpTY2'- eM)*֍2C vRy'OK@ Q4ǷSRcb>֠f;X[w4EQ (GU?`CDuBHAA95K߸xǎ+gϦU'&.&NHٷӹs}<=YNzӅ3fV:x1sɒvvƿ2X/oR~v/0HRu//놋Nվ}OJ<~"v+7>/9;w*+Vc۷DgWOJZ*tNouϞGy}'q617ZŲin^aRꞮ)sN cWGwֱo_ +{w0QAц?tEee5~{eǎK WY:oӦ1̢CDD6&}Eiu; ʝ&֯?C xGBݻZYu`\k߾| (hpǎ/n6ݿF3v"nǪcՋ-tt=lb71|:!L|p_ݝ1̙4`ę4ں5gu"E)w?FT oQV>rOM068uBo]__m7ׯVx-%%hߦIE0HQZZ IDATCWaŊ?mmҩ{znaٸp.甘sF֭gɦ?~c'ڷ~wK87E3qF|^QQqZ_D;'l(kw*۶]*),-}}eUU@o-# :tj;$@@ (&2~Q\\diipPXxVdBd[nNO{̬gię4$266yv]BX?Yleuuu9;kD\r*#3GD % .IQt$6PП|ibW%2V)Y߿?:*~w׮Xv6B}%g}8a57nٵkҥ+j"kBڷan6"匍hhhhhhFFlnS_%<7=#aR,(޽;Y#z3 t1I%FWHg? {JKq]H?#Q6l 6mhXM?`]tn%UUuw>%̛p`X>tan6cǼ?ķ)UHC 7q"匍l  qc77HJG?dTKq]H?#/jn Ǚ6TJKK?w\kG>G +DDFO\Q^)ڌW*cc/O4az @?z`b.;П|Ίi׮ɍix 4 ~\ |V__eKС#0B6&E0ka0OtȚj_ߡ|P#?`i(%"DqRISY ݭ<8І?i!'l M_MB@CBO@(#'xUɓ',k&$yzj0rDZПH4QF||! qD%>ahcПH D`_at4F@q?EMB(#T*@ܡ?MB(#T#@|||"" ܡ?Mݵ|30`ǖѣw[;2 @*\]4kw\nR9"]7r.vkׂowtqAfp8j11E 9ݠS11kkkP;L",6wa7B;d&kݹ˚qkHwIX1⧟=+35m=zt/Zl={fg?76 37 Znb[^s'gcq %7BNݿFaao=[z@J?6J(((73cqPH`A_?~ɓ۷b1OJO("]|իuطU޽;[X\Сݻfd\9x=!$..{ݺ׬|ғ|e<j=8UBW_i NffѡC T(K2 coo^r):&mٳi/fvYt6n)=pŊ?GAt g{CpxYOJZ+4.y=d47GڵGgGKK_Ywٲ={itC֮4NCCd{\Zίwti٠Av::jNNfmСK99elp&m[Q^ONGmZThŊ;v\)+)嗤Gݙ9D-b@UWקRux$dpG%..Ç'iqȬY'n0rBI3B>@Ry0s8|K" 1|Al"y!$8)11ƍ[/Ϟh#R4y11Ύ-ZggbeE.\ #FH%4`6qbߘ؇ز/<~߾ŕƺzsʔf>Z\mzH%Q͛x|*}WhiWTҋ ׼u+NNΣ;:=l88Θi؟y@\ۛlnꦡm;yPNܿO>$.._,UQyG#短NEEej 5tMGyrr1=؁Lqʹr__f$}_zH%4zQuu}]~RwQQ^r))OV^7<ܽ{ϸ\{wϟ۶))Ox (XRRnMM}UU]BB~ӛ{iXX Z6 m޿ooIƏ'F?S>;(!USkf*}7)RCd7|@ɓ)7Ujjrll:Ns!zϩnh8fyQ9!DMMu-&a[ -| bК5Wx (+KM=sεv4 ""m@DdࠆysI5}5=L&&wwK,-Ek &o}Vp@\N?ПbB-RG%U$gkkr ۗl(reoՠnכ~:@ށ(( 7H5"FFG#lҳ'Y+<a zHg3ynn IF".*RYIΟYX˗EGU^%UI$%-=s3yC*/%H߸qqI>|512"BɌDMGq h+C*/R0lyܹzKO>|Ԑrߤ?&>> (tm,WjGC^v\]eΌru WM`@ ˔cqAZZ&~,e-?#->]@ B,G}%%41:: d׮ؖ57lbgOF&S &ؤX5c+l"Agq}W+FSgeGE/;؇]cG@1cz=!̴zxT*}:p jN۴i'3СWN;>6mkooYr)?l=**jwqoO^5k<_~!.e8:$=PANw`ƍc;X񧖖ȑ= ;6Ik&bdBgСݻfd\9x׮= Yίo.e{^F{BֺϧoϣG:w.ӧeh99\N-dFUXlб}XioBBU~`ڵ~ihۛl8zϞԢC.F5ثH,K#zL7r[?MGiZm,WhT?˖ \]]gOK: q˗pwP4 M7([ɨQUTD'#۶I-*hT릉7ndin`Ў50h׳y\\5;qs@aa1ѩZiE}Zгӄ++#z}{zj:-`㮮ߌ=*tvBկ_tPcˇ+qΜ93z:=`rQQ!U^[[_::tsuݞo󰳵>lPBBUG\9m4.V&ov1%I]>1qP1jQ@{x{qwjPĔ)aa1OFN}맴DCoݺ9y^=tTyIIqԴiC_߀t5t;ڃ~0a}&O-= ?A sxz^^ދ5k ſooIƏ'FS;Ɖ4w _zvR !éY{iW7p2}e1 IV12T_?ү!dg=ʕwK>ldHYD??'??'yG4 /&7BȍUlQG<<OΟZX˗ɠAoeW h:謌Zx/4tPƑ%UsȈ'_L '3f5ģG8&(De$ĖYl}m0l7ܹC!D gXq2e[%$?2Tր4QvS\B zm"6pHp0Y[55i, M_aB-"Iӧ4QF<=OROmFBHBB/^ >>>(tmD"';zzSQ tmJii)zΝkh@A8βsz\|BxMHe==?@X2E2B券3Z˗/>|hMKKK[[[(qqٿvݧ~|닷mgIqRoƊR%rssN-x<zyy5zQjgcc<8]Su=zt DkXdܹܺj;"4nz9"mLOjٴ~J.QWW^|8ڷS/Zۮ:!5<$dNs@\QQ#ű칚X70{̘62Ҟ3;<\XX Ǐ>t(L=ƍC;HPlvSS:;wP,zʢ'jlx^𣓓KAG̪qѵΟODŽM^2Сzu7+W}\y !0??3.:}qpj|gyaRRyPdRK+*j۷גP!4tl>jFF_~雐C/:}>詡16]d؅ .[6L]]lٲ*ƭ]7hƍ*9+FqZ_/ѻwguu^̗-~HF]ֱc(Ag==͊Z4 IDATv*XYkUWSӥU;w^.)UUF,ׂ^$$dpG%..Ç'5Ҍ:BĞ=͓|}UPQaΙVxϞ):ihp\Fג.hZ'I}8`mSؒ*G5iu :ۿz]W/dii!ܼ! !vvwӳw<M ի"".Ѕ66<[[@ B DJLLLԂDong3gUVq{|;v\Ɂq5؇kfsӦŤpzm򤮎W]]8(6-- 'Ly.25,,&5)˿{iXXԩBibwfpH]iiϟ?^RRR\\\\\ԳO?зů\׿²k$k 9[B…dÇ;VV.[v\׭[/Rݻ ޵+޽g|lt>s!zϩnh8fyكZ֬B x}sK hTJKK?w\kGlVE;(@Ċo:@:~ù\.5I+2KxqqqǏZؚ4O֬+7,ڸc,k栳yzZ/Z{~KqL&䬢BZM)q$(e\\ʒ+(3%>rss ² \]]%T@vӹѦ0 &i"0@ &i"0@ &i"0@ 8BHFF!ޞB-Fp6 rssktZ2mcee%5ݽ{2jŋ,A pm"@[kMQ6Z Mhkrm"߿cypssqg777ΜsεVt R63l0UP;8GeScMfxn"s้m Mf6DDh-HD`4 MHD`4 MHD`4 MHGTTTH)me\\ʒ+`m4XZZJmÇsss h .B\]]%D6"77w|\n}}}}}}]]]]]]mmmmmWnn֐&i"0@Жuرy+"Mh:t@166nƺH&*G4#SD4<^ *--@@O4MHD`kڎ i54c_Yr :Mh#rss ² \]]%T@vq\%<75߿u%9wc6l˪H(Gi%bdd԰} x @TZZ*RҤ Mh?NO7uum)6#G$Hڶ歈4 MH~jf\\:H[[[BH\\\5--- MODym+Q"M/BHDdxÝJQ!M/ BgeDyGF^|oRjfܫA V4QFӃ% 0SrĹ{ !Ç mIx&#NgPR>>>Q {sVjv>pHDI\]EJҋiz$AKVb0 EH@2mfB@i4M]+{Rag]ft|x N35&>:7c9u*EQȞ@ǕbK{[L}r!cy0D^5k<_~@S]; -Hݗ\n8Rgd鉬.H!rPXqkQyƍ7l84<:M)W7rK55 :&Z}2O ਐwFtu tt,;)U/xο{yz=^@lԸw kW{| ѩ'xYI s+媛S) j^?/{Ԑ[^AgDI$ܿB/bba(:MpTnܬv쪫A5>yy??铺וPVmbnbUi"à3țc;\ڥ yr/\~Myu]-!GZݲ&@Щ^n^>!=# zs5amU'5^}5־>}³NUu~4@a!MY>iB H"Uj~+A+ݺ:-k{k!D4ZN_ CP!Z:jjUռ/)Si"C-SGUyn܄AY8&b&iȱu ML i(K?1o 8ie+VUX1,,t={w S(X_a+)){={=~jܰpKnvljlتE358|@K& uw̩-7ݳ˗/iܰ5nXOd/ 89a?J*Z޺̓-5ݫۋaly̛7NhnԠHa>|v'ofNҢ^;;=qadgg^¨͛N4>5 [~n];54obS)P^r^$` b!M,9W _``ׯ⩌U" z{?|}Z}O= ӧh[>$8kOX"}޽m$LD?qlPpuysfNcF{L@``]=eK]ۯ_K~WysGm.^(HCC}lSN,$؉O6RI;ORPhap0=}S<%O,m5;;k|obq[qp_+,)))ws訪UusʔZZZD[?2*kɭֿuۧADu!偏U;}«[5x-ҏpš4 {JYY»Du+۷oV&773>~L:DͿ;Yt2ݻ%&&eDD흜uҵnzEҿݸ~u挩"mF^^t嶶ԩMPj7ĒSZZZZӧkϞ{O&x\fׯ[vYCCCd5''[iQ{$UҺ zOLLS.\^=.n];ӭoҾMRR^[zkҢuiH3),,Kzh4q"U@5kҸq^ ھ=oEjӆթQ#ڻ%K\FFAѾ}GjjOw AF^%?CωT*%' eĒS|E8qsu]ǖ(+?&Pz.ԨQ+P833W.JSIG w <ߠWVݻrll,W^fMް-[c@ҥ+]͗MUUrq+KhRSkGL4gh?oS>AݸAF+йsr%] U`+WhDrsϟ +?tV)5e֬Ç)5K9" hÆ:߹ U eĒS|epW.%$$dggǿ߰aMVmتf͚푖e> w+/IJJJLL\lq~\H&$q[oQQ4iΝۏ=njO'OmzzZllw'ګW+&''%%%\+2ts߼7;;+<Ŵ3DGGeffdg ,xN=QTѿR@)&6F$ [lIo͜I7nPǎy%+Vо}ԯSt(-Z$59˗ԥ 9Wq#mNffF&&momoOjjԾ=mJWuN_ғ'ymivM GӠG٪mwޓە+׸-kݪ٨E~`ֻw֭ۨNN F: eә3ZwΖwq߭lfۚGi"rs#"Z'ߢ9:6@@ZZC~ Mޤ_H@׮H;‚S' 塡_EϟZZ/[Y4gojؐZ_(nF;! QttXoz+ݥuď $PvmG:Q˖4iկOEjjyU?NcѣԳg~Ϛ@*O"*TDPA\SIS3o5-UoKKzuJK;֭5[iT+8Z(p50{{f劥?JLLXz%wհdS\\[7;7/Gv>ZEADtdoOǏ2=;֦ ݹ#E _-W}|~. 4kmB  SP!M,9%ܔ3:: zֵĉSJrVJN&[[ UI+VǏ]$ᝋ]Ѝ4myx,^Lf/edׯt.s΂4aݾMIyS҄ GYYO&дi[MHE4q"͚_5t(yz+z%Gƴ :|j c1wt8M@u5iBnn9##n'3:w5kh \26蝳zMh, u%ޞ~#(&4yhذfΤa_.TTёM[0*Pfཉ%d~MvPw{˽{r K* P\M,9X|IC7S@ i"@@u޽P'CXBLڷ;w_ IvBx+ib 177%"oﻊ };&ssSPV{@ Di"H4$@ M &H@ Di"H4$@ M &H@ Di"@0 J\z=qDHHȗ/_~VZ 4}rR!!!.]zUUU ҥKAll fΜY^=ksƍ ]vIttWtt42ma{+g~>y̙3?NNNlРaÔ {?&m$1}[~JSYH EOsirss'OܤI ۷Ѿ}S֮][b?+39\MܱcǶm t+))M8=s'@zj}}}wwwoo'Ojii͝;ٳlϟΜ9s̙D''YjsssMv̙9sܻwowdܹDV¶?jq}:t8t萯ѣGk֬qFn)Ji~j&(3 ջwzxxR{CC'N6CCCC-Z{Wq(rl=RǏ -,,$iI>Ҹ=Nc(m~& ~D!qȄlkkkhh{v۷;vxʥ^M5i҄]mڴ)WX`׉J*Q+8b*7@@DiVGzҥKl1c޽+ӧO:uxӓ l۷oj]>b\"Ν;gcccjj:x`oom۶!""BP˷o,--'M#u&ׯ_O2C#F$Omfff"tg0ϟ 60QHS'B"##׬Yvʕ+TO5kfoooi &L`eeefffoo%\[njcnnnnn')n6m4طo߬Yܹ@D󏳳{֮]o߾{qGѢE;w=z433s߱-ys޽{WXdɒKի[d rϞ=:pٳݻwܹJ*-_\9r5hЀ]e]e޼y#Kmxx8͟?Ғ D^JWq(1 ü~ժUD4`~LCo IDATm <҃.]gwfoGnvَ;QTTԦM|||nݺ5hР={U>>>...k׾zۥ=;lٲ3f}˗/ xF-{ Oc$>իW͛w-q}77ɓ'߽{w˖-4E+*- 8rJe˖o;ceB:Qjť̚5k֬YUV%v#Xis ?~͞=[֒Ꮔ(½"&gx(Jk̲eo*H E{FbG v9;;G"CammͮϩPaW߾}0Lzz:veddčP\Hiii§S 655dߖ3zhȆ2>CEfi[9Ϗbl# 6?i6y7J; 9skNb0ş('''CCW^?644߿H2N(Р)'|RJi +3g ())}OXl痘+\% m۶ITT!jhhɭ6lؐ[\2wJG_^ L>ښ-HKKRSS#l"Ԕv쿒SNu֡C#)IZ\qAݓ[```nn۷o7XB]2cw3z#G,]nݺUn]mr7F_|IDzzz* N:$tߜ𦒒pOC< 4bĈN:hX'@ŅK ImѺukJxY. EOEs޲E>"^CCɓjjjG XŜ(0"2dp۷oE:q/^H;4(g&O<ԩ"^nnw [Ο?_OOoǎkVSS611-O?¾GMFܴi@ 7oJ/_~}Jrr2 ֎QZV-zcM$OCQRRqqq~pG*MǎwڕPFǏ:Tby;;;:TKK+77 QY}y Ϙ1gϞ>ܰaÈ#실?CgB=Yrm0QDQ1^M4la\\d(dqJ*4(D>,v-{ց$o֬)S=~"<qO<p'O]wM6)))L1,‡U.:::Eg9jwg#'{{'OK*44FED!!!µyҀ)LDM6uppXrÇ?E4iҤ ?}[^lC(1Qy{ #\._(=z6,Ŝ(-ZPAFىիWDԬY3t_qD333 GGfffΛ7O=Ǝٳo߾L:߻woJJJFF+U͚5?~,rψ#VZ`Cuss{9rذa]vm۶MIIEjvޝw5jpk^F-[|yƍD4f̘F3陔t^l <-rUVsUݺuۿ׼ys":uTFFƫW{O 6mڔiӦ"sɒ<~xhhhjj*+,ڵ]?޵kWhhhnnn----(66͍aÆÇwuue7qƍ:]paϞ=_ݺuWX!ę3g:_NK|iqLL̆ KFFFӧO_>׌ݻw7n=ztǎ%NZ017{EH{ 8~xpppjjA߾}w.ˑ0227oޡC޿_V1cƈ8>}zݺuW\a?)Çk֬nذ… ww5z=@}U|XKRRRwm1sO ÇҦM3fw$>EܻwoΝ5jٳg獡Cnذח*lBϞ(3cx\OOݻwԨQ{6,DAMaaa{ JMMZj "??ݻw\6m$Fi/P>'MYݻ]d)'O￳gVt %'<<|…NRt +*Kx*@ :~ H?o߾[hQLLLNNNDDCRNP. ##5j]0WW&&&ӧOm͚5Ww*S(\Mg7@ Di"H4$@ M &H@ Di"H4$@ M &H@ Di"H4$@ M T:a:.]͛?5(=TN)ýi_ > M T%op_Li" @y"4QeOOyn4GG?q7/rriԩ֭b/] NKKX7}6OO ޾}VF̝OSҹs[XilܕXqyӦii)W4i-M 8JDdc3͛4WDRrm;mY^@p"ZQ0))]WO766bjj5jmթӐ~.hn:VQQ=^=IM]w_̘#5),MiE 3:8Qzװ0CEx. K++\=..*77ӧ7Oakw_hï_STU55+Q*/HaW7n9uꖛ7o6۷ZZUtu[9qװ_chإO $HNfdiggg}͟  Di"H4$@ q/^,8@\]]b@y Di"H t622XՊ/I;wB3f%@4Ֆ^xIηaԩS[n%_D&_ʢ2zذ?|nݺݻw͘1CAg?s```},R^fӧэ7K9'k׮l]]ݑ#Gv҅իW>|˗ʭZ={sDVvmkkkGGG555,Xo߾'\tǏ .400(B`;t/>p@lllZ{)]^xADM6urr211jѵk<<ryB.1~j֬ɕ1yQ۷/]tk̎R~d[h$GNNٳkժUBSў={x6Y`AÆ lmmMYǏ7332dyyyl>a„+ZXXpTXcǎ 228{SN^zժULBDok׮\ᖹw#)STZZjsݺu\_BB}JJJݻwn&mn<\jI5!!ЫW/PƇϋZ=|}}())W]]/=z0"b۷oy6c455(77]MHHضmRRRNN[GԩCDjzHIIaaXǍ|5[neff _!s  UOtttU62&}:::ZGGm۶?# SX8~xUUUww>\zu̘1EyDtԩW^-]Vʍ?^IIiٲeϟ?|ٲe˔ƍ'=11199yD4j(ޜ6mRRRƍnڴ)999))iӦM"qm@,[a+++46:p)TIJ*D뒉MX-,,={a&lllhrUXغu={4nܘdȑ#>|8a„"trʎ;zxxtU سgOΝkeeeee5w ٳ{o/fر={[dH-dӦM}ӧOvvM/?GK.ٳs޽k@uIII!)w-<@>RIٹrʶ~ng`c>+)߇ w%%LQ*g^ArLyώJ޽{mVJׯs @PoDvvvo„ UT3geP3H D@κ?^b  P?|J|ognn( MR8+:9۵{'Li"j~cR?3HaiDHCHCrzm@>mGXf͚թS'SS!C={Vl<ׯ7n1551SUGF'|v*|999[l޽;/ooBuA:?~,ARa"⨨KRhLhC&Lƍ}||֮]{ݽ{UK.9sݻwO?DGGX9nܸ;vx{{\޽{D{S2,֮]ۻwoOzxx,Ydw cرW_r)Q+JK8AG޲eK˖-UTTǏU'Oo߾O8Q`ӧ4hnݺ˗/%;w2|CBBD\|y 4PQQ100pqqC(%BǏDX(RϢє}ʊK?\x*"| IC4;~=IK)f$==RWMj&MHM zҥK 055۷ŋ|||Faffֻw3gpFFF3f033ۼyŹs#Gؘ=zM8Q 4JJJrPP9WeaaT`m1xGFcdd$rKh#!}mܹ剉:::jF={wP}'^3QL?^)e͍llH]6nMkwIUjբk˫ۼڴ!UUjݚlkWmȈԨUb(C>|%+>Sղeh}}}v۷իW/fr'ym4F:IQܩSD^ vwwWWWgW Ϟ=366fWCCC*IImۖ[+p_<!<-[ңGdiWny^B4QM͚5O:WVmZ$VV=66ADMޱ}kn=߻xrg_2F[:|mK;vP߾?|ҙ_ts-#Sʢϟe* Ja?5y2SV={F&ьyfф W5}z!,=zW^|r\KDDDvvvjjτ  pVbXjNJ%~P\R}L9dm4x:,tcѢEqqqV4hBY/_ddd͚ܿ5\[hhhvvW֭[UOJEzjW^^z'2t 4=lzdxkt>tH!C/\0͛_L:Qb3i={^biRRRbbemÖ[XX}|+6k֜_\WƆ#MMѯa ɪU4e / "c%KRRi(0rs‚>w!SҀ-5lHgСyQj*AK вe4lO 'ڵkzz9sjժ5f̘ŋUڵ֭{Ennn֭ p߾}9sjժ5dȐ 7`B[[[7oLDSL.s玖ڵkcccׯ]jѢŨQ\\\޼yoIH]SUy|јΚ5+..f͚:uڱcjq:#\SmժU=z`YflUΝ\\\ԩ3~ UF9/ϟ;w.rDfڴiYYY>|X~O;JT/!ݾ}׼!EJ(z\ e{V-:|p:8ĉժE#Gψ)@֫WUV~jժ{iboWZuٲer PDRUXpJu7d5ow.7PjB MiB#, &@fҾݹRt r}פ};EGa_?۸ҙognn( M Di"H4$@ M &H@ Di"H4$@ M THIIyCժʓޯXaCD6)jbnnέ[GKn:P-Mlܸe@ϟO7nR^@IMgVw{'srts\fn8͋:uu4uتK<"#R*VM_MWQPjӦ*[>~PFN=n-rOnibbm=aJz͚n}F;wfs88++ظ+88&楣MR"#^3iF*"MW^:~۷oL!PÇ7nɛٿP] ʸsݻ'2t&}9NRzDt!WDRrm;s9Owf44QPPB}\oUw\(v5ZQC DTvVV" bcÉhjG¤8v^=؈[gרQ_WUNC~:ωWx}bխ}rܓg((u4ĉ5DԹ0@ʕ+/khT/wppzuϫWORS?{]!3f:xpItGYY Ԭ;sfga~ /"P3M$"c7n'Y4 zڢxmu\(=kXߡC zu4TUUbb^ujkQ\\\(+,*']]O(+KH@{[YY…qqQ9>%ܼy [cBC~Y4hV`NS"zBVVF\\kع0"|ygjǔXx="`E﬈Ɔ߼y8:ٷo_63޿>}Ʋ9̙M99ٵj5iȡC˄}!Nju:uzj%%un'rOibv 5ϛwPѱjr\ ݻ(66<'';)ٳK +W>uH\\--@iWD(i"HP^xsuuD@? /(: 93iTQi"Fl4Yсٮ;LdH4蔚@_ϝAPt ÔQ#BDBD*K_OǓSdiIN_]fI իmiGuuu@Bm+?xI+ 13*))ijj֩S|…޽+B'r+W<ٷo_--ʕ+7::Zڴ7jH]]?m3 ӋζSb+AĔLQ*3ETѽ1I|!bڷ/٘Ĵk1ׯ+8Ԕ6mDDfffVAX!rF|H544BBB܉|}S\\\ҥKv޽De˖YڋCackѢy]"E-6}AvOĕQk׹+zte"]qcRW-EZ#5526KSZBkӰa_{鑚hA VU%"\{]~I۶$%O@@mLJre #n@@AAyVVԡC'$^~c66r)22׷Yf'OnڴÇٙOYxAٙ3g455+Uԭ[W^qU7o޴PB ,,,<==E:OOO6mZ͚5%v.ȑ#zzzjjj-Z8xHuʕ544Dбڵq-[x… իfll,/^8==}ʕ\hҤKFF[%sv {QUU)W\!6mڰm۶%˗/R{"ӧ{WWq() "mmmvUGGKEI?jW٫C2?3NڪU~cccpIOg#G+K"Ғc޽c 0D̟Jmݚ!b=/ٲ!bz/ٓ!bn?֝; ӡä0 s@^q y30Lr2̨0 0 3~|.=bCC,Zh1 þ޻rD$ۋ<lIv^xj*vq㆒_߱cG%%7noŋ\iu1>>&wQQQСCLLLJJ# :tϟeȐ!j+G?ijODuJ.\xѤIx:?;,X@D~S(##]MOO'ʕ+R[Z5"255Xj-#K 1di/r3<HbĘvD%m ʄKz/XTv@mDq'VZMCCWog!/fK__GUUU؉3k׹7jXUUyW{^F 2jjjp'D{#"<{'On+++"zѐDDܣ{QQQ ä}W^1 #G?rLY$"UUUQ͚5y:?;JwMщsqopg=lذA=hQ_[$O֬YÎ[RRRbb-HۄﲏZk׹O#>}>z]6}O8vp{Yn=|?1sv {ͮN>U6l31Q_@ѱC3wY31< ĜUa^I Lԩ۷yU 4qz륆1?1k0 ì^1P!oUy>}bTTaetuƍaaTT=fg3DY!ݻs%ݺu#ﳫS|--nID"MMMmsGl$B $FDv*q_\iF'w5ݻwFVQ.xvd^Jw}FbWѤVTĮ5h@@8m{x3<Ŷuf͚TX̌jԨQ ~yibT,=}̭֫}TzuYgUeevfZ^eϛwkժ{mm#b#^kkk{yIIg%%UΉ4dլIii;Uq@^ii)C QP4hP*2c`8P?ԌaD 2Rx9?p\L_χ93{iF?e{ZTTh6XǎDDސ=hϞW9}~ǽܹ3Çt>>.]M ;bޟXnҥK[hcǎSlllVKJJ""[[[sɇRc _Z]jH":uj̓ȐKv[nqJʻ.]4> -MCCMOw{Nav9k\ԹiٞZڭaI 1&'y9q!bGWX,-ǏG]޽WinCH$ ~=%6m˗2Cf>F1֭=rv3Zq60^ji63\]]+廸Ç# 6ܼyPuDT9+ cƌa_133 KT;+]bŵif/_.++{}d&}I7c\?|2==}׮]}СCdd$ িQQѕ+W Rh;+WQ˖-SwﮧӣGRܜ=zTz6QU"nuUj7kׯ_/--ܹG8-ڲ?(=ɭTD$;{̘cw6ƝQ.&o},sq^t$cf͚߼u+V622,h*7 La„:H$>Njw=-bggסC:۳o&{]''N:ȑ#bUhk­xwkm}֭[GfE hAc7oGD7.ZݣVV w1ypv:iW[Ӳe;V[WJ4+))4x0 L7o*nYM@uP9 HKRS~=@'OyQCWDJJOr ]5srD*+i""_ߚږ2 IDATluk "e˔Y=,_ND`bD{Ӱk~/S]Jȑth[.UjtdC5J&ԢF&:;sCW х, a"4RV{\M`#G6k(YlQ ] Q&?/UOw6-  L믿*|aumM43318s$tD0x LD0x LD0x LD0x LDQWa<XQxnCYfOg_CYoDLo(pɃzPWab\\ܠAΝ;JSMh6+IzM0ܹsK, EAQæNS^u&?{l„ "9rHv6JՍp阘[[ݻ0ӓ;v}ʕVVV=zسg|h˖-|An>͝3gN>}uV6ɓfffݻw6lخ]dW7=%ޤۿ#e˪ڐJ5Q5̜?{׵k޽{O<ԩSA]]=""bȑsϟ?mmiӦ=y%0H\Aj200HLL& Iq(ĉ'J6V[yQ:p^ntuu9";zk>>III]v?>7k֬Y /YM6޽;==}ǎܬ+VY&--ҥKjjj_~%7K`bve FU&L_ɣGN0M P7?]f̓jW4YA^YQQQ۷og f%p8 61/hTʿYsپ}k*}< ε366f'e.,oEEE΃e{lO>b,\e~wccce7:qD-XYYM2ݝÇU7";*5JxVWJJիWUTT6lؑ#G $\C1QK.;v,o^͈?9Ȗeaab v[aSmU[ 3gΌ?lUe͛7LMMa5?rzzzbYfMLLǏR)ps%l65(ە*RiqUʹr^}}+WT;K钓k5S,?7661b߯_?]]ݑ#GYQ:::ׯ_&ʥ՟={ƦK,F FU رcbb^BBBN ׮]322v1jdaNeU:::ͰQbN_м)Ç}zllRQQ-E EPt/Wk<'LpQzs5D*{…fԆl*p7&;wq줻sLVJKK?٫5tK.۫q\^xڶmNհ=㭼@YTyR#Fϝ;wVhE5wߕ|BB7Kkf{YX>}æFVVbb"w-))IR<5lJߕ5Ǐwtt\zudd]jkaÆ?s{YCO6lX||ѣɤZnfPpl5;}@sqqq针LMM+~&}_|yڵ%KpF痓SVVh"n{\PPPRRt5Ke͜9sOYYٵk׼g͚Um)K,;w.Vaaɓ'f])/ٞr劷77Kkf{Y333RiNN΁}1+.Yڵkeeeׯ_fVV]jzzkkE-^… eee/sT=8 ye͟?IIIkF=xӗ'凉Ν355&MMM+aÆ7jhhZJ0qO?t=zpssϹޫW/___???nΝ; ҷoڿ,Pyf̘ڣGiӦ}'ӧO33ywپ} [t[oUܝOD}X'''OOOww=z$R@@Y˖-;q℉I̲d'hiiM>O?uvvv J%V5a„jkK`XYYY믿ٳG}$eK1/@,GGDž ~ZZZnnn_|kJ?G a` :yӹy ]lYLL̰a.k4Ș̴ꮈҞ={~#RTzӹ~}M"FPzŋ_xR?~|uZ@/<7M)S,^8%%k׮...z*pn~:*$D|4^Ђ0aB';('tD0x LD0x LD0x LD0x LGZeTa3@<&a"@<&a"@<&C aׇ^^ [F>j"hU?@))Ld#ӧYǏx墢BU46vo8X֭>>x2*jK?/-7#FTlո]{Gk{|{_škxDEŅec\ҢEo _-[[eǍV:&&D77))tt,u̓Ү&$=6mۮeV:u9rȑc?yr0:u!"O|R5mۼ_ya֒v:QC4.磏;0QEEME üa۶uf&ZXL;w׈ 茳ke~sw.߶m[":x`]Txy֭555UTT>6ӳ}_b=444꿶tɓ'qyy9MѣBd}뭷K.ӭ[7"jժ5`%; o\%o@Vt=}G줏Onnn``T*5668jjj>z(::=q:w\PPPXXةS'"*,,$"555vӝ;w6o7Ps<^xID֭[n9w6mt=##ѣG7ۥK'O<}TCC#77 j*ovzzzVVjgffΕ7ySIJ{3443g:txŖ-[dWQUUo7Ȃߢ/.X@hZ觟~******b c3hɒ%w4hI$6G2xeԩD|h;bٷo_iiiXXM0~ZT\]]hƍs4hޔv5QOOujjj}󳱱QQQagYfÆ ]vuww]k;wttt6wqqH$lӦϞL .ܹɓ'ٗz1wܙ3gs탂_2| 8bdGGN),|͐woukfrcucڷgf`JJ*򋊘O>aڷg45gд)ċ/6M߽{wСlzȐ!OHHζ211ꫯ [7d[!KپuΝ+Hd3ܽ{W\hmۖw񈈈$.Ν7nH$ܬӧO[YY-\0...$$k֬aٺukvvѣG=m۶n/_f{@f7~x;Y#[[ں,xɞUBv֭$t?vhW:EQ^OgWSzl,]FQf&XQlRZ]JJi%@3Đ֭[iii줞T*eRTOO|??e˖WGW+B-}1_...b^dɪU V\/P.4Q<`iii>͵`:::l$7HAjmm]-5LL~56=Pj~d"䩺0q#$&Y);ٻ7sFE:+|XNNEsU+Jt-F["ܽ[}W^'W.]~^z9ڵ+..VUU%"6!թS'"Zx=]  ^mY7((h…-ZTvZkkkUUI&U@hrrrr4 'OL8Tyslm۶&R)fڹuٰaO?*HH@~dydO՞ciLQ4dȫIٝܥKE}{z[׏*YYԻwEOм)'L "s(?99YOOnܸѯ_?nį YHŬgqXMMmÆ lcjgѳgO"zl]uVv҅lFFFö:th@@]paʕlS~xc7NVgܜh"V|mO+ߧ*ݻWdjjRZ(-[TlmmCBBrrrnjoo/TXXXXXhjjZվm\T |W\UZZz̙۷ϙ3G U```~~~^^rvv^vmzzT*s,,,>}?|Gʮ}7iiieeeeQyЩST|'x;C}ٳBWN7oʝs]z])S/egӓ'p!MZ9e }%Pv+@D4i$.'66}NNNl#;W^CffSIIwb\$/_Z!-M/1|pwwgϞ >߿4GKXlIDAT~i|ww={xzzfggg֬Y mv޼yׯgǛl&&&^^^{^z5)܊طX7NV=+)xA˖_ٳV*M~_OcңG;75,Ԕ{=ɓϯ"sj;zN/?j|cǎlѣG/_|С$%%EFF ?x`NNN111wr劼yyb YHL}D"ߎ9~1o2suuݿӧO" Tņlzƌl:>>~̙Ǐ_Bm,E[[JSSӂ6]PP0k֬K lڶ@SWZZglllaagn_KM67lΡC &M5a۷os]zѣGv҆i"Ο??iҤQF/l&o?h*7U[ӽLMM ͛ûXn&yH7sa;;QFpaaaֺF#௿Zl٘1chტرfffaaam.o%%%˖-SR/^6l{СC!Cݻ'meeebbW_*nɶB-yݻwŗ om۶?~<"""))߹s7cbb$Ipp07+>>>44VVV . 9}5kenݚ}ѣGfddl۶˗{xxƆ&''^bïWݷo_Q3;v8}ի'$$_jnaaq++uɻ};wGFFfff EL? w;fCBB?i#”r޺u1--ӓJlZ*U痟_PPl2jbVREb]dɪU V\/j̀tZZ߿Ϧsss-,,شN~~>...4 0+++nZ[[yKj&"""33S6Sz ow E;v,b&yGj"斗5Aށ#ޕ+WhmmZ)S^[URd?۬uzz:~<ԅV 7/]~^z9ڵ+..VUU%"6!թS'"Zx=[  ^m>>>k׮VUU4irMŦ=ydĉܤ ܹ3h۶mITʦsssٴvnnn]5@y6lO?J$ooo3337owUrڵ7o'1x(ihhpiy;Z^8x?O>2n8-gyyt=&  e?99YOOnܸѯ_?nį YH 55 6cǎ`XCSѳgO"zl]uVv҅lFFF.]R:5tЀ"pʕ+٠ЌvW%K.]pjqqѣy7ղe˒v#" U!>>~MMM۴i#P UgEu%33 lbxxxpp-[*EW!!!999[nwpp *,,,,, 455}j YD|ʕ+JKKϜ9}9sT]W`YYY,ggkצK;w/"000//ӧ?G}+7|VVVV^^]P:uJMMm6U[JKK%I6m=z$P`o߾'O[NsޓhyMw }tƍcǎm@37>ikkO8lܹ ]hpx3@<&a"@MXZҒڵ# rs@}@qť+P7&4Cvpp044}{Efq1*uN6JEɓdmMԵ+}h& a"@zi++ Ņ>}|͚52;wqFxxxLLD" f/_,ݷo_6?$$QQQ7W+JTΞw޽{矶m,\[n" Kys+QİS:u;ve=zC8p )))22R8/vrrٽ{+W䕘{f/f1yhUO6-:::**o߾_}:Tņlzƌl:>>~̙Ǐ_Bm,E[[j.YRR[7yu.==p޼y999l~iiŞ={Ѣs!{{{I&%%%EEEM0b\vvvFruuMII6TVV?vX3306SGG',,ZWWaTiӦq<]ud[ͤg/^8l06}ݡC!CܻwO8?!!!;;䫯*,,Tnēm,E[t?~Y MkLLLttt߾}m۶?~<"""))IvӧO[YY-\0...$$k֬ZD||;N>mffzhUl߾Ν;ᑑ\~rrhΝ7nH$2^ŗ/ou7C[n9::zzzRMKR==j -[#\\]M YHfyXXX8heeЀ57vvv'O={ĻڵkӥR;wucNRSS]ͤD4i$.'66}NNNl#;W^CffSII wP^BvEE[j3믿w^Ν[6_L[bE@@%K455bbb|oVUUuڴiqqq [z0w܍7N2fϞͻ={<==3k,6_^7ǝL* 4ta"@<&a"@<&a"@< (^{IENDB`roundup-1.4.20/doc/images/hyperdb.png0000644000175000017630000002333611741527216016156 0ustar ralfprivPNG  IHDR`l JPLTEckkcck֥!s{199{{{119)11Zcksss))1֌!))kkk!!)ccc{{999111ޔZcc)))֌!!!΄99BRZZRRZ!!JRR{JJRBJJBBJRZcέZZZRRRks{{JJJcksBBB便ZZcεƵs{{ss{9BBksskksXO IDATx_VO2v(T) e(XLtth,EYrasq}! I IףVy<%tL'5ļ&|b^Әzļ&|b^OkyM>1'5ļ&|b^OkyM>1'5ļ&|b^OkyM>1'5ļ&|b^OkyM>1'5y/(Zעy-Zעy-ZעI!˺vo6~ܐ|bHݑrëÇgcmx}Stwi}z^{z{yo;N6Фl|>]mI7Ct̅5:n0mg}8CQ+mn6~' 4)b7b!UCoS|N/nK<ƛa܅v!::&l@=zGk$]uMp?x~P &:{,Ös bY+Os1܇{SWKG rS—Flq\8i$; vU6dGe/#D!FHMۅaWG,rPq2ƿmB'>$/ϗghl{ %eø{\@/r8ta髟qQJM}iĠ]}Q| Dx@nZ8LJN*ɱJJ[(>u/1b$ܵ "fN"kEn&'f2Okz"ۓ+œ,ybO]=bL2?qb U^k+ ňU/M&0LMk_VsFUf\Zԗ0d΀1|5^Pn7 w}-b<JV%r̥0?^˫Z?(Y4]F%*142:U,9US%I'5N1-gЯb#y?uwaUf+N2˳F䮿>`S&q:e;`L"{E狼J,TtbT[6Ҝd0rBx+!vOL||z\R< ѱlO.Nss#G;ij. >k * xwI;c0r!m'z~kt^&~J>]cu>8pyKD$NDm5ZS]UC]pM's_ Į\pG k88AYFۭ m'JX b h :8b Ûdpz3E/dB\;XWx 8~.keE1m$oX6uLD?0F4|8 taUķo z_( ]W8'Ay4bw`7(]&uEm?U1N,5}GXAgxPWq(\oR_^syd/b߆/SF|ۧ/}OO0 uPѪQ͛]\{}-ٓ#h|qtWJZl-dǑ/p±ooNm>H즋cz`ǗRkIyXv6*@C"*ĉF#FuB5T)5ZhE?ф||c6Ǣ+#&q"IOO#:('Ki&xS@ ĔE_!NU*G}):LBbn&b"3/=q!ɘ:a,$bbpl\|tuX0T S7t<}%:֮P1V)0!'}n%czDW%3$]Jg3W|198.֝J6c[&^VXW+2V{1bHrW-4n$TeZ8QޅXɖ i=LDV̢eJDg'u+G☺MWIۈu,՗e=MDLˈu;Qfe$NT7"f/0lj`ELOKfUĸ^! a$NToDڜDhKGa* UItHXRN{DTk TDI'gI4Q&hT8L3 B!N$/lɊN)4$ݢc!($s+I4" ItOt%2$Ve3lyIw^1\l81+:$6Ef>LXD(>8+P= C #DF)^3Va"a:[Ңc!5Y>TJ͆d乆 h*"ZĨjZWZ^EuH+(7ͤ)\V7lqQUm$2bVX iac$X4+VE+!-ZRa$}]1a)$EM)yi1]I1Fki8P+fDK)>dc%kbtEL1'Sgtkm$ibu8BL{1!ّI#E=Z1E=x0УU8ӚsUL4&%6b9׉ItYtgW8HLcĴZuOHH*9өOČg 8;KFpxn0~bB3eSli;1 DQ)>@2 *1ʐL)3fn-I`{yOMl !A|4"U*sT1#g(C> )j|FR&T.\3^oUUIٞTak>fS&_`?Q$ sJJ81d"*k( ȣ K6]a9K`ѓ / \-2!.K%Lc ~$6U%dm OZs^OB"Mtݿ0D#R^$;ROxDT:U;:U=Y ؠ^gȤ`FXOYNH0\+|fG6 */dO&0"}bmHJJo%$&x sѕsOHE`-By*ʈ'$"2:!7:Z{9C%B tHEdk!cPDxmҴ筞""DM׆PD~BX|bFӗOLW>1'+ӕÒVE_E#23l I-h#f&ve"5ӁL pl4[%f%^5йf{.Ȱ.r6Ys[,g-4X$f):{WL5Uf5!+ZIYm-Dڸ”c+bB9`mqh6jogU>k,Dg^U̳,fö,ӄ`Y>^DnlU/}V:"uk!v 2B:[*Ԭ u6%jn*1!T*Eb ?xL,ʏj{]j㪳cycW0z9N!KS6Iw9:LpXgWM,7 bJ2@M>5 Lg֜2_K]W/Jͺ5!6>cugiEعG2^z#Z1ʷدa*W}t`R"21?^z#Z1*QY}_:brs˪hĠpT+`G e\?U.rF'Z%1wUp⓱OZb{}|'Z11W=mzNjvVMUcrbd][GfoAO'FNJ4Z0\1%mu9A ܇/]|lWr6 pb},oM 1e mw9D=c9K>8]DN\>_)b}dVo2_fkA#/CKr>F'r[qwͶIbnq'rY ѳmM\&Q9J5CD\ohL ;OL=TSԓ6\&0˻wKV+a3_0KwãW_%s`r,V\B o2L׹\X@ECn!}t0 3r~09Qk}||gQyrb>vw#+UUgꖪ>.]S7'r1 ##z~"7sS{mɮ~"WsHiɿ'r -t!e3/w#10Og?\J,r1Wh[K fNn#FQ:åaؽ\Gݾ WV_Wi8>"+smxhI57Nre~"7satup#:W?+ASビmsIy:W?;~b HB@ZȝĈ`VC<詜@j]D.%ɰSCAtu@P޽RW?;}`e드bU޽r'#+<ҟ$KqrѺ<H=RrD}OGҤ"?(KU:e]K r`EᵔcqKvN&ãK{rZ^7WP,~iW!wg;vab_6#!+FK>1{e_h'f-}Y%/k~ba_ߺoK>/H6ֲ%(me-r?14T#vm%f-x3 ѻ)B/6Wst>Dгxk+$ogpgcn5O U!>fh#B0dX{m6JGN] 1:7)>A瑴Q!S6qӵVa#BWq!8*~m1^,R1+qw9\XKIC(YHI>:A;p2ZH*%6[n'$mRoxAHGDbYG]\{$v~$ J'% Nnoo r@C3#}WH޶O!w^#<[/z0Ғ~9p+[( l:{,/$e:RW):/b,n@b!kϮ5:L^Pq\8~IHƭ"G2TN]H!75Dz8bq&q ~2~:zuqM2o({ sC0;kijL,q7~ <+>pdy9(_[qV^oMQV_T~ ϲ1I"[7e}Y%،.oH8xH'Wdz$^Z$FSR#1*/kWe&˴|b^OkyM>1'5ļ&|b^OkyM>1'5ļ&|b^rk]׌t+U SkVĂFD~u1~=[[@,ɾ u2!;Vʙ"F,'<6Y&@$Ԕ=pHm-E@lϮʟ$N>[XxkuF[?R#8^#m^**{x1 r ~.o"<>:$.aK2yvxsUmSt|| O ^~SzO龩j?VP/π>+{''NݯF9翆$Pʸj=DbwxTÅ}IKv+ĺ"81rL&1|sݱc7yzf7˯/Yy;W)P!/ ∲ݾ~!-B e1'5ļ|yF>1'5ļ&|b^OkyM>1'5ļ&NYdrKL S ,7& Y&V^>9CԻ XAd LlEXȾUb1ҨzGoz(^|)($ EO]o:*K^ u"@OK@aS4v vey%ùRZ"2҆LJvH{T/%d*qe[pP QmluNm$Ka | 4c %׆å\V.Q\/Q؜}@&Vcԧ f 6$Y VHaٌQ! %"ڡ,Y|3ϳ4C$j4r}אlA81T  In*lGVE9RXIw¥qݩb2 p=@n{6J 妢@_UB6ѐEX&Q XQiBiY16 *b  &˜ $T=y8IHEBБX^SVWT4#d+)/-ugJJ9S`XFɝpl]WjXQmA?!A;Ao5 $D'^*_!rVB!\ޏ Ihu-IT 16P 谢&|J<[OHt 5D'r Ju`K 1l͖R:,6yXJgVΰ|b X"60"0`(QHnې tipU=1^AZgUH$(PKEBǻI*л |\+&v^mF"g zte&_&B=M!)-X*(섶QF+ƅd``1886:PحthZIpvn n+ >N<4XG'YҎt;! LvRRls!huhӐhY7Hy1 "vB4&j*(1 p'0xނz#ɧfd+Ɯ/ qΙHZ=k؋9P77${@uQSD6!=\7am:]ńJBu͋D5?bDJ2laJ)rfg"wx~@l7^AdM׼|b^OkyM>1'5ļeZ˰]qIENDB`roundup-1.4.20/doc/images/index_logged_out.png0000644000175000017630000024356711741527216020052 0ustar ralfprivPNG  IHDR? IDATxwXI  (x" ϓӳa/^iٱ`ATb(EigH@-1$ gl6o&$C14u)@ 2@ 2@ 2@ 2@ 2@ 2@ 2@ 2@ 2@ 2@ 2@ 2@ 2@ 2@ 2@ 2@ 2@ 2PKHx񓦮E8rӧh DyY3"rP(BH4?0=0z&RC=fܥ!N"?);O[( ¦Q]S>}4uE~R} j'&&6aM%$$:u͛7eeeK$Ҵ3Dޠ%ha1۷WZԵ$&&^|955Jjjj,^+222~wє [m ;s6(rB0|yӦM RQ7 Pm۶ϋ&VVV#ZFU;ǧI*?-ȵiյ>C~lYYYݍŋx#GoM[n޼)xM$Pvܬm9^3<䩒ĭD7ꇘre2,V?DӚ .֭kݺ5BJN>=00j`㋋eVUU*..wqqiʚOFb*j9&'OE|`Xjjj۷ׯ߸q444j)"s 7NDEEEGG{`tejjj9CLnٲ`{EDDH$++3fK,<;Bhȑ?~ĖCCCw훛62}ܹ#={6{l,˗e6StҥKmۆC $3~0aϾLBHGGwðM|~pl6k௃nWSSGlܰ<WA;۴i2`j#~]f.۾m |ނKf>'Ï߷˗V;:lX 4SNvZnnnee%/**z=<<diiiw~eYY/++KNN޽{GZZZ69x=9iҤ۷?yׯ<<%%e׮]&Mz@@t&ϖ-[v˗/EEE~… <!{jj*O[+4 ز1 X8%9Cϕ+%&>aVݳ+5%&1[7cf9m'/?}aޱ㦍k>%eԵo;߼rFt$$ğ9{!E~ 6ݻfűK.ņrE_TTxB={IzFVҋW%ئ^Nvv 7hCŗ?~g#G9#$&>K\n@_MS䉼E5zh<  CC?}tXX#6r{!"DGEuM5՝4i.B(::Zt<ޢV1_9r<̠skQ<O;vuV6i_WdggxK9Fh9V-"1cmذӧeee۶n4 N:U>??{i3alZ={@ZX\Y>HTk׮^^^{޷o JwF Tggg2}WIJJȼR6~~~???<n J#3]pɵkQC jc1a貲!UfamZ,]6hukm:9uc/$%`YǑ#p70P8 P( .3gΫW޿]Zpa}744'1bش]ׯ+W~A$F'm @i$ٳWϞ+&$iY^s] oo2EE&W{媿E#&7PJ;vѣGr|Y6X`$snp4Mld @ߗi}wm6c#:Tt[[[l;h46ny޾ ňN#پ}{o.1'#T虑͍X u3ycǎ%'' u-|m5{-9–.`ڞ ВZl=`mm/:t$''۷O==='**QӧOCCC'NXת6ǏO<ҥKUUU\.7==}xQF)V={bOg0b.zoMMMl\T~ 4M37 Fl;ZOEHOO8RWW>|xhh(f͚5kS~VBBB|}} ͛'uAAA%t NΦӧO66mZXXXmuuu__|6~׳ϟ?jȑƍSX^^z^).Ot\啢2dJ@R ؍:4@I\,y:{233 y<ĉ3gTUUqFnnnEE<|\\\VVVyyZ;n8SMz{Ç~Z XXXxzzJB]t9{Kbcc333}F&[nݡC{{׵ k˖-_~f0006l<&$P׽sӅDo֭{jYZZZFs >y@ck7& @ q dA!>K;Uj*5ѦJ9|x:S %1E %ÎLi.\!Hc4]4uuQlwrb_,-F"5LLڮ^=v~PZzWiVɎ{2~6/3Ξ}}o?zVr5^x| (11OBS:|Bt|@Z8Aur4BhPg:24342OS׫zye+~~TTTBZZ{mUk IZyՙ'ٱ.Y⩴~Tel?lXߛ7ӳ>.40hgtܝwl## ~B$%_ oٲ?sÇ/Dt˃߽ҧO7lգGa;v,SF #vW2##o}MLڎ㊽hp%;Çu67o*A|!DRZqv6~J $ǏGfki>]5`KbB1X]V3gOu|0~k? ^L}.Bxvvu FMз|ӧo;"^bea֬qmm;Pd%@б9CKKK555u6bĈ]=eE,%%%ĉo޼۷8qb~d(m'PJ[e466!4`@OЀWcc'N}.篿vhM333[$WQ:efw y߽!믽r.g`wNݻ߿GöD@[~UjjG\ |9337A9q@9ԘoXillJ! _~;{KT*XyׯJyMI7PIɷ/h~dG}vSWg|k)BŪxDg[ӧo3ǵnJ2e$BY4Z5K3l$8>>iڽ7no8i;4:6lX?PBB&LJy!Aut4ްa7B=/ֶ^UǺÙ3B)#ut44cP(MP? Vnp.B?Ghjy͛hdGRuFefa#45ckBT*UWWU*Dx?~/XM6!//7o&'g\]^^q],҈/_f,L޿]vF32jcmm>zkÞՙ,V񧧱wP,$55m-`$Ձh_gCO33013v@P(7#OV<##cٲet:n„ [&^E,==!%'{J騴=$"ވ'&&{cǯ:ݿ,:Qb)rn;dH{M5ϡ9*O F00ر݆ ! :uĉC罹*Ə 0>>S""---{惿R#UǏSӳ "ahR hcJyLSzy^:,,˗߿}ZZݻW;1.]RSPbA#}M*2nܯ#v":vF,B?~ iƌ1T*4%י2fڼm99nLܶm Z[&%y(9-=F۶Y0IM}^w!dm !KlرT*!mbUTT6,ҥSBl6ٳW;vXFH0b˓'.޽-Q~033~CvǎMBٟBo(y> Vnꜛ!C^o|رo(FMEQ؝:6lKt1Q_?㗖?zv^ߏ ĉI$ұcWJK?E"<<)(y> Vn;v Bĉ%%eǎ]~+)vDJXyoL&SCC!dii)sUǎ322.]4jԨ;?\|yhh^틋_xq5,AB!3݇uVTf여};;w{dשٳwn=\^24ԛ8q0s!N]_hr?z:uQ(dl$ӣG6mt B%4sΝͶlY|k66'7EMAg`ѣׯUTp|C#;"IGCΟ:u3< v8!ZlM,tw3khRի;'dy:/sl/]*/3glB]UU\?ÇY% NsM{-Uu2 !K]/"_U1*SP@ZimզNvڅ >rM__fD-/7RSNN5 ]&IRS]WW7pd޽k VYɝ7oU*ܮefdlW;ឞC:346rd?P\\R= n3TzuEǎ5c 3GڶbBC3gV"fά KK8;KJMMXRB&~JRWIV3ջt_XH \_='{ K/AD @%Ry6];(ĉ/^d?ϖS͂1o??&|sg5a.]MMVVPzj*y*{Uj*A *>Z{ݻGޝ/1G(U t!B(!r raEG3dcמ_xAƆw=&ORQQtf!ۏA""6]ʕ PfAF} }@[l6e ul__fb"EϞQ|}sl5jT֭STR(!D)S*oW>\T?իU##i$>Μ^W!2`FI !ݼI;fĉqq E qW2\.JJ"/[DrXbӧJfbc)'VO^r25=&O ȑUKpfP32ҙ=[m:Lnfa4eϞsodx #\SS !wd~=N׻)`HN#"##>?dڣGўwܩ΂ex߾~}0v-Kv-ĉxXi8p >߮q'MM?!Vܹj7oȋ3ǎΟ\!hrb7\Ƨ&ge۶Gܹ%Dފ𨒝y?PmsƼPc:r~ -=ڶ8;LՋX)N !eFӀ(J?/S[[Jз NbZjv)և8̏UVVaV9&jݦyd̅ B!x6eLȈOedU.[X-,^^M\cf3gh> al.NL,D8scc>Vnn@ƹs4ȑUєP)ׯSh؟yy*y+s$m'LZu$M&aa?N?ɓ~'O;wăne;poԩիþ}XSV._>WgozCx3kk6:@m2"Aɥq(t6uq j:|իFCm7ndGGWW8 b;:h4 dGG tܼIE !޽xTww.$&|>to3L&Mx׿~jw&-[6ˆJPKK=}!'>>廸թq׬laaL,]: R6H9U t( -3 s?oUU;ݝrڵ̅ Ock;u ~lvusе3ի!FU]L?*6/=zL uE$޾{kժzBVB޽{T77n JC"h۷OkɞE$URcƬ(''!ьCϞ{#B|ZZjXf 9fK][Hgv#b7Sbrs tu5<<  K+ u.313&9{ UUSiTl՝;ϟ+*֦֨Qᅥ֭Ki5fL7uc tt> ՌǏ)ΝU[ ҍTd}}ɓkҷne ˖q<=48{7#7WHlkrA `Jv 8eʹk::wi4 vB> eGZDB[+I/x;wVo%!F۵Ү`bΔ)u.U_rذiQA2z@ڱlƇ$ɓ4uu4bwӦ &n&8䬃~W0,&3X VΞlνH{իsrx.AOˈ|{ދEEeVy-N6g"YTzuEllMd&q|بQUwRX,Rh(}J̙2RZYr|v$L8'wwǀȌ<.# s3rwwש玮\ylCi:а_;vɮ'N,?xpW%n5kɓ+]d퓐G6xq-KLJz?&NbŊ?yڶšC++i%0 ]U=vvbc/]zo||%#"V.Y2qqOOܒ +է۷kVdg?_S{2oϏSڱq:UZv1`< `\JYիԠ ơC!! 4wMsNENNɳge%KKرysEVVijjYǎ#׬Q/NffiJJ[VI6n~6R|g[w/]t/ǎGHGS уUAkN gDĵv'Nn ٸ3aa7iI?>zܹ8qI%7P"jDhWy1yO'>ֹJ.գ㦦++Ah(=5W=SńJhh0c`yy8єjGsЋt\…{R):x{V8:Z2t*ܩѲervT*EWWѯ\yީV&W>3ge;*laa4gΰGrY+,srTݹB ?z҃+\\xt:Qp Kf6K]lvphXBYHH= ݣ/(/M]WWS*wWՖ3\6e<:|&NVҎ:IK+4N*D#K[WI\[i(y򈑿 -eiiLT~e$|{b<~0={2['bmğ]J , #ʪU}dD^ T= e)իkNdFUmެ|yB 2rvC$\Ξ .=wNgѣ+*maffvرM:xTT'ݸq'qk|id]JeCZǭ[O3pڻwNvЦJJXwޕh >|ٹ Ʀ=u3rY)3fThQգ_?KOI!'$Gp _dʔYHj*Yl+vMS\8Q5y! R$n%STdnENՖ?7gzh(!t D`c#biǒ H6޽KW.}pRb$qm#FN&[sCp67o+Lp"z55kۄhR[ߎ-_IZYONXffVæ6z{sƏ6jT֭JBSV8ProFD7mbN/&D ~k|gbq6mT ǟ0a<kl޼ã*6իU,H (/^Q")Q;3곡[uG3>V108@3G򩵮Rm3fu*76B.t#( H7s/[llϷ)\FL.yS=B.2mZiO >n[==AvaXɏۗ PHJPFN׮=| S۶U Ѩ,eB|D ocss?Y[B̬Y[З/ŋy~ߓwSjj}{ׯs3_ebխ[GWrڷ7sS*,-%aÄyogǷ#TһAnkˏ $ޞOoQ]]y_ŶR`udSY@8ֲ_FB" +CCi#FpssUF:$X-G($2Y^ݺxy!8dA I\[iTV"6Kz-eg\.z뫺paB,ķJL$ɿ:UE9D ҹseLGR6q㪢;w2MHK8CCA|<&kk~PnޤΝ`u x HJ;ɣV qݔSsso ηFzۚj4~~ʯK91hZӧx1aO:..vGR"v~G<ۻ̛7spvڌ\0e[Hů_ BllT'fd' ;v43 s 9;۰XEEezz#F89R"J ܷ/_s'חioYUE/^\9kV'WD *ݛwx5nnyS}_6,ކ 72=# {w;~",5K i/gHBF▕fP?ŜKuu*YO%%$mmEf5`S\ICR6* Gʕ&aK9s0?}R %_p0חieEGu$XXMpԮ+Hp@V/JZްb[:!8-o :WXX=fL5TcV~-]Ap8t:! Ν;KJڍTr˓iǎ v͗3ѷI"<1DWWH< j:Z'OEW,[44Զ^f_ٳ5[qul+rEm[SSS ~0-suxȈسg y3u|+O(߱q:_;`FJ/$[W0`ƶm>#nߦ.[\"+ؚBBB۷K55w#> C I IDAT'#4\:oBAoS 7Nz`ݬaøt:ڕ k֚:5tҤ*:]h` g_ROٓGBP#(ݿ?NGК>ՋG6m764{P**twU>JM-,\V%%==uuuj~MV.dZ[kiӮvaa;lmyHI!] S'>-ħ3 衩ݭfDMb|j۶Z:iN>wֿ$Znzz=zhՔ cU j~vvUq}MHJ=>{ѶȑiihʰaFFZFFZ5Y֮A0ADK__E/ \Mի+bc[k1=9S]]]-//_"D## "mjٵq+/AV@Uj<h*ᠵk;99%Ϟih,.}*5(q+/vHCLl仸'nwRcqrrJY.]0f啄vbJ=ֶc}抬Բ#FY_Ҕ2==7a+ B4@_~,RS_ Tޥ!"-[$TX -)66G..W,V2Xuگ #22kggǷ#TһDj&l32=#.\(XYkk~PnޤΝK4 `A%^rb"GvCCA|}JDl6)6Q{׼y__fREIIe˘l%q u:ŅfjQEOPfάZ\bbb(5Mpp]#? *gRU12rFa/ʊInG{(nYi S11,^YT|9s0?}R %ކ 72IhU1;[ZJOULLsV^*s{,)!ik7̼:wN/^di_r֬ھHR{+UH@p8t:! Ν;KJy(ڍT2h}L5j}ȭW eR;K>l̙|^CdooT)Pȉ4OOE6&ϓSRSöC͑+o$TڶLR5o uyܿ_ar9 {L/զe8x(!7yxTyxp5Q䝜:sƬr mq.wp讄_ `|G!cP(? Hv;rh}=z2.6Ϳ5^};gfWNMƍq-K/^4%5/_$5 sR_F] }f,ݬQ];A7El:ΝؽFs8+VmܴU[[!4c]Bb^8 !fOω+Vտ N3ݩS߽{go?zpxڕRTUU?TPohhy?A""Ӈ7@-[r06J/DP{vh |ѣWccB11omfB2OMMێl/ZU^}Jgh_+VL <歧ek@ƒěut.kiic aEe"P 6lϞ'/j-c v?j{_^o$HDRƼ?<| KLzu+**߾˩k'!H$q7 ߀OjyȚM\@4sJʻUx{WDDl233ܶؽ{OE;woIgl4ipRRz``8WЄ8ƵngKYsmHw޽CfɥEKdZ*[&J[]VV.m[he+EHID soLcaAk8s|ygsΈY;C^4!+6w¹n0bZY "}f𪪪Mz{9jtlW@:2.vϨQn'²=+`<>`e /F:mڵ+\NAAeKZ y}XX !Կ>@RPPoe˦M[*/ӯ_c@C!*uhUpѿ|0ȨhK c(Є ^KcGrۼ)E 8;ؾeH7`+Ǻ{䥉iDo8+÷[rϞݽzZ0 I͙=⭹E i3o#qL&K(wԩjj idI\ y=f1bBO~TQQucVFF<->ZZ.N[,ĕVV)g+~&D"- X0`1~Dd2g9**4˗ɓ7עF=(ghkmu}tXffl[VVsR&S#I}CgDKgΜ@R5ǯz?̧{wuPiG׼kPh}R/'MuMHH8gίzz=&OW($"bS׬[_ߠacc:{'|O?/^ [3oŃ}}@5]is_77{77{lw>}z K];Or^xJ3oŃٲД)WVƦ"nQoXVqs߳'5F333\vy JKM5ʭ"_ϴʹuťM g΃PKWak3:PdTt td26qp V뀽 Ts_׼ASSSZZZm@:&tjOX }6odR4ͼ/_<ښfJ(R7o](7%rw$fo&ii=/;1k H5A"_09tU|>:t~ӧd&ԧՕ;{6{pnKCikkUWwĿ! k;mQ6ܧO [Jr䈈dI]ZʕN (/01ߦtdn/ښ5l{N{,YⓑqO!'Mr]~!J14]|wϛ嗘 ^@O{A EĀ |M@ D&+ЌG&i}}Ö-;{6!_WWJJz5BtboMMu$r7lIO(-R)vvyEZ-4:w?YZNGGlW_zuqu( 55Nv}UUVs~EӰ˗KIɪ{NK>}򃮮rr(Re.жneSzX1-,:vL%"QVFۗ˅ ;OP?̜>1*11W}.eΚո^[[z~>EEE0f 72BAܩC \]M6;ڂ ҎDnĉMv--*Eآ5I;d&)@!g!BhR HH_tSWUbC(qm i ?'"@]Ȏf!AEE͑#nj+3gǏ ~~L+ޯܧd/Zend[wg z Ʀ\]LM ۗjxQ[lwݧ|5{2""ICC!m /!!i4jqqHy>}y?~lHH8zr@'Bƍܴ!!{GE 7|2D|$CeH^DE5,Znisr⚛~PǍPQxzrBHF|| Cjv_P={ꝜI]vv܇橩ɓ'vFl2),L&N9B i9Bhk Bch[ܼq#cFDD~gP `:TO =0f&Ft⎟sK6%mXiqrr&'gL26]TT,m8Apv ߴ9'N8Ej)・M rT7Ѣa,Wĉ{.=v;w[?+[Ogƻ<4tFP9ӧIk{!qh--9s_B,v4˖;v+qtTShss+H l̘4UGG# Ev|2y{x8 ._]dRii|7U]h͚FuvwoaăggեB=XMְX.w*Lϟ޺;X&LƦ55u =X[ִ2.4Xq@sVUxzrZ^u'BQOϋw<~Lqv ;;K_#ʨQJKl77Qupv?Қ JZѺY HqFZⷢCu*ꭴ 8ApᶆsviR9qjI9Evpp @!2d`38ŋwμŘ'6#WfilA" $B@_?~|5ҼhG>xPիxMn#ҎƢE]$x߾@W=P@ 8ԫ"6ong_FoڵAA,ѻJdK\ڋZxL mDڰ3ouAZmzNlSubvwwG T/cEEҮ6w'37nCvmRQ56*$Il"njiLLN޹3 ))Kb'OJg߃F"?.~DfӲ^nnSͼc޲ۆ?r?ZQAnFߧHp ?4cmkʒPjeſqʾ:a;۷UU/iP˜|:]&z!``^p0С̏tu3v4g"@;iq^ ?H&+q6IJ\O3l////~￟݅qao'L`<<LB͗,]:yđVzՙ/줶l9p8/^GExx yGY,ýETT ))ڼyn>:m;M\[[K##SE=zh<~Rt=mۑ#p,cXQx1L&B==W(KN޹S{ia7ߌؿ篹\ ,wK8q8@;V%Z}="PS_)իzBkר7*VP_ ޥJEh(+$Du* ɁbBCޥ #,^̒0,`G߷p!KxHzM $]H[Xճ :Rf&_?Si;"wzժ*nߖjҜq{e J38ٳqqᶆsv ޴'y">Ev|rm9k ?dk֬ ޲ePNNֶm ٵ+^ lܸ?ډGW ?}ām;*Oz@ 8q-?!dhs!>pԵR̙ܿ\\$CLBBB( WĿ[Q~;.|``4W5*q=kGttJ[[ٳɉexdfAAJr݅Lڿy!fn^]]1#"j{Қ8qġC"77[kW*$??WOmקWY\l IDATB2!07okƙ܍KСɱ4zYȈt)srɓ?_T>m|5쭂˖1/67k{NI 7Aqs kPa3h˗ff[ ,tGێm,08_wv:ulNw˼); h?2y߽{/'mm@fش{pW va9n#` oM]\\-sqq7+W$%]y]Ϟӧ3rv#Jz.aM5&$\GwʔQQ@GӶOX._yb2cݺ fH$i[ݾsg&;v,[it@%e7ZZs~JJ̼jtCs|vH3odlGtŋWD*,|_hjj6"@'%zQ: yPBq!L&|6}|\t+CPIca3otЙkkj>>|!4e;!p*bbNnU;r$i` @<<^gΜ_+*އL$4tƐ!WGF&N4Vt+Gǁ3޼3իxx8"d8:4x {}XR}}@1nnnnB}lD@ANi˖CϞrTwߍSvRyI*.~M ׮d̼tJ΃+; h׼,K׼hpRSSZZZm@:&tjOX 5o̼h0ݸb3flHhR^^ITTʜ9;L`Xy8*_}-)IZ+A(X'lܦr#hgy:Zɧ>/Zf'2-N[f:!va;;5k)/{ӧD"˳m KK۠,|ݻ x7)aaQliPJ>Zp 5o[ǜ[w- 2*QY' jFtСfyJ'Mr]~!J14]|wFg[|]`,i >sjjww 4+k,s_ܽK6K6 s>ty;88!"+峵qZ11#ĤO|1]B![oزe~g攔T D4?OIIY/^Fٙ.^쭩D.Z-_>T*t"O:HKۀKw.a;w,-}?}իÇ/=yRpz؄O'O^r循*}s'/UUQWW)6p.=}fe]]M__D!UXݽ.X2nI-\#\((]˸ubx,??v~D"}l,S2Iڎ'O(zzfN {mVF~>wo3?g8PCϧƌFFؽ;*ݺ965IoaƫW}K2gjJ1FYo_~PSb8iV47D8/ &^yݻ=zt5vh2[%%l675u=6 t'~:svrS׫jEDĶ.a+YPVciʌ&_4]]͢rssᚢ7eeU+9:u;mJJE^L&;1uG{ppMllʕ5*0T}VM&=߽tPСf^UFD$ih9:Z"m;5<$d2F-..?}iCmhΛ7džG/x"nMK2ظwqqyTTṗ))!!LLwLb0TFQo~Hժ271C- }P=.xÐ8Fd+2`gǥ}>f-4TuϞz''ni)yv6cȠW?n2gӓ'oȆ!C>TI-!+3Fc:͵դ60""$sʮ]z;;ÇylPz:-2_gc{`Z6#p:HD7|ሏϧO\̷>EEoR#F 15CR36ӊa@CN䬐IƽʣzΉ.Q8G'U";5ݻ7>=bCet9n%2eԮ]֍y_xq:+[Ooߊ64tr߾!:sKwnkhشf([DiiϙՂZ^rr%bcn-۳,RM277Xb ˰ŋwc#ř39wssE<--",, NFd][KёpUG_]M"'GCC9RG|[":|Ν77.B̌(ƞ=جƆw@]pQ!97OСz BHWWiS݅EBނC5$μccQQ \72a66gGDE'_\4" ICdG#>>/Ziaa0\ĉYp!agm>ӁiglayoؔωrKΑIHN oM]\\-sqqrfc VUI$aMMAu5IWW|=Iڍ͟vsӘ8mo5kh'-Kv#==M @Brny--A}}{qjF}'OȮMru6V}YBq6#p:HD7|ᤍϢ):HZ[~45~Hagm>ӁCq[ns"8sdJ0rw|F yjaa Z!#Wfi48m"?H W37wѣm<(_q+jIN#@+Ґy (/r8&`\}"1g= AK#ȝ̼h቏XW&XiJJs6oۧNK#˳69zrmm=/LKinGX ÏaE=*f8L&B==;KN޹S{ia7ߌؿ篹\ ,wK8qyxpƎzzvzZ_O']J즩)5qs_ZUEpےWʢ64H~$ϱggS NCCY!!ׯSlTXH z5_TܡXt_]91p /2^SCb2I//'0v.AwRBBT/n<AA+",jbNIi b?Y{,.ܷ:K3l ͵b'x:svq6k ND?-ܩ/_;$$d4ssg{{saiBBB( 7MV[mqq9^߾~~QQY<޿(?zښ̞MNL(((ffɓeT*ʪʕw2iE" zΚ5.:qpuTWnjHKk]ŠlY,ή]\<^ٲ!5UeƣG:Bݝojvvl%oR…팻w4ɉV$*, Y :g_T>m|Xѣ763磡Cy,Luj`T)mwzR@>e˘}|8FFK˗\4" 4 D8@sTTw5={v2e8&`\+}18gggzfddreem^Z.p%miNd2t:BԩˢeGiOX""],qkj]--6ٵ+5&G mVu|?))IHQ#pK[uSa5~1Z!>Mh[i_Wxⷱƍ]]aڴn55]p [wжLl9Q^^GGcĉmY~3 'yжF=VY@G`PVin\)"[@!]]J׼hpo\oNbjtv'x J3o̼to͘7LٹҏIk_yfn5fƟ҈Gg-:򷷝{L"msnZ0%`۵+MSS}yND*$JIsƎ夦O*+I7n>a4…ʳ2lɧu=-HhxKSaS @ٳ?ʽ{#Gr +϶Vugo{ ]u ̼Ysi5?wo;Ǐg>U×<)pFF|}]\]b%-oزe~JJ*lnjzЕ+O^UU |:yڕ+UU#FX͝NϿz%=|@vu"ϞҌ ږ- Mkkkk8PCϧƌFF) -Bc#1Q[74q"gӦ55̰Ħ :ic*2r߾ ̰pK0+(]˸ubx,??6ҺZbźer5im]#/\x򄢧gljkk_Kdj$v~i B_߰'>}Jٙ.Z䩩y$eƊpF{eI DvlNIIB$KK󿮮kX!.]{.C۹E` oʔQY3˗1JCC޽w :իʈ$ 5GGKжm'LѨO:0-mC'B())+(S>T*!499+$dqdPO;o2uZ$&̚–gb;F߰q4th"w7Ab=2a0՟~RKH#117o&mذр l7=Ɉ=|H]@xK$=6cZ@Сz:]%&&vu/Zh=NNR l/2`gǥEZ RRLM ۗjwXظ?K^)pRocJJE^L&;1uG{ppMllʕ!23dL21/**߹3P9rr 1vK#qTvސ!, _pk8B(99{ɒo@Ql߱cWj5jjtbnnbiq },- !inn@Q,, - l̘4UGG# <;ф&JKDBUU555ͧC窨 ]]M WRo[;mN͏h8s&3EG#"y**)y&J {{ޑ#-*bZ-7NzTTNGfflwoanԪ.WN0/FR3_mA^⸍dJ~CC'[[hT 3?}j $޽KOmiE`ٳ9NxF'PdTt{%|6X&Mrϛ˖!nKX![O$_1a°NNV66uu5 25#ZT\ں𣵵p.!!޽55u|>_4; Gz88p\yxpBCvPZ![[޵kS<Ზ2}qjF"_ޥUǏ)M{qvH_a|ĉl{{ިQ\CC0wu2jT<яV*P.nTY,ظ38ऄ"34)\VWg >[#콒 4..>A[qQ fF:yut4oM8[/_`4dG>xPGfW0b)IʔX[[Ξdh(ի}D+9#lϾvߴkUX!!L5wuvoNBg vhV*hgϏ=I!6n 8v"kW8:x h$oϟw>a75⍴LLN޹3 ))KJl"X//c^^pɓٳѨǏetߓ23/_~+.p*qݝh~䘔 IDAT9msraaLSS>.@eg~UUM$c2# I'E]n, XH" 7JBkTVVٺ88<θ3+ NJ-:02-݅qoÂ3oVFt~aj(ӦMs;rW<|X~ + ?Q1a25^C㗢o 8ٳ2.WP*..]Xԯ^rr֧O l6ΝݻO_钒Tӽg"MM9ҥkרV?}"O_蓈ȋԐLŋŋ[󀣛wzժ*nq@}}~v6gZB){+NVWf>2#geQHuuLj~|[խ9 Q~fBr`E8Am6xѣkk\^~~iddJ 86h,8)D{x\]t?҂]sGr 0NZ@&utz͟*{ ڎ[SRd{?g^[{HQ(q?N[ Nbjtv'Z7ttl6wdC~!!Z "o袪V~?G(K^^ɹs~iΞ?:< G"'Hgn޼~hlIIQ3f,?lêUiiNj^H$+AԼOL̙^2H.dzڂr-v%RNN߳gO ,,={jk;EASTУGE >;j{ཷE]wq<`˳f-ڿl6lkJ}58EAlr11g*|dHDSP['^dBC:R(Ki.0`)3[PqB'زe-V3U.ێ@ |I Kֲ'uum;BFj5|t}Ri[G'eg@<?0pBho։3&+ͨYY42gރ"76^ ݲ!@~L{!CteyyPU?߿_P]]5V71ٍ9@y?|t{-t:-fY:UM|~>>U|7߄%,ᢸ} KA>@NFG^H,-(xeY?oOM@kޑ;}6u.5?}W\!pƍI5^z:~bv#7BSRTz~NϞ=z4l}3kns Mۊ,lڴ^UU sQ5JCþz##Jl]>زMM-l`x\]5֩QxL--6 oNbjtv'w=KKK咗/{)\_V [.--E"zшB3oO/[«*++7 k-WUUUm` $μ--E?3}ǸQz/]U^thgoPTyfVAEq#wŊgTQ!/JUUѾaDE̙sʔM{ϟ/K-JK֚1C£ZWJC2h>)uM At6m&!Dg0vDD!*,|q lb 8k ĉ̀!rs֬wnT#]{t͚&&}ƞraiih4Vv"hY׼e]n[?||cW_}:kKJZ|Сft:N j>kGz-:ُ9E4uT*PwIQH]`,;mcTVFu5}Jٙ.Z)VGtii? (&sN㙥tt4GֿzUy'OJ9Q/__WׁPSkWWUa5wWt: +|>>Nt7++?jxx %ؽj5/7_#c#1Q[74q"gӦ55 ﯏?}Jf2IUU5°knQY,/8]D& gXFGZ+W:uBqgnk8giMS9-iEBr";vyO6}[ f YTTnnn \STJKzzr̙WA8RR-27g2ىW׭;ڳg =kbcWV^uūK_J(l-6޻|AA>CzUhڶ턗4ͼCCL>v޼wG/x"nMK2ظwqqyTTṗ))!!LLwLb0TF%Q}NN9C_i3Fc:͵դ60""$֌4qiϘ>tN5L]s [ ЗFhiϙȔdɷÇѨ^رXQEEFX1EZec Ѩ::7oaϜ 477",Z)$=V`e_baaulN2ߵ!,L{y9ͭ6M zGD49CVsaħ'GCCyGH/&Oڰ3e P E'8s"8KSdծ׼cf3XU~roj?u.2J{jSaO2V5,+=ݺJ#篷n=!DjN0,$dM]]Mjj2{^SSE#'iee$\.)y;h'[!X+W8Pߢ E{GڸQ5#ZQAB̓~|ĉl{{ިQ\CCb@'mXiqߊMC46iHD"p9%aɘy.oĈ#F ~yjaxU?+Zqĉaadޯ"/S"[I=M ?Pi^4wѣm<(_q}2aY*\]^uQCBTq68I23?WY%KX<>efRD&H֛XHS 735u@8Q)q} &]_;$$d4ssg{{sDNH@ʕw{+?xŝ px}FE`EfyDGQ `mmMf&'&f 33__gɓeT*ʪʕw2iE" zΚ5NCIUUziM8СKX-ٵ+{ek byxh?[}h]w6di[pm U027+sb `oθ{J ^'3i$ٺ\YY۫qᶆsv ޴'y" @1=._p4kj)QKK"w=ovJ`}[@yGuU՞>^occύ7D6aqȸDPB#u~IG/W:;;-[N:q"ܾ aq~6KPQڰ-_6'dhѣm|`7Q( ލ'U/P.\@۶|Ի7ZMT(* ~EAAh9vAZݺX,de,A~~rNӥHGšXLTf m"2tL0:UۥDg<*.Fף1cV^~ĠqPA;ik#OOcÚ! 2A@pV(غJ"B[w-* Z.2TB]U"pTE%d#iBr3syϽ7nN΅xض | T۶a۷ Fa8;@!BIUд903;fΔ , rr<=a"pw:syw ׯ|Lu{BVVicpV-BeIN[!m^MIuaY] g90yxU^LT+岯%~<<\\`PVAA5B!ZRRN\\O_;o.KI9!Z{̭ׯٷo…?ݱCq-Jyfoc?Oph3tù/5Ɔ{_4cq{8vLz0XV=gOX~A~ O++ `Օ*0֮] BCcG; B5ފJPpw:thO&XZv={\VV9-^<ޚB![[ED?~AbX v{Nƙ6wo{?if {6ܓm ^p:*~aY7VVp)v46MmU>ϑ#aHBesyJH*H$]]gL!)\UפI#jop%Y{ȥW$3z9=gc DE ҈o(s1ridX"_"k"Isjd655:bx7))' J\M'!nn}VgOG8q/i4'o甗7337nPB1I'N\̼MOO5wJ77{@ cI#5Uw:g:]ݐz IDATfbEDVɦիWk{k+j4II-[h:$_+Rt4M2xx֯gvb| %EwFzYN.9sXSp$%%1hX /UB *5iGRU۱j=&*esD$/ȋ/_$GG~XחSXMR++ATt7nuu">|{Xے\]_ZաPvgzDHpQv'uZzA\Ϭ9GACPʮ]ELMoCzps%%LL1Y<''_as JϺ͵\+ÍpΕWl&!!^y>YVj,4签Pxx<χp(3uq<.WXXv/ }yQgt%7 sOgϼ[ZZv嗝;w8q?''56npTVcyl}}-[%j22=ܰ!3 `Xjs\)Zz_[. x񞲜d{{={s8ܝ;rΟq>2r|jp̋MoΝMjꂹs}22}'C|u똮|]]ߟSN)pnwqh4`ݺAx4 ,yuk%O(?![}W',j<ɗ:>MT\|Ҏ|6'.Kwo~ϞL 8矺={ *PVG;wCjy( ^!?>ܹ3ht+נ'O^~I75A76+U޵jk:XF-ߙ36nĤaae{CMhzYY7;tssu?Z|@Oh( cwӧ6߭[oxrbܸQ7YllŠT ǟ0a:ig^Ç ')yOv?͘aл7ȑBMBN*xMF /@YYCL4D\Y9{ru"Yؑ,);^E8e MR_z(h}+ LáQ5 ~.ֵkkb5b5h B;vee˦ZZvPAR}gϤ^"HrUѣʲ޽c|tt9 N:ҲJbɪ]- JdRJ_ؘ߽+My^I׮*-);idwŕgP޾Uܐ˿pApAz􄕕ҭ.],]C@  7J'Qp|Qz[ٙ/]F>4vrsr⇅o~t\)}*{=e}t Jy9i(ѯ?'GA,o_KXCcݭlns]w:Fxu0w{MUK\v5o[5yݝ֮=PX(eQѳk OݻrGƦ{z~fǎ]^rzΦS74';wbxҘtɪ ʶ}c?fso~|yhw\scss7lmr>|pxWmؐ)Y?|?|<9/ɪc$&/*zz{TYu۾}kp(e͚Ŏҿ~µkH=*wwz$.\t4B>ZE"754T/&5BHпv" 84, Xw*&t,@*KKAn.E N0vr7ɡԐI[[HJ~`͞t7Jht5ZE&Ï?lmB dKHwÁǏuBBso J9`呹\#GE mϵЄcwx,17 n[5mQQ7mw`aa$lپ}g Kں៎9ٓAAk/G<ڻ̜961xHH<-6V23>>BP~OwoVQ3Pسxxy}vQ3 Evl܂8kЯ]PAxv)Y@Ѭ UGkY֔)hKJH:<=kݿ/P\@<R|}i ?ױ-#LE .0p'iU AYDpc#LVLLڛ7U: IH8FY BP/Y =uEǎ'L:hPʹU#X,Objf'̮g`fzdpw5ksgKԈɓ &⩨hGH7Rm/mi3JN=xvm&-Zc<ͥYΥ۷Ɂ/o[o˯i;ֻCi]hDӍ61mA,!~ -3; 3?/&$9R;)ޭ+dRHG:<'OX)lQˁMC`Ӧ \=~}˗oMM ==]5sf!jۚ\>wmRHJkӧpo_kMIB!4e;1bbAWhiƌCCرC G&n`v RQ͛πɸo"Ktm@W`r8sO8{2!BYZ M RSL ?.lJ8plm):bbpXw^HLֈdmǵrо}EB!Z,-{ގp [f~t''=o3g 6VPÄI$5 !/JKaXeAetUQQץO|YJ訹4B!Ĵy׮ ׯU55P] gςx%|3~=L&<$(/rr l0\.AT6 {!P3R=wݎ۷L&CYt0~xm` ] ׮ ]JoS(0i,\X뷌e_ |)p8aaڌQ!*+atxll $~EjX8[B!Jxz|Ӌ՝ݾ AApRsǡ|z|۠pJ ^%EEE/Sl#?,ˑ-6%EwFzYN.9sXSB*`/ZlOWŋ5w2BC# ?a!5L?jݪҊT](qqfYYEb"3:ۜ#BmW zz)XXM`mܡテb&OG%+)3kh(WWR&$bck>bbjVBׂz-so ЌwwCoo+P@Yʂ77䭛ݖUB!f%6Bv⠕+k`k+8u11 !Bb-7BH#zzJ]TTQNN|''>SA ##Yߴ~rt?O1B|J>Jo#B>7B;obr WP~Q_ 'RSC&egSlmŝiKKAn.ExHпvµkHPd-B5Z=+֯g29v`Y~J۵K ˥KY!!/^N8L&)(H_4`x8 ^B5|cy.u-;z{!ԜVwH[WS[zox) Z<@!6j6i ϰD IƶxfsB!m&o}!P+s B!EnEL:i;֫Y=oB!{ޞkj.W?,;BAx!B=oB!BΝݻy]:tȺD#O~y;?/533<9h1 B6fp8z4A!Pk{޷n] y۶LK;սO<,׮ݜzS'5k_[B!jԺ-W̮]|>֬ (hw6OO/瘚v 5kʾ}; p%^p+##ѣ2Gn~fSCRm`ЮrC!V8?~I>0 I$ٮݻE nf΢_{;w:.ꯋhnnz𹔔#{bcÛV!ݫW}鋋^T oSd]zoݺOѬӧ{7m!T_$Ej'O*8M B!G[mB!PS7B!BM{!B5y#B!Tmbj*~A&9xyC)}ۊoi!B!Yy} o˗ eer B!QA466n[P˄CjhJh<[ vpussv. S:w[[:޼&KMq;vп? ů,_й38;޽EE`apQ!B#nϛχG , << 8زJK!!!-Max]!<\Q[ǫ[!1JK!9Q'?@T,YŐgϊO _~ PR11p"BgXi67sLvpXF=yV,r\,15K^2/ ,0AA鵶ZGAm '˨QFֹ3\ jvʞaY ff^x.ɩԩ:IY*>#c_EݻWWo~R]㳴n m}kb">@zzBss #F!>ULL߽;Jm-BAAMy.ƮR':uF-VWÙ30g7L,^UPnnҔnnpZALu~> $]5xZo8AusU?x{  Frdgڷ/;(ȫO[{8>>c 4}<-Jqvpݻw~mB±j5XjxQ̟͆ͥX Uk8B=610ca&ذAbZoS.fr%8p  11̉sqIQi4ݔ)G^~~nOPf~Ǐ__m66dgtɫO;!G IDAT^[u6=-[ .\k/\޽ůoʿo_xQ:* W ү_*I+>!2Ru[W*w,{[Pߛӓ&}K&p8\e}|9rjƌ޽p`ΣGٹGhX##i4"/3|i #8x\"22HH\Z٥MM !MJʉR.gcg[Uӧot>K*ZuffF JH8&)ĉk߼433>20Οھ-IIݸ^Vӥ`֔)&&̍id]]/&ٱٿZE/,$[XX&q$EuZzA\Ϭ9r1HFˆ͒*˭TuϞt"5%[\]e D5em/j}\R:7@ޞ>}sUj65%EHן^|CCwWʜ[;6 ~18 v \~  rwŋ\r|,7Jg( ٳ5Ο.`s55P] gςZulQ̌ ""|8w}uBU'VM pu@̞ `m NjW_Х DEAXZ5̝ 3fÜ9U̘quիab__p@XwGG8v vR-ʄ Cݼq87?a}l<{ذ~iWdPNNN$H`ۆJO<ÇH?%yynիW*lll0mڗ7o*HRZZ ЋJtfzFի?wקQd;; 2ٺ5ÉJ4JCp&G^ ЅJ%[x9rIELLUUQ\ ؚ?B-[h۷3 ꂙpŊ3g=Xh'qi5< z$'k3\Y*3[tuKSB͂ `Mo4ˠȕVcvhm3^Y/YcTupWFF:sRTVViLӧ+ vDґI`$iTgHF]'d3F~ȑҹPZ)'==g?~) z[m$X[ *2xpov73ͽ{˧j.k뎒עVV XYunNOa>|j>%9XF,22i G;;O>fff(޳'+/aEE@ ɓ{K qt.)o߮}t-.ޯE"ޑă7 tܤSޕmgǗ662P'^R翍!Z߹S2|mV7e%"AݣGq2A{[\l{ۻ 47mk:%K U͜[5 8$o/]ooo,qϞV/_IYt qʺ-u>lj~ ? NWG ֭7nNU4֏l_r#C%hutj}O,V}ZGۨQPj:x j|AFy/::i JNiiفJ@~~dU׮%2)mlޕwkW uS$YCŹu뱹|I`kkC ûzhÆLɪt<IV; 1xQ3_TTx{FpTg>]wv}}=6˅kȑzl-XWW)l60g)' ;2R9 넄+RKѴS2YQQׯ%Ցrw-^W^Nrʏ?*L8% FjY&hoǍtiU%KR%&OiQ3V G|Ծ=YѨvvV vuM0ztH:ݺOKxYm&&MS&+cvl=#ټ֥u]/QxZZM {?8SPP޳%>|Q`;wvѢg ~۽{/&$XC 9jZB_%ĀBuav棛7t2Y<ٹsOj6qpMnkky~3j^!k$ce@M?jǏ_"vxMEE'tS3IFȾ%u8>>K}TR `5cW}8x0ѣ#4tܴYᗙy BvvK*ZuffF JH8&)ĉk߼433U}>ɒ"E˗)l6ёuZzA\ϬW-,QQI8 enD/-ձDE^ TuϞt"5%E411NNfnH+,$ =Y6''_Y*sng<”ϟҥ S>}kO}%7 sOgϼ[ZZv嗝;w8q?''56npTVcyl}}-[%j22=ܰ!3 `Xjs\)Zz_[. x񞲜d{{={s8ܝ;O?7#|dᾙ%dgNOϝ;sHO-k<+2sy={Ǐ+m6n'&VU$'W[G?z2G11[*tInMrzڵ5%%ׯW կC\mʚ=/ǕwTu$ WBʊۿ_7.4:Q [ {ZOU۹͕vsIDq|BOY<< ]]U99Y,XDz_;e'1"8!UvR'❬fl- A}`Ό_3O&hAy{{H?Qq+v651%+3ܹ> ""|8' U[5y޿3kϻvEMI9}K.@ 8w_ɿ|ԩi{PCCɓG|P'$/­zB MMiTJK˝=zQn,}SSψVzU4*lgg5AY&[{x8QSSà1>×Yr#!!cPd{{#G.?u6))3kh(WWբѴww={ 山ťOտߟ ki*3[tumŋ~)og335g(ovA< LMKԼ{Gں֒ӧbPYq ؚ?Ije MV*R?l5e2InE[xmnPn%ܽ[wMha!XFg~4:K*;eF;  `MO?f*8xlSNmkb=%+8x5JU'TurnpGFΙ󍽽譋]D߆ NufajjEww>|T+uGkQJ+ZKlU:H^k0J>_jɭѣ?L4O)̡zϞ@6'O^m+Ih#y]Ro߾]%oZ\_͘B:cۛ:gm--s>CA(LSPP:uHKT*%v(((I)}mcc~4{%]Z!==ae9+*"ˮur⇅o~#_?~NkF)A&}Ңhl.kմO7e#!`lvRn^T[ 3l>¥KKY=zh4!jyP8Ze':ii=P^4˄{O%o+yk]Ѫ^*صyCܪa6v쀝;O=pxƍ11i~~niΝ}u4 3==ر+WNٴACVS;OVU1y<~aaiLLdUOe;}c?e\.޾xTѪS)f,֭o&ښ|P^-ڰ!S&%9yr_UcHL<.kQQYbQo;obr WP~QANT]MΦڊ; ;2R9 넄7YzWRl`2IgRF>*Ju2 ׯ ٳ*35-5rd$7(SsH6ERPeF9s~=}tEI/Nٹ$ai)ͥt|**H,←Z֎P$8!UvFش;c냠ALގ78)FT?+ZKrbW#2A{;xpo&MUN>>C5TմӺUAd786;0aA*Vbh4տFmCEE?ظrn{?~whƟL*Gdڃ:k֨N#(G Ը{/ߚzzx{Ǫ!M;Cht&&MZL} GPV_Z mq& [P˄Cj6A!B`!B=oB!B!j F!x.]?Na? !j{@wӏl jpFbԇB"FIIuumߩ񧟶OMU+׹QD>I-711NK6hT7oHRSu?}NƮSR9nKS_Q5477vrjgOvuv/­,QÇvldii%9Cjgeedee4iR*_n$K&: agFzbbuYYErruGuxx򋋫rr޳Xd ]jzڵ5%%ח IDATW -?zCߺ"1OStݻ ?eeQfԟ7URRgOu\qqxxmeeII7UnNuV).ϯSnb_~a=~\yNUNp-w$QxwVV0VpDB!|aٺ(ܷ&&feQWB `'*Ju2 ׯ ٳr **H,←Q5-5rd^4\ AWx֯]Ja$=K9`呹\#GE髳jmuw-^W^Nrʏ?J7ɡԐI[[h!6A5hPZce%`0X_͕Km[5߫ R=n4cg:669sX[-] BG(T9<\dޭ;i6"B G kN΂)S83gʏPWǹs&G!Zy#?oW#HhB!!BB!j 8コ .oB-NrijJ8 B!B{!B5mY*$#c/N_]͒]ҽ{%}'լΝMџH XHc//='711V8wj3%@N<--&=o!CzZCXf\(ٹ…v˷ >|V`m|T8tH׷mNB j獈l8S &N.mmm =6ok^^Cr;v6w,! ^[yF1iٷdUgix#JJ^fݻ>K#"23ϗP=ȥ-"#chhd$ի]ZII9QPPll: qs룰 ?{M==ӧIQENS^hܸA $8q-37ffF>>C<=]ܽffHVT~JWHkm+V 8)e @DVռzE߼ILeZE/,$[XX&IoڥK\KY}}ke[-_LaI0/X($e5RFIB}T^^))7tt̙Ú2(fBfīNy*QJP:A&pc#:(Z_^Zxׯ+;th?tpgڜ|e̹Þ7iӢ߿14spuspn"֗ Azznp%ٻ/ر}`ɒ*$īGj֞=l9:|Yη\pÆYƹ,+{nACC?wիyy OR_ff^Pf0'N޿ٳ'kΓAAc##wjQ\26d鹑~ݻ[>y2: D{IV@'-z0Ia/+޽#-[F_n]hU\-&ŅW]MZBۻ`Zpݻt賲(3go9<.g}E"Qp.{KdߢPrNk_Dn EJ%JhojiiT̳9s:s3fhA8:yvPʁE66X9tj͝[v`1C2wV?۷\MdHj#<sv.'O*_lk?֬Yjjj0aB!lɺ,S\i)?~}tOffSR>_F( ӁzhLq;>rq| 8ҧOG]r6izZr{ݻh;w~4nwze/й)T:uhaaO)/^$Kձc6>cƈ/EݑfkzǏ%s{RSL +&jdwcY-ZhΝ@_x\G cy*./Gyxjf3--=ɚ٭+W^XXRV4ϋ^}6}o5Q֘1CB&%}II!!`3JWRX[ 9 .jlUܵkU>eBq1}k ˗x jtjJJEE{XmۖGFFˆxM1"Y|U˗EGx\;o]ܨjn(`(kg0u~*ݾCܠy'0wʉ^ԫʂcBBzxx6mZ? <6mxPй_  ҵkӕvR :t070zt'%%}`:uj*uw?^5[N6,(bѿ"|qrsu2kР]ǏO&.,>\S:vpn7m*ټY3P=z/.GǏ1fPT25Ο_zZsDGlh7nzx}DwFwIʊs窑w\K؄B(Y}v..7_pOqo8g?mRkFr9is[ѩw>ݽ?9jQQ P}iW'u{@x\Mu<"t.3@ LM 2lXwu!d%>KMwF}/u5 ekkuߟ>}mBsNNT`k Odh(6lk9BHu5 ڤy& [ NiR$\mB!P9oB!B!Re<^$4TǥH!B7B!B7%#HxRBBZ@3vNeW>!fKM͐ĿQP@4[ሪ7R] ~Ŧ@MBGY0F!!W&O$zU&8o=J8ukS^5or5tw{5> PUy#uHliib1MLZ-[իOiYΝ[_'f W'x`@ۤ+(( q㴪YnnC_2JeeE)IIիUXN_Fv}ʫWLoq"}6Df+nnevPgJzp WQǕwPAM…\6iξ}Żws|- cZ矜W lVعSK& ߵ#/kwp*Jm,NVNݦ_x( KG9aoyn# \"{HN6eٌUKYCx[U'>H8!)z+R[F"l(w_W<,YW<9Ⱦ>WiɓҲuu5\]j+ ?i2VK ŧcU矉b\QxxuxduUnˍy7w]wte`+ŋRȭ{L̙#¼Ȩ5GCm߾_ut4/K5g_ܻwJ)#<;G&xO4+WN:s&ɓD2o{{?1:22AV˗ﳴ4 WV;r&as=='?t#EU̙_{?uAxxukMmH~||~zzŋ7˗__}36~6k5//nZZnXXQ`ի2}wedWcl\Wٿ(##7$8(HE]Sܵ}{IZZgįUm!0SߡIc:5kKJ%ݝ<\LtNӧZvfKT#G8NN<dK$2pj`M;;Ajj~xx WG'q'$uoe蔑@}iM?~}tOeܹ,+9Y³goBBN:֯wIe?^l?c\(/EWܨaݬ|3fLo%㢢^O>{Sw ;,MMSxz$EZ;a::3fxRJNXhllVv.]2+;;W/+55ʹ0^bF_:xp76բܹs:ZX-̛(rr+6l6cKi߰LKSrtij`g'8|L߹P2hÁ*Qflڤz6+;[I pRj'$uoe蔑@ SMRSįx;ӽɺS\ſ.O66JKYtŧűLb\(lQȻY)L|/ƍH@n-:?l4 ohH'UTf͜9r11?{u&PX@iSS͛jf|ĉ[tǧյ}֭[իU.,ʮ B!dd䩫jh_$j_kVf-SZݦ_ɉ}ʧOJ[ CB8R&RUgw,tĄp+tCurE:TXɺS\7 kPWO=Y]n?O6iD+E@+ONit##=HW.*!G`0$qss䏲jpܸ~˗OعsSR˼z>}0##=6qq,3֯^655(ffZ(yy$xnnŋK,w߿] ånk+ FǣGLmh.]jjut$|HOSEJCC_`+99ɜ0z.C}ǏY7rͅ58A I[Y'?25PWG$$LH,]^q]233U'(?dŸlQtLĕ+Q>>3 [(nuȑ| 11(KOO3.x=eʠÇo$&fxRۼ8yR[@dR۶O>M,g>W߾ofwM5fLII|ARRFHe'>AuT|ax ݽڮᬒFQ=V۶奞;%*ܵkU>eBq1}ȯXǃgϘ˗3 IDAT.ZT* KEZ"ãtrhhKKGV֫~*{k̙e,QsIH|\_5  I[Y'?25@7ťoh7o*.k](\]_M?~ :'u͛!!WƎK'NP|:PŸlQ&H0wʉ^tV{צn-X0&$䪇G'hӦʃiӆ rvkӧ+At`òygX6.RΙ3Cɗo}}97oYRĢ"FdKK˰!抷flڤz6+;[I PEEuB%.)r SDUrph_|V l&@"r]wA4ML0G!$F5Agw,tĄp+~f0(֖PyjRu\b||J\]gݺ^ZuROOn D!Դ:oGb۬.]?fm57r8DDKDU0"Ϊlm/_2fu"|"K|=FJUII*v&X{ q B!)0FYB3&Ϟ1/W]Tߟr˞?_r9T ZfzzV-cd$`QܵkU>eBq1}kr ˗x jtjIc]lZ4n/ clLMKp-q#CG%~6lެS(=b0~=|ˌOpl`cbc0ХŬYc Ə_*=z _!g&Byپbzjh͚=߾{oyk),,gsrrwMVޔjpy#P^&ئMzjʕ߾幺hBE F|{JxwWrs h4"6A!T}{%YI#IdIFvfEfU]L"BnK-VPE5s8B.}455ȗgcb7m a2sMHZ6iSHLL#Q|l EM>Uk!7BUd2Lj'1mԩA QțRBy#K** ȗ8e߽m ɗ2Nw,BaB.Benn!W u5P"BU?~{Ruw>/3DU_I|>|G07oCTRR <|~7_{,BaB.=@GGԩ_}wM]]mGR O4`9r۷G/1 Wd.7_33=)Q]KK22duګ`Br9NCw)ȷo w7df~>|B\[0cXCÖ:Cq׿y* m7dǎv-ĉ%U{H}YPE57^bݼ`B!076A!BH0F!BH0F!BH0F!BH0F!BH0F!BHX PA!B s!B)F!B)F!B)F QO]ad~]{p:Z [p&P}`f3fÇC~AOrr!a| oÏ?}.[Wp200bp&deAQ\vv&ceK!Bp8 DBppU`ʔ֠ȍo }}aJPW,АRGxT2kWس "BQ9NC6l*RBBCPV ؿz<!Ps7~/˷mQ(,c`8~`y ʋ9_#/ ;z*wC~>yZZ0wedozׯ%pU~%ahHOO8sFzǏ?778֬c 9R̙\)%8ͣ-B!\mҼH]烪*xy3Uu*lO Kְa_ `={"!:u*o۷q)",,Gظ|5/Ekx# ȩfCI d?inބ/k{lmQ00˟[۷Т"B 9o_Nv|< T5hƖoONܺ[+n]~ `؊Z;VlVZ."w/0{6 hjBvvyVL CE-9iF;;X4-@n\PSذ22S'=<Ʉ * p0a7B!0F`k+ĉp:/wC^%ksɓACޅB (+. kנ];v lmGz {ÃtwfÖ-pvE̙ZKMB!7**+k+EMvvй3 110v,=0|8)^"",,@E޽bݺAxxZP4=zp$<{[JoaBؼ\trqEWJʪ4ݱ#hkp0Я̮"BȻ*+Q}a%K`xx< oSM8֭9s@X0glUo{noߠ.]+jyz/'dhdh{n݂vի\.~$JӪY 1|8BݺGT?Ѯb:R%W $ׯ *zнhկCC!j0FMv{̛i0! |#B #o=z2wΜⲆHPw;y#B #o$PH>wrJKvvuͤF   8# QޏPQa[[:988~!{{h/:wx266##j0Fry++kx-O>ztzu* BbE4Q6 rr =K۱㖽U#뼫Hll7B! xo$ŋ7ԩ5bMdW[L(ʘWVfi;9لMɸ~=AaMv'J)+kBчsHmm Ξ}yhԧOӧ;noy'䔖BV2Ȋ(Z۲ر'))9l6wv#tuD u7D?=#f BX> t-JQSS7/FL֭E_f/}ӿ_GB58#׮Ծ}K[[33]MLtDW:ܩSׯVСkع!4VPv+GtlXPPg=w/)r6fC6mc%zEє(WV"ΛFnk:aÕum뼶b9W۶9++"#d_68ڶ)/4!yS4wܺRի/Jn>r$j6KKlYե"m~;!'\m?ܓRH5…|ZD8=kWc6iccrǣEu7lp՘ nmVnLlfj:$**UU5@a!ΛfoJaa)}PY(+3--}|ɬ̼A,9l6o,~`^̔-lq;~hpkkCy"ϲ=]rYP:/3fpww'3f`BH.F򩨰 g"|aPM5sLE%L߾=/zٱcj툺53m--ES3ii7mZԪ>gϝ/uw@Jj&33z2aȐw>&6t;:fJru7u ''Ǐc]Fߺ}_SSZfy*OX^WSCn'$|<+ 7&Lvx+kkÞ=۶n]>Ĭ=M @;ݽ{QV(VܙG|[.P8m4vO6~;!'QXJ0ln:ţc0њ:o7lx]vnڴ>( KUSS  2ۛ$X'㏎3w>_G]jjj$j-XgQ h5Umb^JpI6y2Ҁ )W-lGG'OR=zxͭ׬YT h yuBaXX(p!\ɱ}MRdk mntQ:!gzZQ#J7}8Èp27lnPKKkڍybgAO͜.ZT^| ԔM6FjOMN8>o%S()ᅆ>;:~-sE{ǎӦ޶%,ldAttZMbiEZ-,Zd^|_W<_v7B!0FrS'/_fx{_8;eL>_{Քgq^Z'eD$ųɓbMod٦Vɗ۵3y_ ^ܽ!#&bEw ]PQQa]}*!ۊ۴?]7uꑮ]Ew)7]߇2AI ɓK",9ˢR̞{< =ۺu6EͭI"=}}o$$| *Uz.'O,|cBH.\m8xo][Nť|ѺlK}gt݊ŋ[w=jFd^+W@ ݭV߿of&yDe!g1|ďذGT <40`g۶P"`BƓtR,R%ҫ6!OޥPJvɒ{>(ֆS&&M dݻ1L|Ŋav}.{ܦ( jUXX}!+H{ƌ>[^|FTXXry)wh%m* ]]]L!\ .p%)SWPP$JӻV~)kW^']1>> zɺڑ 3znnݼ~PDD-z򲥋N0IQC(,,w[sْ)11k͚KNSi u3g/4ÈBFMV?_-7445kεkW رyA ~YD&v;'WSnnӷlr@qYm\\+esW&fݺs||C rQB5M8獚 I 2wh'﷨{&MΧw 7ګWx1vl3><!T8A IDAT獚&3A(=s*#=}QF?=qtvtiB7jȻv4޻uhdd?`(}N3\mB5Q԰s/c^ ҇$BFިij9ET8!oy|cCa`B}0FMP޽Ν=ݻCCw!}@QQ}z B𮂨z0qԓEЧw~o)!b`B!"(5tB!jp7jp }!R QD6tGLpH(!P}5A̝3;0@ r|:]Ǝp,F!P}5M$ޏPQa[[:988~!{{h/:wx266##j0FMS>=adZV&)|,mǎ[V6"˖..;F!(`䍚&yGFPF1eeСV3g~=auzPO" j0FM"缥~zFb: AX>M&)ϛ))_ZpwܕLOIx'&&L`nrڴ#FtUٷar-59;P7eoeӱcORRrlfGꪑ.(++P{Ԋp/;~|d[Bv*T PFB#o4ߜi,}U-P ͫΛFnk:aÕum뼶b9W۶9++"#d_68ڶ)/4!yS4G<^rDΆ{wpn[:[Z~zE}U֭GDmbi-Ty# |j!@{ ΛfS[[>t(rzA,>>NdVffޠA6imm7L?x0{d^fLS[8m nǘfhCRG[[L+GHaaOWAV6N^(bBH.FMvfF[||ܻ'++jPǎtW._g7lVVVngffm  %5l!,ȞrV+s۶mڎjjr턄OgsƄ bemmسg֭Ĭ=M6KmkiۈzGQD%-P7EB!D5MZ0"S紴mٹW`߬O 's^^e7I~br +(( +C݅nUmb^JpI6y2Ҁ )W-lGG'OR=zxͭ׬YT h  tjY]*BɅMPD哾r- ZvO_t~ -Z\~ӅgETYg.]z:-VY/l-ImrDy %%ЇcǖS'ׯd`ڴ۶8x0L2NZ)Y,-_璣fa*&&C2&!Pm`䍚&Hy U[[VQQ򦦦999bA yK-Ј666gnSV$AoVoަU-_'1ʕStj,Kɼy?u> JJxO_dɩ4.W\\jdMϞ{< =ۺu6Eͭ|0..F΍{#!/LHWABRaB6p jʫMR $ɗzz-޷mk߿oٲU_Rkަ)Iue!6fei*^u }r8,==.]<=?òWK ޻A|G6:75i]hhD\\&dǎqdz~+V ۵w96EaPRo YYFF3fٺ:3bDҕ+ϓէL C.6A!TyI4Lp՗cm㳝 -7Dkii''mޜ G;n۲y/A۶mZ(a:1jD ٙId߾m/ij7S\\t?V umEULLt(+{YY!yIXoXJ^dٖ-FYh)>}^%ST!r_׭[=xP_uuf?_߰F箂M̺uk''\e>s2uABMr8Bu? u†])={00F!Oy&O^Ξۡ;Ґ>|P>{kgB!xWAT=|8ICqӻW~BH10F!BH!B5 5Mڄ>\mB)Fި "nys# &8$CBoBa䍚QOΙST\iyy  \>.c_8#oBaݬzĉ/3rr [k;;w5i>I#7TTzzֆNN6꼟u/:K|ݸy+66c!ǣϞ}9k…ԋʞ>}cw)vQFzRǓ'@Y 'ٳ;nۛZ5λڈ/[L@y#w3y1GLUW/СV[saV'P9BŔFFFF6CZ͜y#;eGA}H OkmB #f꯿ΙOv()1/Dnm<ĉg))9'O~g_=Sǎ&*,/XjcO>~30rwd#cotؓ6ٻw;oj^96&W^1sTa8yO~!˪~#GmsOJʖU]*B>IjiH$D)68vjLpDȮ]lʕ#+Gtbf3t1~hboҭ lB_DENR B@Q_MwP[[>t(rzA,>>NdVffޠA6imm7L?x0{d^fLS[8m?z4kK(%5JKKׯ_}%pv~f̌!f:u 3ǺucU{]MX^WS|mRB'OϳJJ rc„nn 6ٳmCNLh dIIg>ST{O#=MB0nll>M90Zۍ F5:SuwTTobҀL/&뺺-?u ;wf}tn8A˖oY>]y{w l5j1c"61o/%$; V|nii@n ăKԔ[l' k,ۄS4|뼺To"y# W4S'ۅDsUܼ颗ϟwЊVQaRRW|R-KK_]Q>+]nczzz-׮x9Q::K׬@_~Ӆgˋ6j/_$"55{S KMN8>o%S()ᅆ>;:~-sQ{ǎӦ޶%,ldAttZMbi/qFUE ˘jBw3ի٘16nnϞ} x{Oߓٺu6Eͭ":'?&$|T$#oBMYlmMN|۷b6ihս{Çʪh]RRv%3g:n%&fYs)wbA?w.sf{BсsިiV9f̈Qz{e[l>l5fѢgϝsqqq«d2]heoy!k[:wlb֭X;99纋KWUllϜ Lu!Pӄsި5Ȝ*2wh'ETTTI& dի<;̙5B!DyzఛTuWasBS#Fj{<GGkGGF!hykDbvݭkG##A A>I6A!Fިi~Ja/c^ P$BFިi?o4NDYjD`PbK[E,7e *m[ŪUi"m TP0t*%q4 !l7<<}N=My@߄ zȗ9~(;Qׅھ/( >iTҔͶ 8qDك||Ү]QluXvquHޠw"~I3{ȭ[Qo+<^/S;/<55;!!ӓUS833JNE9tJll16|19s !.7ͺj!y@wM:2wB4| yk9ܸjAS44f.uS45520fۤRl4q24`!))sƏ70`aaaC_E%?&fmc`0666PmM?|jll 5ͶQyo:5oOگpMJNtǵ;v5j4!9>~S' !!! #l !N6ʪ>:}-5͛?{洩Yx*/I~{'yk3cK/Q풒cGG5zk6&8>/-0ANEqr6~jyy^yy[鞞CI_p Sr9r|`/>?~ӄ驵55/dȝ;l.l_^q($-;sGZcccn_*\tS}pInɍB7aT[*gbb:,** (]XXurEFŋ9C1NEׯ~-usZa:&d?};DIL 6|РA+VSߞں-*.Y㵙NV8yĖ-qClaaekiI[.T~ kYkff~h8z۷k= ew_QOTLԼiӉF[[e|SRinjj͢/\蓑q^˹m݁ Iެ`ՏAAy2,)1a``.m(~5+8*xDa]/,oVv*h2oooo51v# Te(̙3nΜqsz*<}\OZ{!+W]]!m y~v/,ծIJJJ15B_2;"=Eʷ yiɯm(~X!.n)~fK.͹{a6O3qqEC|{<<9v1=T ~bHe̛Iׅh|/ٞobf !y3VLB7 ɛ V7=}Bt&7PL~ @ [% y3KAf(ڒ#kxFWۃ Cf(铺Ma,$o7Ci=іjzyz4r[gE~е ɛ:ۤ>!?nX;{Ns;iϟ@#=dZ633 _Ν2c[[WƍnMSS#ܼGr|<3Owv/8q?__'dzG1YƎq6%d:YRɛdrufy_S7a6&f7n>w "m /[ZZXmݖB066=zdkc]~t[VVV۶}p"Ҭ3g362~<"#&M6drI"#UU9..]\B!y3Tּݩ|㈒"ffTW ˗U__OD쩶Zǟ͙4UnvYQ,#Yׯ`c]]Xc~T{fH$ݻw\mll} ɛ:X;w۶Hmmm/SMUQ㭬 - J뉉Y`СJ"֭Yhi^~aiY%hk`Nɛbm=ۇCQゅnjkk--uCԘP^Joק$HTwtT&MI^$%'}4K, ͛W<5طvmBf%oL6 9oL1*:*rFԀ CL7{v$ɓZqSwdȏzL=kF@E*SS<Ǐ^"XUBx7 't]j=-x䩮 yn(_oWtm5fُ̚?~ٵ+tޤPt=^K]G*=P(xNMkLP}Gigg+~!y@P}-yB=EwFg{@fi{]Af($o] kL?vצɹ `$o&HɹBtƗˡM:t>o: yHt@7 @$o: yHtQ-IENDB`roundup-1.4.20/doc/images/index_logged_in.png0000644000175000017630000003737011741527216017642 0ustar ralfprivPNG  IHDR~ IDATxwX.. HSBH(hłQQ(X IXJTFQ4*F EEFu?f,̲)̝3gg{sϥ`lDGIA$ H"D$ H@A$ IA$ c~n zӋ 54sh8J/66UAt8;Pu@t:Iv@} H"DR1 |YDl*^e˖6Oԍ7}nnn7nLMMm3]ޅ 8PGG[b)AAAϟW3ȔɌ ^x&~"::z?իW8~vgϞ]xqXk//sV/BDzO<W_yxx8;;)FEEeddTVVjhhX[[O2eРARK_0sssE|?zQvYY9^*nsΙl߾] p۷'%%_z5<<ĉ<0`h51|ڵu/^XPvoOZX3*ի#GjT0k4ٳgP ;SSә3g&%%ݽ{w̙[Y,@  䐈͗_~)kO>D9^R]]-ω|.9 >~fCm-%)ICU:u*33FKKjҤID!/_Ȩаyӧo35UI  “'~MGWz*~͛%%T k]jՂ $ewwwWM\ 7>~x;wq۱cčak-[X,]+%ҙ^)))m޼yفUUUۣ桤vر63gvKgE-[" Lh .j Ԑ .ݑA h4A$ H"$vT H@AЌw4#6d\ƺ!ڈf"#ψ)LLL/)) {" q˧H"F$$ mma$ #A4FSxJ"DffZ_|ma9;h{zK-og 'ݷ/6/qbfnf\m#Gj԰,UfP'HC\DCYp>cA̚Mnu뎊JdJm[UT~'NS+OT43W/42u8qV]'$d>lh"""}@GGY1=zVׯF2D3&Q敏ZZWRAGGL`G]]l͚@hgohjj(55 ȑݴ#vڵF42L]MIQvt S +,K^aaݻUl6e&uOl)իd:sOBM:E/+:%P\L}bc#oN.a"dJET{k=G*0`+Wd~vdRu+L2Q)cc7haH~e_M%ߠ2j޽j^kթ&VYISJn;#Hz=e6fb";*>AG;02,]Z~jDD\M\L5Dþs 5ʕlxyx×KK@__[Dlmy(O"Qj 齷+W0rAOO}b.Я_M0i8!!*557 [[^vӧf+D5YCCldt[ԙ  ohe'CVT48L`Xп "4a&Nde)EF2F:ʣLuuN#.VUEa)O({ym*g@~@~XJErm҆ cj*X`Դiݻ糕a|=ɓ:xZD*`0#RyJz_P@ Pg03,[VlA{4χq4Ҕ1UTDup̬{Gɩl9b:HCP`bwAaxD$ H@A$ IA *ڣO/ж\\#; uv@ 5oP_@bEEJw(&$tI -DDbbyy |_#v;2LԬH9QZ } &::H<%wwucc-}}m ?Ű~'MĞ\Ʀ)k,Q<Ν3g腅{D(o+gf*Qfi;r/\en.XZjWAAuc _BBT`Kg`8:j__{Jrr\ڦnH!*'pFƳg``Zeh({MVt=G~۷i3gz2yre'Nr{U0n\ ]\hnn9GxD;M? 7Xz0"̬a&P89rrq=m(H//Ɣ)3H@\."+K/xB%`dY-s 뒒$@iUUle''r#S"֩;6y_ SYIYHj>~G/uH֭;v47Mʩ,^5OUWw Ė뫖: BK =SWĩ w@Q9EEJOOҥۣ6ID9tBx/ IA$ 0bbebU]-xF;M4.njb\H??&01&iSL$s&>TQAqr} QPC[#%?")q˖++í[o՘9S6G.}{ <\eڡCyt:BCk-SåKٳ˜9 CtӋ&[WwhMVV`Ŋ &(0QOÓ_F4Iz<@BΎ'س'fgkdh#}G&)*Q,0Q<11W 8<dAGL,+$%ъE}VܹCktijg~h$MMMlx. F52%rfkUJLs&{jΊKi/Y40JD&sr<6 &vd&xDKAA;jj$;ZAE;(7 HpznR}=@ě?=lX{dhy0'))3;E}=GOOkNhnN>|--oˏ)jZ⁉ EZņ}WT(܉AУGkv>" %% L܌#1[@͛늊ϟ` "0_S$ORPmm?R? "BNL_K]3&*"ځ7 XIcn*JroSwTJ޽VND&nn{K@)MY> @]e K>gf׮99/l.v^6ՕRigJS0aIȨ(0rFH"D$ HyG$#3=] diggh fo.ue""ө^F4d>vv6PVE:OfCa}x ~p}8 ks%?JvDZ1bDkĈ"t2ksv:]R"2`ͤdރ,9cJ\MȚkJ跅ٖӎ];jrawܣwDocƸL&GtZ-irUZZZ4q'E";y2iP~}&O?9xy}}u,-YZlXX__aب9a_]ZZaؿ>5r~e/ܾ0nY>N#Pt*۳g͠\]vډVȠA}EKس:~5xy辐oKoKH[\j FrI~ð8:aÇGnj_XzժU3N9PCVPfF?o)N/7ۻ^iJ^Q^{[Z*dwWQQY$;;/7nٳgM]z ð#]`v;1a8 ÒLfii wKP2${Ǎ[UU7\夝vj/۵{)7n^%—UTT|#/>}|ðCӟ֦.ayꓔC~χ<״qv B!ɓYenf0Ǐx_6"`]Q"۫W5k .a_ٳ׫W//^ 0abbzѣAMM% >~&ٰ!`q TTT\d/8{A_@²tIZ-ѣG~?Vɓ޵Ǐ>|ؾcSoG9ˈNp&/ΣX=p>'(QGjkkKKK6n Y=gnp𺢢B^nnf܋!0{܃?;9a%KU[ӳgOױ\G\/wvUUY8ðÝ!1~˗X /d߾ o}СD||~puܷR=vǎGϮAozI]}]bB;vk|9wV8*zGӚt+$Қt+$Қt+$Қt+&["]-fo.,/^8<ܽ{g KH~w4jkKw{N;xЕ9vHlhlX{ҕ eBHr~K^xG+[Y%yBtI?}更 ;l9ZD\]]/@k< ޽011/64(UGξfaaYoQ A4 Os8</A4Ŋd9TPPaSSC$$VQC Ȕĝοz̙5iYؾ=&3ܜ[mh2226nX__?q6[' J Yiӆ%n߾FN">'%uVXX-[%gdXϞ=_}35 ޽{۶m311 痔oիWS(ϗ-[fhh>yb- o>]XUUuMHHH~]޽=<<}.--Ȩа2eʠA}- $χ2̽{J#===00=zܹs&&&۷oǟM<nݺ%0f|:;;;;;233E;@jj X,VLL F JMMի'O_:uj߾}x۷'%%_z5<<ĉͽj cdɒHl~~~L&bbb`=zHMM#|ѣGT*u_<^(#>>>:::ZZZɓ|嗔8:}4x{{kiiikkL&F= Ȋ+(ʯ!)77|}}3f@II wñի~^$''$''@vv5o߾*7:"#*8&mIDATeʔ~~eΐEL?sPPжmll$#.]&-@vʔ)ϟu떻;.Qjjj999)))&Ln*...++4hlj(ҦCRt'88هB4{3ŝ2eʊ+8ƍ>|(*߿?4]p ^xq޽ :dEmMllluu7|SUUuY`nnN0~Haa!*Z9{ 6l"d!W ݶm۽{… h|>Çx9::6 m\~]WW믿<==TjxxxYYYEEEdd$J7og(֭{aUUNѣXXXH7= C*W ߱cG||<XZZ8pxڵ&M{ED 6LOOOˈ߿:srrP( ɉkhhw8vsxvf~w͙3gUΝxI&ݻw̘1r^IN|?~\SSDk{xzz"}t" JܻUSSzӻnݺSXbڴimtD[pEMMݻնqٳ,#ښ^&E$ H@A$ D"ٳ̴BBs|;wh)::ڞRߙ. Dvfb?~\uӟTl )ߊ´TV.\3ȼ}v:֭j_kk5?o/ٻvҤ^Bf- CW::gnjadի&~6r!11 ޿jk^Z[KmO3 tu5kUe ?|}C]XSCԬi39R#5[?>}[IIQvtX S +,i˗][E9kz-..=llxwVٔ-[PVV{irFS9o+$ĩS2ʩSB )66sSӦq._ϚIJa}9vqŅ{mJ6)2%bk{@ymRU^PS")ooWTa 傞;xy/\_N0aSӦqBBTjj( oo6x{#"le'O:{F/LJd%+܃U?dS%l350ܿP(l @C5 26/`de)M9YYJQC S"cp]]&􈋣UUQlʓ'^^j[J__il6R=<0 J}= ҥ 7 RW bLM 6{7s|6ʢ aNLIzhAuO`T33e˖ᇟ?O߼YRB0eG>0nFZ<;FNNesN3R60qPx"A$ H"D$ H@ H@tw%&(tM]P/D$ H@A@2ÃӧOd)mضسvZrIJJr4ܽ{}|Bt(Hj/ܺukҤImȎ>DyuNNN>}+WyPA6~6?uǏ{ٳ7oVVV㹔qՍ/o޼ٳϟ?WRR8pĞ9EZn:;wv t:]WWwԨQ=N'# D>K.7N/^C_ӧO7lPTTiӦ7o;v,(((77w߾}g?}t@@K<==SSSCBBC_I%]v]zSN #D|υDTرc`رT*ϸC_ĢETUU .\PUUuĈ E"_~%N:u*]rKL+{eoow/y&2tXd64YYY%%%={^zؤdgg8ȲRATQQ@tSSӼ@adddee&@ |㩩|0i6RLd g\(w0?~,/..H?<{*_;<==g̘.Ə Odѐ.r9CsfϚ5ݻ˖-fffمxuVs@mmmrrݻϚ2gx:aCQQ>y<t.ѣG>}>|H`0o >/_+Wp8b; Ld?;00}y{;+]"xE/g4#>ѣŋ}~ӧݻwܹ;Q`"̝;,V9.,ڣGW3.9MWܼM'F!ڂw*LL KD,,,/AA4 Os8<o h8s&H"I;8A/@A4bB6fssr^nʒHMUEP*|"[' J YiӆwPkaooaohР3DՍ]\4'_ D@IeM7hڸqܬʗ/oHN_brV9y:q"GNމ$y%&h"Ż^^l)S866o_=(((EA4"!!_Vhk F ݠє%" 1q}b\-F f}jݺ$D#NUW YlbU4M^.]Un ALӼ/Gg!D{"ϣo-74oE.WE5_.--}}m]E4BI:k7f}#*AE$ H@A S"vv)Q ЦA$ 7i,VgY|>﫯L=<8;%=ɓG,Vd^?`fA&$\w> $:u{ZY͛W++MM45mȚBW-ESyG=w.|qqG|Tjj +X11Wi4ZPd9O|/$d_:um߾n޼KOڰaGQQM߼)>v\Pܜ}]=A?|>o bb0&Ŋ>>ttziix{'OʛRKKG[*\ZhڰaN…KUUFpP(])R$JUw())&3۷/55.)j7EYp)F)??.>7Q>T*5<eTy$G={!DE(+/u.]54C8zԱk|ұ]hQv:y2sߵ~'{Spo֯>gD}s,zäK }"gϲ""yw"VUt`؉D+(  [49oWp¹sry!!PX> AV| Õ+$$&v-x,8׮)n{B/- Ǝ2ph AH$$@YY# HHkk40T04AV9I'Oe`|`JwZWEkHX[5895:9AN\^,n^^S 8;I9ii0o^Q|ֺ'm\D"㶜s `l T WOi țޯ`h0`pɄJ)薕XYxÇÚ5D'2>#w8"K@@ K 76Ǐ">yx{PW55$']6mǏ͆ZHJk E<<,bKY~ 8+׷}|`^HI !:#G֭c@66b5+0v(|G^!H@A$ IA$ H":V`"׻7ŐV'X@б7l6} 7@XMEG Ld0O{s=[>S舁"`z8yDj!~ Q;%{ CC07իV9HQM#&eYQx 66PXw#H 舁x`hFEjj=t8+p~(NyyCAaS޿m 1޽>dgH@G L'9,-QMuur ޾@(HS[ {U(C 0Qd0*ܿOr.(KxNW2鈁zO  3f ᯿UZBXTT@}=ܸTP;wY9s\XG3fH LoGUT@Oll`  lK0L8t 6oe& Eg[z;fcc rѸ8ujrCC4BWWTI77.<}8^Zڿ+&bd2ΜI*((0!BEA Dؾ=&3ܜ[PChķڝ-ؼs@c`Ź"00OO ȵkO>BIhe{D+yy@W=A|^Nx@vv mժćZ`D# {kwwakkVPP"EOAB|v$TWׅ5de }&튜޾-|קAþ<%׮u*$ H"D$Ȕ-ёA$ -}bE>{cئ-ڵ[Ϝ͛WzcNl'ѢZ$==50>wsML̷o_y޽}==%gG$q3::_U7n\9:I>>ttzJ?O6IꞈDR$JEw())eo~ ϟ?kC+]tGMMvEOܿ_|_03O;C",\FED?|x_4?}(//;zg={aKΎhZXZ9pS֮]ZSIKKv"Ǐf͒׺zmC}NA;Mwrv~D$8t^P- IA$ D()L6Z;m?OEQ-Ϩ]Z .AqqÔʈnMu;hoZ@his`(00ccX>|h?Ʀa9 8vv©Y > \G 11¼755<C9D E#? ;wBl,C|<8;ۡD U`i 62(ؠQͶOY{+1}¤5hGCgPA` >w/-U[̘1Ԁ?}Nmz.?(0V HAL[s"6>G9L`B$R1ix ޹D; @ "j Anz;/h)H"PCǏU߾@B:7{(@ B*G$DSNLav$$Ǐߜ8q6$hӧ&O&*\IENDB`roundup-1.4.20/doc/images/my_details.png0000644000175000017630000013167511741527216016661 0ustar ralfprivPNG  IHDRlJ IDATxy\?봪$[i!$S4 g&J l-02>"uJIhWN{;i;1뾮}c}  &%|$tW@!_I|$tW@Ɉ;p#"QG"E%+K\ hOyhE,s++Z(K** gTbb;,<Io0DXYY;?a2$э7{@]+)%w4--111YYY]vׯѣ%hO:Yf/GFFAAAjjjM0JDGHW&Nd2#%%sLBț7o4 RҒ={];Ν|ԩSÆ ۾}3)G dN^5˵ܟZ[3_III9{/9N׮] fccӯ_?>} yaZZZIIINtuuǎ;}t%%%&M\rʆ~7Ԋ;H=zhbb"ן9s7|P>u/wnH xT4F*sΣG_Z͆jhFOUknh7nӧrUUUDHaa!#b߿O{IeddVZ|򤤤􌌌p&UVB455+]oYb``@ [wpuu7oP|MMM׺j-_wFϯ >9?ciDDӧO^kEYYYSSiӦmذ>۲20j(m6;u[}փ>'O%--MZ.gvaaamEFr ǫ?_;w.=׳o߾O>UVV>zhTܹsU̙s/^s;wЭ ]v{ )++y?s3g g>qċ/JJJ\nIIɽ{mf5k=YvС jPܹs5ɉ(|922ȑ#m9a4)Yxxzz}SL:w/5))))))ֹsgzݻݻwÆ ??MMMMMMmuڴi}W3};x<^qq+zgg3g4iC&M:|0UVѧO-[Pϋ˩9sGXA4xѣ/]tȬRB.rtth .={,##())|̙W]pʕ+={2dȘ1c?xALLLfffII PRR255744~(9::jii'$$xϘ1ή­[޾}[YYR544;F?ƌcƌ󕆞F0ڍyQe##Zh7-XLq=% !~׹֪:mzyYB? $ms8EEE렠+WPڞOhOx\^$`kkqy՛#4^AL111n} dG5ڀʲn}S?+˗/oݺ]YY٩S'UUU##Ǐ5ѧt5H:+ 鐯C H:+ 鐯C H:+ 鐯C H:+ 鐯C H:+ 鐯C H:+ Ǐw 0xܹBab~cǞ|Kat6oM',,-(qf{U..#&O,}v W: {{㊊Rmuv>ilٳ 9=N\57nl+Vݻ+y{ߡzFDdnvcґkwvxdh+{W:Mٷ/n͛+'M2QU6/iΝCVQQXjlIɧ;k"#3ǏGlj?vl99i5// `0 СC Z}`0BVa>QW2{]__TThddfaa#HI}ATV~IIύH!)))'N2dHxxNs-_XOW_iUEE٠ 'OEGoZ(uO94t充Hrk4$%%aÆݻ姟~*kjj<<<g͚UZZJo~ׯIXXةS  //?txۧO,Y|*n2119|pQGJ ڱ0~_R򉮌^zv799BHL̛ >p`fr=qF۽{BCC?|^~޽{>|6mڒ%K>[lyUjjjFFgS7o?\ZZ?4Uttgϸ\nwA{gfSȭ['~-ӧϡf?Tw]5lvܹ'?ϮTTTYWIwzwZVV*˗T977W^ZT֢ ULMMZZZuK_~O>{}5rrr. wJGlQH/z{Oϱ;`mxժ1ܼaӇ wzʘw5t7bECG9#%%USS_SSSCO\t633־rJQihhT x/Q陑qBvDDVAjТVhh>!$55&;;P}v횩)]7uLnnZhѻw}\UQ@+ fG_˗/]4==Ϛ5IcΜ9sݺuNNNuXbőUUUUUUl6eT9sRRR\.}ڦQ@!_ҫWEѓ8#G7n9sM֤TUU{}}6n8KR-Ydnn:88())mܸ::ylZA|ίC H:+ 鐯X#Z?Ђe.(#A :(qGg2w!_I t2:}0|DJV,XLqGHY@P UD|0荒32^_(##U-Bh;޶=KO/<}-(Z W$Z Oѧ& ݻkk zWD:r$\Ba%K,,t[z[}|֭7e*7T_9Orr4Դmn mm_wYow_nt"''2a#䤩Vj7?QPP$$䄄Pa`eD5zg94!!éS?5~AjsV)--u겐岲ҫW_̤Z?N몪__qKv$sGk,ml ll nLCzz޽?~}Ӈ;4 A>ѯϝ[cCzzu?^59))w۶t7˗\޽jWoHh4J;r$幺Zٹ{wի\ѣO'"!kYU5ւGۼNG#Plps,gσOu[W5bD?YY{kYrȑz*JJ IK+:6W.rքc" 1!d|0mڴ%KP}lْիԌ OOϖH+mn+))F@²{II1\\FP XB -*(,, |1mR7o+ !.&&sܹ55^z۷ "~PwCݻG::"ڻ{~Qot{ZZ]ge7cҧQw&5;w:uС'XxĖ-6lȐǎ9?]⏲̴WP\]] |Se=z 9UׯUC/$˛7EӦ۷KKK+P޽=zOIKK߿?U֎Ҫz4J jcc{ֱ_*(|_II7'x׮{E̙nݺ‚k˗/_tizz:É5kV9su ]]]Z;p+7qΝwFsr ٽ{ڷߚ;6?C[[{РAG6rww9rq̙3mڴ& 奪߻w۷v 5_)X H:+ 鐯C <-(E-X:"Ɏw|,s+p @W@a>$w@\PɊ)@t) *YjVVV~W@pJmLxGc2}?,.GC\zMtLp~EEGo$pns85KwPb+mkSʕ*s{{ߡ2&ӗ.ptTff4aۭbׯgaa9XTT~Hkrk((B23=lm6T;..;j̊ӏ^袨(G̞}|ӦoLL45}sb23 8QQnGPǡ:ݼxxՓ6mک!޽';w:o|Ntw52(-ڿݻ5݌ž=Eg+W^֦Ӂ=rs@ǿfsd99STRoAAO**fF-XC^BܲHCFFajppd伆@Sa>HXE{x|3xAg63gqw56֐66pwwooGS޲ݻ+Ycfg bM 7)ݶm7ѕ7o4DNNFUۄSc+)S1NP3,,.\ݶ?v6fL!x !۶E%+S7Nչ^8"B33mzL;- UNM27_槣ӝ.wҩsCۢnbwqphUYXXF4$%ŠO|f}ջ?޽d;;=z(M`x36;BѣW7ׄCCuSSC/և+CFCWntU__5(I]]YNNZX좚֭7z4cϞTP=>>1c1&&^3}Ζa}kiK-ƾW>mnC->owrr}j11o[ {"Ϙq>+SoJ~=ϟIM3?!d3ҡxu]jϞҹv:4\",''sw-?gpII7))nпZppDIɧ*NXX֭7&W o>}\QQfgij'? (ԩRiiS:@wt?!J=>5:zqeeO?Pw.\hI&ck;J^^sϞmdvv!::WC,3g>$!!GFFz޻vM\O:]S>ͦϦBu] (+J ,A(%%o˖ \d?\t8j߼PSMM8Xjk^yo IDATٳ;"J밷77w@!_I|$tW@D,X`; 8Hv`[YY; 舐|H:kSݱBYYiKö[7E'T`2}ot\LffaU'*j  ㋊ʏ{e.!ټَV~zӋEEG_(gmmv,!$3=?Ϯ9ov`CNƬ>ŋ.rT7mDSׄGhh̝@+S7zF¤I?ϮxIoJ|f:}?XOݲ}<fƌo8?Vxyݶpr ٰro㥤'N<_U9?t뾾w;r|LY٧jNBB_*1#F>>Z|n?m@RL9Cuvp_vf;T7^/Xp*""^ OG _hAU]*ۚ^hfM/i]ut.]:UV~ntsjtu{SFēC]àE}b2.GO|f}ջ?޽d;;=z(M`x36;BѣW sjujjС}anط66Xhu7;6t%MWUՕd8.i:{{㨨O3ylNe9 ՏoxГcfF1c1&&^3}m4cưGë9O$}KWƾWi|zXmnrr27cx̂*<>&55̘cg,##I@ٳ Ў#xeeU՜wW %891}|p8܄gω_-88SU',,mt7>}f45UBƏdTSii)BԩCzbmT߉έCjA+G?b<=o|ۃgB++)MwBK;;#q8o>>wBttZ5NYf4;|IBBСw*2~!5S>ͶԥktƍPVVq5/TSSe o;{cbx$]{~!H}0|DJV,XLqGHY@P UD|c2}zG XHI1b@(W?*)&'yypj.)uR۶}{ q48ұ׳^};BYYiKö[7ESع%*糦L1L;Z+,,-(qf{U..#&O,v JǒQسxdFiin=rf^^TON۴jNvvF׊ܶ톧oxEEEa̘bShO0Qpmva]hj[VV{w5kl,ܹho֒4HF:~ꡧ36-V__ѵ앜זq@|pr||Ξs-ŋk*+?GEvu h|޸B.oܼa\ܹ,D ;v?p:WqG@!_I|$tW@!_I|$ y$;~ -X:"$w@\PɊ)@t) *YjVVV~W@pJ[~=aӣFp7D+4 ί;Z5vȐބ}%&ss ЄdFGowABC_ļ=ujUcikj5oީǏGMp }1B.][ԊNV(rKx1WL';YXUʼnr#\<88"?TCCe\so;tPTT~Hkrk((RptTff4aۭ";G~ϞϟgWW7ek;P=D Sع%*糦L1~%&c23}ыǏGn05Tr^ɓ;?^céSluOk>|zjξ}覓'#Ƈe;yyݮkH7WRyItFOnL<~<_&=~_&;$h+B]JKm֒#8sww[cc YYicc ww[uo\9ijg7 9ioYY׬ajm7'xJJȾS!E{x|3xAg~y+P4ZrOݻמ).TV [SSs)墢C##3 ˸\!DJAt]t\kӧuv>ac3Xc]!ff_i]>f@ԣ_׈ТkЋ$M9+p8\ ]t+8GEe=}gggE,y 0MrOBeGN>W}<^|_zXmnrr27M а׼y;ϜY iEzz=ccҋoU@ _i}k-YrΪ\YZw&S[s 񹛔pr}}MG|℅mzI^~犊j6;KSSaԔcb֛891}|p8܄gж0&lv-BBoMbk;J^^sϞTdvv!::WӤeLÇ$$H{׮zXO<^'WVVSu?…vvF Nxmrႋi5~xP;~A7jj^^w;" bavjjL1={#W$8I|$tW@!_`G?Ђe.(#@t#"Qςenee)(#BA 8}0|DJV,XLqGHY@P UD|E0 5EGod2}72I|E#N(?k:&_g\L';^`0LL4֯_TT~ӗ/s !,vݺ)R+=|eɃkmN._;u[2>kSz;N,ft=rr** V-)cMdd&[ǎ/''m`hlvօ T~C9w.de tw={˫MM{Jw﮸f %;{kCLf hj_ܽ{֖dRZ6ڮö|_9T=atψ5k֌?bl6zg6)JW]VV[SY*'%_]))W~~(Gդi̴ҮЋ::r.c555gΜ Lft4^oC w/a ww[n:(((<<|ٲe򋢢bjjɓ'~aڵ3gΔnaWV FTn 充sZK7o㨪ڹٛfCоEFFӧL5#Ԧ=-((055mhvqyyyĉhjjFEEYZMj앜WoSyyՆ 7nЯ_Oz=oط-{=ʵ!}n߾=gΜ#F3Ç !W3fݻwVSB|}}&L]YYȍY7 &y֭yYYY9r^bK.5jԨQ6l@C ={ɓ]? 5ΨQkm4,,lܹ#FV- O|)S,,,Nzʗ3R]xÇ>zOC_4KvHsRY˿豈CCь#vy:Y-X*?>44IefP5lvܹ'?ϮTTTYWI5\ۉu7}FA/^sM|ݺؒ x~O<bA )))^433l֬Yqqq߿ׯ__w}wꄄ77z<oĉ!!!UUU[nmhFٌ3bcc>~emm_aӧO>|XUU2eʔG|ϟ?sM<~xĉl6כ7onj7nܘ4iϫ_x1iҤ[n kBEuG_l||ꍡ!M n-<2eJBBR+?!EN5Z_fܸqEEEINN3g|ErV_/[vvH#v/Y{XXݧzcX_L9tr\ 7C^4;^sbqqȑ#kuk4ֶ nffft}IIIz%K%&&Κ5'1@ob…aaa'O̝;W]R-;2bC# %Z,XA/<4Tuk9ʬQ~Er}:]#33mgE}7IQ {JJJ.]ԝ>}(dGFFr\BTSY/]]]\'))ichhX)))Ç&OOO733҄Z8lCr ᷛ:t76. ?H-ԿS͠\ZZڵk׆:(((|CW())wE@Ԧ[>**ӧ{qvv^hQoڴI__?((H]]]NNXXX4;H V×smm PkwNi}.B~ΐ>bIII싉Ill,}Z]cFZfvv6`0YCCyܹ̙3?..nrrrf֩SRz133BԵ ;>bccX}}}ܒ]0l~_ٳg" ?0GvM#Ƒ*#""ƌGUI[---! cc/^Ћjjj111MMY\]]?}TQQf555[KJJ¶nڤ(恁?~p8/^زeK3!,[, ŋՕQQQG[x?ozzz6uNNN>>>'!!gTSKvM5 8>~+Ԥ/🋳obb"DwRC]ҥKo޼.Ǜ7o/]M1bȑ/.MLLطoٳg&&&(@+;v'f͚E-^ӳ b`̙NHH:t]Z!DGGgժUHYl3yϟ?Λ7y)\ܹs] PUUuqqiv+++\ Rl +XCSKUw _.ƍ+++\`wS~ׄ;B5?~ϯTYYy~~~ 08mʕC ?<==TTTLMMpʕ+9x9sZm@={.z{P8"b2d/C?@ VehS~D'<<"%(,XVV-}i@3 _I tDA_; _ VGHY@P UD&ӷ":-r+V  ~$qB +MFͧllwxDAAv~Cږ- wܴ!DNNZ]]eŋGILrVCOrsKϟϪV*OzvRqqeT!ٳG<[p@K _!nUIYrȑz䭭  V֊YL%75_0ȉիwW"Zϛw,bnWGxГۄ^P\ر*_II#(*JΈZQ@@K _!YבhhB;ZZ] !RR ⠠06;.UCE%xuj&Vb;BȂ,eeyBHBBNbh؋/PfX޽z{O{wUh;!^4Wږ:!…gUUW o,X`IٻaQQ{>^+Vx_S/y3w·Wv҉_~# %0DH۶Νv;tɁ::=/eKH2jzboPK…olא!s~beeUݻ+i\9j]xğn~kWʼnVh@K0xx@v?p:WqG@!_I|$tW@!_I|$ y$;ZQ@ZtDx^NxxD$;JQ@Y̭,tDW@a>$w hSlǐPɊ)@ݢE]B"B%+x?3Ǐiiio޼i v|!\IIg2}7ahm[٦BV9hz?deܸq>!v|Ν<{7yTӺG@y֖PH@Ҏ5~PHHiCM/%G-%%̴ !?V~]Էow =B0=|eɃ۷O:w.&3dptTff4aۭ"տȑk嬭 ֮QPmFNO/d0&&׏/**?v˗Kgf;5.!$$$ѧyy%*sEj{{nzn36ZKubf3c7UU+n[[89lpYhlu}wO՜77c(/ɓ'Bywҥ>yyyyyy999.]x5 AJM **~R~,?WHL̙5911OFF!X\\9r_WkFk13bIɧ5MVT NEDdM grw^^I0oP<+t++O3LL4vUvU01ьȰ|&))w5RR lh^Η9.]:UV~EEFFfqZ4iV]t|ݚE[uSSC6KY@sӇ:;`l1|x_u.1@ѣ!DUU@ܱH W] ݗ55F.VQQֺ F6]W rRWWpQ!c*Ѷ uul퍣>س灳E1@Q EUU5??_5WFEe=zs/\)-rpREEd08Wrr^7C\\޽dL̗GFM@-Y@ٳbl[a|| {"ϘqN2oP-ѽ{Z5jjjHY:gu+ih}d,?jIjj11oy|y^lT@/B3_-88SU',,m"h %:;&$.BtO>WTTY*1@ {iʵk/֭瑂g4kVyo>ܼa\ܹf=箝BNU?-hGܶ)mɺ (+𸚗W`Fp|f4;|IBBСw*C@I._,mkjj"""M~X6m gUZZ*@" _ !|6  A p~$ԛ7oƍNzݿdi< FW@rIKK yXÄ6 t_Mrz‚G57n$7OCK{)|A;wމYj! !qq=JL|6Aܡ@ W11oOGRTk޼SˢU}+.-]jE'+EE%KF\K+LOGefJXݺ)R҂gfWU2b mȑk嬭 ֮QPBB}W2w9Z 51۷O:w.&3& {|hc΢O#暤]=$Cu<] "_:OFF^A22 ]53AMkŊSֽڧa.jk|̩dN8,-_??ٳէhEKw5@_lP"yVys7ѥK>ly.]:Qff>wՠA/ŋsUK2dƌaaf~&1hPouvچ4#xٴ~wܹGDT"yw=bkkΗwٱW}ɓ'O8::k;_xЯߡ_C|~ړ={j;+nуzD@Я@6+,,/+kM"{J&8h>|0 1 hWЯߡ_+_=v W/\ h> T~~qԜhkkB-BꫯJ_a_WW4AwWЯhJJJ4 hWxa hjw nyNZ`"x(fА|:qUx<}wvP/+9 E@K_4~x h +W@s4&gѯO_E#&ЯO_a.<{on}m+/@K_y?z$߾5!E vup5c,GG';m70WhZnn/>p!6C>[_0e#X~*%叅lmG+9ٯ,őf.|aʝzl'?/o.ǢM\/m⤶ݻִܻDѣC , l|&ƽyx~4y>.,t{Wwmc1U.ވv·X/]\SSM:|hߛ￧}̸w]?iℛ7ong@3"߬^X_Ν7nݺu޳\Fލx~^OgACY~2}ZII ;%φXܷK-m~s,\#6~.MxwQy&tu<۷[ u2v hN/9B[K22*++zlr~O=sBү+))r豘<߹uq?Ζ.rV˗/w*Ԋ 3tV$,wBACmwzeeU,_{{'7z:ς ڪߟ9q3V{¢?ݹ;oJoil<#Jn݉H"d/k;M#}YY^qW|d/feI$w\3.+43'ƺɌ~o[YYH$>>?yySSV%|vq"٠ʇڪ7mbrďzKV4Yc2shp hs0(hIParrܿ…0:bxW_Wmظ?>`o?d:$c:֣…* ̙AgenС$m;ov͊\'*lXؗi[63?m=jҤ Çԩ;_(dƴ78x l|wϛ֛C/+WS}fzvSNkiŠ&+y,)o mW<}/ؕWюP<=h)+9܇RWWKƍ?,:sfߟ^orӚ'[2l"z3◸ȋ+++^}_ݚ1c͈ҭ]roטX@Kvm>:thK||lsHaϞ\$۬۷W^]ѯy߾nen…3NN6}'QZܵ D$oϔ{HWWW;xqo=N_~90Q"}|w  vxĈNEdggTT{J$_OB/eJ#Tu0ffV{wp.:zzk)=mo^dgC>},q:nΝرS._#:໦G_^+y(Wy;+wWeakkkk:z IDAT 6nc"5#/a9;88:t0sYf9;;;99-\… l\_x www{{{\ƌ3f̘w獵^{iӦ5jĉO&Eocȑ|M}}3gxyy988x{{gffj@]'q1?읦/EO8pSN}DE1 Ν]GGϏp]\\m+wKޙݻw޽{ppLEO? P_))).^8wѣGٱn[\\LD'O}w_ i'||سgPCґߤo߾+**b^*W>}uÇ;::Ν;I׮]C|~]v ~/Zpr+/F1Фm-ÇzzzNNNٵkɓ'W@@{DEq"޵kץK ,X!wPӒ)ssEUjEEmvJJw}w5]]]@\ ڋQ&i ܫBC)2?6on&w~7I%'}%**711166pŊlɓ.]9~xffSCBB *ёϚ5+--͞=[y|ŊgΜQ P(lQ\iUjiEr~rgVWWMQбݻw/((1 @$񚚚u999?СCsI(xxx888x{{gff}SN}67N4iȑӦM|-%<7[7!++-::Z ,3lӹ3caۧ,mX[˄2z1zz#+ёIL''e0L׮3as~뫨bOg/DLDcať\jf̦!j%0s0FF3>S]Fh)ǯ8''gС&77Wy<==944T,q6*d6'H_(g؂Moiim66w޲3g;v,33S&iiiOJJruu <|DDDRRرc7m|iiiHJJ3fƍ5Q )Sh+=1֯G(9ӕټ23E*.&==ZL,7 )+zzRPPq1PPPXbˣ+W(9)%ieYCt.]BI*rѪUTXHoSv6Қ5-TPܺukyyyE;;:v\WWggg2nݺ򊊊իW/_\^J*dM[( 竟3<$ɸqر<˓]i(//7hooM;grWvuCk|~ΝLWAE+̍qQcbp_W^a^D:WUWٳU199Ϭ4]2LNұ2̘J&;25e%˛733ju([_xbHHHXX9ӓH$츪@e<((($$$%%6h]բ7BVK+j[>>>6lP?gخ^:w\[[[gg犊 6.17`]]&BvAQQQQ666ZʋH($ssN"7HAؐ@@C%%*wJKǏa MMwޡ]I zUtܿ3W!Sm2R:Ӏұ]3__jՖ-[d쭬_Ύoܸ1aQ8p܍g4h]\nZZQ˒Y*U3tl˖-駟ӓ={B%wgϞ=r䈶;;:}JKi>Z@YS'F{}0 ׫aĽHgfҰa-ۛI,nE*8&&'߽z׊ӇQidbz&ʑ#Gvܹ{&o"tϞ=ܗ===w!bqxx -F*d"z_nzܺ:Hg8urFGGK.em߾]򜩩ccc:\xחnݢ'Oq-AnppںOQNիР3h* iJ9J$KtN%-3e -^L"Rp]+*חᎧ!Ƌ>>LTRBԩ-+DE ظɓk===߿U]]۹9;Nչ*doҊ*ѣG\277xԨQaaal\c[vۗ.]jbb2cƌ6n``wyg&&&O>#vsw's(eHSڸe(6V -[h8*,!Chժƍ7av9d|%{ ӧ/o)(>%KĉWٸ#ss22EUQ\,ngbƗ.%;;zXZ?I g͛4q"{‹,(ʨ,?v6ZW#!Cښuu봝 > ;+wWЯߡ_hžTw hSuW^ ㍼cTIJMM=|ܭ[7"bf׮]'O^^^@Iwu%CC Ȟ8P2T;\QZQQw]vMWW!00wj >:t*4I?U^NGzzD"1BzAc &hn_ (__XCCC<'OtRLL333O:<啐p˗/+###C+:U!UG󋏏 U?gIO-ͥo}uh|-e D"qttddzfJKKciiigV_bř3gT# [WlZZܽܙrsS3tl rqqqtt DlfݺuNNNǏ?t\ 111ޙqqq|ԩSo߾M&M9riӲ5_cK& VMrss|½Ɵ= tXX0) 6y[.2L^Èx׮Çsmo`gnxx0]2::̄ ϶>gLL/h1ӹ33bsr3n?D̙1FFLu >~%557`999Cٳ6RQ<==944T,q6*d6'H_(g؂Moiim66w޲3g;v,33S&iiiOJJruu <|DDDRRرc7m|iiiHJJ3fv{{%U$''N2E[͌~==zDɔ,͔I/Rq1ѲexOI]kкupD%%T\L66lW&JN~DJI2z}3GE9kPi)ݽKWйs0ŮZEt6egSn.YFh3ۙ[nM<9//hggWWWǎT׭[W^^^QQz˗+ Z_iRVԶi BP8~|sD"7n;vss'yyy+ &irsL2ܮ_b>yy5IuT|ٳٚ199Ϭ4Q2)*̌iXdَ,SS[y13S] <'e+/^ 377g#zzzWUUqZtUjiEm+###99gÆ իWΝbkk\QQE";,cccvb]]]].("**FKySB dnN?,HAؐ@@C%%xD=|HffdcCSϞŴu+t?5yv% g[TDq\գtO*)*"KK4W\LHVVT\Lh JVڲe쟽74<\KZ˭BVK+js~~~YYY ۲e˼dagϞ츰P{ ]ddٳg9DΎNRڷ,Pԉc }_a{~***~w˗o6@T_Obq-g11<]wr;wToĤqZn.#<'ʑ#Gvܹ{&o"tϞ=ܗ===w!bqxx -F*d"z_nzܺ:Hg8urFGGK.em߾]򜩩ccc:\xחnݢ'OqAnppںOQNի4kDB?H5I$KtN%-ʖ3e -^L"Rp;DQ9,w< uN7o6^`*-ZNUGxN%<%Kĉgٸ#ss22EUZQ9ӂ,^LqqҥdgGK.$++""j'͛4q"uv# 2*.%K 5+X[Ӑ!dmMݺ5=-t<+wWЯߡ_C|~]zB;R-rO{Z~1;Fn;|ÇzzzNNNݺu#"avuI@ ĉ(;;{׮].]244\`eq/CMN(U!I)))}ݵktuu{fӡJǨB4[OqqG?0J$y -4 br__MLL544SxK.?~<33ԩS!!!^^^ |<222)3MQZZQ[}Q??8KKPs(?nݢ\[i<2Ο5:wj0H$GGGv>X,w<|(?x8;KǎLbt89I/'`vett [mU3}:Ϙ0_|'b"" sgfeTW33g6Q,312bjy>)ǯ8''gС&饥Ρb-V!9Dmoo*E9C4mڴxKKm۶{9sرc7IKKۿRRk``#""ƎiӦHKK;p@RRҘ1c6nܨ^%U$''N2E[͌~==zDɔ,͔I/Rq1ѲexOI];H(,9 *)bgz5ŔGWPr3W%&RJ Ӝ9*YJK]rΝ{Fi,v**,۷);rsi ̭[&O^cuuuvvv*֭+//Xz˕4BVK+j۴BP(?~|~~9C"7qϓ<ٕrn~4 |&w}EQnnnׯ_|<Ľ4yETbaܸ!1&&G1~<|<|̚t$ "1"JggkfHǷo?ҰFTV633aQ~f;LMniMLEu\x1$$$,,ܜI$v\UUe``2dlllddmj͛W!d 63tlW^;wsEEDfffفnuuuwrB(OOO-SO $9"dcC ԧH! O={J]Gdh(g;Ե+ d`@>ۢ"=}}UQNQYZJܠb0@:b3(WWZe?{++ׯ7n hx(w Z7[VdJU ۲e˼dagϞ츰P{ ]ddٳg9DΎNRڷ,Pԉc }=gMh IDAT" CxϞTUEEEԯ4>lq/ҙ4l) O[ϖcbByyݻECwގI\21QGxN#GܹsM2#""D"Qii={/'+{zzرC,pZUjiE|vչuuu"hϞ=q* [MMN.] epuu ////++۾}39SS:tH۹/ݺEOP}}u+UUQA7nߟͣW;4h,'~֭k}:Gm%Nt LQQʂ駴q#K˖Ql4J[иqTXHCЪUQ@4j# "ϟO~oa|S9Y}-GQ~d 8l97ҼydnNFFh;DQ9ӂ,^LqqҥdgGKOZ||ޞt 7oĉ¾ ځ (ʨ,?v6Я@G`mMC5uxZ0yWЯߡ_C|~ @͙5kޥ+W9i$a?Z mΦ\ZFŽm@y;s֭ɓ'q]]u+**V^|r{A+MҊ6mP( Ǐ~H$ƍcnnn$//Ov䢽=7M|n]_QTftt5'q1;w2M^QXX07nHEEt|ca!]{` Tdzl}ffLNt|3+ kLe%өr̘EI&;25eɛ733SrŐ0sss6'HqUUxPPQHHHJJ mкE7o^VԶ222}||6lؠ~б]zuܹ...l\$cn266fM.5߅ 킢*<==mllO}=%$PHʂ $P>TR"ϚE3fPY=|HӧӬY*RSwkWӊ v\QQ⢼{jUj+mvWT /ѣJY$|AE5U‚(Mf[_ϞeQԉEgVhݓ.]9NEGGE\3ǎ1L}=#&[& Wd,Gv}EvLMݺMrȑ;w޽ɿQ#""D"Qii={/'+{zzرC,p{YVD mW^[WW'NбtҥP WW򲲲۷k1C355ݿllC ҭ[ 71 N[RUi<ɡZz>CRX8?eI$KtN%-ʖ3e -^L"Rp;DQ9,w< uN7o6^`*-ZNUGx^ra3 Wcƌ3fΝ; ݻwܸqNNN˖-ZTQe\+BV+*R'=%qv0e{{ǯ] ۯ:yd{{{9~?hԨQͿ${s//k*G^ad:ub c~YYEfS'_?PWl,o0ǏKYY̻2zz.3f ".iʊԉdiMM*+__FO11a6of:wn:EEH$̙~(,#͞2̜9DA!` 8ЁܼI' Z'h~: *+bZ>P@[C5 Bԭ ;ߡ_C|~ vy _Ku_x)t7Q&u_ p+F7nܚ5k80;w|wǎ_sݢ(ND?\\\&NxEy6P34EUjEEmvJJʼyF5v+V4\Z!##C)Q&ii20 0|\]kW׏|!{/1:JTTobbbll+ɓ'/]sSN)燄xyy%$$۪*ftF_yyG`._VQNu53sf5U:gcd17>~%557`999Ce666 TOOO/--uuuuvv mmM Y-I$h{{W):iӦ%$$[ZZn۶ݻ̙3ǎ̔IZZ\]]ϟ?4vM65EZZځƌqFMT("99900044t ތ~==zDɔ,͔I/Rq1ѲeҸ+}UTPy9mBKdjJTV&gD%%T\L66lW&JN~DJI2z}3GE9kPi)ݽKWйs 4~j۔Mf{ڀv֭['Oc/ձ㺺:;;;u֕WTT^zwWT!mBP(?>??_!HƍǎܸI^^JCyy97E{{{n>h77ׯk>O^cvd *Z_`nܐ=B`aH㯾̜ɈD̃錏T++=[#߾J%SYtꤢ33aQ~f;LMniMLEu\x1$$$,,ܜI$v\UUe``2dlllddf͒ hvу^{v[6SSwkWZ@YS'{0 KJ+WRnԽ;\I*3 ~7PAדXirLL(/O:{W>}11iK&&I~rȑ;w޽[{DDH$*--ݳg򸧧;bX,wqqyŨYVD mW^[WW'NбtҥP WW򲲲۷k1C355ݿllC ҭ[ 71 N[RUi<ɡZz{@CRX?h, 2*+ŋi$9YI$KtN%-ʖ3e -^L"Rp;DQ9,w< uN7o6^`*-ZNUGxNFÉۛ$''{yyq//ɓ'*{zz޿˫Yɷs7rv(ssEUްUڣG^rennQظ:9CǶv۷/]dƌ l?,,͓LLL"##>}:Gm%Nt LQQʂ駴q#K˖Ql4J[иqTXHCЪU#p!}1 984nglˣAi>9Y}-GQ~d 8l97ҼydnNFFh;DQ9ӂ,^LqqҥdgGKOZ||ޞw&MH/k=~iCmgm t4dY[Snnσﰾ|~ ;+wWڥ9/D~/ GW^ ㍼cTIJ5&koϼM.QPDOEcfDAz46P$+LdeP+SQcD+}Jځ.A0 a2Xao8WD z^ٳwUՙ3gnܸaggrС/xۧK<<ɭ[Fᒮ]KٰAPƯ_5kS777Ϙ1ԧ򠠠XT*xe2)17ѐR ̙K>>jjQ==='--m۶m{G+Rhcn-ŋ[[[MLpPT-b`E{k?`sΜ9\3}1 ߧ'O&Gr[wt>ڈAd2 qu[77MMQgȨQF⸹EIZ?LDI)oׯ'%%eff3Jb^H$rvvvrrJJJd neoDCKmmD"ؽ{)͝;w֭[8{쀀FW(nnnL Lakk;`SV9R燅y{{/ QZ tɐ aŪU1UUcǂǃcn;:֟|K}xHxz5WIg'Lak//tv2XWJJJRSS콼޽{SӦMym?3m:Shcn!>**t&999<<ڵk555/@d2noo9N^^իW Fڈe_~\cgH5 lM L63B;RRq㐒7Ux8!+(O[Lw磥=2>8&CfR)|#R󕂂>|x)3$$$77WP#Gp_N֧8p@T*ʜwژownߴ4TV ő#GTLLn?ncc3f̘v2rrrGС3iҤǏ:ujXxW4k :>?2bc5=bz47KܹЌd_IT->\Yb[˱y3 HL4>8HLdᮧ0nlFD 1r9 ~ 4OoHÚ5ĉHNFQ¢EhoHMel܈}@|}5@+Lw"'cn922~=MF}qvB\(.f[ヿbS|=6nDDཽl}G) bEܿ;Z'edP(5  ز|3n(C P( :ӧcT =}PyBP(KP( Bt|BP(C+ BP,:_P( b() HIDAT BP(KW(y.{a?2 p襌8tB|Xlj:R 'V:s̍7]\\B:tE' y<@ccCn޼@mЀGM]_ mhHlWVV>}ݻ Ǐ7328t:R 'pFl܈`6`w2;x7E'W###ʊ^x͛.\8|]]˗ 뭭IIIBɓn磶VHnJ3})17P>{lTTTIIIqqX,63BX720wAyy@CoĉuI1T*STWW3uuuի ۷or!8YʹShcn"n[g˾>yX7mmm"(00/>>^P0/^|)wI \p!447<ّT ckڲ)+c2^s3 %c%V{{}4:@rs=|9uH>w4?\ҵkqr"6hʻ+UUUfb3f0\ J϶LF;6&rT*Uaa9s|Igu#V\YZZZRR~F?zhWWו+WΝ;WWWKuu˃***rss.\gϞ7>qDyy 222#;@ D sƮ]OH$1$c^utvɬ}Ӄnde!(]n$^Gaa :; hnҠToC"y2TV _kٱr9=7Vd> "DPj*ԄFHرȻG Og-8dkxNL^^ے2T蝯feei{yy5440{sӧM~guDC}TTTRQ&999<<ڵk555/@d2noo9N^^իW Fڈe_~\cgH5 lM L63B;RRq㐒g30sWሏLWTj>m1-~/΄ xx?|T >D|p U(rȑ#ܗaaaP*J2'''00116&7--M*jBq:S]ZZ豱!!!QQQ#Ӓyyy/^X} Ah(>ħ"?ߐ5k"VEގӑظjٰR);}ٰkp<[য়vˑ''lڄ_5苳k~6oFq1o ظ^^ wP(q> kޏBP(րH.tvb|H 5tBP(k`TLSvCjA BP,BP( ҡ BP(P( Bt|BP(C+ BP,:_PK0㍴J?tB|Xlj:R 'V:s̍7]\\B:tE' y<@ccCn޼@mЀGM]_ mhHlWVV>}ݻ Ǐ7328t:R 'pF''@w7bcQ\ ;;ǿ x+eeeEEEs /^x .?ˆ֤$PXZZz[nQ[[HO7ژhl={6**S,BP,B4?bzV׿`gV릭M$+ FyzzŋO:.  ./[wŊMMM\3(**ZdܹsW\8EqDyy 222#;@ D /_>R,hڅ?DC>E]_Gg'쐜n]}A$“'섷7DMKRܾ 䵗PY.|5֮5gxo^¬!1MMhlT;o!tK.miia6}||j5SjzzzzwwwOOOZZڶm WDCk[ ŋR-ZIKKJCww7~9sf:s ,+b744 O ;OL$(:E}+= |kj2}:~׿YC>%O2jϞWus#lJ%yLcC_77ҿ(I_GI4y>qs3Z_~zRRRff;٩T*upp0D"ggg''JFXff mM4J$ݻwbܹsgݺuga] []ٙ)lmml70@_0ooe3JK!.!<x-8dkxNL^^ے2T蝯feei{yy5440{sӧM~guDC}TTT}})Mrrrxxkjjj$ _vuudL>r-Wǎ!.ΐ8jz{ٚ@8mmKk_ź?u G| ~ 4OoHÚ5ĉHNFQ¢EhoHMtw/n8yEOåK>]Hoز?d[ _ww89a&7D_]y3Y}V௿EƍOBPWxg_뤌 ~ BD"tu[oF e BXSbtL wuB?P( b BP(P( BtUnAIENDB`roundup-1.4.20/doc/images/edit.png0000644000175000017630000002335011741527216015442 0ustar ralfprivPNG  IHDRZ%UPLTEJBBcƵ޽޵ƽ{kΌ֔cƄskZ{{{{RssssZJJB99{{ֽkkkRBBss޵εcccޭƭ{֔{ΔsΌk΄999Zs111J99{{)))޽ֽss!!!ތֵkccΌޭkkքޥέƥBk1Zc911!JΔ{kkcZZscc諒քRsJkνBcZ9Zk9ZkZZZRRRkZZƵJJJ{cccRRƽBBBﭽ祌9cƵRks֜s{sZRRRJJ絵c{>- IDATx}u%-MԞ[C\rҊu4fZ6-2$\96KδrAWɈƑtBZH˜$W)$u{s{'˛-Û¬ L9~qA@+#w\k?ss`εL+7oܹsqWp 2k\W]'~_[Yyʹsfbl>k>~>Ǟ19{_y9?ZvܮН/]昹g'o.ܒyC&q]Y\av۟V{#ܱcЮud/s&40p]\zbs'/ݺ'o=qne'o]3O>uog=uk7Y+7/'Z0o>/̙g>ywZǖ-^u/> W~ڱ_5X^מ+[WJ/]6\4]uSO:뗗vo[ډsKκr[/>"*/Υcnҙ'./^>sno~ܙ']|v39qĵ[+JSאa Ɛ7v_[5..};-o9c<;̵K]ڙ'6 &<}=\k?8\U2hsOt\GBϱ{:z^*e< mU3A-cp"ѱ yp-`VY=T])x˞p-ފf9N ij|AWo Y [O|B/ob714zoʷYPrQ Lfi 6oK?͂0oL /4[҅ bJlƎkie"? j:X͆ V e6 \&S z#o> mp2nJ9g7㺵,pi1۶0h,|T`C@OMAcp9~0\k?ן}mp_G 珜X{^>||$;j>!njmCjzA oj߹ a}-(& d̵-3*>ό%\wmp=z|_[['xٯs`ε̹p~r̊'Jo䏭ĜNӭnE[<=k&x2X3~qyeqSv32?<8ѡQq%WuA [ÕJ/h*ۦ,JIƕMи]^s)+H\AZI+ VF4h\PEk\Un WPJ-+K_3ƼT %Wި@ךhgבq(\^Fx>eQ#@/iZmsN.WGŤ:q~4m~k?ss *Ջ@r_.d݀ѽ A"Wrd؉\dY`hP¸;WU"u (7Tl i`&71YWY=j|c-]vJfq@) x)2={6pɜ^i$'YwO!W ~>%Mk SF~:^qcM\jCesCو{+p??ܝhl^0Ҙ8z \:z酿qG/]zۇWW}ҫ/?/'^r=/7DS\B2zĉ\_tٗ>ǎgrҿ}Ç/=z}_^:`jc\[%G Gz W>~o+gO_9zի.^x#^8 o?78{= H呴ug\8[=z=˻~k+\ݴk?ss@rjc5F{H8ss]{`\?ixϚs\k?ss`ε̹9~06pq5us c*2M}X.2We 9]mK ZZUq?+X gk[auss`ε̹9~ =O_#ۉ|c+5kzs~|mӮ/~Ҙ6WC}G_3?~߹&c3ơC7~ϴXз~>?|o|ſoΚzu߾q77W_?N\oan_pp߬]);Ĝk?:N"C"vS ER$,,vǵκ6`+|F9VAuF ɤ>W]M??1),vŵ2#IUigD [9kU0E>YfH 8c$7ڤ"fL~Q28 !sʨ\LKK52p)eTL#׌)Z,4բNLJL$VnF\ $JQVVc,&꙳py`q\䪈L,*d&*jc*x H9FBKflĕK("˺~_,؝ c@@Bb$$l֢Y-c8ȣ}+v <#NUUkEz,\qx=0ƪo[ņ#Dҋp8l656$Id׺ؠc.#Wx˵:[ȼ5nBLTe_ҩ22YREA1Z('\TEeh_2㽎;E)\AC ܲ('PTUZzJTCkEBTdRIVӠ?7J5lz[;;K͛nl,[`xiH5n x\'qAѡ-u$t5++KKґ z*UڤJ}R=`I*KK˺n$ig{>s=e16}:fڗ y̮VW;kBy}R_.1אmm)>\V[Ѳon~ᄭlLY$(k$Rt\Di;t=/OއL2&%K+r I4$1B)n<#:Q#צ̌Y1HLdYUa$@3PP)& UV 'X>;k%uyAȃ2瓖ƞ+p kd>m* ڂU:kPW?avTWtV3Lծs`ε. [,-.Tobw^V2SgVKW-KIL].ʂN$.wvPjwt=$уDy`dŲg~Hպ|lL0.]pu}//]"kہ_3^uTkԔ4\`W\;˨*FV_P: b:֩dyW{0ԬkE=̪>a-*M($cQm/ɓx}e@+0UA<:SZ&p:+8r,C9\`H _8"9q&:\iL onȑ+bDhR.~Em ݽ CPUr:\&K TÍ,T Z+L[BM 0J:THW[. A}vtB{2 JC}&*=-v˺Tm|[uQ[UһSh<LI^ݼNˈ5n[7yosErRA5^]Ӻι X1Y+Ы;XdHo*80Y,YAts mQD2Э4.tYBot p,k݄d n4>CDƭRXw4^>Fy|Zh,içDP#зƌ̻Cmp5<SڀD}u˵lF$P\ 2#Q!Kue:ճZ4aĤb,0SDM\,uWO&@3͠ 0aZǜ:2+Z 1Wg$0|ƹǘss`ε̹w#/5#<gfLL0h49~0Zy@_zs`εDmc?^_E5ӏ`_hN#}?OS#/~wOp|Swz>#}?N}fk+ߗYi Yc\3̹9~0\ZԮ`3|18Q7|D`xL+&øRSqUfƛPT9$(Bk@Dĉgcpr)ː,# 3]N2tH1eUT. |xg섫AXd$#:*Y8 1\hMqfP& %w%)&2GyLD^u&FSE9rUT+D*$Āʋ vA1$:)=EW} c1C(xL\3j{EJ cCG)0-"7ΐ jFۗMPWdFϼeIpWW!ÕqMA)`; OI9-Ixq&(KYFuӊvwnp]Q =rV`RQnjsrz*4 aP $Vs!THF3窹a9"d{*@f4T\gɸZf>V:ՃwRJkk)*![NW6r)8ZC'Xe̫d҅5cn}\AM : s4V+RrI[FhZ)pE J؁Eݐn-*L"WTXIDƟ Q/cVQ;*`H" xAȴ`:H9I\z3 :gntwg3W.5d`eJuƆ"˶bHx^BR" eڢ6Q씫9:Y`=$VaWhM=܀$ m^F Ӱەa"N?ԎQuT%H~ZxM( "M*K :uQSrE$P\6#wnN}bZ9(Oo)BE%1M4i1*qzI\a9ͤ`"4AwHR"M!4C`yZw\1-dTH 65 o)0 4$U;_X5mѾѧm2'WŵiTNĤb^AJժh  "fK ;|kQl||6ULu{aε̹iru3[UcvUk |eXSP!9 @E-ΦsYV򔅊b"Dc`xIS: LR $FS$$Qz8ɔգ;0RNb*iiZF΀aF#y;4hJL.IhBe%,\f2&E,5ء)&Ҕ`n-lH 9B1ykL! vьkZUF+vMY!eDvRB)f 6ÄTL5p(5|Ld>I@/VӸI ?}Aߘs?5\9zYLƿ cٖW9-'iGD~M07vlΰJ:Ltwɋ:% : 0äu>8J"Q34ajJܝ=`O8e|T<- \Eˠ]@zȵ{0SB Ơ3ڪaTq2(>l;pW\u bȭa#u˵.( m)E+$" oifLQERN^jm}Jj9Tȵb(-kylF0O|[ '%%aY,<^n2=kT0s V3hQD~+fܔ1 nSvM7U*!᝭m)#]Nw>Uœ1jۼfvMŔ7*%f]=iYQ8k*a@v+kB('CukhK9=h}f%ѩP]?ή鱼 &jn0BZ.%BcrMٍqgVXA65ke8JUmO(DʶKfYlȸ& ]*Xו,qj.W/qi3#ZjF2:7Q6>xj\~f0[ &~G0-tvs' A/ Z+V]M. )tHxS+<6 L #3 9GO+ 7qvV.XQ~kSig4::bqͲbNBzd$NYlbTEcR54 9)ft#mJH2h!”~$V%Xdë̝IA1mM zjr 4?k3h2=YeA)FrMV$քvIO6Ȯ``[z]0%x(>X$EHxIa\@m'3P짊lֺ̟r]o$7lQupБ-Tػ F%J5,!BIDATh08㄀ C5f/ r=VjEN\I֦^l*ӆbEble۪lخ 8$]S4)mo8U&HjLv u n+ F˪ Rk$ gX6\k?sW 'ǟbWgص#]q o4VtiLRmf0"z޵6[#̦&sn2hҨsS;68X*Vz̥h'Bs,#  j5:"=&!ѠZ,A?`ZZ),K2Qguj$C@R5T)jx m:0havjÔZcg0G2ʴt(Je)Ec ܫ'M1wڤ5#\ߨ I.5!ېAFbա&lhuzn!N e WYE@4<&3abyL|<,}y*"!1  U٘M"4ˉNQ,.ʙ7&OILXD  !e$)=F$X$F*Ste]+C5FLd@ P1E#Z>0ENE@ OK&8 u"z7Ŭb[|2NPs\/ȵ .Nv,]:86vƠO5،kl* sSZi6.,t0ⷝ1 %'E2ap%M~,nhK eŔm]U[޴eѷRyUieE![X2YLdm 8;9\~v#Lw2ItEqgQÇ.i\p.svkmN^\j7Wާ,TmuVu!=S36c̹9~0i xTqگ.H7S/IENDB`roundup-1.4.20/doc/images/roundup.png0000644000175000017630000005367611741527216016227 0ustar ralfprivPNG  IHDRfT*PLTEk֭sR){B)kRssJkR!{BkJ{{BcJ19JJZss{Rkތƭ眜c΄RR{{{{{s{ZckZRJ!9)sss!1)RJBkkkZ{Zccc{ΔޭRsRs{޽{ֵ֥11kcZ{֔k{19BJZkޜJZΥƭ猔Δ޵kcRZƌZs111JRZc9)9)BJRB91{s!)!Μޭ!ޥksέ罵99ksskZckƥckkcR))Z{1JZs֔ƵRZcs1ZscsޜRc{BZ{ck{))!R1޵ތskν֭1B9ƌRB99Υ{!祄֭sJk)1ss!s9BcZZBc!c9{kތ9Z9Z{ZZZέ֥Z)9BJRJBB{sBZB{kccZRcZ޵BBB{sB!kJνέZc1B1cBkcckJB9΄!!s޽ƌs{B9kJcֽε9RBs)19Zcs9BBBJRJJ絵n IDATxT[U?΃%"eZd(f I3S,c MZ/:kD~Բ +k}/Zڨ4G.`miQƨL>UP !g}>g}>5C A>_> \}_$ sŞck} k0Z7`! 5o|Ch Bk0Z7`^Lt쥗dt#udK_ڵ^ޅ(}R[RX -cKw ݾ^{׭pWC8vrݺ+T|lepkwu^ڏRXtҭ>c[)3ҽ+׏{7F?rWN]zx@|6dLw_SN+tV|C^ܫ.ߊ_鏑~>//?Eؾykw{>_}g\_~}{ySnv[9yI=\&t=F(+pPc'~*kWܭlOӏ{Տ_P|;=~_:(cr>H{]0o$*6H7JN{6l,9)݈dƍ%{J"={ƉyyyyǎI7m+9 ~h%uqXf>jJR(;n?ӏ秣0 ]w]-*5cEW籒PRRAI@`V ?zLhr)w7<jU }Cᤴ$(e߰7RX t+F0?7ބ-qr_6w߼nO`w ;{ nv%DձcΏRaUw77܉`>|r|ElH_"A\b ٓWQrBZ"-ِw?_I6ݰH%y_lpR*!MQDY֗ߌ 2 }Ì<ػ_~ʳ`FEzfܯa[n]^fϞ "/1++N9"'={ 0;7C0`HjieN!D"=w'^x/oId-չ4ʗ_E/OW~S@g~/_{OKݰ}|o󗟟"?ŝ9|ne-1`=ÄdcƓғ'|# 3f)fT=`qJ6z`ZrK6ܟWSٓW@X)]=1OZ;]R')tQdDá-fX_y?}ӯ<l/G~g˻(/S#VS~_Rfqu^|W_ܿx "G0K{Uh㻁HJ| OZ d4>"捇Aѫ B K@!\w?9xIƍs |죒 g%y ? 2xjdU*g_}{Q߱[闏LG#H?wKWoy;^;y,\v0 *.tvo;yzJQ!n8YB1@Ǥ޿ 9[G^z(d/p!`\`{|E|JheЅ@(V'*Һ] ߦdU5skt-F7*Di U!w rpnϧt_5W`0>111>X"*vM3ؾ5WAjh(˃5K`^*|wsj*)oI}__fߵ opJu+$A&ɽʈ~Kj;lWi.'\{!«s0dr*BYI|ƙxFvW fǝiU&%:aM(T/1 X'gWy|!v"?#DCoR7e50Jh)B'![ސ_^N;!Lt)Xpn{JmXNIMZ1r_kۅe*|{92j%s[5|Y2#qI)l0K"*9Z:^\ztDٴ6 Hr(j'6`QCٚ߮JJ-(yhOT:}6gbJOV远Pf+>nχav3 N|\X^0]h%qeAեa;DHh$ 6 Bx%sT֘AϸmA;ʫC`֜ 3񄐠`.WR0gs6`kc`F&94! #•SŻJ:IRLN>ߍaBm't͐EY=.ʲr0`]R9?j ćl(]绣0C f>:†yS\NAX'60FFCB3muiyQiCF))iQmᄠGLk%|(נ'qqe`QHìvn.ƍ+JPR?*p`3W cFŲ0/;`` }!0a5C;)У*قB CZ G6q 9F64%\%Ԏrk0Ƿ&^MUf"gr s0r"s`S҆E^2ȏAІ^E( # .pPMA[7ZAh&FfR1Z:g]v\dk1s\⯮*Py,qqmWXyhaqWXSͅ0W^k0Wrbܲлѕ3q…Cզm4 ;}ʉ65:'6gn1 2,_ٍx]r|_:.qqѩVDȭ<9r.JB:D >[vIwa6tXhE "\sء Z}(X\YMͅ} ϹOp5i|8 0q%:/s1'xaMBqtz8 eJߒg %YFR.cǐī#j't-c+ʱHqgVBvph3*zBkwa|7noU܉\G n>A̙ƷbAO |·gQ:*}B UqC5 Vw;FD;碞|!ΧFrkA綷rJOBZA?D>e<*wg &s0SgA0^ br藞/kƇaFl0eb; YY#l p8hk'A_v mbe9l&ԩVh+//,/p1_fg3n><f3`N]f '#hm\Á-,p[}gm\npl-8(N15m5Bnffښ]l/1~bftAT0:uuSZhlAl $9ݭvw+uN_aF=>!hłmmBX9 s1t;jBBH܆a?;)Ą0L!m4>`[s]lpi6mBt< E5gl QY-/*r55`bPN`x 309 q H)||)vO}h* X6; {Wo4nz fgcn#v. 9>DD _(1',x>,Iq Z5Xpn;,=Z%JpkrK PX6Dl+A dRk5|ʔ%ZG"s{[T"Ol R ``A- 1nyEOntKYNt_p aRR.:A ǖtQؓX*!Gץjzs܄HD9Mԉ+zEI})A4P=q&#TtHrVT>)4<{IoO7 wSLw|ӧpUg도_Yv?2/L+j#=F|bݽmڱ]ʁMHu]\1~?̿Ma]j4W0Qٯ+˕b`T򃙗>:^!#w=MGx(<}[~ewȿȿPv_{w3WT^yrA+_\M];}|~ 8 s_3+<3G}(ȕRڱpt0ݻ`/=n3/~0 ~qyǑݗzj|fMwOU0߫fUO9rgm2.ݕ\~P+b42]ʯM4+S͎raǑ /ߕ̿aJf?͗=^0{Uih~4n샟]-j3P랺ru>t}!f7#GVS]x+`. { JH܎JG.ĩ23? gwU)Ԯ/LZʚě hab|vvk{v[Gb_^?At,Rr\$U'+oio[_B0V__QM-*Moi]|?Ry0o@m/p[C.p[ZETя>iil`1i?FKS{^ȥ&32 Ԥ1r2\dZ*ŀJrl weK2{=#?ԚQfM /ѝW18#CH nW>-L!˚+`2 z i,g9#24O5y{ FYOW(UW2)^PY'ͷ}`bS{ƠWfv0b0sȡ!@џ#" R1k&ұFP9/V+Ҏ0Sr1 `uȂ泺z"C *(..`~fqAx4q74*^c@ }yI1/8V2DfXY,N5xHE?A0AqYAsP}A,VUd3V\V\z& ' s p1 ez-0zA+ Ҽ)̡b?I%04LNC Wz8VQ8w\VuuBZcnLs)3(QPȩ?7CyVVK1O'EsdW tPoo/#Y,:,MSi ioqn(&?KV72G IKR Hŝs^o&Y2`L 2MP񦤤LqfoPژxc9F5RJi^4iۛff/c{+( "F^텴_hAZm}TٜR|fKTBV5ȔjS)ir0Ռ4s}Y ԁxey@_(ƚC9ԥUfF5(8IRE$RPbBS\gz@I9Y!B9c6ZƀaZ&\O*~߫)c^<2rRM^f&{L ^\BQKfBO)9?9E=lY%Is# I fb%ʌVT>BQ~A&R@WrD^$f4qPPڛ&jN#E`a7@͊zQA5p(fTW9߫\ĐV{(*K)Oy\NCl>G\LJ"/,~/D'Ոm5Q,%9XQ*H(Hch Q{`T1`fXIŽ0$z-0NJZěY 3!wC{ @_j/XX1 CLB9DP>#A.|B5Q;HNм9K4P@/1] IDAT C@04ZGfzhFA=҅2ά LK2C z0zl-""A揪Q?-Tzźbѻn^QE5YpJ[ۿDmqԙ-&sv yo0{)L]CNN= ,XRv;טW"J땎 j똊h tK?T.khQk`leUh{`.vR(Zi\Wh0 f}XDiC2HZ3ЪyoW+cI9_4; u 215{SƆ92~^ +UN8|oJ5Gs0,@!S1:DI]D}ID DŠ̴ⴴHbtJ]EBCcZ7nZC\hTA4'\!T0 i뉖|)o,EN'xǏ>|r++kI}]ZH}_S1at'(~ H]UG83q\mX&Q8ZBMliF,!Z yԕ'=Jqf<%P-&5N@U0wwdHwp ;thP8-?X[SXɩ/uHvCK QoToP \Lci!gXY.,O~^8sgŀ8PZgBv`}ȍڎP؉37F&{},eMכBwaf=$\zNu[;t譭[.ԅKls(H oj= !>d xFu8Sst9 XH}!LWq0r"$SЁb4$i.aQB¡Wx, jjЩ/7v. ۱$8G9(9mG5LXēTn7xrv "Z8f@cjǖ_}Of8vE1d(6nFyKA^`#vrv(RA tP`1yфp;&+>9xv1`S9)-7M<#L+y+#Txri뿏8@f9Gb1xzQg2H7a!T4(1VsZ_A 70UlѬa*[xF1>7#.g`.7 s:fĊ8\Ɍ-8.v "<;bepxYgr`10uesE̓d09 Y#f1ԥS +xP79cPQcZQX98CDwTM9:`] ގL: >nxAC\Lx,]σ(TfqAڸDw O.։>%0s$3 RezL4 Y,[l Ht9{2X,foaq L>G A@-33 z0a"͠"&p,[,&0:||EPbLjǠ(bY})g2γѲ1T,` Mz$f u<5O' l;e7PBܥuV|ϗXz@gkq }8EAyjOƱ|=nTa`Yn8&UnQQ>Ws039TP80 9}9P xq8r3U,ɄPl28AbL*XTzCUX@_  *q_9L<]*S.Q`s,|~҃j;5FW^b=|DUx5G 54-}1Z,tc5ʠ"AOl5gSy\8Pu\TÈ6~XyyasG5 V;?TzhK l|Sa.{T`0cjaYݨCQ0"0ۍl3R?΀}F{۠ϳt-௨1#lBBg N03ts<67#djaCzy@' uС8]pGBqn07pv3;džJ8Eg=p^7y.3*}-K8V\h!pܺzMDSvTd},=q0 D92xY7& 6- <]a|Gf|`n͛Lͪ|V L=Ys-sz]&x aT<^\?q̇@ x(0͛LzFr6I49پ95h.urphq\/JOW^c/m`BOekIYEF#2lhqLQAmӡ\>`,fv9ȍ 4gUhcP 9D >:gGl1 ⴢ(=: Fc+e4Q|`8{[/l}2jNnNVc".v^>nR 11.a^q\gSmXQZDg@eLHm MWXM "i1Ւχy_UR`28z W_E1;TA>XݡSoUEFAe۱v:AGll=t4<C'GOP[ J-0=L(f2FĆއ `dt٬I7˓frJ'pnn`76Hm1c8n% 8P3ky*ܾcusP`rLSC8j3fy[f9hjj}d6plL'8СCqxlQ&f[f@*Sf8; ʤ lvUMf]?sGqp#n&qԴ?}5Gf]Ѿ>p3 Z-xy /+lqv=h&4BGA@ =uK^߂Ͳ- =A~ t]^E͆p%r!VJ z &Tu\.g"WmНGG`]'1g -|XZ"ńg P;i bB>ّ3; {a?[9.*lIGN7OKEYal_5I~MH׏w@Ve l0UBC'bWӦlJ_MP 6~Κes6[~'G3߁9۸r3Ht;nwYwknmB>Z~!nu`mr>hs;:.$o2k03]x%v'b72*˹2ho#Λ D+㑁1i3f> \~Z\vSf~ Wԭ`?ZDNxo"0-9FIIw\ Pn.˥S'C~]RnHkՉI_'7r$Y|_SyոŲz<1~b4X!ee=F>1zrY,%*ˊ-'VjEZMlܮhZҵJJ,1WF.]VMCyso3cAFO>'?1OrQ߮q:N j cwA לqȃP8\\j/%ֱ.D;ϐуZCFq NTK.]q#w-!Ś*(IbI'+@uMj=]q 9mh=.d*f2,0) DTX%5.hRӥ%~i!a .DuuXN~ "qcmVK7E`(֊ǭJj/@.j-RL8ZuQ$.VF]K=P,=P7tV,Ҋ" GZHOFԸй+B"%vk%I*5Y;?y Pr;'VY;%-Wg4bVKQ֯s%󵵚J2ofv1\ah Ťw*̋yr^+fjÜg0'Ie5k"/dvՊ ZR*e2 yl̤%.k 0$3JRae2⮲I&Y⧺q!1YfR+c"LtI2VzQǡq3\k4I4hs!=UjՙL3.Vf%VVQI\1HU05cZk> ]lyqzgMNt}DPi8!0Kĝhp8ϿYGD_GT?]Y G!ǹY܏0;molg΢?(: 7oˢԎ)Y! BGߒW泟?)ԩGO;_C?G-eSH·ef@THFm3T[gǷb(! ։M H$G|+^ozpy ̿O|Q0y4pLo$^é3懓:&d$ttfg$' oiڒ53QQ15eKچgR;RnN&fSGnHHr&kHpa!@%k*#un:s_ 欇+-IjDrgLu$%v$6uʶ |32œۣA :l òP:4\P*4vv&tM u1=b+L1:2Eؒ +Mt=`r8L4cVŽabdD¾Uz4Ub$wttt&uDRRB' ixpm~YzOҽ}PԬ[BaprTSI[JK$ SJֱib8GTױgɅRe u&p[6H?N Cᦙm#=uu=.p/'Rv$luNd4tȒdpk+M. hz"y_gViDpB}#uY=۶fxExOf|l;痔`INMN.LIN%ؒl 3䋤&R Rd3I} DgTr V S3I'2s: ɝOOoy7l|w¹te4PBg("X鬩Q7:9 љpuVjKHnbfWy)ħ3~(moa=Px?>Gs :o͠|-I[K$ОTGTOiFgOFhr32Rm[öuf: 3l3uufF㖤Osti-agb0™i[G]VB'A=3Q.#)c"ilL*NK@TSSu ÎatS0Q8aX] n@j39FӶxO~۟Sy)57''LXHp,)i[l[JC2`^@H;*Jsb+ ƣDO=zwޟ==S~YOjOO6t: 73(#5RTԬ_eLJ':lT eӶn_<4pHHVRiVaV0ºJN΂%T30SY3#=YLLmYS京*6q8t805N4#ZO♬:ppSVag"oic8'#+ bHFViRVj)(ַyEn""J'o *6uKXo*(Z>-ir [b;ͤV疇Eku*2SW>Ԋgafֻk.d,hjG2-K(iDo6H觛ymEg9o \4-q_GF^{o.yoe}^]=Ŧޞ CyYHFJ+^nE|kFoA?^CCo5hE]e;Q>^CQoYCލ[ksb2OOxԛr4Gexo?fx4.Kxƥ7= r\ p7X"B^ä"HϘVMZۥru- g㱈,]EeR `Z4V&Wc\DSE.8b g`x4( Lv5HsH\'˪]bm#8Z0ÿ 0?\ܽeIvDIF;).brԊч$ E$bt)rEDaM{E"Lq1VH "adԊ,Z"%Z eLDZd|P֥1DLhܣa&63ӫ1jQ~q#S^kW22-! 8AW_ `P'_|e4 ŵ90p Q"rCX8:P%8{>ZD_["mרkX>07`47dM=%Z Qّ\iBl:)Za8IfT?N$Hs|̧gglUW_]?6`N#Q"\ v' H:x.:T8y Q& 4򂴫ւĢh3qRF־fFyA N\0%恡auMh~nDlF8eP\rg88BTqiDܰET:U~))#5OzQ ?B?SqIENDB`roundup-1.4.20/doc/design.txt0000644000175000017630000017225611741527216014566 0ustar ralfpriv======================================================== Roundup - An Issue-Tracking System for Knowledge Workers ======================================================== :Authors: Ka-Ping Yee (original), Richard Jones (implementation) .. contents:: Introduction --------------- This document presents a description of the components of the Roundup system and specifies their interfaces and behaviour in sufficient detail to guide an implementation. For the philosophy and rationale behind the Roundup design, see the first-round Software Carpentry `submission for Roundup`__. This document fleshes out that design as well as specifying interfaces so that the components can be developed separately. __ spec.html The Layer Cake ----------------- Lots of software design documents come with a picture of a cake. Everybody seems to like them. I also like cakes (I think they are tasty). So I, too, shall include a picture of a cake here:: ________________________________________________________________ | E-mail Client | Web Browser | Detector Scripts | Shell | |---------------+---------------+--------------------+-----------| | E-mail User | Web User | Detector | Command | |----------------------------------------------------------------| | Roundup Database Layer | |----------------------------------------------------------------| | Hyperdatabase Layer | |----------------------------------------------------------------| | Storage Layer | ---------------------------------------------------------------- The colourful parts of the cake are part of our system; the faint grey parts of the cake are external components. I will now proceed to forgo all table manners and eat from the bottom of the cake to the top. You may want to stand back a bit so you don't get covered in crumbs. Hyperdatabase ------------- The lowest-level component to be implemented is the hyperdatabase. The hyperdatabase is a flexible data store that can hold configurable data in records which we call items. The hyperdatabase is implemented on top of the storage layer, an external module for storing its data. The "batteries-includes" distribution implements the hyperdatabase on the standard anydbm module. The storage layer could be a third-party RDBMS; for a low-maintenance solution, implementing the hyperdatabase on the SQLite RDBMS is suggested. Dates and Date Arithmetic ~~~~~~~~~~~~~~~~~~~~~~~~~ Before we get into the hyperdatabase itself, we need a way of handling dates. The hyperdatabase module provides Timestamp objects for representing date-and-time stamps and Interval objects for representing date-and-time intervals. As strings, date-and-time stamps are specified with the date in ISO8601 international standard format (``yyyy-mm-dd``) joined to the time (``hh:mm:ss``) by a period "``.``". Dates in this form can be easily compared and are fairly readable when printed. An example of a valid stamp is "``2000-06-24.13:03:59``". We'll call this the "full date format". When Timestamp objects are printed as strings, they appear in the full date format with the time always given in GMT. The full date format is always exactly 19 characters long. For user input, some partial forms are also permitted: the whole time or just the seconds may be omitted; and the whole date may be omitted or just the year may be omitted. If the time is given, the time is interpreted in the user's local time zone. The Date constructor takes care of these conversions. In the following examples, suppose that ``yyyy`` is the current year, ``mm`` is the current month, and ``dd`` is the current day of the month; and suppose that the user is on Eastern Standard Time. - "2000-04-17" means - "01-25" means - "2000-04-17.03:45" means - "08-13.22:13" means - "11-07.09:32:43" means - "14:25" means - - "8:47:11" means - - the special date "." means "right now" Date intervals are specified using the suffixes "y", "m", and "d". The suffix "w" (for "week") means 7 days. Time intervals are specified in hh:mm:ss format (the seconds may be omitted, but the hours and minutes may not). - "3y" means three years - "2y 1m" means two years and one month - "1m 25d" means one month and 25 days - "2w 3d" means two weeks and three days - "1d 2:50" means one day, two hours, and 50 minutes - "14:00" means 14 hours - "0:04:33" means four minutes and 33 seconds The Date class should understand simple date expressions of the form *stamp* ``+`` *interval* and *stamp* ``-`` *interval*. When adding or subtracting intervals involving months or years, the components are handled separately. For example, when evaluating "``2000-06-25 + 1m 10d``", we first add one month to get 2000-07-25, then add 10 days to get 2000-08-04 (rather than trying to decide whether 1m 10d means 38 or 40 or 41 days). Here is an outline of the Date and Interval classes:: class Date: def __init__(self, spec, offset): """Construct a date given a specification and a time zone offset. 'spec' is a full date or a partial form, with an optional added or subtracted interval. 'offset' is the local time zone offset from GMT in hours. """ def __add__(self, interval): """Add an interval to this date to produce another date.""" def __sub__(self, interval): """Subtract an interval from this date to produce another date. """ def __cmp__(self, other): """Compare this date to another date.""" def __str__(self): """Return this date as a string in the yyyy-mm-dd.hh:mm:ss format. """ def local(self, offset): """Return this date as yyyy-mm-dd.hh:mm:ss in a local time zone. """ class Interval: def __init__(self, spec): """Construct an interval given a specification.""" def __cmp__(self, other): """Compare this interval to another interval.""" def __str__(self): """Return this interval as a string.""" Here are some examples of how these classes would behave in practice. For the following examples, assume that we are on Eastern Standard Time and the current local time is 19:34:02 on 25 June 2000:: >>> Date(".") >>> _.local(-5) "2000-06-25.19:34:02" >>> Date(". + 2d") >>> Date("1997-04-17", -5) >>> Date("01-25", -5) >>> Date("08-13.22:13", -5) >>> Date("14:25", -5) >>> Interval(" 3w 1 d 2:00") >>> Date(". + 2d") - Interval("3w") Items and Classes ~~~~~~~~~~~~~~~~~ Items contain data in properties. To Python, these properties are presented as the key-value pairs of a dictionary. Each item belongs to a class which defines the names and types of its properties. The database permits the creation and modification of classes as well as items. Identifiers and Designators ~~~~~~~~~~~~~~~~~~~~~~~~~~~ Each item has a numeric identifier which is unique among items in its class. The items are numbered sequentially within each class in order of creation, starting from 1. The designator for an item is a way to identify an item in the database, and consists of the name of the item's class concatenated with the item's numeric identifier. For example, if "spam" and "eggs" are classes, the first item created in class "spam" has id 1 and designator "spam1". The first item created in class "eggs" also has id 1 but has the distinct designator "eggs1". Item designators are conventionally enclosed in square brackets when mentioned in plain text. This permits a casual mention of, say, "[patch37]" in an e-mail message to be turned into an active hyperlink. Property Names and Types ~~~~~~~~~~~~~~~~~~~~~~~~ Property names must begin with a letter. A property may be one of five basic types: - String properties are for storing arbitrary-length strings. - Boolean properties are for storing true/false, or yes/no values. - Number properties are for storing numeric values. - Date properties store date-and-time stamps. Their values are Timestamp objects. - A Link property refers to a single other item selected from a specified class. The class is part of the property; the value is an integer, the id of the chosen item. - A Multilink property refers to possibly many items in a specified class. The value is a list of integers. *None* is also a permitted value for any of these property types. An attempt to store None into a Multilink property stores an empty list. A property that is not specified will return as None from a *get* operation. Hyperdb Interface Specification ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ TODO: replace the Interface Specifications with links to the pydoc The hyperdb module provides property objects to designate the different kinds of properties. These objects are used when specifying what properties belong in classes:: class String: def __init__(self, indexme='no'): """An object designating a String property.""" class Boolean: def __init__(self): """An object designating a Boolean property.""" class Number: def __init__(self): """An object designating a Number property.""" class Date: def __init__(self): """An object designating a Date property.""" class Link: def __init__(self, classname, do_journal='yes'): """An object designating a Link property that links to items in a specified class. If the do_journal argument is not 'yes' then changes to the property are not journalled in the linked item. """ class Multilink: def __init__(self, classname, do_journal='yes'): """An object designating a Multilink property that links to items in a specified class. If the do_journal argument is not 'yes' then changes to the property are not journalled in the linked item(s). """ Here is the interface provided by the hyperdatabase:: class Database: """A database for storing records containing flexible data types. """ def __init__(self, config, journaltag=None): """Open a hyperdatabase given a specifier to some storage. The 'storagelocator' is obtained from config.DATABASE. The meaning of 'storagelocator' depends on the particular implementation of the hyperdatabase. It could be a file name, a directory path, a socket descriptor for a connection to a database over the network, etc. The 'journaltag' is a token that will be attached to the journal entries for any edits done on the database. If 'journaltag' is None, the database is opened in read-only mode: the Class.create(), Class.set(), Class.retire(), and Class.restore() methods are disabled. """ def __getattr__(self, classname): """A convenient way of calling self.getclass(classname).""" def getclasses(self): """Return a list of the names of all existing classes.""" def getclass(self, classname): """Get the Class object representing a particular class. If 'classname' is not a valid class name, a KeyError is raised. """ class Class: """The handle to a particular class of items in a hyperdatabase. """ def __init__(self, db, classname, **properties): """Create a new class with a given name and property specification. 'classname' must not collide with the name of an existing class, or a ValueError is raised. The keyword arguments in 'properties' must map names to property objects, or a TypeError is raised. A proxied reference to the database is available as the 'db' attribute on instances. For example, in 'IssueClass.send_message', the following is used to lookup users, messages and files:: users = self.db.user messages = self.db.msg files = self.db.file """ # Editing items: def create(self, **propvalues): """Create a new item of this class and return its id. The keyword arguments in 'propvalues' map property names to values. The values of arguments must be acceptable for the types of their corresponding properties or a TypeError is raised. If this class has a key property, it must be present and its value must not collide with other key strings or a ValueError is raised. Any other properties on this class that are missing from the 'propvalues' dictionary are set to None. If an id in a link or multilink property does not refer to a valid item, an IndexError is raised. """ def get(self, itemid, propname): """Get the value of a property on an existing item of this class. 'itemid' must be the id of an existing item of this class or an IndexError is raised. 'propname' must be the name of a property of this class or a KeyError is raised. """ def set(self, itemid, **propvalues): """Modify a property on an existing item of this class. 'itemid' must be the id of an existing item of this class or an IndexError is raised. Each key in 'propvalues' must be the name of a property of this class or a KeyError is raised. All values in 'propvalues' must be acceptable types for their corresponding properties or a TypeError is raised. If the value of the key property is set, it must not collide with other key strings or a ValueError is raised. If the value of a Link or Multilink property contains an invalid item id, a ValueError is raised. """ def retire(self, itemid): """Retire an item. The properties on the item remain available from the get() method, and the item's id is never reused. Retired items are not returned by the find(), list(), or lookup() methods, and other items may reuse the values of their key properties. """ def restore(self, nodeid): '''Restore a retired node. Make node available for all operations like it was before retirement. ''' def history(self, itemid): """Retrieve the journal of edits on a particular item. 'itemid' must be the id of an existing item of this class or an IndexError is raised. The returned list contains tuples of the form (date, tag, action, params) 'date' is a Timestamp object specifying the time of the change and 'tag' is the journaltag specified when the database was opened. 'action' may be: 'create' or 'set' -- 'params' is a dictionary of property values 'link' or 'unlink' -- 'params' is (classname, itemid, propname) 'retire' -- 'params' is None """ # Locating items: def setkey(self, propname): """Select a String property of this class to be the key property. 'propname' must be the name of a String property of this class or None, or a TypeError is raised. The values of the key property on all existing items must be unique or a ValueError is raised. """ def getkey(self): """Return the name of the key property for this class or None. """ def lookup(self, keyvalue): """Locate a particular item by its key property and return its id. If this class has no key property, a TypeError is raised. If the 'keyvalue' matches one of the values for the key property among the items in this class, the matching item's id is returned; otherwise a KeyError is raised. """ def find(self, **propspec): """Get the ids of items in this class which link to the given items. 'propspec' consists of keyword args propname=itemid or propname={:1, :1, ...} 'propname' must be the name of a property in this class, or a KeyError is raised. That property must be a Link or Multilink property, or a TypeError is raised. Any item in this class whose 'propname' property links to any of the itemids will be returned. Examples:: db.issue.find(messages='1') db.issue.find(messages={'1':1,'3':1}, files={'7':1}) """ def filter(self, search_matches, filterspec, sort, group): """Return a list of the ids of the active nodes in this class that match the 'filter' spec, sorted by the group spec and then the sort spec. "search_matches" is a container type "filterspec" is {propname: value(s)} "sort" and "group" are [(dir, prop), ...] where dir is '+', '-' or None and prop is a prop name or None. Note that for backward-compatibility reasons a single (dir, prop) tuple is also allowed. The filter must match all properties specificed. If the property value to match is a list: 1. String properties must match all elements in the list, and 2. Other properties must match any of the elements in the list. The propname in filterspec and prop in a sort/group spec may be transitive, i.e., it may contain properties of the form link.link.link.name, e.g. you can search for all issues where a message was added by a certain user in the last week with a filterspec of {'messages.author' : '42', 'messages.creation' : '.-1w;'} """ def list(self): """Return a list of the ids of the active items in this class. """ def count(self): """Get the number of items in this class. If the returned integer is 'numitems', the ids of all the items in this class run from 1 to numitems, and numitems+1 will be the id of the next item to be created in this class. """ # Manipulating properties: def getprops(self): """Return a dictionary mapping property names to property objects. """ def addprop(self, **properties): """Add properties to this class. The keyword arguments in 'properties' must map names to property objects, or a TypeError is raised. None of the keys in 'properties' may collide with the names of existing properties, or a ValueError is raised before any properties have been added. """ def getitem(self, itemid, cache=1): """ Return a Item convenience wrapper for the item. 'itemid' must be the id of an existing item of this class or an IndexError is raised. 'cache' indicates whether the transaction cache should be queried for the item. If the item has been modified and you need to determine what its values prior to modification are, you need to set cache=0. """ class Item: """ A convenience wrapper for the given item. It provides a mapping interface to a single item's properties """ Hyperdatabase Implementations ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Hyperdatabase implementations exist to create the interface described in the `hyperdb interface specification`_ over an existing storage mechanism. Examples are relational databases, \*dbm key-value databases, and so on. Several implementations are provided - they belong in the ``roundup.backends`` package. Application Example ~~~~~~~~~~~~~~~~~~~ Here is an example of how the hyperdatabase module would work in practice:: >>> import hyperdb >>> db = hyperdb.Database("foo.db", "ping") >>> db >>> hyperdb.Class(db, "status", name=hyperdb.String()) >>> _.setkey("name") >>> db.status.create(name="unread") 1 >>> db.status.create(name="in-progress") 2 >>> db.status.create(name="testing") 3 >>> db.status.create(name="resolved") 4 >>> db.status.count() 4 >>> db.status.list() [1, 2, 3, 4] >>> db.status.lookup("in-progress") 2 >>> db.status.retire(3) >>> db.status.list() [1, 2, 4] >>> hyperdb.Class(db, "issue", title=hyperdb.String(), status=hyperdb.Link("status")) >>> db.issue.create(title="spam", status=1) 1 >>> db.issue.create(title="eggs", status=2) 2 >>> db.issue.create(title="ham", status=4) 3 >>> db.issue.create(title="arguments", status=2) 4 >>> db.issue.create(title="abuse", status=1) 5 >>> hyperdb.Class(db, "user", username=hyperdb.String(), ... password=hyperdb.String()) >>> db.issue.addprop(fixer=hyperdb.Link("user")) >>> db.issue.getprops() {"title": , "status": , "user": } >>> db.issue.set(5, status=2) >>> db.issue.get(5, "status") 2 >>> db.status.get(2, "name") "in-progress" >>> db.issue.get(5, "title") "abuse" >>> db.issue.find("status", db.status.lookup("in-progress")) [2, 4, 5] >>> db.issue.history(5) [(, "ping", "create", {"title": "abuse", "status": 1}), (, "ping", "set", {"status": 2})] >>> db.status.history(1) [(, "ping", "link", ("issue", 5, "status")), (, "ping", "unlink", ("issue", 5, "status"))] >>> db.status.history(2) [(, "ping", "link", ("issue", 5, "status"))] For the purposes of journalling, when a Multilink property is set to a new list of items, the hyperdatabase compares the old list to the new list. The journal records "unlink" events for all the items that appear in the old list but not the new list, and "link" events for all the items that appear in the new list but not in the old list. Roundup Database ---------------- The Roundup database layer is implemented on top of the hyperdatabase and mediates calls to the database. Some of the classes in the Roundup database are considered issue classes. The Roundup database layer adds detectors and user items, and on issues it provides mail spools, nosy lists, and superseders. Reserved Classes ~~~~~~~~~~~~~~~~ Internal to this layer we reserve three special classes of items that are not issues. Users """"" Users are stored in the hyperdatabase as items of class "user". The "user" class has the definition:: hyperdb.Class(db, "user", username=hyperdb.String(), password=hyperdb.String(), address=hyperdb.String()) db.user.setkey("username") Messages """""""" E-mail messages are represented by hyperdatabase items of class "msg". The actual text content of the messages is stored in separate files. (There's no advantage to be gained by stuffing them into the hyperdatabase, and if messages are stored in ordinary text files, they can be grepped from the command line.) The text of a message is saved in a file named after the message item designator (e.g. "msg23") for the sake of the command interface (see below). Attachments are stored separately and associated with "file" items. The "msg" class has the definition:: hyperdb.Class(db, "msg", author=hyperdb.Link("user"), recipients=hyperdb.Multilink("user"), date=hyperdb.Date(), summary=hyperdb.String(), files=hyperdb.Multilink("file")) The "author" property indicates the author of the message (a "user" item must exist in the hyperdatabase for any messages that are stored in the system). The "summary" property contains a summary of the message for display in a message index. Files """"" Submitted files are represented by hyperdatabase items of class "file". Like e-mail messages, the file content is stored in files outside the database, named after the file item designator (e.g. "file17"). The "file" class has the definition:: hyperdb.Class(db, "file", user=hyperdb.Link("user"), name=hyperdb.String(), type=hyperdb.String()) The "user" property indicates the user who submitted the file, the "name" property holds the original name of the file, and the "type" property holds the MIME type of the file as received. Issue Classes ~~~~~~~~~~~~~ All issues have the following standard properties: =========== ========================== Property Definition =========== ========================== title hyperdb.String() messages hyperdb.Multilink("msg") files hyperdb.Multilink("file") nosy hyperdb.Multilink("user") superseder hyperdb.Multilink("issue") =========== ========================== Also, two Date properties named "creation" and "activity" are fabricated by the Roundup database layer. Two user Link properties, "creator" and "actor" are also fabricated. By "fabricated" we mean that no such properties are actually stored in the hyperdatabase, but when properties on issues are requested, the "creation"/"creator" and "activity"/"actor" properties are made available. The value of the "creation"/"creator" properties relate to issue creation, and the value of the "activity"/ "actor" properties relate to the last editing of any property on the issue (equivalently, these are the dates on the first and last records in the issue's journal). Roundupdb Interface Specification ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The interface to a Roundup database delegates most method calls to the hyperdatabase, except for the following changes and additional methods:: class Database: def getuid(self): """Return the id of the "user" item associated with the user that owns this connection to the hyperdatabase.""" class Class: # Overridden methods: def create(self, **propvalues): def set(self, **propvalues): def retire(self, itemid): """These operations trigger detectors and can be vetoed. Attempts to modify the "creation", "creator", "activity" properties or "actor" cause a KeyError. """ class IssueClass(Class): # Overridden methods: def __init__(self, db, classname, **properties): """The newly-created class automatically includes the "messages", "files", "nosy", and "superseder" properties. If the 'properties' dictionary attempts to specify any of these properties or a "creation", "creator", "activity" or "actor" property, a ValueError is raised.""" def get(self, itemid, propname): def getprops(self): """In addition to the actual properties on the item, these methods provide the "creation", "creator", "activity" and "actor" properties.""" # New methods: def addmessage(self, itemid, summary, text): """Add a message to an issue's mail spool. A new "msg" item is constructed using the current date, the user that owns the database connection as the author, and the specified summary text. The "files" and "recipients" fields are left empty. The given text is saved as the body of the message and the item is appended to the "messages" field of the specified issue. """ def nosymessage(self, itemid, msgid): """Send a message to the members of an issue's nosy list. The message is sent only to users on the nosy list who are not already on the "recipients" list for the message. These users are then added to the message's "recipients" list. """ Default Schema ~~~~~~~~~~~~~~ The default schema included with Roundup turns it into a typical software bug tracker. The database is set up like this:: pri = Class(db, "priority", name=hyperdb.String(), order=hyperdb.String()) pri.setkey("name") pri.create(name="critical", order="1") pri.create(name="urgent", order="2") pri.create(name="bug", order="3") pri.create(name="feature", order="4") pri.create(name="wish", order="5") stat = Class(db, "status", name=hyperdb.String(), order=hyperdb.String()) stat.setkey("name") stat.create(name="unread", order="1") stat.create(name="deferred", order="2") stat.create(name="chatting", order="3") stat.create(name="need-eg", order="4") stat.create(name="in-progress", order="5") stat.create(name="testing", order="6") stat.create(name="done-cbb", order="7") stat.create(name="resolved", order="8") Class(db, "keyword", name=hyperdb.String()) Class(db, "issue", fixer=hyperdb.Multilink("user"), keyword=hyperdb.Multilink("keyword"), priority=hyperdb.Link("priority"), status=hyperdb.Link("status")) (The "order" property hasn't been explained yet. It gets used by the Web user interface for sorting.) The above isn't as pretty-looking as the schema specification in the first-stage submission, but it could be made just as easy with the addition of a convenience function like Choice for setting up the "priority" and "status" classes:: def Choice(name, *options): cl = Class(db, name, name=hyperdb.String(), order=hyperdb.String()) for i in range(len(options)): cl.create(name=option[i], order=i) return hyperdb.Link(name) Detector Interface ------------------ Detectors are Python functions that are triggered on certain kinds of events. The definitions of the functions live in Python modules placed in a directory set aside for this purpose. Importing the Roundup database module also imports all the modules in this directory, and the ``init()`` function of each module is called when a database is opened to provide it a chance to register its detectors. There are two kinds of detectors: 1. an auditor is triggered just before modifying an item 2. a reactor is triggered just after an item has been modified When the Roundup database is about to perform a ``create()``, ``set()``, ``retire()``, or ``restore`` operation, it first calls any *auditors* that have been registered for that operation on that class. Any auditor may raise a *Reject* exception to abort the operation. If none of the auditors raises an exception, the database proceeds to carry out the operation. After it's done, it then calls all of the *reactors* that have been registered for the operation. Detector Interface Specification ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The ``audit()`` and ``react()`` methods register detectors on a given class of items:: class Class: def audit(self, event, detector, priority=100): """Register an auditor on this class. 'event' should be one of "create", "set", "retire", or "restore". 'detector' should be a function accepting four arguments. Detectors are called in priority order, execution order is undefined for detectors with the same priority. """ def react(self, event, detector, priority=100): """Register a reactor on this class. 'event' should be one of "create", "set", "retire", or "restore". 'detector' should be a function accepting four arguments. Detectors are called in priority order, execution order is undefined for detectors with the same priority. """ Auditors are called with the arguments:: audit(db, cl, itemid, newdata) where ``db`` is the database, ``cl`` is an instance of Class or IssueClass within the database, and ``newdata`` is a dictionary mapping property names to values. For a ``create()`` operation, the ``itemid`` argument is None and newdata contains all of the initial property values with which the item is about to be created. For a ``set()`` operation, newdata contains only the names and values of properties that are about to be changed. For a ``retire()`` or ``restore()`` operation, newdata is None. Reactors are called with the arguments:: react(db, cl, itemid, olddata) where ``db`` is the database, ``cl`` is an instance of Class or IssueClass within the database, and ``olddata`` is a dictionary mapping property names to values. For a ``create()`` operation, the ``itemid`` argument is the id of the newly-created item and ``olddata`` is None. For a ``set()`` operation, ``olddata`` contains the names and previous values of properties that were changed. For a ``retire()`` or ``restore()`` operation, ``itemid`` is the id of the retired or restored item and ``olddata`` is None. Detector Example ~~~~~~~~~~~~~~~~ Here is an example of detectors written for a hypothetical project-management application, where users can signal approval of a project by adding themselves to an "approvals" list, and a project proceeds when it has three approvals:: # Permit users only to add themselves to the "approvals" list. def check_approvals(db, cl, id, newdata): if newdata.has_key("approvals"): if cl.get(id, "status") == db.status.lookup("approved"): raise Reject, "You can't modify the approvals list " \ "for a project that has already been approved." old = cl.get(id, "approvals") new = newdata["approvals"] for uid in old: if uid not in new and uid != db.getuid(): raise Reject, "You can't remove other users from " \ "the approvals list; you can only remove " \ "yourself." for uid in new: if uid not in old and uid != db.getuid(): raise Reject, "You can't add other users to the " \ "approvals list; you can only add yourself." # When three people have approved a project, change its status from # "pending" to "approved". def approve_project(db, cl, id, olddata): if (olddata.has_key("approvals") and len(cl.get(id, "approvals")) == 3): if cl.get(id, "status") == db.status.lookup("pending"): cl.set(id, status=db.status.lookup("approved")) def init(db): db.project.audit("set", check_approval) db.project.react("set", approve_project) Here is another example of a detector that can allow or prevent the creation of new items. In this scenario, patches for a software project are submitted by sending in e-mail with an attached file, and we want to ensure that there are text/plain attachments on the message. The maintainer of the package can then apply the patch by setting its status to "applied":: # Only accept attempts to create new patches that come with patch # files. def check_new_patch(db, cl, id, newdata): if not newdata["files"]: raise Reject, "You can't submit a new patch without " \ "attaching a patch file." for fileid in newdata["files"]: if db.file.get(fileid, "type") != "text/plain": raise Reject, "Submitted patch files must be " \ "text/plain." # When the status is changed from "approved" to "applied", apply the # patch. def apply_patch(db, cl, id, olddata): if (cl.get(id, "status") == db.status.lookup("applied") and olddata["status"] == db.status.lookup("approved")): # ...apply the patch... def init(db): db.patch.audit("create", check_new_patch) db.patch.react("set", apply_patch) Command Interface ----------------- The command interface is a very simple and minimal interface, intended only for quick searches and checks from the shell prompt. (Anything more interesting can simply be written in Python using the Roundup database module.) Command Interface Specification ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ A single command, ``roundup-admin``, provides basic access to the hyperdatabase from the command line:: roundup-admin help roundup-admin get [-list] designator[, designator,...] propname roundup-admin set designator[, designator,...] propname=value ... roundup-admin find [-list] classname propname=value ... See ``roundup-admin help commands`` for a complete list of commands. Property values are represented as strings in command arguments and in the printed results: - Strings are, well, strings. - Numbers are displayed the same as strings. - Booleans are displayed as 'Yes' or 'No'. - Date values are printed in the full date format in the local time zone, and accepted in the full format or any of the partial formats explained above. - Link values are printed as item designators. When given as an argument, item designators and key strings are both accepted. - Multilink values are printed as lists of item designators joined by commas. When given as an argument, item designators and key strings are both accepted; an empty string, a single item, or a list of items joined by commas is accepted. When multiple items are specified to the roundup-admin get or roundup-admin set commands, the specified properties are retrieved or set on all the listed items. When multiple results are returned by the roundup-admin get or roundup-admin find commands, they are printed one per line (default) or joined by commas (with the -list) option. Usage Example ~~~~~~~~~~~~~ To find all messages regarding in-progress issues that contain the word "spam", for example, you could execute the following command from the directory where the database dumps its files:: shell% for issue in `roundup-admin find issue status=in-progress`; do > grep -l spam `roundup-admin get $issue messages` > done msg23 msg49 msg50 msg61 shell% Or, using the -list option, this can be written as a single command:: shell% grep -l spam `roundup-admin get \ \`roundup-admin find -list issue status=in-progress\` messages` msg23 msg49 msg50 msg61 shell% E-mail User Interface --------------------- The Roundup system must be assigned an e-mail address at which to receive mail. Messages should be piped to the Roundup mail-handling script by the mail delivery system (e.g. using an alias beginning with "|" for sendmail). Message Processing ~~~~~~~~~~~~~~~~~~ Incoming messages are examined for multiple parts. In a multipart/mixed message or part, each subpart is extracted and examined. In a multipart/alternative message or part, we look for a text/plain subpart and ignore the other parts. The text/plain subparts are assembled to form the textual body of the message, to be stored in the file associated with a "msg" class item. Any parts of other types are each stored in separate files and given "file" class items that are linked to the "msg" item. The "summary" property on message items is taken from the first non-quoting section in the message body. The message body is divided into sections by blank lines. Sections where the second and all subsequent lines begin with a ">" or "|" character are considered "quoting sections". The first line of the first non-quoting section becomes the summary of the message. All of the addresses in the To: and Cc: headers of the incoming message are looked up among the user items, and the corresponding users are placed in the "recipients" property on the new "msg" item. The address in the From: header similarly determines the "author" property of the new "msg" item. The default handling for addresses that don't have corresponding users is to create new users with no passwords and a username equal to the address. (The web interface does not permit logins for users with no passwords.) If we prefer to reject mail from outside sources, we can simply register an auditor on the "user" class that prevents the creation of user items with no passwords. The subject line of the incoming message is examined to determine whether the message is an attempt to create a new issue or to discuss an existing issue. A designator enclosed in square brackets is sought as the first thing on the subject line (after skipping any "Fwd:" or "Re:" prefixes). If an issue designator (class name and id number) is found there, the newly created "msg" item is added to the "messages" property for that issue, and any new "file" items are added to the "files" property for the issue. If just an issue class name is found there, we attempt to create a new issue of that class with its "messages" property initialized to contain the new "msg" item and its "files" property initialized to contain any new "file" items. Both cases may trigger detectors (in the first case we are calling the set() method to add the message to the issue's spool; in the second case we are calling the create() method to create a new item). If an auditor raises an exception, the original message is bounced back to the sender with the explanatory message given in the exception. Nosy Lists ~~~~~~~~~~ A standard detector is provided that watches for additions to the "messages" property. When a new message is added, the detector sends it to all the users on the "nosy" list for the issue that are not already on the "recipients" list of the message. Those users are then appended to the "recipients" property on the message, so multiple copies of a message are never sent to the same user. The journal recorded by the hyperdatabase on the "recipients" property then provides a log of when the message was sent to whom. Setting Properties ~~~~~~~~~~~~~~~~~~ The e-mail interface also provides a simple way to set properties on issues. At the end of the subject line, ``propname=value`` pairs can be specified in square brackets, using the same conventions as for the roundup-admin ``set`` shell command. Web User Interface ------------------ The web interface is provided by a CGI script that can be run under any web server. A simple web server can easily be built on the standard CGIHTTPServer module, and should also be included in the distribution for quick out-of-the-box deployment. The user interface is constructed from a number of template files containing mostly HTML. Among the HTML tags in templates are interspersed some nonstandard tags, which we use as placeholders to be replaced by properties and their values. Views and View Specifiers ~~~~~~~~~~~~~~~~~~~~~~~~~ There are two main kinds of views: *index* views and *issue* views. An index view displays a list of issues of a particular class, optionally sorted and filtered as requested. An issue view presents the properties of a particular issue for editing and displays the message spool for the issue. A view specifier is a string that specifies all the options needed to construct a particular view. It goes after the URL to the Roundup CGI script or the web server to form the complete URL to a view. When the result of selecting a link or submitting a form takes the user to a new view, the Web browser should be redirected to a canonical location containing a complete view specifier so that the view can be bookmarked. Displaying Properties ~~~~~~~~~~~~~~~~~~~~~ Properties appear in the user interface in three contexts: in indices, in editors, and as search filters. For each type of property, there are several display possibilities. For example, in an index view, a string property may just be printed as a plain string, but in an editor view, that property should be displayed in an editable field. The display of a property is handled by functions in the ``cgi.templating`` module. Displayer functions are triggered by ``tal:content`` or ``tal:replace`` tag attributes in templates. The value of the attribute provides an expression for calling the displayer function. For example, the occurrence of:: tal:content="context/status/plain" in a template triggers a call to:: context['status'].plain() where the context would be an item of the "issue" class. The displayer functions can accept extra arguments to further specify details about the widgets that should be generated. Some of the standard displayer functions include: ========= ============================================================== Function Description ========= ============================================================== plain display a String property directly; display a Date property in a specified time zone with an option to omit the time from the date stamp; for a Link or Multilink property, display the key strings of the linked items (or the ids if the linked class has no key property) field display a property like the plain displayer above, but in a text field to be edited menu for a Link property, display a menu of the available choices ========= ============================================================== See the `customisation`_ documentation for the complete list. Index Views ~~~~~~~~~~~ An index view contains two sections: a filter section and an index section. The filter section provides some widgets for selecting which issues appear in the index. The index section is a table of issues. Index View Specifiers """"""""""""""""""""" An index view specifier looks like this (whitespace has been added for clarity):: /issue?status=unread,in-progress,resolved& keyword=security,ui& :group=priority,-status& :sort=-activity& :filters=status,keyword& :columns=title,status,fixer The index view is determined by two parts of the specifier: the layout part and the filter part. The layout part consists of the query parameters that begin with colons, and it determines the way that the properties of selected items are displayed. The filter part consists of all the other query parameters, and it determines the criteria by which items are selected for display. The filter part is interactively manipulated with the form widgets displayed in the filter section. The layout part is interactively manipulated by clicking on the column headings in the table. The filter part selects the union of the sets of issues with values matching any specified Link properties and the intersection of the sets of issues with values matching any specified Multilink properties. The example specifies an index of "issue" items. Only issues with a "status" of either "unread" or "in-progres" or "resolved" are displayed, and only issues with "keyword" values including both "security" and "ui" are displayed. The items are grouped by priority arranged in ascending order and in descending order by status; and within groups, sorted by activity, arranged in descending order. The filter section shows filters for the "status" and "keyword" properties, and the table includes columns for the "title", "status", and "fixer" properties. Associated with each issue class is a default layout specifier. The layout specifier in the above example is the default layout to be provided with the default bug-tracker schema described above in section 4.4. Index Section """"""""""""" The template for an index section describes one row of the index table. Fragments protected by a ``tal:condition="request/show/"`` are included or omitted depending on whether the view specifier requests a column for a particular property. The table cells are filled by the ``tal:content="context/"`` directive, which displays the value of the property. Here's a simple example of an index template:: Sorting """"""" String and Date values are sorted in the natural way. Link properties are sorted according to the value of the "order" property on the linked items if it is present; or otherwise on the key string of the linked items; or finally on the item ids. Multilink properties are sorted according to how many links are present. Issue Views ~~~~~~~~~~~ An issue view contains an editor section and a spool section. At the top of an issue view, links to superseding and superseded issues are always displayed. Issue View Specifiers """"""""""""""""""""" An issue view specifier is simply the issue's designator:: /patch23 Editor Section """""""""""""" The editor section is generated from a template containing ``tal:content="context//"`` directives to insert the appropriate widgets for editing properties. Here's an example of a basic editor template::
As shown in the example, the editor template can also include a ":note" field, which is a text area for entering a note to go along with a change. When a change is submitted, the system automatically generates a message describing the changed properties. The message displays all of the property values on the issue and indicates which ones have changed. An example of such a message might be this:: title: Polly Parrot is dead priority: critical status: unread -> in-progress fixer: (none) keywords: parrot,plumage,perch,nailed,dead If a note is given in the ":note" field, the note is appended to the description. The message is then added to the issue's message spool (thus triggering the standard detector to react by sending out this message to the nosy list). Spool Section """"""""""""" The spool section lists messages in the issue's "messages" property. The index of messages displays the "date", "author", and "summary" properties on the message items, and selecting a message takes you to its content. Access Control -------------- At each point that requires an action to be performed, the security mechanisms are asked if the current user has permission. This permission is defined as a Permission. Individual assignment of Permission to user is unwieldy. The concept of a Role, which encompasses several Permissions and may be assigned to many Users, is quite well developed in many projects. Roundup will take this path, and allow the multiple assignment of Roles to Users, and multiple Permissions to Roles. These definitions are not persistent - they're defined when the application initialises. There will be three levels of Permission. The Class level permissions define logical permissions associated with all items of a particular class (or all classes). The Item level permissions define logical permissions associated with specific items by way of their user-linked properties. The Property level permissions define logical permissions associated with a specific property of an item. Access Control Interface Specification ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The security module defines:: class Permission: ''' Defines a Permission with the attributes - name - description - klass (optional) - properties (optional) - check function (optional) The klass may be unset, indicating that this permission is not locked to a particular hyperdb class. There may be multiple Permissions for the same name for different classes. If property names are set, permission is restricted to those properties only. If check function is set, permission is granted only when the function returns value interpreted as boolean true. The function is called with arguments db, userid, itemid. ''' class Role: ''' Defines a Role with the attributes - name - description - permissions ''' class Security: def __init__(self, db): ''' Initialise the permission and role stores, and add in the base roles (for admin user). ''' def getPermission(self, permission, classname=None, properties=None, check=None): ''' Find the Permission exactly matching the name, class, properties list and check function. Raise ValueError if there is no exact match. ''' def hasPermission(self, permission, userid, classname=None, property=None, itemid=None): ''' Look through all the Roles, and hence Permissions, and see if "permission" exists given the constraints of classname, property and itemid. If classname is specified (and only classname) then the search will match if there is *any* Permission for that classname, even if the Permission has additional constraints. If property is specified, the Permission matched must have either no properties listed or the property must appear in the list. If itemid is specified, the Permission matched must have either no check function defined or the check function, when invoked, must return a True value. Note that this functionality is actually implemented by the Permission.test() method. ''' def addPermission(self, **propspec): ''' Create a new Permission with the properties defined in 'propspec'. See the Permission class for the possible keyword args. ''' def addRole(self, **propspec): ''' Create a new Role with the properties defined in 'propspec' ''' def addPermissionToRole(self, rolename, permission): ''' Add the permission to the role's permission list. 'rolename' is the name of the role to add permission to. ''' Modules such as ``cgi/client.py`` and ``mailgw.py`` define their own permissions like so (this example is ``cgi/client.py``):: def initialiseSecurity(security): ''' Create some Permissions and Roles on the security object This function is directly invoked by security.Security.__init__() as a part of the Security object instantiation. ''' p = security.addPermission(name="Web Registration", description="Anonymous users may register through the web") security.addToRole('Anonymous', p) Detectors may also define roles in their init() function:: def init(db): # register an auditor that checks that a user has the "May # Resolve" Permission before allowing them to set an issue # status to "resolved" db.issue.audit('set', checkresolvedok) p = db.security.addPermission(name="May Resolve", klass="issue") security.addToRole('Manager', p) The tracker dbinit module then has in ``open()``:: # open the database - it must be modified to init the Security class # from security.py as db.security db = Database(config, name) # add some extra permissions and associate them with roles ei = db.security.addPermission(name="Edit", klass="issue", description="User is allowed to edit issues") db.security.addPermissionToRole('User', ei) ai = db.security.addPermission(name="View", klass="issue", description="User is allowed to access issues") db.security.addPermissionToRole('User', ai) In the dbinit ``init()``:: # create the two default users user.create(username="admin", password=Password(adminpw), address=config.ADMIN_EMAIL, roles='Admin') user.create(username="anonymous", roles='Anonymous') Then in the code that matters, calls to ``hasPermission`` and ``hasItemPermission`` are made to determine if the user has permission to perform some action:: if db.security.hasPermission('issue', 'Edit', userid): # all ok if db.security.hasItemPermission('issue', itemid, assignedto=userid): # all ok Code in the core will make use of these methods, as should code in auditors in custom templates. The HTML templating may access the access controls through the *user* attribute of the *request* variable. It exposes a ``hasPermission()`` method:: tal:condition="python:request.user.hasPermission('Edit', 'issue')" or, if the *context* is *issue*, then the following is the same:: tal:condition="python:request.user.hasPermission('Edit')" Authentication of Users ~~~~~~~~~~~~~~~~~~~~~~~ Users must be authenticated correctly for the above controls to work. This is not done in the current mail gateway at all. Use of digital signing of messages could alleviate this problem. The exact mechanism of registering the digital signature should be flexible, with perhaps a level of trust. Users who supply their signature through their first message into the tracker should be at a lower level of trust to those who supply their signature to an admin for submission to their user details. Anonymous Users ~~~~~~~~~~~~~~~ The "anonymous" user must always exist, and defines the access permissions for anonymous users. Unknown users accessing Roundup through the web or email interfaces will be logged in as the "anonymous" user. Use Cases ~~~~~~~~~ public - end users can submit bugs, request new features, request support The Users would be given the default "User" Role which gives "View" and "Edit" Permission to the "issue" class. developer - developers can fix bugs, implement new features, provide support A new Role "Developer" is created with the Permission "Fixer" which is checked for in custom auditors that see whether the issue is being resolved with a particular resolution ("fixed", "implemented", "supported") and allows that resolution only if the permission is available. manager - approvers/managers can approve new features and signoff bug fixes A new Role "Manager" is created with the Permission "Signoff" which is checked for in custom auditors that see whether the issue status is being changed similar to the developer example. admin - administrators can add users and set user's roles The existing Role "Admin" has the Permissions "Edit" for all classes (including "user") and "Web Roles" which allow the desired actions. system - automated request handlers running various report/escalation scripts A combination of existing and new Roles, Permissions and auditors could be used here. privacy - issues that are only visible to some users A new property is added to the issue which marks the user or group of users who are allowed to view and edit the issue. An auditor will check for edit access, and the template user object can check for view access. Deployment Scenarios -------------------- The design described above should be general enough to permit the use of Roundup for bug tracking, managing projects, managing patches, or holding discussions. By using items of multiple types, one could deploy a system that maintains requirement specifications, catalogs bugs, and manages submitted patches, where patches could be linked to the bugs and requirements they address. Acknowledgements ---------------- My thanks are due to Christy Heyl for reviewing and contributing suggestions to this paper and motivating me to get it done, and to Jesse Vincent, Mark Miller, Christopher Simons, Jeff Dunmall, Wayne Gramlich, and Dean Tribble for their assistance with the first-round submission. Changes to this document ------------------------ - Added Boolean and Number types - Added section Hyperdatabase Implementations - "Item" has been renamed to "Issue" to account for the more specific nature of the Class. - New Templating - Access Controls - Added "actor" property .. _customisation: customizing.html roundup-1.4.20/doc/acknowledgements.txt0000644000175000017630000000514311754173102016630 0ustar ralfprivAcknowledgements ================ Go Ping, you rock! Also, go Common Ground, ekit.com and Bizar Software for letting me implement this system on their time. Thanks also to the many people on the mailing list, in the sourceforge project and those who just report bugs: Christian Aastorp Thomas Arendsen Hein, Nerijus Baliunas, Benni Bärmann, Anthony Baxter, David Benjamin, Marlon van den Berg, Bo Berglund, Olly Betts, Stéphane Bidoul, Rafal Bisingier, Cameron Blackwood, Jeff Blaine, Duncan Booth, Seb Brezel, J Alan Brogan, Titus Brown, Steve Byan, Brett Cannon, Godefroid Chapelle, Eli Collins, Roch'e Compaan, Wil Cooley, Joe Cooper, Kelley Dagley, Bruno Damour, Bradley Dean, Toby Dickenson, Paul F. Dubois, Hauke Duden, Eric Earnst, Peter Eisentraut, Andrew Eland, Jeff Epler, Tom Epperly, Tamer Fahmy, Vickenty Fesunov, Hernan Martinez Foffani, Stuart D. Gathman, Martin Geisler, Ajit George, Dirk Geschke, Frank Gibbons, Johannes Gijsbers, Christian Glass, Gus Gollings, Philipp Gortan, Dan Grassi, Robin Green, Jason Grout, Charles Groves, Engelbert Gruber, Bruce Guenter, Tamás Gulácsi, Satchidanand Haridas, Sebastian Harl, Ralf Hemmecke, Juergen Hermann, Tobias Herp, Uwe Hoffmann, Alex Holkner, Tobias Hunger, Werner Hunger, Simon Hyde, Paul Jimenez, Christophe Kalt, Timo Kankare, Brian Kelley, James Kew, Sheila King, Michael Klatt, Bastian Kleineidam, Axel Kollmorgen, Cédric Krier, John Kristensen, Detlef Lannert, Andrey Lebedev, Henrik Levkowetz, David Linke, Martin v. Löwis, Fredrik Lundh, Jochen Maes, Will Maier, Ksenia Marasanova, Georges Martin, Gordon McMillan, Christof Meerwald, John F Meinel Jr, Roland Meister, Ezio Melotti, Ulrik Mikaelsson, John Mitchell, Ramiro Morales, Toni Mueller, Joseph Myers, Stefan Niederhauser, Truls E. Næss, Bryce L Nordgren, Patrick Ohly, "om", Luke Opperman, Eddie Parker, Will Partain, Timo Paulssen, Benjamin Pollack, Peter Pöml, Ewout Prangsma, Marcus Priesch, Eric S. Raymond, Bernhard Reiter, Roy Rapoport, John P. Rouillard, Luke Ross, Jesse Ruderman, Ollie Rutherfurd, Toby Sargeant, Giuseppe Scelsi, Ralf Schlatterbeck, Gregor Schmid, Florian Schulze, Klamer Schutte, Dougal Scott, Stefan Seefeld, Jouni K Seppänen, Jeffrey P Shell, Dan Shidlovsky, Joel Shprentz, Terrel Shumway, Emil Sit, Alexander Smishlajev, Nathaniel Smith, Leonardo Soto, Maciej Starzyk, Kai Storbeck, Mitchell Surface, anatoly techtonik, Jon C. Thomason Mike Thompson, Hubert Touvet, Michael Twomey, Joseph E. Trent, Karl Ulbrich, Martin Uzak, Darryl VanDorp, J Vickroy, Timothy J. Warren, Jakub Wilk, William (Wilk), Tue Wennerberg, Matt Wilbert, Chris Withers, David Wolever, Cheer Xiao, Milan Zamazal. roundup-1.4.20/doc/whatsnew-0.8.txt0000644000175000017630000001373611741527216015455 0ustar ralfpriv========================= What's New in Roundup 0.8 ========================= For those completely new to Roundup, you might want to look over the very terse features__ page. __ features.html .. contents:: In Summary ========== (this information copied directly from the ``CHANGES.txt`` file) XXX this section needs more detail - create a new RDBMS cursor after committing - roundup-admin reindex command may now work on single items or classes - roundup-server options -g and -u accept both ids and names (sf bug 983769) - roundup-server now has a configuration file (-C option) - roundup windows service may be installed with command line options recognized by roundup-server (but not tracker specification arguments). Use this to specify server configuration file for the service. - added option to turn off registration confirmation via email ("instant_registration" in config) (sf rfe 922209) Performance improvements ======================== We don't try to import all backends in backends.__init__ unless we *want* to. Roundup may now use the Apache mod_python interface (see installation.txt) which is much faster than the standard cgi-bin and a little faster than roundup-server. There is now an experimental multi-thread server which should allow faster concurrent access. In the hyperdb, a few other speedups were implemented, such as: - record journaltag lookup ("fixes" sf bug 998140) - unless in debug mode, keep a single persistent connection through a single web or mailgw request. - remove "manual" locking of sqlite database Logging of internal messages ============================ Roundup's previously ad-hoc logging of events has been cleaned up and is now configured in a single place in the tracker configuration file. The `customization documentation`_ has more details on how this is configured. roundup-mailgw now logs fatal exceptions rather than mailing them to admin. Security Changes ================ ``security.addPermissionToRole()`` has been extended to allow skipping the separate getPermission call. Password Storage ---------------- Added MD5 scheme for password hiding. This extends the existing SHA and crypt methods and is useful if you have an existing MD5 password database. Permission Definitions ---------------------- Permissions may now be defined on a per-property basis, allowing access to only specific properties on items. Permissions may also have code attached which is executed to check whether the Permission is valid for the current user and item. Permissions are now automatically checked when information is rendered through the web. This includes: 1. View checks for properties when being rendered via the ``plain()`` or similar methods. If the check fails, the text "[hidden]" will be displayed. 2. Edit checks for properties when the edit field is being rendered via the ``field()`` or similar methods. If the check fails, the property will be rendered via the ``plain()`` method (see point 1. for additional checking performed) 3. View checks are performed in index pages for each item being displayed such that if the user does not have permission, the row is not rendered. Extending Roundup ================= To write extension code for Roundup you place a file in the tracker home ``extensions`` directory. See the `customisation documentation`_ for more information about how this is done. 8-bit character set support in Web interface ============================================ This is used to override the UTF-8 default. It may be overridden in both forms and a browser cookie. - In forms, use the ``@charset`` variable. - To use the cookie override, have the ``roundup_charset`` cookie set. In both cases, the value is a valid charset name (eg. ``utf-8`` or ``kio8-r``). Inside Roundup, all strings are stored and processed in utf-8. Unfortunately, some older browsers do not work properly with utf-8-encoded pages (e.g. Netscape Navigator 4 displays wrong characters in form fields). This version allows to change the character set for http transfers. To do so, you may add the following code to your ``page.html`` template:: utf-8 koi8-r (substitute ``koi8-r`` with the appropriate charset for your language). Charset preference is kept in the browser cookie ``roundup_charset``. ``meta http-equiv`` lines added to the tracker templates in version 0.6.0 should be changed to include actual character set name:: Actual charset is also sent in the http header. Web Interface Miscellanea ========================= The web interface has seen some changes: Editing Templating We implement __nonzero__ for HTMLProperty - properties may now be used in boolean conditions (eg ``tal:condition="issue/nosy"`` will be false if the nosy list is empty). We added a default argument to the DateHTMLProperty.field method, and an optional Interval (string or object) to the DateHTMLProperty.now We've added a multiple selection Link/Multilink search field macro to the default classic page.html template. We relaxed hyperlinking in web interface (accept "issue123" or "Issue 123") The listing popup may be used in query forms. Standard templates We hide "(list)" popup links when issue is only viewable The issue search page now has fields to allow no sorting / grouping of the results. The default page.html template now has a search box in the top right corner which performs a full-text search of issues. The "show issue" quick jump form in the sidebar has had its font size reduced to use less space. Web server The builtin web server may now perform HTTP Basic Authentication by itself. .. _`customization documentation`: customizing.html roundup-1.4.20/doc/xmlrpc.txt0000644000175000017630000001063011741527216014605 0ustar ralfpriv========================= XML-RPC access to Roundup ========================= .. contents:: Introduction ------------ Version 1.4 of Roundup includes an XML-RPC frontend. Some installations find that roundup-admins requirement of local access to the tracker instance limiting. The XML-RPC frontend provides the ability to execute a limited subset of commands similar to those found in roundup-admin from remote machines. roundup-xmlrpc-server --------------------- The Roundup XML-RPC server must be started before remote clients can access the tracker via XML-RPC. ``roundup-xmlrpc-server`` is installed in the scripts directory alongside ``roundup-server`` and roundup-admin``. When invoked, the location of the tracker instance must be specified. roundup-xmlrpc-server -i ``/path/to/tracker`` The default port is ``8000``. An alternative port can be specified with the ``--port`` switch. security consideration ====================== Note that the current ``roundup-xmlrpc-server`` implementation does not support SSL. This means that usernames and passwords will be passed in cleartext unless the server is being proxied behind another server (such as Apache or lighttpd) that provide SSL. client API ---------- The server currently implements four methods. Each method requires that the user provide a username and password in the HTTP authorization header in order to authenticate the request against the tracker. ======= ==================================================================== Command Description ======= ==================================================================== list arguments: *classname, [property_name]* List all elements of a given ``classname``. If ``property_name`` is specified, that is the property that will be displayed for each element. If ``property_name`` is not specified the default label property will be used. display arguments: *designator, [property_1, ..., property_N]* Display a single item in the tracker as specified by ``designator`` (e.g. issue20 or user5). The default is to display all properties for the item. Alternatively, a list of properties to display can be specified. create arguments: *classname, arg_1 ... arg_N* Create a new instance of ``classname`` with ``arg_1`` through ``arg_N`` as the values of the new instance. The arguments are name=value pairs (e.g. ``status='3'``). set arguments: *designator, arg_1 ... arg_N* Set the values of an existing item in the tracker as specified by ``designator``. The new values are specified in ``arg_1`` through ``arg_N``. The arguments are name=value pairs (e.g. ``status='3'``). lookup arguments: *classname, key_value* looks up the key_value for the given class. The class needs to have a key and the user needs search permission on the key attribute and id for the given classname. filter arguments: *classname, list or None, attributes* list can be None (requires ``allow_none=True`` when instantiating the ServerProxy) to indicate search for all values, or a list of ids. The attributes are given as a dictionary of name value pairs to search for. ======= ==================================================================== sample python client ==================== :: >>> import xmlrpclib >>> roundup_server = xmlrpclib.ServerProxy('http://username:password@localhost:8000', allow_none=True) >>> roundup_server.list('user') ['admin', 'anonymous', 'demo'] >>> roundup_server.list('issue', 'id') ['1'] >>> roundup_server.display('issue1') {'assignedto' : None, 'files' : [], 'title' = 'yes, ..... } >>> roundup_server.display('issue1', 'priority', 'status') {'priority' : '1', 'status' : '2'} >>> roundup_server.set('issue1', 'status=3') >>> roundup_server.display('issue1', 'status') {'status' : '3' } >>> roundup_server.create('issue', "title='another bug'", "status=2") '2' >>> roundup_server.filter('user',None,{'username':'adm'}) ['1'] >>> roundup_server.filter('user',['1','2'],{'username':'adm'}) ['1'] >>> roundup_server.filter('user',['2'],{'username':'adm'}) [] >>> roundup_server.filter('user',[],{'username':'adm'}) [] >>> roundup_server.lookup('user','admin') '1' roundup-1.4.20/doc/_templates/0000755000175000017630000000000011754432235014674 5ustar ralfprivroundup-1.4.20/doc/_templates/layout.html0000644000175000017630000001512411741527216017102 0ustar ralfpriv {%- macro relbar(class) %} {%- endmacro %} {%- macro sidebar() %} {%- block sidebartoc %} {%- if display_toc %}

{{ _('Table Of Contents') }}

{{ toc }} {%- endif %} {%- endblock %} {%- block sidebarrel %} {%- if prev %}

{{ _('Previous topic') }}

{{ prev.title }}

{%- endif %} {%- if next %}

{{ _('Next topic') }}

{{ next.title }}

{%- endif %} {%- endblock %} {%- block sidebarsourcelink %} {%- if show_source and has_source and sourcename %}

{{ _('This Page') }}

{%- endif %} {%- endblock %} {%- block sidebarsearch %} {%- if pagename != "search" %} {%- endif %} {%- endblock %} {%- endmacro %} {{ metatags }} {%- if builder != 'htmlhelp' %} {%- set titlesuffix = " — " + docstitle|e %} {%- endif %} {{ title|striptags }}{{ titlesuffix }} {%- if builder == 'web' %} {%- for link, type, title in page_links %} {%- endfor %} {%- else %} {%- endif %} {%- if builder != 'htmlhelp' %} {%- for scriptfile in script_files %} {%- endfor %} {%- if use_opensearch %} {%- endif %} {%- if favicon %} {%- endif %} {%- endif %} {%- block linktags %} {%- if hasdoc('about') %} {%- endif %} {%- if hasdoc('copyright') %} {%- endif %} {%- if parents %} {%- endif %} {%- if next %} {%- endif %} {%- if prev %} {%- endif %} {%- endblock %} {%- block extrahead %} {% endblock %}

Roundup

{%- if pagename != "search" %} {%- endif %}
{{ relbar('related-top') }} {{ body }} {{ relbar('related-bottom') }}
{%- block footer %} {%- endblock %} roundup-1.4.20/doc/installation.txt0000644000175000017630000011721111741527216016004 0ustar ralfpriv================== Installing Roundup ================== .. contents:: :depth: 2 Overview ======== Broken out separately, there are several conceptual pieces to a Roundup installation: Roundup trackers Trackers consist of issues (be they bug reports or otherwise), tracker configuration file(s), web HTML files etc. Roundup trackers are initialised with a "Template" which defines the fields usable/assignable on a per-issue basis. Descriptions of the provided templates are given in `choosing your template`_. Roundup support code Installed into your Python install's lib directory. Roundup scripts These include the email gateway, the roundup HTTP server, the roundup administration command-line interface, etc. Prerequisites ============= Roundup requires Python 2.3 or newer (but not Python 3) with a functioning anydbm module. Download the latest version from http://www.python.org/. It is highly recommended that users install the latest patch version of python as these contain many fixes to serious bugs. Some variants of Linux will need an additional "python dev" package installed for Roundup installation to work. Debian and derivatives, are known to require this. If you're on windows, you will either need to be using the ActiveState python distribution (at http://www.activestate.com/Products/ActivePython/), or you'll have to install the win32all package separately (get it from http://starship.python.net/crew/mhammond/win32/). Optional Components =================== You may optionally install and use: Timezone Definitions Full timezone support requires pytz_ module (version 2005i or later) which brings the `Olson tz database`_ into Python. If pytz_ is not installed, timezones may be specified as numeric hour offsets only. An RDBMS Sqlite, MySQL and Postgresql are all supported by Roundup and will be used if available. One of these is recommended if you are anticipating a large user base (see `choosing your backend`_ below). Xapian full-text indexer The Xapian_ full-text indexer is also supported and will be used by default if it is available. This is strongly recommended if you are anticipating a large number of issues (> 5000). You may install Xapian at any time, even after a tracker has been installed and used. You will need to run the "roundup-admin reindex" command if the tracker has existing data. Roundup requires Xapian 1.0.0 or newer. pyopenssl If pyopenssl_ is installed the roundup-server can be configured to serve trackers over SSL. If you are going to serve roundup via proxy through a server with SSL support (e.g. apache) then this is unnecessary. pyme If pyme_ is installed you can configure the mail gateway to perform verification or decryption of incoming OpenPGP MIME messages. When configured, you can require email to be cryptographically signed before roundup will allow it to make modifications to issues. .. _Xapian: http://xapian.org/ .. _pytz: http://www.python.org/pypi/pytz .. _Olson tz database: http://www.twinsun.com/tz/tz-link.htm .. _pyopenssl: http://pyopenssl.sourceforge.net .. _pyme: http://pyme.sourceforge.net Getting Roundup =============== .. note:: Some systems, such as Debian and NetBSD, already have Roundup installed. Try running the command "roundup-admin" with no arguments, and if it runs you may skip the `Basic Installation Steps`_ below and go straight to `configuring your first tracker`_. Download the latest version from http://www.roundup-tracker.org/. If you're using WinZIP's "classic" interface, make sure the "Use folder names" check box is checked before you extract the files. For The Really Impatient ======================== If you just want to give Roundup a whirl Right Now, then simply run ``roundup-demo``. This will set up a simple demo tracker on your machine. [1]_ When it's done, it'll print out a URL to point your web browser at so you may start playing. Three users will be set up: 1. anonymous - the "default" user with permission to do very little 2. demo (password "demo") - a normal user who may create issues 3. admin (password "admin") - an administrative user who has complete access to the tracker .. [1] Demo tracker is set up to be accessed by localhost browser. If you run demo on a server host, please stop the demo when it has shown startup notice, open file ``demo/config.ini`` with your editor, change host name in the ``web`` option in section ``[tracker]``, save the file, then re-run the demo program. Installation ============ Set aside 15-30 minutes. There's several steps to follow in your installation: 1. `basic installation steps`_ if Roundup is not installed on your system 2. `configuring your first tracker`_ that all installers must follow 3. then optionally `configure a web interface`_ 4. and optionally `configure an email interface`_ 5. `UNIX environment steps`_ to take if you're installing on a shared UNIX machine and want to restrict local access to roundup 6. `additional language codecs`_ For information about how Roundup installs, see the `administration guide`_. Basic Installation Steps ------------------------ To install the Roundup support code into your Python tree and Roundup scripts into /usr/bin (substitute that path for whatever is appropriate on your system). You need to have write permissions for these locations, eg. being root on unix:: python setup.py install If you would like to place the Roundup scripts in a directory other than ``/usr/bin``, then specify the preferred location with ``--install-scripts``. For example, to install them in ``/opt/roundup/bin``:: python setup.py install --install-scripts=/opt/roundup/bin You can also use the ``--prefix`` option to use a completely different base directory, if you do not want to use administrator rights. If you choose to do this, you may have to change Python's search path (sys.path) yourself. Configuring your first tracker ------------------------------ 1. To create a Roundup tracker (necessary to do before you can use the software in any real fashion), you need to set up a "tracker home": a. (Optional) If you intend to keep your roundup trackers under one top level directory which does not exist yet, you should create that directory now. Example:: mkdir /opt/roundup/trackers b. Either add the Roundup script location to your ``PATH`` environment variable or specify the full path to the command in the next step. c. Install a new tracker with the command ``roundup-admin install``. You will be asked a series of questions. Descriptions of the provided templates can be found in `choosing your template`_ below. Descriptions of the available backends can be found in `choosing your backend`_ below. The questions will be something like (you may have more templates or backends available):: Enter tracker home: /opt/roundup/trackers/support Templates: classic Select template [classic]: classic Back ends: anydbm, mysql, sqlite Select backend [anydbm]: anydbm Note: "Back ends" selection list depends on availability of third-party database modules. Standard python distribution includes anydbm module only. The "support" part of the tracker name can be anything you want - it is going to be used as the directory that the tracker information will be stored in. You will now be directed to edit the tracker configuration and initial schema. At a minimum, you must set "main :: admin_email" (that's the "admin_email" option in the "main" section) "mail :: host", "tracker :: web" and "mail :: domain". If you get stuck, and get configuration file errors, then see the `tracker configuration`_ section of the `customisation documentation`_. If you just want to get set up to test things quickly (and follow the instructions in step 3 below), you can even just set the "tracker :: web" variable to:: web = http://localhost:8080/support/ The URL *must* end in a '/', or your web interface *will not work*. See `Customising Roundup`_ for details on configuration and schema changes. You may change any of the configuration after you've initialised the tracker - it's just better to have valid values for this stuff now. d. Initialise the tracker database with ``roundup-admin initialise``. You will need to supply an admin password at this step. You will be prompted:: Admin Password: Confirm: Note: running this command will *destroy any existing data in the database*. In the case of MySQL and PostgreSQL, any existing database will be dropped and re-created. Once this is done, the tracker has been created. 2. At this point, your tracker is set up, but doesn't have a nice user interface. To set that up, we need to `configure a web interface`_ and optionally `configure an email interface`_. If you want to try your new tracker out, assuming "tracker :: web" is set to ``'http://localhost:8080/support/'``, run:: roundup-server support=/opt/roundup/trackers/support then direct your web browser at: http://localhost:8080/support/ and you should see the tracker interface. Choosing Your Template ---------------------- Classic Template ~~~~~~~~~~~~~~~~ The classic template is the one defined in the `Roundup Specification`_. It holds issues which have priorities and statuses. Each issue may also have a set of messages which are disseminated to the issue's list of nosy users. Minimal Template ~~~~~~~~~~~~~~~~ The minimal template has the minimum setup required for a tracker installation. That is, it has the configuration files, defines a user database and the basic HTML interface to that. It's a completely clean slate for you to create your tracker on. Choosing Your Backend --------------------- The actual storage of Roundup tracker information is handled by backends. There's several to choose from, each with benefits and limitations: ========== =========== ===== ============================== Name Speed Users Support ========== =========== ===== ============================== anydbm Slowest Few Always available sqlite Fastest(*) Few May need install (PySQLite_) postgresql Fast Many Needs install/admin (psycopg_) mysql Fast Many Needs install/admin (MySQLdb_) ========== =========== ===== ============================== **sqlite** This uses the embedded database engine PySQLite_ to provide a very fast backend. This is not suitable for trackers which will have many simultaneous users, but requires much less installation and maintenance effort than more scalable postgresql and mysql backends. SQLite is supported via PySQLite versions 1.1.7, 2.1.0 and sqlite3 (the last being bundled with Python 2.5+) Installed SQLite should be the latest version available (3.3.8 is known to work, 3.1.3 is known to have problems). **postgresql** Backend for popular RDBMS PostgreSQL. You must read doc/postgresql.txt for additional installation steps and requirements. You must also configure the ``rdbms`` section of your tracker's ``config.ini``. It is recommended that you use at least version 1.1.21 of psycopg. **mysql** Backend for popular RDBMS MySQL. You must read doc/mysql.txt for additional installation steps and requirements. You must also configure the ``rdbms`` section of your tracker's ``config.ini`` You may defer your decision by setting your tracker up with the anydbm backend (which is guaranteed to be available) and switching to one of the other backends at any time using the instructions in the `administration guide`_. Regardless of which backend you choose, Roundup will attempt to initialise a new database for you when you run the roundup-admin "initialise" command. In the case of MySQL and PostgreSQL you will need to have the appropriate privileges to create databases. Configure a Web Interface ------------------------- There are five web interfaces to choose from: 1. `web server cgi-bin`_ 2. `cgi-bin for limited-access hosting`_ 3. `stand-alone web server`_ 4. `Zope product - ZRoundup`_ 5. `Apache HTTP Server with mod_python`_ 6. `WSGI handler`_ You may need to give the web server user permission to access the tracker home - see the `UNIX environment steps`_ for information. You may also need to configure your system in some way - see `platform-specific notes`_. Web Server cgi-bin ~~~~~~~~~~~~~~~~~~ A benefit of using the cgi-bin approach is that it's the easiest way to restrict access to your tracker to only use HTTPS. Access will be slower than through the `stand-alone web server`_ though. If your Python isn't installed as "python" then you'll need to edit the ``roundup.cgi`` script to fix the first line. If you're using IIS on a Windows platform, you'll need to run this command for the cgi to work (it turns on the PATH_INFO cgi variable):: adsutil.vbs set w3svc/AllowPathInfoForScriptMappings TRUE The ``adsutil.vbs`` file can be found in either ``c:\inetpub\adminscripts`` or ``c:\winnt\system32\inetsrv\adminsamples\`` or ``c:\winnt\system32\inetsrv\adminscripts\`` depending on your installation. More information about ISS setup may be found at: http://support.microsoft.com/default.aspx?scid=kb%3Ben-us%3B276494 Copy the ``frontends/roundup.cgi`` file to your web server's ``cgi-bin`` directory. You will need to configure it to tell it where your tracker home is. You can do this either: Through an environment variable Set the variable TRACKER_HOMES to be a colon (":") separated list of name=home pairs (if you're using apache, the SetEnv directive can do this) Directly in the ``roundup.cgi`` file itself Add your instance to the TRACKER_HOMES variable as ``'name': 'home'`` The "name" part of the configuration will appear in the URL and identifies the tracker (so you may have more than one tracker per cgi-bin script). Make sure there are no spaces or other illegal characters in it (to be safe, stick to letters and numbers). The "name" forms part of the URL that appears in the tracker config "tracker :: web" variable, so make sure they match. The "home" part of the configuration is the tracker home directory. If you're using Apache, you can use an additional trick to hide the ``.cgi`` extension of the cgi script. Place the ``roundup.cgi`` script wherever you want it to be, rename it to just ``roundup``, and add a couple lines to your Apache configuration:: SetHandler cgi-script CGI-bin for Limited-Access Hosting ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ If you are running in a shared-hosting environment or otherwise don't have permissiong to edit the system web server's configuration, but can create a ``.htaccess`` file then you may be able to use this approach. 1. Install flup_ 2. Create a script ``roundup_stub`` in your server's ``cgi-bin`` directory containing:: #!/usr/bin/env python # if necessary modify the Python path to include the place you # installed Roundup #import sys #sys.path.append('...') # cgitb is needed for debugging in browser only #import cgitb #cgitb.enable() # obtain the WSGI request dispatcher from roundup.cgi.wsgi_handler import RequestDispatcher tracker_home = '/path/to/tracker/home' app = RequestDispatcher(tracker_home) from flup.server.cgi import WSGIServer WSGIServer(app).run() 3. Modify or created the ``.htaccess`` file in the desired (sub-)domain directory to contain:: RewriteEngine On RewriteBase / RewriteRule ^(.*)$ /cgi-bin/roundup_stub/$1 [L] Now loading the (sub-)domain in a browser should load the tracker web interface. If you get a "500" error then enable the "cgitb" lines in the stub to get some debugging information. Stand-alone Web Server ~~~~~~~~~~~~~~~~~~~~~~ This approach will give you faster response than cgi-bin. You may investigate using ProxyPass or similar configuration in apache to have your tracker accessed through the same URL as other systems. The stand-alone web server is started with the command ``roundup-server``. It has several options - display them with ``roundup-server -h``. The tracker home configuration is similar to the cgi-bin - you may either edit the script to change the TRACKER_HOMES variable or you may supply the name=home values on the command-line after all the other options. To make the server run in the background, use the "-d" option, specifying the name of a file to write the server process id (pid) to. Zope Product - ZRoundup ~~~~~~~~~~~~~~~~~~~~~~~ ZRoundup installs as a regular Zope product. Copy the ZRoundup directory to your Products directory either in INSTANCE_HOME/Products or the Zope code tree lib/python/Products. When you next (re)start up Zope, you will be able to add a ZRoundup object that interfaces to your new tracker. Apache HTTP Server with mod_python ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ `Mod_python`_ is an `Apache`_ module that embeds the Python interpreter within the server. Running Roundup this way is much faster than all above options and, like `web server cgi-bin`_, allows you to use HTTPS protocol. The drawback is that this setup is more complicated. The following instructions were tested on apache 2.0 with mod_python 3.1. If you are using older versions, your mileage may vary. Mod_python uses OS threads. If your apache was built without threads (quite commonly), you must load the threading library to run mod_python. This is done by setting ``LD_PRELOAD`` to your threading library path in apache ``envvars`` file. Example for gentoo linux (``envvars`` file is located in ``/usr/lib/apache2/build/``):: LD_PRELOAD=/lib/libpthread.so.0 export LD_PRELOAD Example for FreeBSD (``envvars`` is in ``/usr/local/sbin/``):: LD_PRELOAD=/usr/lib/libc_r.so export LD_PRELOAD Next, you have to add Roundup trackers configuration to apache config. Roundup apache interface uses the following options specified with ``PythonOption`` directives: TrackerHome: defines the tracker home directory - the directory that was specified when you did ``roundup-admin init``. This option is required. TrackerLanguage: defines web user interface language. mod_python applications do not receive OS environment variables in the same way as command-line programs, so the language cannot be selected by setting commonly used variables like ``LANG`` or ``LC_ALL``. ``TrackerLanguage`` value has the same syntax as values of these environment variables. This option may be omitted. TrackerDebug: run the tracker in debug mode. Setting this option to ``yes`` or ``true`` has the same effect as running ``roundup-server -t debug``: the database schema and used html templates are rebuilt for each HTTP request. Values ``no`` or ``false`` mean that all html templates for the tracker are compiled and the database schema is checked once at startup. This is the default behaviour. TrackerTiming: has nearly the same effect as environment variable ``CGI_SHOW_TIMING`` for standalone roundup server. The difference is that setting this option to ``no`` or ``false`` disables timings display. Value ``comment`` writes request handling times in html comment, and any other non-empty value makes timing report visible. By default, timing display is disabled. In the following example we have two trackers set up in ``/var/db/roundup/support`` and ``/var/db/roundup/devel`` and accessed as ``https://my.host/roundup/support/`` and ``https://my.host/roundup/devel/`` respectively (provided Apache has been set up for SSL of course). Having them share same parent directory allows us to reduce the number of configuration directives. Support tracker has russian user interface. The other tracker (devel) has english user interface (default). Static files from ``html`` directory are served by apache itself - this is quicker and generally more robust than doing that from python. Everything else is aliased to dummy (non-existing) ``py`` file, which is handled by mod_python and our roundup module. Example mod_python configuration:: ################################################# # Roundup Issue tracker ################################################# # enable Python optimizations (like 'python -O') PythonOptimize On # let apache handle static files from 'html' directories AliasMatch /roundup/(.+)/@@file/(.*) /var/db/roundup/$1/html/$2 # everything else is handled by roundup web UI AliasMatch /roundup/([^/]+)/(?!@@file/)(.*) /var/db/roundup/$1/dummy.py/$2 # roundup requires a slash after tracker name - add it if missing RedirectMatch permanent ^/roundup/([^/]+)$ /roundup/$1/ # common settings for all roundup trackers Order allow,deny Allow from all AllowOverride None Options None AddHandler python-program .py PythonHandler roundup.cgi.apache # uncomment the following line to see tracebacks in the browser # (note that *some* tracebacks will be displayed anyway) #PythonDebug On # roundup tracker homes PythonOption TrackerHome /var/db/roundup/support PythonOption TrackerLanguage ru PythonOption TrackerHome /var/db/roundup/devel Notice that the ``/var/db/roundup`` path shown above refers to the directory in which the tracker homes are stored. The actual value will thus depend on your system. On Windows the corresponding lines will look similar to these:: AliasMatch /roundup/(.+)/@@file/(.*) C:/DATA/roundup/$1/html/$2 AliasMatch /roundup/([^/]+)/(?!@@file/)(.*) C:/DATA/roundup/$1/dummy.py/$2 In this example the directory hosting all of the tracker homes is ``C:\DATA\roundup``. (Notice that you must use forward slashes in paths inside the httpd.conf file!) The URL for accessing these trackers then become: `http:///roundup/support/`` and ``http:///roundup/devel/`` Note that in order to use https connections you must set up Apache for secure serving with SSL. WSGI Handler ~~~~~~~~~~~~ The WSGI handler is quite simple. The following sample code shows how to use it:: from wsgiref.simple_server import make_server # obtain the WSGI request dispatcher from roundup.cgi.wsgi_handler import RequestDispatcher tracker_home = 'demo' app = RequestDispatcher(tracker_home) httpd = make_server('', 8917, app) httpd.serve_forever() To test the above you should create a demo tracker with ``python demo.py``. Edit the ``config.ini`` to change the web URL to "http://localhost:8917/". Configure an Email Interface ---------------------------- If you don't want to use the email component of Roundup, then remove the "``nosyreaction.py``" module from your tracker "``detectors``" directory. See `platform-specific notes`_ for steps that may be needed on your system. There are five supported ways to get emailed issues into the Roundup tracker. You should pick ONE of the following, all of which will continue my example setup from above: As a mail alias pipe process ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Set up a mail alias called "issue_tracker" as (include the quote marks): "``|/usr/bin/python /usr/bin/roundup-mailgw ``" (substitute ``/usr/bin`` for wherever roundup-mailgw is installed). In some installations (e.g. RedHat Linux and Fedora Core) you'll need to set up smrsh so sendmail will accept the pipe command. In that case, symlink ``/etc/smrsh/roundup-mailgw`` to "``/usr/bin/roundup-mailgw``" and change the command to:: |roundup-mailgw /opt/roundup/trackers/support To test the mail gateway on unix systems, try:: echo test |mail -s '[issue] test' support@YOUR_DOMAIN_HERE Be careful that some mail systems (postfix for example) will impost a limits on processes they spawn. In particular postfix can set a file size limit. *This can cause your Roundup database to become corrupted.* As a custom router/transport using a pipe process (Exim4 specific) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The following configuration snippets for `Exim 4`_ configuration implement a custom router & transport to accomplish mail delivery to roundup-mailgw. A configuration for Exim3 is similar but not included, since Exim3 is considered obsolete. .. _Exim 4: http://www.exim.org/ This configuration is similar to the previous section, in that it uses a pipe process. However, there are advantages to using a custom router/transport process, if you are using Exim. * This avoids privilege escalation, since otherwise the pipe process will run as the mail user, typically mail. The transport can be configured to run as the user appropriate for the task at hand. In the transport described in this section, Exim4 runs as the unprivileged user ``roundup``. * Separate configuration is not required for each tracker instance. When a email arrives at the server, Exim passes it through the defined routers. The roundup_router looks for a match with one of the roundup directories, and if there is one it is passed to the roundup_transport, which uses the pipe process described in the previous section (`As a mail alias pipe process`_). The matching is done in the line:: require_files = /usr/bin/roundup-mailgw:ROUNDUP_HOME/$local_part/schema.py The following configuration has been tested on Debian Sarge with Exim4. .. note:: Note that the Debian Exim4 packages don't allow pipes in alias files by default, so the method described in the section `As a mail alias pipe process`_ will not work with the default configuration. However, the method described in this section does. See the discussion in ``/usr/share/doc/exim4-config/README.system_aliases`` on any Debian system with Exim4 installed. For more Debian-specific information, see suggested addition to README.Debian in http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=343283, which will hopefully be merged into the Debian package eventually. This config makes a few assumptions: * That the mail address corresponding to the tracker instance has the same name as the directory of the tracker instance, i.e. the mail interface address corresponding to a Roundup instance called ``/var/lib/roundup/trackers/mytracker`` is ``mytracker@your.host``. * That (at least) all the db subdirectories of all the tracker instances (ie. ``/var/lib/roundup/trackers/*/db``) are owned by the same user, in this case, 'roundup'. * That if the ``schema.py`` file exists, then the tracker is ready for use. Another option is to use the ``config.ini`` file (this changed in 0.8 from ``config.py``). Macros for Roundup router/transport. Should be placed in the macros section of the Exim4 config:: # Home dir for your Roundup installation ROUNDUP_HOME=/var/lib/roundup/trackers # User and group for Roundup. ROUNDUP_USER=roundup ROUNDUP_GROUP=roundup Custom router for Roundup. This will (probably) work if placed at the beginning of the router section of the Exim4 config:: roundup_router: driver = accept # The config file config.ini seems like a more natural choice, but the # file config.py was replaced by config.ini in 0.8, and schema.py needs # to be present too. require_files = /usr/bin/roundup-mailgw:ROUNDUP_HOME/$local_part/schema.py transport = roundup_transport Custom transport for Roundup. This will (probably) work if placed at the beginning of the router section of the Exim4 config:: roundup_transport: driver = pipe command = /usr/bin/python /usr/bin/roundup-mailgw ROUNDUP_HOME/$local_part/ current_directory = ROUNDUP_HOME home_directory = ROUNDUP_HOME user = ROUNDUP_USER group = ROUNDUP_GROUP As a regular job using a mailbox source ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Set ``roundup-mailgw`` up to run every 10 minutes or so. For example (substitute ``/usr/bin`` for wherever roundup-mailgw is installed):: 0,10,20,30,40,50 * * * * /usr/bin/roundup-mailgw /opt/roundup/trackers/support mailbox Where the ``mail_spool_file`` argument is the location of the roundup submission user's mail spool. On most systems, the spool for a user "issue_tracker" will be "``/var/mail/issue_tracker``". As a regular job using a POP source ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ To retrieve from a POP mailbox, use a *cron* entry similar to the mailbox one (substitute ``/usr/bin`` for wherever roundup-mailgw is installed):: 0,10,20,30,40,50 * * * * /usr/bin/roundup-mailgw /opt/roundup/trackers/support pop where pop_spec is "``username:password@server``" that specifies the roundup submission user's POP account name, password and server. On windows, you would set up the command using the windows scheduler. As a regular job using an IMAP source ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ To retrieve from an IMAP mailbox, use a *cron* entry similar to the POP one (substitute ``/usr/bin`` for wherever roundup-mailgw is installed):: 0,10,20,30,40,50 * * * * /usr/bin/roundup-mailgw /opt/roundup/trackers/support imap where imap_spec is "``username:password@server``" that specifies the roundup submission user's IMAP account name, password and server. You may optionally include a mailbox to use other than the default ``INBOX`` with "``imap username:password@server mailbox``". If you have a secure (ie. HTTPS) IMAP server then you may use ``imaps`` in place of ``imap`` in the command to use a secure connection. As with the POP job, on windows, you would set up the command using the windows scheduler. UNIX Environment Steps ---------------------- Each tracker ideally should have its own UNIX group, so create a UNIX group (edit ``/etc/group`` or your appropriate NIS map if you're using NIS). To continue with my examples so far, I would create the UNIX group 'support', although the name of the UNIX group does not have to be the same as the tracker name. To this 'support' group I then add all of the UNIX usernames who will be working with this Roundup tracker. In addition to 'real' users, the Roundup email gateway will need to have permissions to this area as well, so add the user your mail service runs as to the group (typically "mail" or "daemon"). The UNIX group might then look like:: support:*:1002:jblaine,samh,geezer,mail If you intend to use the web interface (as most people do), you should also add the username your web server runs as to the group. My group now looks like this:: support:*:1002:jblaine,samh,geezer,mail,apache The tracker "db" directory should be chmod'ed g+sw so that the group can write to the database, and any new files created in the database will be owned by the group. If you're using the mysql or postgresql backend then you'll need to ensure that the tracker user has appropriate permissions to create/modify the database. If you're using roundup.cgi, the apache user needs permissions to modify the database. Alternatively, explicitly specify a database login in ``rdbms`` -> ``user`` and ``password`` in ``config.ini``. An alternative to the above is to create a new user who has the sole responsibility of running roundup. This user: 1. runs the CGI interface daemon 2. runs regular polls for email 3. runs regular checks (using cron) to ensure the daemon is up 4. optionally has no login password so that nobody but the "root" user may actually login and play with the roundup setup. If you're using a Linux system (e.g. Fedora Core) with SELinux enabled, you will need to ensure that the db directory has a context that permits the web server to modify and create files. If you're using the mysql or postgresql backend you may also need to update your policy to allow the web server to access the database socket. Additional Language Codecs -------------------------- If you intend to send messages to Roundup that use Chinese, Japanese or Korean encodings the you'll need to obtain CJKCodecs from http://cjkpython.berlios.de/ Public Tracker Considerations ----------------------------- If you run a public tracker, you will eventually have to think about dealing with spam entered through both the web and mail interfaces. The `customisation documentation`_ has a simple detector that will block a lot of spam attempts. Look for the example "Preventing SPAM". Maintenance =========== Read the separate `administration guide`_ for information about how to perform common maintenance tasks with Roundup. Upgrading ========= Read the separate `upgrading document`_, which describes the steps needed to upgrade existing tracker trackers for each version of Roundup that is released. Further Reading =============== If you intend to use Roundup with anything other than the default templates, if you would like to hack on Roundup, or if you would like implementation details, you should read `Customising Roundup`_. Running Multiple Trackers ========================= Things to think about before you jump off the deep end and install multiple trackers, which involve additional URLs, user databases, email addresses, databases to back up, etc. 1. Do you want a tracker per product you sell/support? You can just add a new property to your issues called Product, and filter by that. See the customisation example `adding a new field to the classic schema`_. 2. Do you want to track internal software development issues and customer support issues separately? You can just set up an additional "issue" class called "cust_issues" in the same tracker, mimicing the normal "issue" class, but with different properties. See the customisation example `tracking different types of issues`_. Platform-Specific Notes ======================= Windows command-line tools -------------------------- To make the command-line tools accessible in Windows, you need to update the "Path" environment variable in the Registry via a dialog box. On Windows 2000 and later: 1) Press the "Start" button. 2) Choose "Settings" 3) Choose "Control Panel" 4) Choose "System" 5) Choose "Advanced" 6) Choose "Environmental Variables" 7) Add: "\Scripts" to the "Path" environmental variable. Where in 7) is the root directory (e.g., ``C:\Python22\Scripts``) of your Python installation. I understand that in XP, 2) above is not needed as "Control Panel" is directly accessible from "Start". I do not believe this is possible to do in previous versions of Windows. Windows Server -------------- To have the Roundup web server start up when your machine boots up, there are two different methods, the scheduler and installing the service. 1. Using the Windows scheduler ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Set up the following in Scheduled Tasks (note, the following is for a cygwin setup): **Run** ``c:\cygwin\bin\bash.exe -c "roundup-server TheProject=/opt/roundup/trackers/support"`` **Start In** ``C:\cygwin\opt\roundup\bin`` **Schedule** At System Startup To have the Roundup mail gateway run periodically to poll a POP email address, set up the following in Scheduled Tasks: **Run** ``c:\cygwin\bin\bash.exe -c "roundup-mailgw /opt/roundup/trackers/support pop roundup:roundup@mail-server"`` **Start In** ``C:\cygwin\opt\roundup\bin`` **Schedule** Every 10 minutes from 5:00AM for 24 hours every day Stop the task if it runs for 8 minutes 2. Installing the roundup server as a Windows service ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ This is more Windows oriented and will make the Roundup server run as soon as the PC starts up without any need for a login or such. It will also be available in the normal Windows Administrative Tools. For this you need first to create a service ini file containing the relevant settings. 1. It is created if you execute the following command from within the scripts directory (notice the use of backslashes):: roundup-server -S -C \server.ini -n -p 8080 -l \trackerlog.log software=\Software where the item ```` is replaced with the physical directory that hosts all of your trackers. The ```` item is the name of your roundup server PC, such as w2003srv or similar. 2. Next open the now created file ``C:\DATA\roundup\server.ini`` file (if your ```` is ``C:\DATA\roundup``). Check the entries for correctness, especially this one:: [trackers] software = C:\DATA\Roundup\Software (this is an example where the tracker is named software and its home is ``C:\DATA\Roundup\Software``) 3. Next give the commands that actually installs and starts the service:: roundup-server -C C:\DATA\Roundup\server.ini -c install roundup-server -c start 4. Finally open the AdministrativeTools/Services applet and locate the Roundup service entry. Open its properties and change it to start automatically instead of manually. If you are using Apache as the webserver you might want to use it with mod_python instead to serve out Roundup. In that case see the mod_python instructions above for details. Sendmail smrsh -------------- If you use Sendmail's ``smrsh`` mechanism, you will need to tell smrsh that roundup-mailgw is a valid/trusted mail handler before it will work. This is usually done via the following 2 steps: 1. make a symlink in ``/etc/smrsh`` called ``roundup-mailgw`` which points to the full path of your actual ``roundup-mailgw`` script. 2. change your alias to ``"|roundup-mailgw "`` Linux ----- Make sure you read the instructions under `UNIX environment steps`_. Solaris ------- You'll need to build Python. Make sure you read the instructions under `UNIX environment steps`_. Problems? Testing your Python... ================================ .. note:: The ``run_tests.py`` script is packaged in Roundup's source distribution - users of the Windows installer, other binary distributions or pre-installed Roundup will need to download the source to use it. Remember to have a database user 'rounduptest' prepared (with password 'rounduptest'). This user must have at least the rights to create and drop databases. Documentation: details on `adding MySQL users`_, for PostgreSQL you want to call the ``createuser`` command with the ``-d`` option to allow database creation. Once you've unpacked roundup's source, run ``python run_tests.py`` in the source directory and make sure there are no errors. If there are errors, please let us know! If the above fails, you may be using the wrong version of python. Try ``python2 run_tests.py`` or ``python2.X run_tests.py`` where ``X`` is in the set 3,4,5,6 depending on the version(s) of python installed. If that works, you will need to substitute ``python2`` or ``python2.X`` for ``python`` in all further commands you use in relation to Roundup -- from installation and scripts. .. _`table of contents`: index.html .. _`user guide`: user_guide.html .. _`roundup specification`: spec.html .. _`tracker configuration`: customizing.html#tracker-configuration .. _`customisation documentation`: customizing.html .. _`Adding a new field to the classic schema`: customizing.html#adding-a-new-field-to-the-classic-schema .. _`Tracking different types of issues`: customizing.html#tracking-different-types-of-issues .. _`customising roundup`: customizing.html .. _`upgrading document`: upgrading.html .. _`administration guide`: admin_guide.html .. _External hyperlink targets: .. _apache: http://httpd.apache.org/ .. _flup: http://pypi.python.org/pypi/flup .. _mod_python: http://www.modpython.org/ .. _MySQLdb: http://sourceforge.net/projects/mysql-python .. _Psycopg: http://initd.org/software/initd/psycopg .. _pysqlite: http://pysqlite.org/ .. _`adding MySQL users`: http://dev.mysql.com/doc/refman/5.1/en/adding-users.html roundup-1.4.20/doc/admin_guide.txt0000644000175000017630000003172211741527216015552 0ustar ralfpriv==================== Administration Guide ==================== .. contents:: What does Roundup install? ========================== There's two "installations" that we talk about when using Roundup: 1. The installation of the software and its support files. This uses the standard Python mechanism called "distutils" and thus Roundup's core code, executable scripts and support data files are installed in Python's directories. On Windows, this is typically: Scripts ``\scripts\...`` Core code ``\lib\site-packages\roundup\...`` Support files ``\share\roundup\...`` and on Unix-like systems (eg. Linux): Scripts ``/bin/...`` Core code ``/lib-/site-packages/roundup/...`` Support files ``/share/roundup/...`` 2. The installation of a specific tracker. When invoking the roundup-admin "inst" (and "init") commands, you're creating a new Roundup tracker. This installs configuration files, HTML templates, detector code and a new database. You have complete control over where this stuff goes through both choosing your "tracker home" and the ``main`` -> ``database`` variable in the tracker's config.ini. Configuring Roundup's Logging of Messages For Sysadmins ======================================================= You may configure where Roundup logs messages in your tracker's config.ini file. Roundup will use the standard Python (2.3+) logging implementation when available. If not, then a very basic logging implementation will be used (see BasicLogging in the roundup.rlog module for details). Configuration for standard "logging" module: - tracker configuration file specifies the location of a logging configration file as ``logging`` -> ``config`` - ``roundup-server`` specifies the location of a logging configuration file on the command line Configuration for "BasicLogging" implementation: - tracker configuration file specifies the location of a log file ``logging`` -> ``filename`` - tracker configuration file specifies the level to log to as ``logging`` -> ``level`` - ``roundup-server`` specifies the location of a log file on the command line - ``roundup-server`` specifies the level to log to on the command line (``roundup-mailgw`` always logs to the tracker's log file) In both cases, if no logfile is specified then logging will simply be sent to sys.stderr with only logging of ERROR messages. Configuring roundup-server ========================== The basic configuration file layout is as follows (take from the ``roundup-server.ini.example`` file in the "doc" directory):: [main] port = 8080 ;host = ;user = ;group = ;log_ip = yes ;pidfile = ;logfile = ;template = ;ssl = no ;pem = [trackers] ; Add one of these per tracker being served name = /path/to/tracker/name Values ";commented out" are optional. The meaning of the various options are as follows: **port** Defines the local TCP port to listen for clients on. **host** Defines the hostname or IP number to listen for clients on. Only required if `localhost` is not sufficient. If left empty (as opposed to no `host` keyword in the config-file) this will listen to all network interfaces and is equivalent to an explicit address `0.0.0.0`. The use of an empty string to listen to all interfaces is deprecated and will go away in a future version. **user** and **group** Defines the Unix user and group to run the server as. Only work if the server is started as root. **log_ip** If ``yes`` then we log IP addresses against accesses. If ``no`` then we log the hostname of the client. The latter can be much slower. **pidfile** If specified, the server will fork at startup and write its new PID to the file. **logfile** Any unhandled exception messages or other output from Roundup will be written to this file. It must be specified if **pidfile** is specified. If per-tracker logging is specified, then very little will be written to this file. **template** Specifies a template used for displaying the tracker index when multiple trackers are being used. The variable "trackers" is available to the template and is a dict of all configured trackers. **ssl** Enables the use of SSL to secure the connection to the roundup-server. If you enable this, ensure that your tracker's config.ini specifies an *https* URL. **pem** If specified, the SSL PEM file containing the private key and certificate. If not specified, roundup will generate a temporary, self-signed certificate for use. **trackers** section Each line denotes a mapping from a URL component to a tracker home. Make sure the name part doesn't include any url-unsafe characters like spaces. Stick to alphanumeric characters and you'll be ok. Users and Security ================== Roundup holds its own user database which primarily contains a username, password and email address for the user. Roundup *must* have its own user listing, in order to maintain internal consistency of its data. It is a relatively simple exercise to update this listing on a regular basis, or on demand, so that it matches an external listing (eg. unix passwd file, LDAP, etc.) Roundup identifies users in a number of ways: 1. Through the web, users may be identified by either HTTP Basic Authentication or cookie authentication. If you are running the web server (roundup-server) through another HTTP server (eg. apache or IIS) then that server may require HTTP Basic Authentication, and it will pass the ``REMOTE_USER`` variable through to Roundup. If this variable is not present, then Roundup defaults to using its own cookie-based login mechanism. 2. In email messages handled by roundup-mailgw, users are identified by the From address in the message. In both cases, Roundup's behaviour when dealing with unknown users is controlled by Permissions defined in the "SECURITY SETTINGS" section of the tracker's ``schema.py`` module: Web Registration If granted to the Anonymous Role, then anonymous users will be able to register through the web. Email Registration If granted to the Anonymous Role, then email messages from unknown users will result in those users being registered with the tracker. More information about how to customise your tracker's security settings may be found in the `customisation documentation`_. Tasks ===== Maintenance of Roundup can involve one of the following: 1. `tracker backup`_ 2. `software upgrade`_ 3. `migrating backends`_ 4. `moving a tracker`_ 5. `migrating from other software`_ 6. `adding a user from the command-line`_ Tracker Backup -------------- The roundup-admin import and export commands are **not** recommended for performing backup. Optionally stop the web and email frontends and to copy the contents of the tracker home directory to some other place using standard backup tools. This means using *pg_dump* to take a snapshot of your Postgres backend database, for example. A simple copy of the tracker home (and files storage area if you've configured it to be elsewhere) will then complete the backup. Software Upgrade ---------------- Always make a backup of your tracker before upgrading software. Steps you may take: 1. Ensure that the unit tests run on your system:: python run_tests.py 2. If you're using an RDBMS backend, make a backup of its contents now. 3. Make a backup of the tracker home itself. 4. Stop the tracker web and email frontends. 5. Install the new version of the software:: python setup.py install 6. Follow the steps in the `upgrading documentation`_ for the new version of the software in the copied. Usually you will be asked to run `roundup_admin migrate` on your tracker before you allow users to start accessing the tracker. It's safe to run this even if it's not required, so just get into the habit. 7. Restart your tracker web and email frontends. If something bad happens, you may reinstate your backup of the tracker and reinstall the older version of the sofware using the same install command:: python setup.py install Migrating Backends ------------------ 1. Stop the existing tracker web and email frontends (preventing changes). 2. Use the roundup-admin tool "export" command to export the contents of your tracker to disk. 3. Copy the tracker home to a new directory. 4. Delete the "db" directory from the new directory. 5. Enter the new backend name in the tracker home ``db/backend_name`` file. 6. Use the roundup-admin "import" command to import the previous export with the new tracker home. If non-interactively:: roundup-admin -i import If interactively, enter 'commit' before exiting. 7. Test each of the admin tool, web interface and mail gateway using the new backend. 8. Move the old tracker home out of the way (rename to "tracker.old") and move the new tracker home into its place. 9. Restart web and email frontends. Moving a Tracker ---------------- If you're moving the tracker to a similar machine, you should: 1. install Roundup on the new machine and test that it works there, 2. stop the existing tracker web and email frontends (preventing changes), 3. copy the tracker home directory over to the new machine, and 4. start the tracker web and email frontends on the new machine. Most of the backends are actually portable across platforms (ie. from Unix to Windows to Mac). If this isn't the case (ie. the tracker doesn't work when moved using the above steps) then you'll need to: 1. install Roundup on the new machine and test that it works there, 2. stop the existing tracker web and email frontends (preventing changes), 3. use the roundup-admin tool "export" command to export the contents of the existing tracker, 4. copy the export to the new machine, 5. use the roundup-admin "import" command to import the tracker on the new machine, and 6. start the tracker web and email frontends on the new machine. Migrating From Other Software ----------------------------- You have a couple of choices. You can either use a CSV import into Roundup, or you can write a simple Python script which uses the Roundup API directly. The latter is almost always simpler -- see the "scripts" directory in the Roundup source for some example uses of the API. "roundup-admin import" will import data into your tracker from a directory containing files with the following format: - one colon-separated-values file per Class with columns for each property, named .csv - one colon-separated-values file per Class with journal information, named -journals.csv (this is required, even if it's empty) - if the Class is a FileClass, you may have the "content" property stored in separate files from the csv files. This goes in a directory structure:: -files// where ```` is the item's ```` combination. The ```` value is ``int( / 1000)``. Adding A User From The Command-Line ----------------------------------- The ``roundup-admin`` program can create any data you wish to in the database. To create a new user, use:: roundup-admin create user To figure out what good values might be for some of the fields (eg. Roles) you can just display another user:: roundup-admin list user (or if you know their username, and it happens to be "richard"):: roundup-admin find username=richard then using the user id you get from one of the above commands, you may display the user's details:: roundup-admin display Running the Servers =================== Unix ---- On Unix systems, use the scripts/server-ctl script to control the roundup-server server. Copy it somewhere and edit the variables at the top to reflect your specific installation. Windows ------- On Windows, the roundup-server program runs as a Windows Service, and therefore may be controlled through the Services control panel. The roundup-server program may also control the service directly: **install the service** ``roundup-server -C /path/to/my/roundup-server.ini -c install`` **start the service** ``roundup-server -c start`` **stop the service** ``roundup-server -c stop`` To bring up the services panel: Windows 2000 and later Start/Control Panel/Administrative Tools/Services Windows NT4 Start/Control Panel/Services You will need a server configuration file (as described in `Configuring roundup-server`_) for specifying tracker homes and other roundup-server configuration. Specify the name of this file using the ``-C`` switch when installing the service. Running the Mail Gateway Script ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The mail gateway script should be scheduled to run regularly on your Windows server. Normally this will result in a window popping up. The solution to this is to: 1. Create a new local account on the Roundup server 2. Set the scheduled task to run in the context of this user instead of your normal login .. _`customisation documentation`: customizing.html .. _`upgrading documentation`: upgrading.html roundup-1.4.20/doc/mysql.txt0000644000175000017630000000337011741527216014450 0ustar ralfpriv============= MySQL Backend ============= This notes detail the MySQL backend for the Roundup issue tracker. Prerequisites ============= To use MySQL as the backend for storing roundup data, you also need to install: 1. MySQL RDBMS 4.0.18 or higher - http://www.mysql.com. Your MySQL installation MUST support InnoDB tables (or Berkeley DB (BDB) tables if you have no other choice). If you're running < 4.0.18 (but not <4.0) then you'll need to use BDB to pass all unit tests. Edit the ``roundup/backends/back_mysql.py`` file to enable DBD instead of InnoDB. 2. Python MySQL interface - http://sourceforge.net/projects/mysql-python Running the MySQL tests ======================= Roundup tests expect an empty MySQL database. Two alternate ways to provide this: 1. If you have root permissions on the MySQL server, you can create the necessary database entries using the follwing SQL sequence. Use ``mysql`` on the command line to enter:: CREATE DATABASE rounduptest; USE rounduptest; GRANT ALL PRIVILEGES ON rounduptest.* TO rounduptest@localhost IDENTIFIED BY 'rounduptest'; FLUSH PRIVILEGES; 2. If your administrator has provided you with database connection info, see the config values in 'test/db_test_base.py' about which database connection, name and user will be used. The MySQL database should not contain any tables. Tests will not drop the database with existing data. Showing MySQL who's boss ======================== If things ever get to the point where that test database is totally hosed, just:: $ su - # /etc/init.d/mysql stop # rm -rf /var/lib/mysql/rounduptest # /etc/init.d/mysql start and all will be better (note that on some systems, ``mysql`` is spelt ``mysqld``). roundup-1.4.20/doc/glossary.txt0000644000175000017630000000153711741527216015151 0ustar ralfpriv================ Roundup Glossary ================ .. contents:: class a definition of the properties and behaviour of a set of items db (or hyperdb) a collection of items designator a combined class + itemid reference to any item in the hyperdb itemid a numeric reference to a particular item of one class item a collection of data that forms one entry in the hyperdb. property one element of data that makes up an item. In Roundup, the set of item properties may be changed as needed - even after the tracker has been initialised and used in production. schema the definition of all the classes that make up an tracker tracker the schema and hyperdb that forms one issue tracker tracker home the physical location on disk of a tracker ----------------- Back to `Table of Contents`_ .. _`Table of Contents`: index.html roundup-1.4.20/doc/upgrading.txt0000644000175000017630000020134311754172436015267 0ustar ralfpriv====================================== Upgrading to newer versions of Roundup ====================================== Please read each section carefully and edit your tracker home files accordingly. Note that there is information about upgrade procedures in the `administration guide`_. If a specific version transition isn't mentioned here (eg. 0.6.7 to 0.6.8) then you don't need to do anything. If you're upgrading from 0.5.6 to 0.6.8 though, you'll need to check the "0.5 to 0.6" and "0.6.x to 0.6.3" steps. .. contents:: Migrating from 1.4.19 to 1.4.20 =============================== Roundup used to allow certain HTML-Tags in OK- and Error-messages. Since these messages are passed via the URL (due to roundup redirecting after an edit), we did have security-issues (see issue2550724). If you have customized OK- or Error messages in your roundup-installation and you're were using features like bold or italic parts of the message you will have to do without this highlighting and remove HTML tags from messages. If you were using
tags for multi-line messages, you now should use newlines instead, these will be replaced with
during formatting. Note that the previous implementation also allowed links inside messages. Since these links could be set by an attacker, no links in roundup messages are supported anymore. This does *not* affect the "clear this message" link in OK-messages as it is generated by the template and is not part of the OK-message. If you have not modified any roundup messages, you need not do anything, the templates shipped with roundup did not use HTML tags in messages for highlighting. Migrating from 1.4.17 to 1.4.18 =============================== There was a bug in 1.4.17 where files were unlinked from issues if a mail without attachment was received via the mail interface. The following script will list likely issues being affected by the bug. The date in the script is the date of the 1.4.17 release. If you have installed 1.4.17 later than this date, you can change the date appropriately to your installation date. Run the script in the directory of your tracker:: #!/usr/bin/python import os from roundup import instance from roundup.date import Date dir = os.getcwd () tracker = instance.open (dir) db = tracker.open ('admin') # you may want to change this to your install date to find less candidates last_release = Date('2011-05-13') affected = {} for i in db.issue.getnodeids(): for j in db.issue.history(i): if i in affected: break if j[1] < last_release or j[3] != 'set' or 'files' not in j[4]: continue for op, p in j[4]['files']: if op == '-': affected [i] = 1 break print ', '.join(sorted(affected.iterkeys())) To find out which files where attached before you can look in the history of the affected issue. For fixing issues you can re-attach the files in question using the "set" command of roundup-admin, e.g., if the list of files attached to an issue should be files 5, 17, 23 for issue42 you will set this using roundup-admin -i /path/to/your/tracker set issue42 files=5,17,23 Migrating from 1.4.x to 1.4.17 ============================== There is a new config-option `migrate_passwords` in section `web` to auto-migrate passwords at web-login time to a more secure storage scheme. Default for the new option is "yes" so if you don't want that passwords are auto-migrated to a more secure password scheme on user login, set this to "no" before running your tracker(s) after the upgrade. The standalone roundup-server now defaults to listening on localhost (no longer on all network interfaces). This will not affect you if you're already using a configuration file for roundup-server. If you are using an empty setting for the `host` parameter in the config-file you should explicitly put 0.0.0.0 there as the use of an empty string to specify listening to all interfaces is deprecated and will go away in a future version. If you are starting the server without a configuration file and want to explicitly listen to all network interface, you should specify the -n option with the address `0.0.0.0`. Searching now requires either read-permission without a check method, or you will have to add a "Search" permission for a class or a list of properties for a class (if you want to allow searching). For the classic template (or other templates derived from it) you want to add the following lines to your `schema.py` file:: p = db.security.addPermission(name='Search', klass='query') db.security.addPermissionToRole('User', p) This is needed, because for the `query` class users may view only their own queries (or public queries). This is implemented with a `check` method, therefore the default search permissions will not allow searching and you'll have to add an explicit search permission. If you have modified your schema, you can check if you're missing any search permissions with the following script, run it in your tracker directory, it will list for each Class and Property the roles that may search for this property:: #!/usr/bin/python import os from roundup import instance tracker = instance.open(os.getcwd ()) db = tracker.open('admin') for cl in sorted(db.getclasses()): print "Class:", cl for p in sorted(db.getclass(cl).properties.keys()): print " Property:", p roles = [] for role in sorted(db.security.role.iterkeys()): if db.security.roleHasSearchPermission(cl,p,role): roles.append(role) print " roles may search:", ', '.join(roles) Migrating from 1.4.x to 1.4.12 ============================== Item creation now checks the "Create" permission instead of the "Edit" permission for individual properties. If you have modified your tracker permissions from the default distribution, you should check that "Create" permissions exist for all properties you want users to be able to create. Fixing some potential security holes ------------------------------------ Enhanced checking was added to the user registration auditor. If you run a public tracker you should update your tracker's ``detectors/userauditor.py`` using the new code from ``share/roundup/templates/classic/detectors/userauditor.py``. In most cases you may just copy the file over, but if you've made changes to the auditor in your tracker then you'll need to manually integrate the new code. Some HTML templates were found to have formatting security problems: ``html/page.html``:: -tal:replace="request/user/username">username
+tal:replace="python:request.user.username.plain(escape=1)">username
``html/_generic.help-list.html``:: -tal:content="structure python:item[prop]"> +tal:content="python:item[prop]"> The lines marked "+" should be added and lines marked "-" should be deleted (minus the "+"/"-" signs). Some HTML interface tweaks -------------------------- You may wish to copy the ``user_utils.js`` and ``style.css` files from the source distribution ``share/roundup/templates/classic/html/`` directory to the ``html`` directory of your trackers as it includes a small improvement. If you have made local changes to those files you'll need to manually work the differences in to your versions or ignore the changes. Migrating from 1.4.x to 1.4.11 ============================== Close potential security hole ----------------------------- If your tracker has untrusted users you should examine its ``schema.py`` file and look for the section granting the "Edit" permission to your users. This should look something like:: p = db.security.addPermission(name='Edit', klass='user', check=own_record, description="User is allowed to edit their own user details") and should be modified to restrict the list of properties they are allowed to edit by adding the ``properties=`` section like:: p = db.security.addPermission(name='Edit', klass='user', check=own_record, properties=('username', 'password', 'address', 'realname', 'phone', 'organisation', 'alternate_addresses', 'queries', 'timezone'), description="User is allowed to edit their own user details") Most importantly the "roles" property should not be editable - thus not appear in that list of properties. Grant the "Register" permission to the Anonymous role ----------------------------------------------------- A separate "Register" permission has been introduced to allow anonymous users to register. This means you will need to add the following to your tracker's ``schema.py`` to add the permission and assign it to the Anonymous role (replacing any previously assigned "Create user" permission for the Anonymous role):: +db.security.addPermission(name='Register', klass='user', + description='User is allowed to register new user') # Assign the appropriate permissions to the anonymous user's Anonymous # Role. Choices here are: # - Allow anonymous users to register -db.security.addPermissionToRole('Anonymous', 'Create', 'user') +db.security.addPermissionToRole('Anonymous', 'Register', 'user') The lines marked "+" should be added and lines marked "-" should be deleted (minus the "+"/"-" signs). You should also modify the ``html/page.html`` template to change the permission tested there:: -tal:condition="python:request.user.hasPermission('Create', 'user')" +tal:condition="python:request.user.hasPermission('Register', 'user')" Generic class editor may now restore retired items -------------------------------------------------- The instructions for doing so won't be present in your tracker unless you copy the ``_generic.index.html`` template from the roundup distribution in ``share/roundup/templates/classic/html`` to your tracker's ``html`` directory. Migrating from 1.4.x to 1.4.9 ============================= Customized MailGW Class ----------------------- If you have customized the MailGW class in your tracker: The new MailGW class opens the database for each message in the method handle_message (instance.open) instead of passing the opened database as a parameter to the MailGW constructor. The old handle_message has been renamed to _handle_message. The new method opens the database and wraps the call to the old method into a try/finally. Your customized MailGW class needs to mirror this behavior. Fix the "remove" button in issue files and messages lists --------------------------------------------------------- The "remove" button(s) in the issue messages list needs to be altered. Find the following in your tracker's ``html/issue.item.html`` template::
and add ``method="POST"`` as shown below:: Then also find:: and add ``method="POST"`` as shown below:: Fixing the "retire" button in user management list -------------------------------------------------- If you made the change to the "reture" link in the user management list as listed below in `Migrating from 1.4.x to 1.4.7`_ then you'll need to fix that change by adding ``method="POST"`` to the ```` tag::
Migrating from 1.4.x to 1.4.7 ============================= Several security issues were addressed in this release. Some aspects of your trackers may no longer function depending on your local customisations. Core functionality that will need to be modified: Grant the "retire" permission to users for their queries -------------------------------------------------------- Users will no longer be able to retire their own queries. To remedy this you will need to add the following to your tracker's ``schema.py`` just under the line that grants them permission to edit their own queries:: p = db.security.addPermission(name='Edit', klass='query', check=edit_query, description="User is allowed to edit their queries") db.security.addPermissionToRole('User', p) + p = db.security.addPermission(name='Retire', klass='query', check=edit_query, + description="User is allowed to retire their queries") + db.security.addPermissionToRole('User', p) p = db.security.addPermission(name='Create', klass='query', description="User is allowed to create queries") db.security.addPermissionToRole('User', p) The lines marked "+" should be added, minus the "+" sign. Fix the "retire" link in the users list for admin users ------------------------------------------------------- The "retire" link found in the file ``html/user.index.html``:: retire Should be replaced with::
Fix for Python 2.6+ users ------------------------- If you use Python 2.6 you should edit your tracker's ``detectors/nosyreaction.py`` file to change:: import sets at the top to:: from roundup.anypy.sets_ import set and then all instances of ``sets.Set()`` to ``set()`` in the later code. Trackers currently allowing HTML file uploading ----------------------------------------------- Trackers which wish to continue to allow uploading of HTML content against issues will need to set a new configuration variable in the ``[web]`` section of the tracker's ``config.ini`` file: # Setting this option enables Roundup to serve uploaded HTML # file content *as HTML*. This is a potential security risk # and is therefore disabled by default. Set to 'yes' if you # trust *all* users uploading content to your tracker. # Allowed values: yes, no # Default: no allow_html_file = no Migrating from 1.4.2 to 1.4.3 ============================= If you are using the MySQL backend you will need to replace some indexes that may have been created by version 1.4.2. You should to access your MySQL database directly and remove any indexes with a name ending in "_key_retired_idx". You should then re-add them with the same spec except the key column name needs a size. So an index on "_user (__retired, _name)" should become "_user (__retired, _name(255))". Migrating from 1.4.x to 1.4.2 ============================= You should run the "roundup-admin migrate" command for your tracker once you've installed the latest codebase. Do this before you use the web, command-line or mail interface and before any users access the tracker. This command will respond with either "Tracker updated" (if you've not previously run it on an RDBMS backend) or "No migration action required" (if you have run it, or have used another interface to the tracker, or are using anydbm). It's safe to run this even if it's not required, so just get into the habit. Migrating from 1.3.3 to 1.4.0 ============================= Value of the "refwd_re" tracker configuration option (section "mailgw") is treated as UTF-8 string. In previous versions, it was ISO8859-1. If you have running trackers based on the classic template, please update the messagesummary detector as follows:: --- detectors/messagesummary.py 17 Apr 2003 03:26:38 -0000 1.1 +++ detectors/messagesummary.py 3 Apr 2007 06:47:21 -0000 1.2 @@ -8,7 +8,7 @@ if newvalues.has_key('summary') or not newvalues.has_key('content'): return - summary, content = parseContent(newvalues['content'], 1, 1) + summary, content = parseContent(newvalues['content'], config=db.config) newvalues['summary'] = summary In the latest version we have added some database indexes to the SQL-backends (mysql, postgresql, sqlite) for speeding up building the roundup-index for full-text search. We recommend that you create the following database indexes on the database by hand:: CREATE INDEX words_by_id ON __words (_textid); CREATE UNIQUE INDEX __textids_by_props ON __textids (_class, _itemid, _prop); Migrating from 1.2.x to 1.3.0 ============================= 1.3.0 Web interface changes --------------------------- Some of the HTML files in the "classic" and "minimal" tracker templates were changed to fix some bugs and clean them up. You may wish to compare them to the HTML files in your tracker and apply any changes. Migrating from 1.1.2 to 1.2.0 ============================= 1.2.0 Sorting and grouping by multiple properties ------------------------------------------------- Starting with this version, sorting and grouping by multiple properties is possible. This means that request.sort and request.group are now lists. This is reflected in several places: * ``renderWith`` now has list attributes for ``sort`` and ``group``, where you previously wrote:: renderWith(... sort=('-', 'activity'), group=('+', 'priority') you write now:: renderWith(... sort=[('-', 'activity')], group=[('+', 'priority')] * In templates that permit to edit sorting/grouping, request.sort and request.group are (possibly empty) lists. You can now sort and group by multiple attributes. For an example, see the classic template. You may want search for the variable ``n_sort`` which can be set to the number of sort/group properties. * Templates that diplay new headlines for each group of items with equal group properties can now use the modified ``batch.propchanged`` method that can take several properties which are checked for changes. See the example in the classic template which makes use of ``batch.propchanged``. Migrating from 1.1.0 to 1.1.1 ============================= 1.1.1 "Clear this message" -------------------------- In 1.1.1, the standard ``page.html`` template includes a "clear this message" link in the green "ok" message bar that appears after a successful edit (or other) action. To include this in your tracker, change the following in your ``page.html`` template::

error

to be::

clear this message

If you implemented the "clear this message" in your 1.1.0 tracker, then you should change it to the above and it will work much better! Migrating from 1.0.x to 1.1.0 ============================= 1.1 Login "For Session Only" ---------------------------- In 1.1, web logins are alive for the length of a session only, *unless* you add the following to the login form in your tracker's ``page.html``::
See the classic tracker ``page.html`` if you're unsure where this should go. 1.1 Query Display Name ---------------------- The ``dispname`` web variable has been renamed ``@dispname`` to avoid clashing with other variables of the same name. If you are using the display name feature, you will need to edit your tracker's ``page.html`` and ``issue.index.html`` pages to change ``dispname`` to ``@dispname``. A side-effect of this change is that the renderWith method used in the ``home.html`` page may now take a dispname argument. 1.1 "Clear this message" ------------------------ In 1.1, the standard ``page.html`` template includes a "clear this message" link in the green "ok" message bar that appears after a successful edit (or other) action. To include this in your tracker, change the following in your ``page.html`` template::

error

to be::

clear this message

Migrating from 0.8.x to 1.0 =========================== 1.0 New Query Permissions ------------------------- New permissions are defined for query editing and viewing. To include these in your tracker, you need to add these lines to your tracker's ``schema.py``:: # Users should be able to edit and view their own queries. They should also # be able to view any marked as not private. They should not be able to # edit others' queries, even if they're not private def view_query(db, userid, itemid): private_for = db.query.get(itemid, 'private_for') if not private_for: return True return userid == private_for def edit_query(db, userid, itemid): return userid == db.query.get(itemid, 'creator') p = db.security.addPermission(name='View', klass='query', check=view_query, description="User is allowed to view their own and public queries") db.security.addPermissionToRole('User', p) p = db.security.addPermission(name='Edit', klass='query', check=edit_query, description="User is allowed to edit their queries") db.security.addPermissionToRole('User', p) p = db.security.addPermission(name='Create', klass='query', description="User is allowed to create queries") db.security.addPermissionToRole('User', p) and then remove 'query' from the line:: # Assign the access and edit Permissions for issue, file and message # to regular users now for cl in 'issue', 'file', 'msg', 'query', 'keyword': so it looks like:: for cl in 'issue', 'file', 'msg', 'keyword': Migrating from 0.8.0 to 0.8.3 ============================= 0.8.3 Nosy Handling Changes --------------------------- A change was made to fix a bug in the ``nosyreaction.py`` standard detector. To incorporate this fix in your trackers, you will need to copy the ``nosyreaction.py`` file from the ``templates/classic/detectors`` directory of the source to your tracker's ``templates`` directory. If you have modified the ``nosyreaction.py`` file from the standard version, you will need to roll your changes into the new file. Migrating from 0.7.1 to 0.8.0 ============================= You *must* fully uninstall previous Roundup version before installing Roundup 0.8.0. If you don't do that, ``roundup-admin install`` command may fail to function properly. 0.8.0 Backend changes --------------------- Backends 'bsddb' and 'bsddb3' are removed. If you are using one of these, you *must* migrate to another backend before upgrading. 0.8.0 API changes ----------------- Class.safeget() was removed from the API. Test your item ids before calling Class.get() instead. 0.8.0 New tracker layout ------------------------ The ``config.py`` file has been replaced by ``config.ini``. You may use the roundup-admin command "genconfig" to generate a new config file:: roundup-admin genconfig /config.ini and modify the values therein based on the contents of your old config.py. In most cases, the names of the config variables are the same. The ``select_db.py`` file has been replaced by a file in the ``db`` directory called ``backend_name``. As you might guess, this file contains just the name of the backend. To figure what the contents of yours should be, use the following table: ================================ ========================= ``select_db.py`` contents ``backend_name`` contents ================================ ========================= from back_anydbm import ... anydbm from back_metakit import ... metakit from back_sqlite import ... sqlite from back_mysql import ... mysql from back_postgresql import ... postgresql ================================ ========================= The ``dbinit.py`` file has been split into two new files, ``initial_data.py`` and ``schema.py``. The contents of this file are: ``initial_data.py`` You don't need one of these as your tracker is already initialised. ``schema.py`` Copy the body of the ``def open(name=None)`` function from your old tracker's ``dbinit.py`` file to this file. As the lines you're copying aren't part of a function definition anymore, one level of indentation needs to be removed (remove only the leading four spaces on each line). The first few lines -- those starting with ``from roundup.hyperdb import ...`` and the ``db = Database(config, name)`` line -- don't need to be copied. Neither do the last few lines -- those starting with ``import detectors``, down to ``return db`` inclusive. You may remove the ``__init__.py`` module from the "detectors" directory as it is no longer used. There's a new way to write extension code for Roundup. If you have code in an ``interfaces.py`` file you should move it. See the `customisation documentation`_ for information about how extensions are now written. Note that some older trackers may use ``interfaces.py`` to customise the mail gateway behaviour. You will need to keep your ``interfaces.py`` file if this is the case. 0.8.0 Permissions Changes ------------------------- The creation of a new item in the user interfaces is now controlled by the "Create" Permission. You will need to add an assignment of this Permission to your users who are allowed to create items. The most common form of this is the following in your ``schema.py`` added just under the current assignation of the Edit Permission:: for cl in 'issue', 'file', 'msg', 'query', 'keyword': p = db.security.getPermission('Create', cl) db.security.addPermissionToRole('User', p) You will need to explicitly let anonymous users access the web interface so that regular users are able to see the login form. Note that almost all trackers will need this Permission. The only situation where it's not required is in a tracker that uses an HTTP Basic Authenticated front-end. It's enabled by adding to your ``schema.py``:: p = db.security.getPermission('Web Access') db.security.addPermissionToRole('Anonymous', p) Finally, you will need to enable permission for your users to edit their own details by adding the following to ``schema.py``:: # Users should be able to edit their own details. Note that this # permission is limited to only the situation where the Viewed or # Edited item is their own. def own_record(db, userid, itemid): '''Determine whether the userid matches the item being accessed.''' return userid == itemid p = db.security.addPermission(name='View', klass='user', check=own_record, description="User is allowed to view their own user details") p = db.security.addPermission(name='Edit', klass='user', check=own_record, description="User is allowed to edit their own user details") db.security.addPermissionToRole('User', p) 0.8.0 Use of TemplatingUtils ---------------------------- If you used custom python functions in TemplatingUtils, they must be moved from interfaces.py to a new file in the ``extensions`` directory. Each Function that should be available through TAL needs to be defined as a toplevel function in the newly created file. Furthermore you add an inititialization function, that registers the functions with the tracker. If you find this too tedious, donfu wrote an automatic init function that takes an existing TemplatingUtils class, and registers all class methods that do not start with an underscore. The following hack should be placed in the ``extensions`` directory alongside other extensions:: class TemplatingUtils: # copy from interfaces.py def init(tracker): util = TemplatingUtils() def setClient(tu): util.client = tu.client return util def execUtil(name): return lambda tu, *args, **kwargs: \ getattr(setClient(tu), name)(*args, **kwargs) for name in dir(util): if callable(getattr(util, name)) and not name.startswith('_'): tracker.registerUtil(name, execUtil(name)) 0.8.0 Logging Configuration --------------------------- See the `administration guide`_ for information about configuring the new logging implemented in 0.8.0. Migrating from 0.7.2 to 0.7.3 ============================= 0.7.3 Configuration ------------------- If you choose, you may specify the directory from which static files are served (those which use the URL component ``@@file``). Currently the directory defaults to the ``TEMPLATES`` configuration variable. You may define a new variable, ``STATIC_FILES`` which overrides this value for static files. Migrating from 0.7.0 to 0.7.2 ============================= 0.7.2 DEFAULT_TIMEZONE is now required -------------------------------------- The DEFAULT_TIMEZONE configuration variable is now required. Add the following to your tracker's ``config.py`` file:: # You may specify a different default timezone, for use when users do not # choose their own in their settings. DEFAULT_TIMEZONE = 0 # specify as numeric hour offest Migrating from 0.7.0 to 0.7.1 ============================= 0.7.1 Permission assignments ---------------------------- If you allow anonymous access to your tracker, you might need to assign some additional View (or Edit if your tracker is that open) permissions to the "anonymous" user. To do so, find the code in your ``dbinit.py`` that says:: for cl in 'issue', 'file', 'msg', 'query', 'keyword': p = db.security.getPermission('View', cl) db.security.addPermissionToRole('User', p) p = db.security.getPermission('Edit', cl) db.security.addPermissionToRole('User', p) for cl in 'priority', 'status': p = db.security.getPermission('View', cl) db.security.addPermissionToRole('User', p) Add add a line:: db.security.addPermissionToRole('Anonymous', p) next to the existing ``'User'`` lines for the Permissions you wish to assign to the anonymous user. Migrating from 0.6 to 0.7 ========================= 0.7.0 Permission assignments ---------------------------- Due to a change in the rendering of web widgets, permissions are now checked on Classes where they previously weren't (this is a good thing). You will need to add some additional Permission assignments for your regular users, or some displays will break. After the following in your tracker's ``dbinit.py``:: # Assign the access and edit Permissions for issue, file and message # to regular users now for cl in 'issue', 'file', 'msg', 'query', 'keyword': p = db.security.getPermission('View', cl) db.security.addPermissionToRole('User', p) p = db.security.getPermission('Edit', cl) db.security.addPermissionToRole('User', p) add:: for cl in 'priority', 'status': p = db.security.getPermission('View', cl) db.security.addPermissionToRole('User', p) 0.7.0 Getting the current user id --------------------------------- The Database.curuserid attribute has been removed. Any code referencing this attribute should be replaced with a call to Database.getuid(). 0.7.0 ZRoundup changes ---------------------- The templates in your tracker's html directory will need updating if you wish to use ZRoundup. If you've not modified those files (or some of them), you may just copy the new versions from the Roundup source in the templates/classic/html directory. If you have modified the html files, then you'll need to manually edit them to change all occurances of special form variables from using the colon ":" special character to the at "@" special character. That is, variables such as:: :action :required :template :remove:messages ... should become:: @action @required @template @remove@messages ... Note that ``tal:`` statements are unaffected. So are TAL expression type prefixes such as ``python:`` and ``string:``. Please ask on the roundup-users mailing list for help if you're unsure. 0.7.0 Edit collision detection ------------------------------ Roundup now detects collisions with editing in the web interface (that is, two people editing the same item at the same time). You must copy the ``_generic.collision.html`` file from Roundup source in the ``templates/classic/html`` directory. to your tracker's ``html`` directory. Migrating from 0.6.x to 0.6.3 ============================= 0.6.3 Configuration ------------------- You will need to copy the file:: templates/classic/detectors/__init__.py to your tracker's ``detectors`` directory, replacing the one already there. This fixes a couple of bugs in that file. Migrating from 0.5 to 0.6 ========================= 0.6.0 Configuration ------------------- Introduced EMAIL_FROM_TAG config variable. This value is inserted into the From: line of nosy email. If the sending user is "Foo Bar", the From: line is usually:: "Foo Bar" the EMAIL_FROM_TAG goes inside the "Foo Bar" quotes like so:: "Foo Bar EMAIL_FROM_TAG" I've altered the mechanism in the detectors __init__.py module so that it doesn't cross-import detectors from other trackers (if you run more than one in a single roundup-server). This change means that you'll need to copy the __init__.py from roundup/templates/classic/detectors/__init__.py to your /detectors/__init__.py. Don't worry, the "classic" __init__ is a one-size-fits-all, so it'll work even if you've added/removed detectors. 0.6.0 Templating changes ------------------------ The ``user.item`` template (in the tracker home "templates" directory) needs to have the following hidden variable added to its form (between the ```` and ```` tags:: 0.6.0 Form handling changes --------------------------- Roundup's form handling capabilities have been significantly expanded. This should not affect users of 0.5 installations - but if you find you're getting errors from form submissions, please ask for help on the Roundup users mailing list: http://lists.sourceforge.net/lists/listinfo/roundup-users See the customisation doc section on `Form Values`__ for documentation of the new form variables possible. __ customizing.html#form-values 0.6.0 Multilingual character set support ---------------------------------------- Added internationalization support. This is done via encoding all data stored in roundup database to utf-8 (unicode encoding). To support utf-8 in web interface you should add the folowing line to your tracker's html/page and html/_generic.help files inside tag:: Since latin characters in utf-8 have the same codes as in ASCII table, this modification is optional for users who use only plain latin characters. After this modification, you will be able to see and enter any world character via web interface. Data received via mail interface also converted to utf-8, however only new messages will be converted. If your roundup database contains some of non-ASCII characters in one of 8-bit encoding, they will not be visible in new unicode environment. Some of such data (e.g. user names, keywords, etc) can be edited by administrator, the others (e.g. messages' contents) is not editable via web interface. Currently there is no tool for converting such data, the only solution is to close appropriate old issues and create new ones with the same content. 0.6.0 User timezone support --------------------------- From version 0.6.0 roundup supports displaying of Date data in user' local timezone if he/she has provided timezone information. To make it possible some modification to tracker's schema and HTML templates are required. First you must add string property 'timezone' to user class in dbinit.py like this:: user = Class(db, "user", username=String(), password=Password(), address=String(), realname=String(), phone=String(), organisation=String(), alternate_addresses=String(), queries=Multilink('query'), roles=String(), timezone=String()) And second - html interface. Add following lines to $TRACKER_HOME/html/user.item template:: Timezone timezone After that all users should be able to provide their timezone information. Timezone should be a positive or negative integer - offset from GMT. After providing timezone, roundup will show all dates values, found in web and mail interfaces in local time. It will also accept any Date info in local time, convert and store it in GMT. 0.6.0 Search page structure --------------------------- In order to accomodate query editing the search page has been restructured. If you want to provide your users with query editing, you should update your search page using the macros detailed in the customisation doc section `Searching on categories`__. __ customizing.html#searching-on-categories Also, the url field in the query class no longer starts with a '?'. You'll need to remove this question mark from the url field to support queries. There's a script in the "tools" directory called ``migrate-queries.py`` that should automatically change any existing queries for you. As always, make a backup of your database before running such a script. 0.6.0 Notes for metakit backend users ------------------------------------- Roundup 0.6.0 introduced searching on ranges of dates and intervals. To support it, some modifications to interval storing routine were made. So if your tracker uses metakit backend and your db schema contains intervals property, searches on that property will not be accurate for db items that was stored before roundup' upgrade. However all new records should be searchable on intervals. It is possible to convert your database to new format: you can export and import back all your data (consult "Migrating backends" in "Maintenance" documentation). After this operation all your interval properties should become searchable. Users of backends others than metakit should not worry about this issue. Migrating from 0.4.x to 0.5.0 ============================= This has been a fairly major revision of Roundup: 1. Brand new, much more powerful, flexible, tasty and nutritious templating. Unfortunately, this means all your current templates are useless. Hopefully the new documentation and examples will be enough to help you make the transition. Please don't hesitate to ask on roundup-users for help (or complete conversions if you're completely stuck)! 2. The database backed got a lot more flexible, allowing Metakit and SQL databases! The only decent SQL database implemented at present is sqlite, but others shouldn't be a whole lot more work. 3. A brand new, highly flexible and much more robust security system including a system of Permissions, Roles and Role assignments to users. You may now define your own Permissions that may be checked in CGI transactions. 4. Journalling has been made less storage-hungry, so has been turned on by default *except* for author, recipient and nosy link/unlink events. You are advised to turn it off in your trackers too. 5. We've changed the terminology from "instance" to "tracker", to ease the learning curve/impact for new users. 6. Because of the above changes, the tracker configuration has seen some major changes. See below for the details. Please, **back up your database** before you start the migration process. This is as simple as copying the "db" directory and all its contents from your tracker to somewhere safe. 0.5.0 Configuration ------------------- First up, rename your ``instance_config.py`` file to just ``config.py``. Then edit your tracker's ``__init__.py`` module. It'll currently look like this:: from instance_config import * try: from dbinit import * except ImportError: pass # in installdir (probably :) from interfaces import * and it needs to be:: import config from dbinit import open, init from interfaces import Client, MailGW Due to the new templating having a top-level ``page`` that defines links for searching, indexes, adding items etc, the following variables are no longer used: - HEADER_INDEX_LINKS - HEADER_ADD_LINKS - HEADER_SEARCH_LINKS - SEARCH_FILTERS - DEFAULT_INDEX - UNASSIGNED_INDEX - USER_INDEX - ISSUE_FILTER The new security implementation will require additions to the dbinit module, but also removes the need for the following tracker config variables: - ANONYMOUS_ACCESS - ANONYMOUS_REGISTER but requires two new variables which define the Roles assigned to users who register through the web and e-mail interfaces: - NEW_WEB_USER_ROLES - NEW_EMAIL_USER_ROLES in both cases, 'User' is a good initial setting. To emulate ``ANONYMOUS_ACCESS='deny'``, remove all "View" Permissions from the "Anonymous" Role. To emulate ``ANONYMOUS_REGISTER='deny'``, remove the "Web Registration" and/or the "Email Registration" Permission from the "Anonymous" Role. See the section on customising security in the `customisation documentation`_ for more information. Finally, the following config variables have been renamed to make more sense: - INSTANCE_HOME -> TRACKER_HOME - INSTANCE_NAME -> TRACKER_NAME - ISSUE_TRACKER_WEB -> TRACKER_WEB - ISSUE_TRACKER_EMAIL -> TRACKER_EMAIL 0.5.0 Schema Specification -------------------------- 0.5.0 Database backend changes ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Your select_db module in your tracker has changed a fair bit. Where it used to contain:: # WARNING: DO NOT EDIT THIS FILE!!! from roundup.backends.back_anydbm import Database it must now contain:: # WARNING: DO NOT EDIT THIS FILE!!! from roundup.backends.back_anydbm import Database, Class, FileClass, IssueClass Yes, I realise the irony of the "DO NOT EDIT THIS FILE" statement :) Note the addition of the Class, FileClass, IssueClass imports. These are very important, as they're going to make the next change work too. You now need to modify the top of the dbinit module in your tracker from:: import instance_config from roundup import roundupdb from select_db import Database from roundup.roundupdb import Class, FileClass class Database(roundupdb.Database, select_db.Database): ''' Creates a hybrid database from: . the selected database back-end from select_db . the roundup extensions from roundupdb ''' pass class IssueClass(roundupdb.IssueClass): ''' issues need the email information ''' pass to:: import config from select_db import Database, Class, FileClass, IssueClass Yes, remove the Database and IssueClass definitions and those other imports. They're not needed any more! Look for places in dbinit.py where ``instance_config`` is used too, and rename them ``config``. 0.5.0 Journalling changes ~~~~~~~~~~~~~~~~~~~~~~~~~ Journalling has been optimised for storage. Journalling of links has been turned back on by default. If your tracker has a large user base, you may wish to turn off journalling of nosy list, message author and message recipient link and unlink events. You do this by adding ``do_journal='no'`` to the Class initialisation in your dbinit. For example, your *msg* class initialisation probably looks like this:: msg = FileClass(db, "msg", author=Link("user"), recipients=Multilink("user"), date=Date(), summary=String(), files=Multilink("file"), messageid=String(), inreplyto=String()) to turn off journalling of author and recipient link events, add ``do_journal='no'`` to the ``author=Link("user")`` part of the statement, like so:: msg = FileClass(db, "msg", author=Link("user", do_journal='no'), recipients=Multilink("user", do_journal='no'), date=Date(), summary=String(), files=Multilink("file"), messageid=String(), inreplyto=String()) Nosy list link event journalling is actually turned off by default now. If you want to turn it on, change to your issue class' nosy list, change its definition from:: issue = IssueClass(db, "issue", assignedto=Link("user"), topic=Multilink("keyword"), priority=Link("priority"), status=Link("status")) to:: issue = IssueClass(db, "issue", nosy=Multilink("user", do_journal='yes'), assignedto=Link("user"), topic=Multilink("keyword"), priority=Link("priority"), status=Link("status")) noting that your definition of the nosy Multilink will override the normal one. 0.5.0 User schema changes ~~~~~~~~~~~~~~~~~~~~~~~~~ Users have two more properties, "queries" and "roles". You'll have something like this in your dbinit module now:: user = Class(db, "user", username=String(), password=Password(), address=String(), realname=String(), phone=String(), organisation=String(), alternate_addresses=String()) user.setkey("username") and you'll need to add the new properties and the new "query" class to it like so:: query = Class(db, "query", klass=String(), name=String(), url=String()) query.setkey("name") # Note: roles is a comma-separated string of Role names user = Class(db, "user", username=String(), password=Password(), address=String(), realname=String(), phone=String(), organisation=String(), alternate_addresses=String(), queries=Multilink('query'), roles=String()) user.setkey("username") The "queries" property is used to store off the user's favourite database queries. The "roles" property is explained below in `0.5.0 Security Settings`_. 0.5.0 Security Settings ~~~~~~~~~~~~~~~~~~~~~~~ See the `security documentation`_ for an explanation of how the new security system works. In a nutshell though, the security is handled as a four step process: 1. Permissions are defined as having a name and optionally a hyperdb class they're specific to, 2. Roles are defined that have one or more Permissions, 3. Users are assigned Roles in their "roles" property, and finally 4. Roundup checks that users have appropriate Permissions at appropriate times (like editing issues). Your tracker dbinit module's *open* function now has to define any Permissions that are specific to your tracker, and also the assignment of Permissions to Roles. At the moment, your open function ends with:: import detectors detectors.init(db) return db and what we need to do is insert some commands that will set up the security parameters. Right above the ``import detectors`` line, you'll want to insert these lines:: # # SECURITY SETTINGS # # new permissions for this schema for cl in 'issue', 'file', 'msg', 'user': db.security.addPermission(name="Edit", klass=cl, description="User is allowed to edit "+cl) db.security.addPermission(name="View", klass=cl, description="User is allowed to access "+cl) # Assign the access and edit permissions for issue, file and message # to regular users now for cl in 'issue', 'file', 'msg': p = db.security.getPermission('View', cl) db.security.addPermissionToRole('User', p) p = db.security.getPermission('Edit', cl) db.security.addPermissionToRole('User', p) # and give the regular users access to the web and email interface p = db.security.getPermission('Web Access') db.security.addPermissionToRole('User', p) p = db.security.getPermission('Email Access') db.security.addPermissionToRole('User', p) # May users view other user information? Comment these lines out # if you don't want them to p = db.security.getPermission('View', 'user') db.security.addPermissionToRole('User', p) # Assign the appropriate permissions to the anonymous user's Anonymous # Role. Choices here are: # - Allow anonymous users to register through the web p = db.security.getPermission('Web Registration') db.security.addPermissionToRole('Anonymous', p) # - Allow anonymous (new) users to register through the email gateway p = db.security.getPermission('Email Registration') db.security.addPermissionToRole('Anonymous', p) # - Allow anonymous users access to the "issue" class of data # Note: this also grants access to related information like files, # messages, statuses etc that are linked to issues #p = db.security.getPermission('View', 'issue') #db.security.addPermissionToRole('Anonymous', p) # - Allow anonymous users access to edit the "issue" class of data # Note: this also grants access to create related information like # files and messages etc that are linked to issues #p = db.security.getPermission('Edit', 'issue') #db.security.addPermissionToRole('Anonymous', p) # oh, g'wan, let anonymous access the web interface too p = db.security.getPermission('Web Access') db.security.addPermissionToRole('Anonymous', p) Note in the comments there the places where you might change the permissions to restrict users or grant users more access. If you've created additional classes that users should be able to edit and view, then you should add them to the "new permissions for this schema" section at the start of the security block. Then add them to the "Assign the access and edit permissions" section too, so people actually have the new Permission you've created. One final change is needed that finishes off the security system's initialisation. We need to add a call to ``db.post_init()`` at the end of the dbinit open() function. Add it like this:: import detectors detectors.init(db) # schema is set up - run any post-initialisation db.post_init() return db You may verify the setup of Permissions and Roles using the new "``roundup-admin security``" command. 0.5.0 User changes ~~~~~~~~~~~~~~~~~~ To support all those schema changes, you'll need to massage your user database a little too, to: 1. make sure there's an "anonymous" user - this user is mandatory now and is the one that unknown users are logged in as. 2. make sure all users have at least one Role. If you don't have the "anonymous" user, create it now with the command:: roundup-admin create user username=anonymous roles=Anonymous making sure the capitalisation is the same as above. Once you've done that, you'll need to set the roles property on all users to a reasonable default. The admin user should get "Admin", the anonymous user "Anonymous" and all other users "User". The ``fixroles.py`` script in the tools directory will do this. Run it like so (where python is your python 2+ binary):: python tools/fixroles.py -i fixroles 0.5.0 CGI interface changes --------------------------- The CGI interface code was completely reorganised and largely rewritten. The end result is that this section of your tracker interfaces module will need changing from:: from roundup import cgi_client, mailgw from roundup.i18n import _ class Client(cgi_client.Client): ''' derives basic CGI implementation from the standard module, with any specific extensions ''' pass to:: from roundup import mailgw from roundup.cgi import client class Client(client.Client): ''' derives basic CGI implementation from the standard module, with any specific extensions ''' pass You will also need to install the new version of roundup.cgi from the source cgi-bin directory if you're using it. 0.5.0 HTML templating --------------------- You'll want to make a backup of your current tracker html directory. You should then copy the html directory from the Roundup source "classic" template and modify it according to your local schema changes. If you need help with the new templating system, please ask questions on the roundup-users mailing list (available through the roundup web page on sourceforge, http://www.roundup-tracker.org/. 0.5.0 Detectors --------------- The nosy reactor has been updated to handle the tracker not having an "assignedto" property on issues. You may want to copy it into your tracker's detectors directory. Chances are you've already fixed it though :) Migrating from 0.4.1 to 0.4.2 ============================= 0.4.2 Configuration ------------------- The USER_INDEX definition introduced in 0.4.1 was too restrictive in its allowing replacement of 'assignedto' with the user's userid. Users must change the None value of 'assignedto' to 'CURRENT USER' (the string, in quotes) for the replacement behaviour to occur now. The new configuration variables are: - EMAIL_KEEP_QUOTED_TEXT - EMAIL_LEAVE_BODY_UNCHANGED - ADD_RECIPIENTS_TO_NOSY See the sample configuration files in:: /roundup/templates/classic/instance_config.py and:: /roundup/templates/extended/instance_config.py and the `customisation documentation`_ for information on how they're used. 0.4.2 Changes to detectors -------------------------- You will need to copy the detectors from the distribution into your instance home "detectors" directory. If you used the classic schema, the detectors are in:: /roundup/templates/classic/detectors/ If you used the extended schema, the detectors are in:: /roundup/templates/extended/detectors/ The change means that schema-specific code has been removed from the mail gateway and cgi interface and made into auditors: - nosyreactor.py has now got an updatenosy auditor which updates the nosy list with author, recipient and assignedto information. - statusauditor.py makes the unread or resolved -> chatting changes and presets the status of an issue to unread. There's also a bug or two fixed in the nosyreactor code. 0.4.2 HTML templating changes ----------------------------- The link() htmltemplate function now has a "showid" option for links and multilinks. When true, it only displays the linked item id as the anchor text. The link value is displayed as a tooltip using the title anchor attribute. To use in eg. the superseder field, have something like this::
View:
The stylesheets have been cleaned up too. You may want to use the newer versions in:: /roundup/templates/