Index: pykota/trunk/pykota/version.py
===================================================================
--- pykota/trunk/pykota/version.py (revision 3413)
+++ pykota/trunk/pykota/version.py (revision 3426)
@@ -22,5 +22,5 @@
 """This module defines some application level constants."""
 
-__version__ = "1.27alpha5_unofficial"
+__version__ = "1.27alpha6_unofficial"
 
 __doc__ = "PyKota : a complete Printing Quota Solution for CUPS."
Index: pykota/trunk/bin/pkusers
===================================================================
--- pykota/trunk/bin/pkusers (revision 3413)
+++ pykota/trunk/bin/pkusers (revision 3426)
@@ -22,4 +22,6 @@
 #
 
+"""A users and groups manager for PyKota."""
+
 import sys
 import pwd
@@ -27,142 +29,11 @@
 
 import pykota.appinit
-from pykota.utils import *
-
+from pykota.utils import run
+from pykota.commandline import PyKotaOptionParser
 from pykota.errors import PyKotaCommandLineError
 from pykota.tool import Percent, PyKotaTool
 from pykota.storage import StorageUser, StorageGroup
 
-__doc__ = N_("""pkusers v%(__version__)s (c) %(__years__)s %(__author__)s
-
-An Users and Groups Manager for PyKota.
-
-command line usage :
-
-  pkusers [options] user1 user2 user3 ... userN
-
-or :
-
-  pkusers --groups [options] group1 group2 group3 ... groupN
-
-options :
-
-  -v | --version       Prints pkusers's version number then exits.
-  -h | --help          Prints this message then exits.
-
-  -a | --add           Adds users if they don't exist on the database.
-                       If they exist, they are modified unless
-                       -s|--skipexisting is also used.
-
-  -d | --delete        Deletes users from the quota storage.
-
-  -e | --email addr    Sets the email address for the users.
-                       If the addr parameter begins with @, then
-                       the username is prepended to addr to form
-                       a valid email address.
-
-  -D | --description d Adds a textual description to users or groups.
-
-  -g | --groups        Edit users groups instead of users.
-
-  -o | --overcharge f  Sets the overcharging factor applied to the user
-                       when computing the cost of a print job. Positive or
-                       negative floating point values are allowed,
-                       this allows you to do some really creative
-                       things like giving money to an user whenever
-                       he prints. The number of pages in a print job
-                       is not modified by this coefficient, only the
-                       cost of the job for a particular user.
-                       Only users have such a coefficient.
-
-  -i | --ingroups g1[,g2...]  Puts the users into each of the groups
-                              listed, separated by commas. The groups
-                              must already exist in the Quota Storage.
-
-  -L | --list          Lists users or groups.
-
-  -l | --limitby l     Choose if the user/group is limited in printing
-                       by its account balance or by its page quota.
-                       The default value is 'quota'. Allowed values
-                       are 'quota' 'balance' 'noquota' 'noprint'
-                       and 'nochange' :
-
-                         - quota : limit by number of pages per printer.
-                         - balance : limit by number of credits in account.
-                         - noquota : no limit, accounting still done.
-                         - nochange : no limit, accounting not done.
-                         - noprint : printing is denied.
-                       NB : nochange and noprint are not supported for groups.
-
-  -b | --balance b     Sets the user's account balance to b.
-                       Account balance may be increase or decreased
-                       if b is prefixed with + or -.
-                       WARNING : when decreasing account balance,
-                       the total paid so far by the user is decreased
-                       too.
-                       Groups don't have a real balance, but the
-                       sum of their users' account balance.
-
-  -C | --comment txt   Defines some informational text to be associated
-                       with a change to an user's account balance.
-                       Only meaningful if -b | --balance is also used.
-
-
-  -r | --remove        In combination with the --ingroups option above,
-                       remove users from the specified users groups.
-
-  -s | --skipexisting  In combination with the --add option above, tells
-                       pkusers to not modify existing users.
-
-  user1 through userN and group1 through groupN can use wildcards
-  if the --add option is not set.
-
-examples :
-
-  $ pkusers --add john paul george ringo/ringo@example.com
-
-  This will add users john, paul, george and ringo to the quota
-  database. User ringo's email address will also be set to
-  'ringo@example.com'
-
-  $ pkusers --ingroups coders,it jerome
-
-  User jerome is put into the groups "coders" and "it" which must
-  already exist in the quota database.
-
-  $ pkusers --limitby balance jerome
-
-  This will tell PyKota to limit jerome by his account's balance
-  when printing.
-
-  $ pkusers --balance +10.0 --comment "He paid with his blood !" jerome
-
-  This will increase jerome's account balance by 10.0 (in your
-  own currency). You can decrease the account balance with a
-  dash prefix, and set it to a fixed amount with no prefix.
-  A comment will be stored for this balance change.
-
-  $ pkusers --delete jerome rachel
-
-  This will completely delete jerome and rachel from the quota
-  database. All their quotas and jobs will be deleted too.
-
-  $ pkusers --overcharge 2.5 poorstudent
-
-  This will overcharge the poorstudent user by a factor of 2.5.
-
-  $ pkusers --overcharge -1 jerome
-
-  User jerome will actually earn money whenever he prints.
-
-  $ pkusers --overcharge 0 boss
-
-  User boss can print at will, it won't cost him anything because the
-  cost of each print job will be multiplied by zero before charging
-  his account.
-
-  $ pkusers --email @example.com
-
-  This will set the email address for each user to username@example.com
-""")
+
 
 class PKUsers(PyKotaTool) :
@@ -201,26 +72,67 @@
                 ugroup.addUserToGroup(user)
 
+    def sanitizeNames(self, names, isgroups) :
+        """Sanitize users and groups names if needed."""
+        if not self.config.isAdmin :
+            username = pwd.getpwuid(os.geteuid())[0]
+            if isgroups :
+                user = self.storage.getUser(username)
+                if user.Exists :
+                    return [ g.Name for g in self.storage.getUserGroups(user) ]
+            return [ username ]
+        return names
+
     def main(self, names, options) :
         """Manage users or groups."""
-        names = self.sanitizeNames(options, names)
-        suffix = (options["groups"] and "Group") or "User"
-
-        if not options["list"] :
+        islist = (options.action == "list")
+        isadd = (options.action == "add")
+        isdelete = (options.action == "delete")
+
+        if not islist :
+            self.adminOnly()
+
+        names = self.sanitizeNames(names, options.groups)
+        if not names :
+            if isdelete or isadd :
+                raise PyKotaCommandLineError, _("You must specify users or groups names on the command line.")
+            names = [u"*"]
+
+        if options.remove and not options.ingroups :
+            raise PyKotaCommandLineError, _("You must specify users groups names on the command line.")
+        elif (((islist or isdelete) and (options.limitby  \
+                                      or options.balance \
+                                      or options.email \
+                                      or options.remove \
+                                      or options.overcharge \
+                                      or options.ingroups \
+                                      or options.description \
+                                      or options.skipexisting \
+                                      or options.comment))) \
+             or (options.groups and (options.ingroups \
+                                  or options.balance \
+                                  or options.email \
+                                  or options.remove \
+                                  or options.overcharge \
+                                  or options.comment)) :
+            raise PyKotaCommandLineError, _("Incompatible command line options. Please look at the online help or manual page.")
+
+        suffix = (options.groups and "Group") or "User"
+
+        if not islist :
             percent = Percent(self)
 
-        if not options["add"] :
-            if not options["list"] :
+        if not isadd :
+            if not islist :
                 percent.display("%s..." % _("Extracting datas"))
-            if not names :      # NB : can't happen for --delete because it's catched earlier
-                names = ["*"]
             entries = getattr(self.storage, "getMatching%ss" % suffix)(",".join(names))
             if not entries :
-                if not options["list"] :
+                if not islist :
                     percent.display("\n")
-                raise PyKotaCommandLineError, _("There's no %s matching %s") % (_(suffix.lower()), " ".join(names))
-            if not options["list"] :
+                raise PyKotaCommandLineError, _("There's no %s matching %s") \
+                    % (_(suffix.lower()), " ".join(names))
+            if not islist :
                 percent.setSize(len(entries))
 
-        if options["list"] :
+        if islist :
             if suffix == "User" :
                 maildomain = self.config.getMailDomain()
@@ -238,10 +150,10 @@
                     if entry.Description :
                         msg += " - %s" % entry.Description
-                    print msg
-                    print "    %s" % (_("Limited by : %s") % entry.LimitBy)
-                    print "    %s" % (_("Account balance : %.2f") % (entry.AccountBalance or 0.0))
-                    print "    %s" % (_("Total paid so far : %.2f") % (entry.LifeTimePaid or 0.0))
-                    print "    %s" % (_("Overcharging factor : %.2f") % entry.OverCharge)
-                    print
+                    self.display("%s\n" % msg)
+                    self.display("    %s\n" % (_("Limited by : %s") % entry.LimitBy))
+                    self.display("    %s\n" % (_("Account balance : %.2f") % (entry.AccountBalance or 0)))
+                    self.display("    %s\n" % (_("Total paid so far : %.2f") % (entry.LifeTimePaid or 0)))
+                    self.display("    %s\n" % (_("Overcharging factor : %.2f") % entry.OverCharge))
+                    self.display("\n")
             else :
                 for entry in entries :
@@ -249,32 +161,30 @@
                     if entry.Description :
                         msg += " - %s" % entry.Description
-                    print msg
-                    print "    %s" % (_("Limited by : %s") % entry.LimitBy)
-                    print "    %s" % (_("Group balance : %.2f") % (entry.AccountBalance or 0.0))
-                    print "    %s" % (_("Total paid so far : %.2f") % (entry.LifeTimePaid or 0.0))
-                    print
-        elif options["delete"] :
+                    self.display("%s\n" % msg)
+                    self.display("    %s\n" % (_("Limited by : %s") % entry.LimitBy))
+                    self.display("    %s\n" % (_("Group balance : %.2f") % (entry.AccountBalance or 0)))
+                    self.display("    %s\n" % (_("Total paid so far : %.2f") % (entry.LifeTimePaid or 0)))
+                    self.display("\n")
+        elif isdelete :
             percent.display("\n%s..." % _("Deletion"))
             getattr(self.storage, "deleteMany%ss" % suffix)(entries)
             percent.display("\n")
         else :
-            limitby = options["limitby"]
+            limitby = options.limitby
             if limitby :
                 limitby = limitby.strip().lower()
             if limitby :
-                if limitby not in ('quota', 'balance', 'noquota', \
-                                            'noprint', 'nochange') :
-                    raise PyKotaCommandLineError, _("Invalid limitby value %s") % options["limitby"]
-                if (limitby in ('nochange', 'noprint')) and options["groups"] :
-                    raise PyKotaCommandLineError, _("Invalid limitby value %s") % options["limitby"]
-
-            overcharge = options["overcharge"]
-            if overcharge :
-                try :
-                    overcharge = float(overcharge.strip())
-                except (ValueError, AttributeError) :
-                    raise PyKotaCommandLineError, _("Invalid overcharge value %s") % options["overcharge"]
-
-            balance = options["balance"]
+                if limitby not in ('quota',
+                                   'balance',
+                                   'noquota',
+                                   'noprint',
+                                   'nochange') :
+                    raise PyKotaCommandLineError, _("Invalid limitby value %s") \
+                        % options.limitby
+                if (limitby in ('nochange', 'noprint')) and options.groups :
+                    raise PyKotaCommandLineError, _("Invalid limitby value %s") \
+                        % options.limitby
+
+            balance = options.balance
             if balance :
                 balance = balance.strip()
@@ -282,31 +192,29 @@
                     balancevalue = float(balance)
                 except ValueError :
-                    raise PyKotaCommandLineError, _("Invalid balance value %s") % options["balance"]
+                    raise PyKotaCommandLineError, _("Invalid balance value %s") \
+                        % options.balance
             else :
                 balancevalue = None
 
-            if options["ingroups"] :
-                usersgroups = self.storage.getMatchingGroups(options["ingroups"])
+            if options.ingroups :
+                usersgroups = self.storage.getMatchingGroups(options.ingroups)
                 if not usersgroups :
-                    raise PyKotaCommandLineError, _("There's no users group matching %s") % " ".join(options["ingroups"].split(','))
+                    raise PyKotaCommandLineError, _("There's no users group matching %s") \
+                        % " ".join(options.ingroups.split(','))
             else :
                 usersgroups = []
 
-            description = options["description"]
-            if description :
-                description = description.strip()
-
-            comment = options["comment"]
-            if comment :
-                comment = comment.strip()
-            email = options["email"]
-            if email :
-                email = email.strip()
-            skipexisting = options["skipexisting"]
-            groups = options["groups"]
-            remove = options["remove"]
+            if options.description :
+                options.description = options.description.strip()
+
+            if options.comment :
+                options.comment = options.comment.strip()
+
+            if options.email :
+                options.email = options.email.strip()
+
             self.storage.beginTransaction()
             try :
-                if options["add"] :
+                if isadd :
                     rejectunknown = self.config.getRejectUnknown()
                     percent.display("%s...\n" % _("Creation"))
@@ -314,5 +222,5 @@
                     for ename in names :
                         useremail = None
-                        if not groups :
+                        if not options.groups :
                             splitname = ename.split('/', 1)     # username/email
                             if len(splitname) == 1 :
@@ -322,5 +230,5 @@
                             reject = 0
                             if rejectunknown :
-                                if groups :
+                                if options.groups :
                                     try :
                                         grp.getgrnam(ename)
@@ -336,33 +244,51 @@
                             if not reject :
                                 entry = globals()["Storage%s" % suffix](self.storage, ename)
-                                if groups :
-                                    self.modifyEntry(entry, groups, limitby, \
-                                                     description)
+                                if options.groups :
+                                    self.modifyEntry(entry,
+                                                     options.groups,
+                                                     limitby,
+                                                     options.description)
                                 else :
-                                    self.modifyEntry(entry, groups, limitby, \
-                                                     description, overcharge,\
-                                                     balance, balancevalue, \
-                                                     comment, useremail or email)
+                                    self.modifyEntry(entry,
+                                                     options.groups,
+                                                     limitby,
+                                                     options.description,
+                                                     options.overcharge,
+                                                     balance,
+                                                     balancevalue,
+                                                     options.comment,
+                                                     useremail or options.email)
                                 oldentry = getattr(self.storage, "add%s" % suffix)(entry)
                                 if oldentry is not None :
-                                    if skipexisting :
-                                        self.logdebug(_("%s %s already exists, skipping.") % (_(suffix), ename))
+                                    if options.skipexisting :
+                                        self.logdebug(_("%s %s already exists, skipping.") \
+                                                          % (_(suffix), ename))
                                     else :
-                                        self.logdebug(_("%s %s already exists, will be modified.") % (_(suffix), ename))
-                                        if groups :
-                                            self.modifyEntry(oldentry, groups, \
-                                                     limitby, description)
+                                        self.logdebug(_("%s %s already exists, will be modified.") \
+                                                          % (_(suffix), ename))
+                                        if options.groups :
+                                            self.modifyEntry(oldentry,
+                                                             options.groups,
+                                                             limitby,
+                                                             options.description)
                                         else :
-                                            self.modifyEntry(oldentry, groups, limitby, \
-                                                     description, overcharge,\
-                                                     balance, balancevalue, \
-                                                     comment, useremail or email)
+                                            self.modifyEntry(oldentry,
+                                                             options.groups,
+                                                             limitby,
+                                                             options.description,
+                                                             options.overcharge,
+                                                             balance,
+                                                             balancevalue,
+                                                             options.comment,
+                                                             useremail or options.email)
                                         oldentry.save()
-                                        if not groups :
-                                            self.manageUsersGroups(usersgroups, oldentry, remove)
-                                elif usersgroups and not groups :
+                                        if not options.groups :
+                                            self.manageUsersGroups(usersgroups,
+                                                                   oldentry,
+                                                                   options.remove)
+                                elif usersgroups and not options.groups :
                                     self.manageUsersGroups(usersgroups, \
                                                            self.storage.getUser(ename), \
-                                                           remove)
+                                                           options.remove)
                         else :
                             raise PyKotaCommandLineError, _("Invalid name %s") % ename
@@ -371,11 +297,22 @@
                     percent.display("\n%s...\n" % _("Modification"))
                     for entry in entries :
-                        if groups :
-                            self.modifyEntry(entry, groups, limitby, description)
+                        if options.groups :
+                            self.modifyEntry(entry,
+                                             options.groups,
+                                             limitby,
+                                             options.description)
                         else :
-                            self.modifyEntry(entry, groups, limitby, description, \
-                                             overcharge, balance, balancevalue, \
-                                             comment, email)
-                            self.manageUsersGroups(usersgroups, entry, remove)
+                            self.modifyEntry(entry,
+                                             options.groups,
+                                             limitby,
+                                             options.description,
+                                             options.overcharge,
+                                             balance,
+                                             balancevalue,
+                                             options.comment,
+                                             options.email)
+                            self.manageUsersGroups(usersgroups,
+                                                   entry,
+                                                   options.remove)
                         entry.save()
                         percent.oneMore()
@@ -386,79 +323,78 @@
                 self.storage.commitTransaction()
 
-        if not options["list"] :
+        if not islist :
             percent.done()
 
 if __name__ == "__main__" :
-    retcode = 0
-    try :
-        defaults = { \
-                     "comment" : "", \
-                   }
-        short_options = "hvaD:dgl:rso:i:b:C:Le:"
-        long_options = ["help", "version", "add", "description=", \
-                        "delete", "groups", "list", "remove", \
-                        "skipexisting", "overcharge=", "email=", \
-                        "ingroups=", "limitby=", "balance=", "comment=", \
-                       ]
-
-
-        # Initializes the command line tool
-        manager = PKUsers(doc=__doc__)
-        manager.deferredInit()
-
-        # parse and checks the command line
-        (options, args) = manager.parseCommandline(sys.argv[1:], short_options, long_options)
-
-        # sets long options
-        options["help"] = options["h"] or options["help"]
-        options["version"] = options["v"] or options["version"]
-        options["add"] = options["a"] or options["add"]
-        options["description"] = options["D"] or options["description"]
-        options["delete"] = options["d"] or options["delete"]
-        options["groups"] = options["g"] or options["groups"]
-        options["list"] = options["L"] or options["list"]
-        options["remove"] = options["r"] or options["remove"]
-        options["skipexisting"] = options["s"] or options["skipexisting"]
-        options["limitby"] = options["l"] or options["limitby"]
-        options["balance"] = options["b"] or options["balance"]
-        options["ingroups"] = options["i"] or options["ingroups"]
-        options["overcharge"] = options["o"] or options["overcharge"]
-        options["comment"] = options["C"] or options["comment"] or defaults["comment"]
-        options["email"] = options["e"] or options["email"]
-
-        if options["help"] :
-            manager.display_usage_and_quit()
-        elif options["version"] :
-            manager.display_version_and_quit()
-        elif (options["delete"] and (options["add"] or options["remove"] or options["description"] or options["email"])) \
-           or (options["skipexisting"] and not options["add"]) \
-           or (options["list"] and (options["add"] or options["delete"] or options["remove"] or options["description"] or options["email"])) \
-           or (options["groups"] and (options["balance"] or options["ingroups"] or options["overcharge"])) :
-            raise PyKotaCommandLineError, _("incompatible options, see help.")
-        elif options["remove"] and not options["ingroups"] :
-            raise PyKotaCommandLineError, _("You have to pass user groups names on the command line")
-        elif (not args) and (options["add"] or options["delete"]) :
-            raise PyKotaCommandLineError, _("You have to pass user or group names on the command line")
-        else :
-            retcode = manager.main(args, options)
-    except KeyboardInterrupt :
-        logerr("\nInterrupted with Ctrl+C !\n")
-        retcode = -3
-    except PyKotaCommandLineError, msg :
-        logerr("%s : %s\n" % (sys.argv[0], msg))
-        retcode = -2
-    except SystemExit :
-        pass
-    except :
-        try :
-            manager.crashed("pkusers failed")
-        except :
-            crashed("pkusers failed")
-        retcode = -1
-
-    try :
-        manager.storage.close()
-    except (TypeError, NameError, AttributeError) :
-        pass
-
-    sys.exit(retcode)
+    parser = PyKotaOptionParser(description=_("Manages PyKota users or users groups."),
+                                usage="pkusers [options] name1 name2 ... nameN")
+    parser.add_option("-a", "--add",
+                            action="store_const",
+                            const="add",
+                            dest="action",
+                            help=_("Add new, or modify existing, users or groups."))
+    parser.add_option("-b", "--balance",
+                            dest="balance",
+                            help=_("Set an user's account balance. The value can also be increased or decreased when the value is prefixed with '+' or '-'. Users groups don't have a real account balance, instead the sum of their members' account balances is used."))
+    parser.add_option("-C", "--comment",
+                            dest="comment",
+                            default="",
+                            help=_("Associate a textual comment with a change in an user's account balance. Only meaningful when --balance is also used."))
+    parser.add_option("-d", "--delete",
+                            action="store_const",
+                            const="delete",
+                            dest="action",
+                            help=_("Delete the specified users or groups. Also purge the print quota entries and printing history matching the specified users or groups."))
+    parser.add_option("-D", "--description",
+                            dest="description",
+                            help=_("Set a textual description for the specified users or groups."))
+    parser.add_option("-e", "--email",
+                            dest="email",
+                            help=_("Set an user's email address. If this parameter begins with '@' then the username is prepended to this parameter to form a valid email address."))
+    parser.add_option("-g", "--groups",
+                            action="store_true",
+                            dest="groups",
+                            help=_("Manage users groups instead of users."))
+    parser.add_option("-i", "--ingroups",
+                            dest="ingroups",
+                            help=_("Put the specified users into the specified groups. When combined with the --remove option, users are removed from the specified groups instead."))
+    parser.add_option("-l", "--limitby",
+                            dest="limitby",
+                            help=_("Set the limiting factor for the specified users or groups. Can be any of 'quota' (limit by number of pages per printer), 'balance' (limit by number of credits), 'noquota' (no limit but accounting done), 'nochange' (no limit and not accounting), or 'noprint' (printing is denied). The two latter ones are not supported for groups."))
+    parser.add_option("-L", "--list",
+                            action="store_const",
+                            const="list",
+                            dest="action",
+                            help=_("Display detailed informations about the specified users or groups."))
+    parser.add_option("-o", "--overcharge",
+                            type="float",
+                            dest="overcharge",
+                            help=_("Set the overcharging factor applied to the specified users when computing the cost of a print job. Any floating point value can be used, allowing you to express your creativity..."))
+    parser.add_option("-r", "--remove",
+                            action="store_true",
+                            dest="remove",
+                            help=_("When combined with the --ingroups option, remove users from the specified users groups."))
+    parser.add_option("-s", "--skipexisting",
+                            action="store_true",
+                            dest="skipexisting",
+                            help=_("If --add is used, ensure that existing users or groups won't be modified."))
+
+    parser.add_example("--add john paul george ringo/ringo@example.com",
+                       _("Would make users 'john', 'paul', 'george' and 'ringo' be known to PyKota. User 'ringo''s email address would be set to 'ringo@example.com'."))
+    parser.add_example("--add --groups coders it",
+                       _("Would create two users groups named 'coders' and 'it'."))
+    parser.add_example("--add --ingroups coders,it jerome",
+                       _("Would add user 'jerome' and put him into the 'coders' and 'it' groups. Both groups would have to be existing."))
+    parser.add_example("--limitby balance --balance 10.0 john",
+                       _("Would give 10.0 credits to 'john' and make his printing be limited by his account balance."))
+    parser.add_example('--balance +10.0 --comment "He paid with his blood." jerome',
+                       _("Would add 10.0 credits to 'jerome''s account and register a comment associated with his payment."))
+    parser.add_example('--delete "jer*" "rach*"',
+                       _("Would delete all user accounts whose names begin with either 'jer' or 'rach'."))
+    parser.add_example("--overcharge -1.50 theboss",
+                       _("Would make the boss earn money whenever he prints."))
+    parser.add_example("--email @example.com",
+                       _("Would set the email address for each existing user to username@example.com"))
+    parser.add_example("--list",
+                       _("Would list all users."))
+    run(parser, PKUsers)
