Code

minor edit
[roundup.git] / roundup / admin.py
index ef7f9217585797379646436c14bdb92d52096994..08fc092ddeed0f8877979e22234db22fefc2478b 100644 (file)
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 # 
-# $Id: admin.py,v 1.8 2002-02-27 03:28:21 richard Exp $
+# $Id: admin.py,v 1.23 2002-08-19 02:53:27 richard Exp $
 
-import sys, os, getpass, getopt, re, UserDict, shlex
+import sys, os, getpass, getopt, re, UserDict, shlex, shutil
 try:
     import csv
 except ImportError:
     csv = None
 from roundup import date, hyperdb, roundupdb, init, password, token
+from roundup import __version__ as roundup_version
 import roundup.instance
 from roundup.i18n import _
 
@@ -248,20 +249,31 @@ Command help:
         backends = roundup.backends.__all__
         print _('Back ends:'), ', '.join(backends)
 
-
-    def do_initialise(self, instance_home, args):
-        '''Usage: initialise [template [backend [admin password]]]
-        Initialise a new Roundup instance.
+    def do_install(self, instance_home, args):
+        '''Usage: install [template [backend [admin password]]]
+        Install a new Roundup instance.
 
         The command will prompt for the instance home directory (if not supplied
         through INSTANCE_HOME or the -i option). The template, backend and admin
         password may be specified on the command-line as arguments, in that
         order.
 
+        The initialise command must be called after this command in order
+        to initialise the instance's database. You may edit the instance's
+        initial database contents before running that command by editing
+        the instance's dbinit.py module init() function.
+
         See also initopts help.
         '''
         if len(args) < 1:
             raise UsageError, _('Not enough arguments supplied')
+
+        # make sure the instance home can be created
+        parent = os.path.split(instance_home)[0]
+        if not os.path.exists(parent):
+            raise UsageError, _('Instance home parent directory "%(parent)s"'
+                ' does not exist')%locals()
+
         # select template
         import roundup.templates
         templates = roundup.templates.listTemplates()
@@ -273,6 +285,7 @@ Command help:
             if not template:
                 template = 'classic'
 
+        # select hyperdb backend
         import roundup.backends
         backends = roundup.backends.__all__
         backend = len(args) > 2 and args[2] or ''
@@ -282,15 +295,64 @@ Command help:
             backend = raw_input(_('Select backend [anydbm]: ')).strip()
             if not backend:
                 backend = 'anydbm'
-        if len(args) > 3:
-            adminpw = confirm = args[3]
+
+        # install!
+        init.install(instance_home, template, backend)
+
+        print _('''
+ You should now edit the instance configuration file:
+   %(instance_config_file)s
+ ... at a minimum, you must set MAILHOST, MAIL_DOMAIN and ADMIN_EMAIL.
+
+ If you wish to modify the default schema, you should also edit the database
+ initialisation file:
+   %(database_config_file)s
+ ... see the documentation on customizing for more information.
+''')%{
+    'instance_config_file': os.path.join(instance_home, 'instance_config.py'),
+    'database_config_file': os.path.join(instance_home, 'dbinit.py')
+}
+        return 0
+
+
+    def do_initialise(self, instance_home, args):
+        '''Usage: initialise [adminpw]
+        Initialise a new Roundup instance.
+
+        The administrator details will be set at this step.
+
+        Execute the instance's initialisation function dbinit.init()
+        '''
+        # password
+        if len(args) > 1:
+            adminpw = args[1]
         else:
             adminpw = ''
             confirm = 'x'
-        while adminpw != confirm:
-            adminpw = getpass.getpass(_('Admin Password: '))
-            confirm = getpass.getpass(_('       Confirm: '))
-        init.init(instance_home, template, backend, adminpw)
+            while adminpw != confirm:
+                adminpw = getpass.getpass(_('Admin Password: '))
+                confirm = getpass.getpass(_('       Confirm: '))
+
+        # make sure the instance home is installed
+        if not os.path.exists(instance_home):
+            raise UsageError, _('Instance home does not exist')%locals()
+        if not os.path.exists(os.path.join(instance_home, 'html')):
+            raise UsageError, _('Instance has not been installed')%locals()
+
+        # is there already a database?
+        if os.path.exists(os.path.join(instance_home, 'db')):
+            print _('WARNING: The database is already initialised!')
+            print _('If you re-initialise it, you will lose all the data!')
+            ok = raw_input(_('Erase it? Y/[N]: ')).strip()
+            if ok.lower() != 'y':
+                return 0
+
+            # nuke it
+            shutil.rmtree(os.path.join(instance_home, 'db'))
+
+        # GO
+        init.initialise(instance_home, adminpw)
+
         return 0
 
 
@@ -308,8 +370,8 @@ Command help:
         for designator in designators:
             # decode the node designator
             try:
-                classname, nodeid = roundupdb.splitDesignator(designator)
-            except roundupdb.DesignatorError, message:
+                classname, nodeid = hyperdb.splitDesignator(designator)
+            except hyperdb.DesignatorError, message:
                 raise UsageError, message
 
             # get the class
@@ -348,8 +410,8 @@ Command help:
         for designator in designators:
             # decode the node designator
             try:
-                classname, nodeid = roundupdb.splitDesignator(designator)
-            except roundupdb.DesignatorError, message:
+                classname, nodeid = hyperdb.splitDesignator(designator)
+            except hyperdb.DesignatorError, message:
                 raise UsageError, message
 
             # get the class
@@ -376,6 +438,10 @@ Command help:
                     props[key] = value
                 elif isinstance(proptype, hyperdb.Multilink):
                     props[key] = value.split(',')
+                elif isinstance(proptype, hyperdb.Boolean):
+                    props[key] = value.lower() in ('yes', 'true', 'on', '1')
+                elif isinstance(proptype, hyperdb.Number):
+                    props[key] = int(value)
 
             # try the set
             try:
@@ -424,9 +490,6 @@ Command help:
                 except TypeError:
                     raise UsageError, _('%(classname)s has no key property"')%{
                         'classname': link_class.classname}
-                except KeyError:
-                    raise UsageError, _('%(classname)s has no entry "%(propname)s"')%{
-                        'classname': link_class.classname, 'propname': propname}
 
         # now do the find 
         try:
@@ -473,8 +536,8 @@ Command help:
 
         # decode the node designator
         try:
-            classname, nodeid = roundupdb.splitDesignator(args[0])
-        except roundupdb.DesignatorError, message:
+            classname, nodeid = hyperdb.splitDesignator(args[0])
+        except hyperdb.DesignatorError, message:
             raise UsageError, message
 
         # get the class
@@ -529,7 +592,7 @@ Command help:
             props = self.props_from_args(args[1:])
 
         # convert types
-        for propname in props.keys():
+        for propname, value in props.items():
             # get the property
             try:
                 proptype = properties[propname]
@@ -551,6 +614,10 @@ Command help:
                 props[propname] = password.Password(value)
             elif isinstance(proptype, hyperdb.Multilink):
                 props[propname] = value.split(',')
+            elif isinstance(proptype, hyperdb.Boolean):
+                props[propname] = value.lower() in ('yes', 'true', 'on', '1')
+            elif isinstance(proptype, hyperdb.Number):
+                props[propname] = int(value)
 
         # check for the key property
         propname = cl.getkey()
@@ -679,8 +746,8 @@ Command help:
         if len(args) < 1:
             raise UsageError, _('Not enough arguments supplied')
         try:
-            classname, nodeid = roundupdb.splitDesignator(args[0])
-        except roundupdb.DesignatorError, message:
+            classname, nodeid = hyperdb.splitDesignator(args[0])
+        except hyperdb.DesignatorError, message:
             raise UsageError, message
 
         try:
@@ -729,8 +796,8 @@ Command help:
         designators = args[0].split(',')
         for designator in designators:
             try:
-                classname, nodeid = roundupdb.splitDesignator(designator)
-            except roundupdb.DesignatorError, message:
+                classname, nodeid = hyperdb.splitDesignator(designator)
+            except hyperdb.DesignatorError, message:
                 raise UsageError, message
             try:
                 self.db.getclass(classname).retire(nodeid)
@@ -741,69 +808,63 @@ Command help:
         return 0
 
     def do_export(self, args):
-        '''Usage: export class[,class] destination_dir
+        '''Usage: export [class[,class]] export_dir
         Export the database to tab-separated-value files.
 
         This action exports the current data from the database into
         tab-separated-value files that are placed in the nominated destination
         directory. The journals are not exported.
         '''
-        if len(args) < 2:
+        # we need the CSV module
+        if csv is None:
+            raise UsageError, \
+                _('Sorry, you need the csv module to use this function.\n'
+                'Get it from: http://www.object-craft.com.au/projects/csv/')
+
+        # grab the directory to export to
+        if len(args) < 1:
             raise UsageError, _('Not enough arguments supplied')
-        classes = args[0].split(',')
-        dir = args[1]
+        dir = args[-1]
+
+        # get the list of classes to export
+        if len(args) == 2:
+            classes = args[0].split(',')
+        else:
+            classes = self.db.classes.keys()
 
         # use the csv parser if we can - it's faster
-        if csv is not None:
-            p = csv.parser(field_sep=':')
+        p = csv.parser(field_sep=':')
 
         # do all the classes specified
         for classname in classes:
             cl = self.get_class(classname)
             f = open(os.path.join(dir, classname+'.csv'), 'w')
-            f.write(':'.join(cl.properties.keys()) + '\n')
+            properties = cl.getprops()
+            propnames = properties.keys()
+            propnames.sort()
+            print >> f, p.join(propnames)
 
             # all nodes for this class
-            properties = cl.properties.items()
             for nodeid in cl.list():
-                l = []
-                for prop, proptype in properties:
-                    value = cl.get(nodeid, prop)
-                    # convert data where needed
-                    if isinstance(proptype, hyperdb.Date):
-                        value = value.get_tuple()
-                    elif isinstance(proptype, hyperdb.Interval):
-                        value = value.get_tuple()
-                    elif isinstance(proptype, hyperdb.Password):
-                        value = str(value)
-                    l.append(repr(value))
-
-                # now write
-                if csv is not None:
-                   f.write(p.join(l) + '\n')
-                else:
-                   # escape the individual entries to they're valid CSV
-                   m = []
-                   for entry in l:
-                      if '"' in entry:
-                          entry = '""'.join(entry.split('"'))
-                      if ':' in entry:
-                          entry = '"%s"'%entry
-                      m.append(entry)
-                   f.write(':'.join(m) + '\n')
+                print >>f, p.join(cl.export_list(propnames, nodeid))
         return 0
 
     def do_import(self, args):
-        '''Usage: import class file
-        Import the contents of the tab-separated-value file.
-
-        The file must define the same properties as the class (including having
-        a "header" line with those property names.) The new nodes are added to
-        the existing database - if you want to create a new database using the
-        imported data, then create a new database (or, tediously, retire all
-        the old data.)
+        '''Usage: import import_dir
+        Import a database from the directory containing CSV files, one per
+        class to import.
+
+        The files must define the same properties as the class (including having
+        a "header" line with those property names.)
+
+        The imported nodes will have the same nodeid as defined in the
+        import file, thus replacing any existing content.
+
+        XXX The new nodes are added to the existing database - if you want to
+        XXX create a new database using the imported data, then create a new
+        XXX database (or, tediously, retire all the old data.)
         '''
-        if len(args) < 2:
+        if len(args) < 1:
             raise UsageError, _('Not enough arguments supplied')
         if csv is None:
             raise UsageError, \
@@ -812,56 +873,44 @@ Command help:
 
         from roundup import hyperdb
 
-        # ensure that the properties and the CSV file headings match
-        classname = args[0]
-        cl = self.get_class(classname)
-        f = open(args[1])
-        p = csv.parser(field_sep=':')
-        file_props = p.parse(f.readline())
-        props = cl.properties.keys()
-        m = file_props[:]
-        m.sort()
-        props.sort()
-        if m != props:
-            raise UsageError, _('Import file doesn\'t define the same '
-                'properties as "%(arg0)s".')%{'arg0': args[0]}
-
-        # loop through the file and create a node for each entry
-        n = range(len(props))
-        while 1:
-            line = f.readline()
-            if not line: break
+        for file in os.listdir(args[0]):
+            f = open(os.path.join(args[0], file))
 
-            # parse lines until we get a complete entry
+            # get the classname
+            classname = os.path.splitext(file)[0]
+
+            # ensure that the properties and the CSV file headings match
+            cl = self.get_class(classname)
+            p = csv.parser(field_sep=':')
+            file_props = p.parse(f.readline())
+            properties = cl.getprops()
+            propnames = properties.keys()
+            propnames.sort()
+            m = file_props[:]
+            m.sort()
+            if m != propnames:
+                raise UsageError, _('Import file doesn\'t define the same '
+                    'properties as "%(arg0)s".')%{'arg0': args[0]}
+
+            # loop through the file and create a node for each entry
+            maxid = 1
             while 1:
-                l = p.parse(line)
-                if l: break
                 line = f.readline()
-                if not line:
-                    raise ValueError, "Unexpected EOF during CSV parse"
-
-            # make the new node's property map
-            d = {}
-            for i in n:
-                # Use eval to reverse the repr() used to output the CSV
-                value = eval(l[i])
-                # Figure the property for this column
-                key = file_props[i]
-                proptype = cl.properties[key]
-                # Convert for property type
-                if isinstance(proptype, hyperdb.Date):
-                    value = date.Date(value)
-                elif isinstance(proptype, hyperdb.Interval):
-                    value = date.Interval(value)
-                elif isinstance(proptype, hyperdb.Password):
-                    pwd = password.Password()
-                    pwd.unpack(value)
-                    value = pwd
-                if value is not None:
-                    d[key] = value
-
-            # and create the new node
-            apply(cl.create, (), d)
+                if not line: break
+
+                # parse lines until we get a complete entry
+                while 1:
+                    l = p.parse(line)
+                    if l: break
+                    line = f.readline()
+                    if not line:
+                        raise ValueError, "Unexpected EOF during CSV parse"
+
+                # do the import and figure the current highest nodeid
+                maxid = max(maxid, int(cl.import_list(propnames, l)))
+
+            print 'setting', classname, maxid
+            self.db.setid(classname, str(maxid))
         return 0
 
     def do_pack(self, args):
@@ -896,14 +945,57 @@ Date format is "YYYY-MM-DD" eg:
             raise ValueError, _('Invalid format')
         m = m.groupdict()
         if m['period']:
-            # TODO: need to fix date module.  one should be able to say
-            # pack_before = date.Date(". - %s"%value)
-            pack_before = date.Date(".") + date.Interval("- %s"%value)
+            pack_before = date.Date(". - %s"%value)
         elif m['date']:
             pack_before = date.Date(value)
         self.db.pack(pack_before)
         return 0
 
+    def do_reindex(self, args):
+        '''Usage: reindex
+        Re-generate an instance's search indexes.
+
+        This will re-generate the search indexes for an instance. This will
+        typically happen automatically.
+        '''
+        self.db.indexer.force_reindex()
+        self.db.reindex()
+        return 0
+
+    def do_security(self, args):
+        '''Usage: security [Role name]
+        Display the Permissions available to one or all Roles.
+        '''
+        if len(args) == 1:
+            role = args[0]
+            try:
+                roles = [(args[0], self.db.security.role[args[0]])]
+            except KeyError:
+                print _('No such Role "%(role)s"')%locals()
+                return 1
+        else:
+            roles = self.db.security.role.items()
+            role = self.db.config.NEW_WEB_USER_ROLES
+            if ',' in role:
+                print _('New Web users get the Roles "%(role)s"')%locals()
+            else:
+                print _('New Web users get the Role "%(role)s"')%locals()
+            role = self.db.config.NEW_EMAIL_USER_ROLES
+            if ',' in role:
+                print _('New Email users get the Roles "%(role)s"')%locals()
+            else:
+                print _('New Email users get the Role "%(role)s"')%locals()
+        roles.sort()
+        for rolename, role in roles:
+            print _('Role "%(name)s":')%role.__dict__
+            for permission in role.permissions:
+                if permission.klass:
+                    print _(' %(description)s (%(name)s for "%(klass)s" '
+                        'only)')%permission.__dict__
+                else:
+                    print _(' %(description)s (%(name)s)')%permission.__dict__
+        return 0
+
     def run_command(self, args):
         '''Run a single command
         '''
@@ -942,16 +1034,26 @@ Date format is "YYYY-MM-DD" eg:
         while not self.instance_home:
             self.instance_home = raw_input(_('Enter instance home: ')).strip()
 
-        # before we open the db, we may be doing an init
+        # before we open the db, we may be doing an install or init
         if command == 'initialise':
-            return self.do_initialise(self.instance_home, args)
+            try:
+                return self.do_initialise(self.instance_home, args)
+            except UsageError, message:
+                print _('Error: %(message)s')%locals()
+                return 1
+        elif command == 'install':
+            try:
+                return self.do_install(self.instance_home, args)
+            except UsageError, message:
+                print _('Error: %(message)s')%locals()
+                return 1
 
         # get the instance
         try:
             instance = roundup.instance.open(self.instance_home)
         except ValueError, message:
             self.instance_home = ''
-            print _("Couldn't open instance: %(message)s")%locals()
+            print _("Error: Couldn't open instance: %(message)s")%locals()
             return 1
 
         # only open the database once!
@@ -964,6 +1066,7 @@ Date format is "YYYY-MM-DD" eg:
             ret = function(args[1:])
         except UsageError, message:
             print _('Error: %(message)s')%locals()
+            print
             print function.__doc__
             ret = 1
         except:
@@ -975,7 +1078,7 @@ Date format is "YYYY-MM-DD" eg:
     def interactive(self):
         '''Run in an interactive mode
         '''
-        print _('Roundup {version} ready for input.')
+        print _('Roundup %s ready for input.'%roundup_version)
         print _('Type "help" for help.')
         try:
             import readline
@@ -1043,6 +1146,66 @@ if __name__ == '__main__':
 
 #
 # $Log: not supported by cvs2svn $
+# Revision 1.22  2002/08/16 04:26:42  richard
+# moving towards full database export
+#
+# Revision 1.21  2002/08/01 01:07:37  richard
+# include info about new user roles
+#
+# Revision 1.20  2002/08/01 00:56:22  richard
+# Added the web access and email access permissions, so people can restrict
+# access to users who register through the email interface (for example).
+# Also added "security" command to the roundup-admin interface to display the
+# Role/Permission config for an instance.
+#
+# Revision 1.19  2002/07/25 07:14:05  richard
+# Bugger it. Here's the current shape of the new security implementation.
+# Still to do:
+#  . call the security funcs from cgi and mailgw
+#  . change shipped templates to include correct initialisation and remove
+#    the old config vars
+# ... that seems like a lot. The bulk of the work has been done though. Honest :)
+#
+# Revision 1.18  2002/07/18 11:17:30  gmcm
+# Add Number and Boolean types to hyperdb.
+# Add conversion cases to web, mail & admin interfaces.
+# Add storage/serialization cases to back_anydbm & back_metakit.
+#
+# Revision 1.17  2002/07/14 06:05:50  richard
+#  . fixed the date module so that Date(". - 2d") works
+#
+# Revision 1.16  2002/07/09 04:19:09  richard
+# Added reindex command to roundup-admin.
+# Fixed reindex on first access.
+# Also fixed reindexing of entries that change.
+#
+# Revision 1.15  2002/06/17 23:14:44  richard
+# . #569415 ] {version}
+#
+# Revision 1.14  2002/06/11 06:41:50  richard
+# Removed prompt for admin email in initialisation.
+#
+# Revision 1.13  2002/05/30 23:58:14  richard
+# oops
+#
+# Revision 1.12  2002/05/26 09:04:42  richard
+# out by one in the init args
+#
+# Revision 1.11  2002/05/23 01:14:20  richard
+#  . split instance initialisation into two steps, allowing config changes
+#    before the database is initialised.
+#
+# Revision 1.10  2002/04/27 10:07:23  richard
+# minor fix to error message
+#
+# Revision 1.9  2002/03/12 22:51:47  richard
+#  . #527416 ] roundup-admin uses undefined value
+#  . #527503 ] unfriendly init blowup when parent dir
+#    (also handles UsageError correctly now in init)
+#
+# Revision 1.8  2002/02/27 03:28:21  richard
+# Ran it through pychecker, made fixes
+#
 # Revision 1.7  2002/02/20 05:04:32  richard
 # Wasn't handling the cvs parser feeding properly.
 #