From: richard Date: Tue, 25 Feb 2003 10:19:32 +0000 (+0000) Subject: - registration is now a two-step process, with confirmation from the email X-Git-Url: https://git.tokkee.org/?a=commitdiff_plain;h=a3cc3b486c6b9677f862158a9891428a106b2538;p=roundup.git - registration is now a two-step process, with confirmation from the email address supplied in the registration form - fixed sf bug 687771 too (now handle all cases of @/:) git-svn-id: http://svn.roundup-tracker.org/svnroot/roundup/trunk@1544 57a73879-2fb5-44c3-a270-3262357dd7e2 --- diff --git a/CHANGES.txt b/CHANGES.txt index eaacd19..2e9c214 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -59,6 +59,8 @@ are given with the most recent entry first. - re-worked detectors initialisation - woohoo, no more cross-importing! - can now configure CC to author only for messages creating issues (sf feature 625808) +- registration is now a two-step process, with confirmation from the email + address supplied in the registration form 2003-??-?? 0.5.6 diff --git a/TODO.txt b/TODO.txt index 618d730..3b995f2 100644 --- a/TODO.txt +++ b/TODO.txt @@ -29,6 +29,7 @@ pending mailgw Use in-reply-to for determining message lineage when subject line lets us down pending mailgw Allow different brackets delimiting [issueNNN] in Subject pending email email sig could use a "remove me from this list" + /?:remove:nosy=me pending project switch to a Roundup instance for Roundup bug/feature tracking pending security authenticate over a secure connection pending security optionally auth with Basic HTTP auth instead of cookies diff --git a/roundup/__init__.py b/roundup/__init__.py index e46226e..e7d2da0 100644 --- a/roundup/__init__.py +++ b/roundup/__init__.py @@ -15,7 +15,7 @@ # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. # -# $Id: __init__.py,v 1.19 2003-02-20 07:11:39 richard Exp $ +# $Id: __init__.py,v 1.20 2003-02-25 10:19:31 richard Exp $ ''' Roundup - issue tracking for knowledge workers. @@ -67,6 +67,6 @@ written by Ka-Ping Yee in the "doc" directory. If nothing else, it has a much prettier cake :) ''' -__version__ = '0.6.0' +__version__ = '0.6.0pr1' # vim: set filetype=python ts=4 sw=4 et si diff --git a/roundup/admin.py b/roundup/admin.py index e13ee77..94e3846 100644 --- a/roundup/admin.py +++ b/roundup/admin.py @@ -16,7 +16,7 @@ # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. # -# $Id: admin.py,v 1.37 2003-02-15 23:19:01 kedder Exp $ +# $Id: admin.py,v 1.38 2003-02-25 10:19:31 richard Exp $ '''Administration commands for maintaining Roundup trackers. ''' @@ -328,7 +328,8 @@ Command help: # XXX perform a unit test based on the user's selections # install! - init.install(tracker_home, template, backend) + init.install(tracker_home, template) + init.write_select_db(tracker_home, backend) print _(''' You should now edit the tracker configuration file: diff --git a/roundup/backends/back_anydbm.py b/roundup/backends/back_anydbm.py index ab7c06a..71ffdb0 100644 --- a/roundup/backends/back_anydbm.py +++ b/roundup/backends/back_anydbm.py @@ -15,7 +15,7 @@ # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. # -#$Id: back_anydbm.py,v 1.104 2003-02-18 01:57:38 richard Exp $ +#$Id: back_anydbm.py,v 1.105 2003-02-25 10:19:31 richard Exp $ ''' This module defines a backend that saves the hyperdatabase in a database chosen by anydbm. It is guaranteed to always be available in python @@ -26,7 +26,7 @@ serious bugs, and is not available) import whichdb, anydbm, os, marshal, re, weakref, string, copy from roundup import hyperdb, date, password, roundupdb, security from blobfiles import FileStorage -from sessions import Sessions +from sessions import Sessions, OneTimeKeys from roundup.indexer import Indexer from roundup.backends import locking from roundup.hyperdb import String, Password, Date, Interval, Link, \ @@ -68,6 +68,7 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database): self.transactions = [] self.indexer = Indexer(self.dir) self.sessions = Sessions(self.config) + self.otks = OneTimeKeys(self.config) self.security = security.Security(self) # ensure files are group readable and writable os.umask(0002) diff --git a/roundup/backends/back_metakit.py b/roundup/backends/back_metakit.py index 91a5934..234a817 100755 --- a/roundup/backends/back_metakit.py +++ b/roundup/backends/back_metakit.py @@ -30,7 +30,7 @@ ''' from roundup import hyperdb, date, password, roundupdb, security import metakit -from sessions import Sessions +from sessions import Sessions, OneTimeKeys import re, marshal, os, sys, weakref, time, calendar from roundup import indexer import locking @@ -60,6 +60,7 @@ class _Database(hyperdb.Database, roundupdb.Database): self._db = self.__open() self.indexer = Indexer(self.config.DATABASE, self._db) self.sessions = Sessions(self.config) + self.otks = OneTimeKeys(self.config) self.security = security.Security(self) os.umask(0002) diff --git a/roundup/backends/rdbms_common.py b/roundup/backends/rdbms_common.py index e663a19..5b6aab4 100644 --- a/roundup/backends/rdbms_common.py +++ b/roundup/backends/rdbms_common.py @@ -1,4 +1,4 @@ -# $Id: rdbms_common.py,v 1.34 2003-02-18 01:57:39 richard Exp $ +# $Id: rdbms_common.py,v 1.35 2003-02-25 10:19:32 richard Exp $ ''' Relational database (SQL) backend common code. Basics: @@ -33,7 +33,7 @@ from roundup.backends import locking # support from blobfiles import FileStorage from roundup.indexer import Indexer -from sessions import Sessions +from sessions import Sessions, OneTimeKeys # number of rows to keep in memory ROW_CACHE_SIZE = 100 @@ -53,6 +53,7 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database): self.classes = {} self.indexer = Indexer(self.dir) self.sessions = Sessions(self.config) + self.otks = OneTimeKeys(self.config) self.security = security.Security(self) # additional transaction support for external files and the like diff --git a/roundup/backends/sessions.py b/roundup/backends/sessions.py index 31c48c9..9c5325a 100644 --- a/roundup/backends/sessions.py +++ b/roundup/backends/sessions.py @@ -1,15 +1,19 @@ -#$Id: sessions.py,v 1.3 2002-09-10 00:11:50 richard Exp $ +#$Id: sessions.py,v 1.4 2003-02-25 10:19:32 richard Exp $ ''' This module defines a very basic store that's used by the CGI interface -to store session information. +to store session and one-time-key information. + +Yes, it's called "sessions" - because originally it only defined a session +class. It's now also used for One Time Key handling too. + ''' import anydbm, whichdb, os, marshal -class Sessions: - ''' Back onto an anydbm store. +class BasicDatabase: + ''' Provide a nice encapsulation of an anydbm store. - Keys are session id strings, values are marshalled data. + Keys are id strings, values are automatically marshalled data. ''' def __init__(self, config): self.config = config @@ -18,7 +22,7 @@ class Sessions: os.umask(0002) def clear(self): - path = os.path.join(self.dir, 'sessions') + path = os.path.join(self.dir, self.name) if os.path.exists(path): os.remove(path) elif os.path.exists(path+'.db'): # dbm appends .db @@ -38,26 +42,33 @@ class Sessions: db_type = 'dbm' return db_type - def get(self, sessionid, value): + def get(self, infoid, value): db = self.opendb('c') try: - if db.has_key(sessionid): - values = marshal.loads(db[sessionid]) + if db.has_key(infoid): + values = marshal.loads(db[infoid]) else: return None return values.get(value, None) finally: db.close() - def set(self, sessionid, **newvalues): + def getall(self, infoid): + db = self.opendb('c') + try: + return marshal.loads(db[infoid]) + finally: + db.close() + + def set(self, infoid, **newvalues): db = self.opendb('c') try: - if db.has_key(sessionid): - values = marshal.loads(db[sessionid]) + if db.has_key(infoid): + values = marshal.loads(db[infoid]) else: values = {} values.update(newvalues) - db[sessionid] = marshal.dumps(values) + db[infoid] = marshal.dumps(values) finally: db.close() @@ -68,11 +79,11 @@ class Sessions: finally: db.close() - def destroy(self, sessionid): + def destroy(self, infoid): db = self.opendb('c') try: - if db.has_key(sessionid): - del db[sessionid] + if db.has_key(infoid): + del db[infoid] finally: db.close() @@ -81,7 +92,7 @@ class Sessions: eccentricities. ''' # figure the class db type - path = os.path.join(os.getcwd(), self.dir, 'sessions') + path = os.path.join(os.getcwd(), self.dir, self.name) db_type = self.determine_db_type(path) # new database? let anydbm pick the best dbm @@ -94,3 +105,10 @@ class Sessions: def commit(self): pass + +class Sessions(BasicDatabase): + name = 'sessions' + +class OneTimeKeys(BasicDatabase): + name = 'otks' + diff --git a/roundup/cgi/client.py b/roundup/cgi/client.py index 1f23fae..435f733 100644 --- a/roundup/cgi/client.py +++ b/roundup/cgi/client.py @@ -1,19 +1,18 @@ -# $Id: client.py,v 1.96 2003-02-20 07:13:14 richard Exp $ +# $Id: client.py,v 1.97 2003-02-25 10:19:32 richard Exp $ __doc__ = """ WWW request handler (also used in the stand-alone server). """ import os, os.path, cgi, StringIO, urlparse, re, traceback, mimetypes, urllib -import binascii, Cookie, time, random +import binascii, Cookie, time, random, MimeWriter, smtplib, socket, quopri from roundup import roundupdb, date, hyperdb, password from roundup.i18n import _ - from roundup.cgi.templating import Templates, HTMLRequest, NoTemplate from roundup.cgi import cgitb - from roundup.cgi.PageTemplates import PageTemplate +from roundup.rfc2822 import encode_header class HTTPException(Exception): pass @@ -104,6 +103,8 @@ class Client: FV_OK_MESSAGE = re.compile(r'[@:]ok_message') FV_ERROR_MESSAGE = re.compile(r'[@:]error_message') + FV_QUERYNAME = re.compile(r'[@:]queryname') + # edit form variable handling (see unit tests) FV_LABELS = r''' ^( @@ -236,8 +237,8 @@ class Client: except SendStaticFile, file: self.serve_static_file(str(file)) except Unauthorised, message: - self.classname=None - self.template='' + self.classname = None + self.template = '' self.error_message.append(message) self.write(self.renderContext()) except NotFound: @@ -275,7 +276,7 @@ class Client: sessions = self.db.sessions # look up the user session cookie - cookie = Cookie.Cookie(self.env.get('HTTP_COOKIE', '')) + cookie = Cookie.SimpleCookie(self.env.get('HTTP_COOKIE', '')) user = 'anonymous' # bump the "revision" of the cookie since the format changed @@ -454,6 +455,7 @@ class Client: ('editCSV', 'editCSVAction'), ('new', 'newItemAction'), ('register', 'registerAction'), + ('confrego', 'confRegoAction'), ('login', 'loginAction'), ('logout', 'logout_action'), ('search', 'searchAction'), @@ -461,7 +463,7 @@ class Client: ('show', 'showAction'), ) def handle_action(self): - ''' Determine whether there should be an _action called. + ''' Determine whether there should be an Action called. The action is defined by the form variable :action which identifies the method on this object to call. The four basic @@ -469,33 +471,31 @@ class Client: "edit" -> self.editItemAction "new" -> self.newItemAction "register" -> self.registerAction + "confrego" -> self.confRegoAction "login" -> self.loginAction "logout" -> self.logout_action "search" -> self.searchAction "retire" -> self.retireAction ''' - if not self.form.has_key(':action'): + if self.form.has_key(':action'): + action = self.form[':action'].value.lower() + elif self.form.has_key('@action'): + action = self.form['@action'].value.lower() + else: return None try: # get the action, validate it - action = self.form[':action'].value for name, method in self.actions: if name == action: break else: raise ValueError, 'No such action "%s"'%action - # call the mapped action getattr(self, method)() except Redirect: raise except Unauthorised: raise - except: - self.db.rollback() - s = StringIO.StringIO() - traceback.print_exc(None, s) - self.error_message.append('
%s
'%cgi.escape(s.getvalue())) def write(self, content): if not self.headers_done: @@ -651,18 +651,16 @@ class Client: # Let the user know what's going on self.ok_message.append(_('You are logged out')) + chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' def registerAction(self): '''Attempt to create a new user based on the contents of the form and then set the cookie. return 1 on successful login ''' - # create the new user - cl = self.db.user - # parse the props from the form try: - props = self.parsePropsFromForm() + props = self.parsePropsFromForm()[0][('user', None)] except (ValueError, KeyError), message: self.error_message.append(_('Error: ') + str(message)) return @@ -671,19 +669,125 @@ class Client: if not self.registerPermission(props): raise Unauthorised, _("You do not have permission to register") + try: + self.db.user.lookup(props['username']) + self.error_message.append('Error: A user with the username "%s" ' + 'already exists'%props['username']) + return + except KeyError: + pass + + # generate the one-time-key and store the props for later + otk = ''.join([random.choice(self.chars) for x in range(32)]) + for propname, proptype in self.db.user.getprops().items(): + value = props.get(propname, None) + if value is None: + pass + elif isinstance(proptype, hyperdb.Date): + props[propname] = str(value) + elif isinstance(proptype, hyperdb.Interval): + props[propname] = str(value) + elif isinstance(proptype, hyperdb.Password): + props[propname] = str(value) + self.db.otks.set(otk, **props) + + # send email to the user's email address + message = StringIO.StringIO() + writer = MimeWriter.MimeWriter(message) + tracker_name = self.db.config.TRACKER_NAME + s = 'Complete your registration to %s'%tracker_name + writer.addheader('Subject', encode_header(s)) + writer.addheader('To', props['address']) + writer.addheader('From', roundupdb.straddr((tracker_name, + self.db.config.ADMIN_EMAIL))) + writer.addheader('Date', time.strftime("%a, %d %b %Y %H:%M:%S +0000", + time.gmtime())) + # add a uniquely Roundup header to help filtering + writer.addheader('X-Roundup-Name', tracker_name) + # avoid email loops + writer.addheader('X-Roundup-Loop', 'hello') + writer.addheader('Content-Transfer-Encoding', 'quoted-printable') + body = writer.startbody('text/plain; charset=utf-8') + + # message body, encoded quoted-printable + content = StringIO.StringIO(''' +To complete your registration of the user "%(name)s" with %(tracker)s, +please visit the following URL: + + http://localhost:8001/test/?@action=confrego&otk=%(otk)s +'''%{'name': props['username'], 'tracker': tracker_name, 'url': self.base, + 'otk': otk}) + quopri.encode(content, body, 0) + + # now try to send the message + try: + # send the message as admin so bounces are sent there + # instead of to roundup + smtp = smtplib.SMTP(self.db.config.MAILHOST) + smtp.sendmail(self.db.config.ADMIN_EMAIL, [props['address']], + message.getvalue()) + except socket.error, value: + self.error_message.append("Error: couldn't send " + "confirmation email: mailhost %s"%value) + return + except smtplib.SMTPException, value: + self.error_message.append("Error: couldn't send " + "confirmation email: %s"%value) + return + + # commit changes to the database + self.db.commit() + + # redirect to the "you're almost there" page + raise Redirect, '%s?:template=rego_step1_done'%self.base + + def registerPermission(self, props): + ''' Determine whether the user has permission to register + + Base behaviour is to check the user has "Web Registration". + ''' + # registration isn't allowed to supply roles + if props.has_key('roles'): + return 0 + if self.db.security.hasPermission('Web Registration', self.userid): + return 1 + return 0 + + def confRegoAction(self): + ''' Grab the OTK, use it to load up the new user details + ''' + # pull the rego information out of the otk database + otk = self.form['otk'].value + props = self.db.otks.getall(otk) + for propname, proptype in self.db.user.getprops().items(): + value = props.get(propname, None) + if value is None: + pass + elif isinstance(proptype, hyperdb.Date): + props[propname] = date.Date(value) + elif isinstance(proptype, hyperdb.Interval): + props[propname] = date.Interval(value) + elif isinstance(proptype, hyperdb.Password): + props[propname] = password.Password() + props[propname].unpack(value) + # re-open the database as "admin" if self.user != 'admin': self.opendb('admin') - + # create the new user cl = self.db.user - try: +# XXX we need to make the "default" page be able to display errors! +# try: + if 1: props['roles'] = self.instance.config.NEW_WEB_USER_ROLES - self.userid = cl.create(**props['user']) + self.userid = cl.create(**props) + # clear the props from the otk database + self.db.otks.destroy(otk) self.db.commit() - except (ValueError, KeyError), message: - self.error_message.append(message) - return +# except (ValueError, KeyError), message: +# self.error_message.append(str(message)) +# return # log the new user in self.user = cl.get(self.userid, 'username') @@ -702,20 +806,8 @@ class Client: message = _('You are now registered, welcome!') # redirect to the item's edit page - raise Redirect, '%s%s%s?+ok_message=%s'%( - self.base, self.classname, self.userid, urllib.quote(message)) - - def registerPermission(self, props): - ''' Determine whether the user has permission to register - - Base behaviour is to check the user has "Web Registration". - ''' - # registration isn't allowed to supply roles - if props.has_key('roles'): - return 0 - if self.db.security.hasPermission('Web Registration', self.userid): - return 1 - return 0 + raise Redirect, '%suser%s?@ok_message=%s'%( + self.base, self.userid, urllib.quote(message)) def editItemAction(self): ''' Perform an edit of an item in the database. @@ -723,16 +815,18 @@ class Client: See parsePropsFromForm and _editnodes for special variables ''' # parse the props from the form - if 1: +# XXX reinstate exception handling # try: + if 1: props, links = self.parsePropsFromForm() # except (ValueError, KeyError), message: # self.error_message.append(_('Error: ') + str(message)) # return # handle the props - if 1: +# XXX reinstate exception handling # try: + if 1: message = self._editnodes(props, links) # except (ValueError, KeyError, IndexError), message: # self.error_message.append(_('Error: ') + str(message)) @@ -832,8 +926,6 @@ class Client: ''' Use the props in all_props to perform edit and creation, then use the link specs in all_links to do linking. ''' -# print '='*75 -# print 'ALL_PROPS', all_props # figure dependencies and re-work links deps = {} links = {} @@ -848,10 +940,6 @@ class Client: deps.setdefault((cn, nodeid), []).append(value) links.setdefault(value, []).append((cn, nodeid, propname)) -# print '*'*75 -# print 'LINKS', links -# print 'DEPS', deps - # figure chained dependencies ordering order = [] done = {} @@ -862,12 +950,10 @@ class Client: if done.has_key(needed): continue tlist = deps.get(needed, []) -# print 'SOLVING', needed, tlist for target in tlist: if not done.has_key(target): break else: -# print 'DONE', needed done[needed] = 1 order.append(needed) change = 1 @@ -1060,10 +1146,16 @@ class Client: _('You do not have permission to search %s' %self.classname)) # add a faked :filter form variable for each filtering prop -# XXX migrate to new : @ + props = self.db.classes[self.classname].getprops() + queryname = '' for key in self.form.keys(): - if not props.has_key(key): continue + # special vars + if self.FV_QUERYNAME.match(key): + queryname = self.form[key].value.strip() + continue + + if not props.has_key(key): + continue if isinstance(self.form[key], type([])): # search for at least one entry which is not empty for minifield in self.form[key]: @@ -1073,32 +1165,30 @@ class Client: continue else: if not self.form[key].value: continue - self.form.value.append(cgi.MiniFieldStorage(':filter', key)) + self.form.value.append(cgi.MiniFieldStorage('@filter', key)) # handle saving the query params - if self.form.has_key(':queryname'): - queryname = self.form[':queryname'].value.strip() - if queryname: - # parse the environment and figure what the query _is_ - req = HTMLRequest(self) - url = req.indexargs_href('', {}) - - # handle editing an existing query - try: - qid = self.db.query.lookup(queryname) - self.db.query.set(qid, klass=self.classname, url=url) - except KeyError: - # create a query - qid = self.db.query.create(name=queryname, - klass=self.classname, url=url) + if queryname: + # parse the environment and figure what the query _is_ + req = HTMLRequest(self) + url = req.indexargs_href('', {}) + + # handle editing an existing query + try: + qid = self.db.query.lookup(queryname) + self.db.query.set(qid, klass=self.classname, url=url) + except KeyError: + # create a query + qid = self.db.query.create(name=queryname, + klass=self.classname, url=url) - # and add it to the user's query multilink - queries = self.db.user.get(self.userid, 'queries') - queries.append(qid) - self.db.user.set(self.userid, queries=queries) + # and add it to the user's query multilink + queries = self.db.user.get(self.userid, 'queries') + queries.append(qid) + self.db.user.set(self.userid, queries=queries) - # commit the query change to the database - self.db.commit() + # commit the query change to the database + self.db.commit() def searchPermission(self): ''' Determine whether the user has permission to search this class. @@ -1152,12 +1242,18 @@ class Client: return 1 - def showAction(self): - ''' Show a node + def showAction(self, typere=re.compile('[@:]type'), + numre=re.compile('[@:]number')): + ''' Show a node of a particular class/id ''' -# XXX allow : @ + - t = self.form[':type'].value - n = self.form[':number'].value + t = n = '' + for key in self.form.keys(): + if typere.match(key): + t = self.form[key].value.strip() + elif numre.match(key): + n = self.form[key].value.strip() + if not t: + raise ValueError, 'Invalid %s number'%t url = '%s%s%s'%(self.db.config.TRACKER_WEB, t, n) raise Redirect, url diff --git a/roundup/cgi/templating.py b/roundup/cgi/templating.py index 3e174c1..dff1bbc 100644 --- a/roundup/cgi/templating.py +++ b/roundup/cgi/templating.py @@ -1568,37 +1568,48 @@ env: %(env)s return '\n'.join(l) def indexargs_url(self, url, args): - ''' embed the current index args in a URL ''' + ''' Embed the current index args in a URL + ''' sc = self.special_char l = ['%s=%s'%(k,v) for k,v in args.items()] - if self.columns and not args.has_key(':columns'): + + # pull out the special values (prefixed by @ or :) + specials = {} + for key in args.keys(): + if key[0] in '@:': + specials[key[1:]] = args[key] + + # ok, now handle the specials we received in the request + if self.columns and not specials.has_key('columns'): l.append(sc+'columns=%s'%(','.join(self.columns))) - if self.sort[1] is not None and not args.has_key(':sort'): + if self.sort[1] is not None and not specials.has_key('sort'): if self.sort[0] == '-': val = '-'+self.sort[1] else: val = self.sort[1] l.append(sc+'sort=%s'%val) - if self.group[1] is not None and not args.has_key(':group'): + if self.group[1] is not None and not specials.has_key('group'): if self.group[0] == '-': val = '-'+self.group[1] else: val = self.group[1] l.append(sc+'group=%s'%val) - if self.filter and not args.has_key(':filter'): + if self.filter and not specials.has_key('filter'): l.append(sc+'filter=%s'%(','.join(self.filter))) + if self.search_text and not specials.has_key('search_text'): + l.append(sc+'search_text=%s'%self.search_text) + if not specials.has_key('pagesize'): + l.append(sc+'pagesize=%s'%self.pagesize) + if not specials.has_key('startwith'): + l.append(sc+'startwith=%s'%self.startwith) + + # finally, the remainder of the filter args in the request for k,v in self.filterspec.items(): if not args.has_key(k): if type(v) == type([]): l.append('%s=%s'%(k, ','.join(v))) else: l.append('%s=%s'%(k, v)) - if self.search_text and not args.has_key(':search_text'): - l.append(sc+'search_text=%s'%self.search_text) - if not args.has_key(':pagesize'): - l.append(sc+'pagesize=%s'%self.pagesize) - if not args.has_key(':startwith'): - l.append(sc+'startwith=%s'%self.startwith) return '%s?%s'%(url, '&'.join(l)) indexargs_href = indexargs_url diff --git a/roundup/init.py b/roundup/init.py index b0e2b5b..9a7ffc5 100644 --- a/roundup/init.py +++ b/roundup/init.py @@ -15,7 +15,7 @@ # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. # -# $Id: init.py,v 1.24 2002-09-10 12:44:42 richard Exp $ +# $Id: init.py,v 1.25 2003-02-25 10:19:31 richard Exp $ __doc__ = """ Init (create) a roundup instance. @@ -37,7 +37,6 @@ def copytree(src, dst, symlinks=0): links are copied. This was copied from shutil.py in std lib. - """ names = os.listdir(src) try: @@ -55,7 +54,7 @@ def copytree(src, dst, symlinks=0): else: install_util.copyDigestedFile(srcname, dstname) -def install(instance_home, template, backend): +def install(instance_home, template): '''Install an instance using the named template and backend. instance_home - the directory to place the instance data in @@ -97,6 +96,10 @@ def install(instance_home, template, backend): builder.installHtmlBase(template_name, instance_home) + +def write_select_db(instance_home, backend): + ''' Write the file that selects the backend for the tracker + ''' # now select database db = '''# WARNING: DO NOT EDIT THIS FILE!!! from roundup.backends.back_%s import Database, Class, FileClass, IssueClass