Index: /pykota/trunk/conf/pykota.conf.sample
===================================================================
--- /pykota/trunk/conf/pykota.conf.sample (revision 1233)
+++ /pykota/trunk/conf/pykota.conf.sample (revision 1240)
@@ -129,5 +129,5 @@
 # logs.
 # Actually only database queries are logged.
-debug : No
+debug : Yes
 
 # Mail server to use to warn users
@@ -236,4 +236,10 @@
 #   NB : I use ISO-8859-15, but Windows expects UTF-8, so we pipe the message
 #        into iconv before sending it to the Windows user.
+#
+# or more simply :
+#
+#       mailto: external(/usr/bin/mailandpopup.sh %(username)s %(printername)s "%(email)s" "%(message)s" 2>&1 >/dev/null)
+#
+#   NB : The mailandpopup.sh shell script is now included in PyKota
 #
 #   NB : in ANY case, don't forget to redirect your command's standard output
@@ -344,4 +350,10 @@
 #
 #
+# An example using the contributed pagecount.pl script which does 
+# the same as above, but should work on more printers : 
+#
+# requester: external(/usr/bin/pagecount.pl %(printer)s 9100)
+#
+#
 # WARNING : In any case, when using an external requester, please test the command line outside
 #           of PyKota before. This will save you some headaches in case it doesn't work as expected.
Index: /pykota/trunk/setup.py
===================================================================
--- /pykota/trunk/setup.py (revision 1226)
+++ /pykota/trunk/setup.py (revision 1240)
@@ -24,4 +24,11 @@
 #
 # $Log$
+# Revision 1.28  2003/12/27 16:49:25  uid67467
+# Should be ok now.
+#
+# Revision 1.28  2003/12/06 09:03:43  jalet
+# Added Perl script to retrieve printer's internal page counter via PJL,
+# contributed by Ren�und Jensen.
+#
 # Revision 1.27  2003/11/28 08:31:28  jalet
 # Shell script to wait for AppleTalk enabled printers being idle was added.
@@ -338,5 +345,5 @@
       url = "http://www.librelogiciel.com/software/",
       packages = [ "pykota", "pykota.storages", "pykota.requesters", "pykota.loggers", "pykota.accounters", "pykota.reporters" ],
-      scripts = [ "bin/cupspykota", "bin/pykota", "bin/edpykota", "bin/repykota", "bin/warnpykota", "bin/pykotme", "bin/waitprinter.sh", "bin/papwaitprinter.sh" ],
+      scripts = [ "bin/cupspykota", "bin/pykota", "bin/edpykota", "bin/repykota", "bin/warnpykota", "bin/pykotme", "bin/waitprinter.sh", "bin/papwaitprinter.sh", "bin/mailandpopup.sh", "contributed/pagecount.pl" ],
       data_files = data_files)
 
Index: /pykota/trunk/CREDITS
===================================================================
--- /pykota/trunk/CREDITS (revision 1237)
+++ /pykota/trunk/CREDITS (revision 1240)
@@ -60,4 +60,7 @@
     - Stephen Markan - Faculty of Environmental Studies, University of Waterloo
     - Jeremy Worth - University of Oxford
+    - Roger Morgan - H�e Technische Bundeslehranstalt Steyr
+    - Pal Monstad
+    - Richard Potter
 
 Contributors :
@@ -67,5 +70,5 @@
   
     - Jo�Collet : PyKota's logo
-    - Stefan Wold - Stockholm University : code
+    - Stefan Wold - Stockholm University : inital code for external policies
     - John Flynn - University of Belize : LDAP support
     - Wayne Godoy - University of Belize : LDAP support
@@ -78,4 +81,5 @@
     - Daniel Mikkelsen : Sample configuration file fix
     - Carlos Fernandes : Portugues translation
+    - Ren�und Jensen : Contributed the pagecount.pl script
 
 ==============================================================
Index: /pykota/trunk/pykota/tool.py
===================================================================
--- /pykota/trunk/pykota/tool.py (revision 1229)
+++ /pykota/trunk/pykota/tool.py (revision 1240)
@@ -22,4 +22,10 @@
 #
 # $Log$
+# Revision 1.65  2003/12/27 16:49:25  uid67467
+# Should be ok now.
+#
+# Revision 1.65  2003/12/06 08:14:38  jalet
+# Added support for CUPS device uris which contain authentication information.
+#
 # Revision 1.64  2003/11/29 22:03:17  jalet
 # Some code refactoring work. New code is not used at this time.
@@ -288,5 +294,5 @@
         self.documentation = doc
         self.config = config.PyKotaConfig("/etc/pykota")
-        self.logger = logger.openLogger(self, self.config.getLoggingBackend())
+        self.logger = logger.openLogger(self.config.getLoggingBackend())
         self.debug = self.config.getDebug()
         self.storage = storage.openConnection(self)
@@ -574,7 +580,8 @@
                 self.sendMessageToAdmin(adminmail, _("Print Quota"), adminmessage)
             if mailto in [ "BOTH", "USER", "EXTERNAL" ] :
+                message = self.config.getHardWarn(printer.Name)
                 for user in self.storage.getGroupMembers(group) :
                     if mailto != "EXTERNAL" :
-                        self.sendMessageToUser(admin, adminmail, user, _("Print Quota Exceeded"), self.config.getHardWarn(printer.Name))
+                        self.sendMessageToUser(admin, adminmail, user, _("Print Quota Exceeded"), message)
                     else :    
                         self.externalMailTo(arguments, action, user, printer, message)
@@ -599,37 +606,50 @@
         """Checks a user quota and send him a message if quota is exceeded on current printer."""
         user = userpquota.User
-        printer = userpquota.Printer
-        admin = self.config.getAdmin(printer.Name)
-        adminmail = self.config.getAdminMail(printer.Name)
-        (mailto, arguments) = self.config.getMailTo(printer.Name)
-        action = self.checkUserPQuota(userpquota)
-        if action.startswith("POLICY_") :
-            action = action[7:]
-        if action == "DENY" :
-            adminmessage = _("Print Quota exceeded for user %s on printer %s") % (user.Name, printer.Name)
-            self.logger.log_message(adminmessage)
-            if mailto in [ "BOTH", "USER", "EXTERNAL" ] :
-                message = self.config.getHardWarn(printer.Name)
-                if mailto != "EXTERNAL" :
-                    self.sendMessageToUser(admin, adminmail, user, _("Print Quota Exceeded"), message)
-                else :    
-                    self.externalMailTo(arguments, action, user, printer, message)
-            if mailto in [ "BOTH", "ADMIN" ] :
-                self.sendMessageToAdmin(adminmail, _("Print Quota"), adminmessage)
-        elif action == "WARN" :    
-            adminmessage = _("Print Quota low for user %s on printer %s") % (user.Name, printer.Name)
-            self.logger.log_message(adminmessage)
-            if mailto in [ "BOTH", "USER", "EXTERNAL" ] :
-                if user.LimitBy and (user.LimitBy.lower() == "balance") : 
-                    message = self.config.getPoorWarn()
-                else :     
-                    message = self.config.getSoftWarn(printer.Name)
-                if mailto != "EXTERNAL" :    
-                    self.sendMessageToUser(admin, adminmail, user, _("Print Quota Low"), message)
-                else :    
-                    self.externalMailTo(arguments, action, user, printer, message)
-            if mailto in [ "BOTH", "ADMIN" ] :
-                self.sendMessageToAdmin(adminmail, _("Print Quota"), adminmessage)
-        return action        
+        actions = []
+        for upq in [userpquota] + userpquota.ParentPrintersUserPQuota :
+            printer = upq.Printer
+            admin = self.config.getAdmin(printer.Name)
+            adminmail = self.config.getAdminMail(printer.Name)
+            (mailto, arguments) = self.config.getMailTo(printer.Name)
+            self.logdebug("Checking quota for user %s on printer %s" % (upq.User.Name, printer.Name))
+            action = self.checkUserPQuota(upq)
+            self.logdebug("Result is %s" % action)
+            if action.startswith("POLICY_") :
+                action = action[7:]
+            if action == "DENY" :
+                adminmessage = _("Print Quota exceeded for user %s on printer %s") % (user.Name, printer.Name)
+                self.logger.log_message(adminmessage)
+                if mailto in [ "BOTH", "USER", "EXTERNAL" ] :
+                    message = self.config.getHardWarn(printer.Name)
+                    if mailto != "EXTERNAL" :
+                        self.sendMessageToUser(admin, adminmail, user, _("Print Quota Exceeded"), message)
+                    else :    
+                        self.externalMailTo(arguments, action, user, printer, message)
+                if mailto in [ "BOTH", "ADMIN" ] :
+                    self.sendMessageToAdmin(adminmail, _("Print Quota"), adminmessage)
+            elif action == "WARN" :    
+                adminmessage = _("Print Quota low for user %s on printer %s") % (user.Name, printer.Name)
+                self.logger.log_message(adminmessage)
+                if mailto in [ "BOTH", "USER", "EXTERNAL" ] :
+                    if user.LimitBy and (user.LimitBy.lower() == "balance") : 
+                        message = self.config.getPoorWarn()
+                    else :     
+                        message = self.config.getSoftWarn(printer.Name)
+                    if mailto != "EXTERNAL" :    
+                        self.sendMessageToUser(admin, adminmail, user, _("Print Quota Low"), message)
+                    else :    
+                        self.externalMailTo(arguments, action, user, printer, message)
+                if mailto in [ "BOTH", "ADMIN" ] :
+                    self.sendMessageToAdmin(adminmail, _("Print Quota"), adminmessage)
+            actions.append(action)        
+        if "DENY" in actions :    
+            self.logdebug("Final result is %s" % action)
+            return "DENY"
+        elif "WARN" in actions :    
+            self.logdebug("Final result is %s" % action)
+            return "WARN"
+        else :    
+            self.logdebug("Final result is %s" % action)
+            return "ALLOW"
         
 class PyKotaFilterOrBackend(PyKotaTool) :    
@@ -651,4 +671,14 @@
             self.username = self.username.lower()
         self.preserveinputfile = self.inputfile 
+        #
+        # Export internal data to the environment
+        os.environ["PYKOTAUSERNAME"] = str(self.username)
+        os.environ["PYKOTAPRINTERNAME"] = str(self.printername)
+        os.environ["PYKOTACOPIES"] = str(self.copies)
+        os.environ["PYKOTATITLE"] = str(self.title)
+        os.environ["PYKOTAOPTIONS"] = str(self.options)
+        os.environ["PYKOTAFILENAME"] = str(self.inputfile)
+        #
+        # And opens the correct accounter 
         self.accounter = accounter.openAccounter(self)
     
@@ -683,4 +713,7 @@
             while destination.startswith("/") :
                 destination = destination[1:]
+            checkauth = destination.split("@", 1)    
+            if len(checkauth) == 2 :
+                destination = checkauth[1]
             printerhostname = destination.split("/")[0].split(":")[0]
             return ("CUPS", \
@@ -737,5 +770,5 @@
            was returned by the external command.
         """
-        for passnumber in range(1, 3) :
+        for dummy in range(1, 3) :
             printer = self.storage.getPrinter(self.printername)
             user = self.storage.getUser(self.username)
Index: /pykota/trunk/pykota/reporters/html.py
===================================================================
--- /pykota/trunk/pykota/reporters/html.py (revision 1239)
+++ /pykota/trunk/pykota/reporters/html.py (revision 1240)
@@ -22,6 +22,6 @@
 #
 # $Log$
-# Revision 1.2  2003/12/27 15:43:36  uid67467
-# Savannah is back online...
+# Revision 1.3  2003/12/27 16:49:25  uid67467
+# Should be ok now.
 #
 # Revision 1.1  2003/12/02 14:41:17  jalet
@@ -56,9 +56,9 @@
                 for (group, grouppquota) in self.tool.storage.getPrinterGroupsAndQuotas(printer, self.ugnames) :
                     oddeven += 1
-                    if oddeven % 1 :
+                    if oddeven % 2 :
                         oddevenclass = "odd"
                     else :    
                         oddevenclass = "even"
-                    (pages, money, name, reached, soft, hard, balance, datelimit, lifepagecounter, lifetimepaid) = self.getQuota(group, grouppquota)
+                    (pages, money, name, reached, pagecounter, soft, hard, balance, datelimit, lifepagecounter, lifetimepaid) = self.getQuota(group, grouppquota)
                     self.report.append('<tr class="%s">%s</tr>' % (oddevenclass, "".join(["<td>%s</td>" % h for h in (name, reached, pagecounter, soft, hard, balance, datelimit or "&nbsp;", lifepagecounter, lifetimepaid)])))
                     total += pages
@@ -68,5 +68,5 @@
                 for (user, userpquota) in self.tool.storage.getPrinterUsersAndQuotas(printer, self.ugnames) :
                     oddeven += 1
-                    if oddeven % 1 :
+                    if oddeven % 2 :
                         oddevenclass = "odd"
                     else :    
Index: /pykota/trunk/pykota/reporters/text.py
===================================================================
--- /pykota/trunk/pykota/reporters/text.py (revision 1235)
+++ /pykota/trunk/pykota/reporters/text.py (revision 1240)
@@ -22,4 +22,7 @@
 #
 # $Log$
+# Revision 1.7  2003/12/27 16:49:25  uid67467
+# Should be ok now.
+#
 # Revision 1.6  2003/12/02 14:40:21  jalet
 # Some code refactoring.
@@ -45,6 +48,4 @@
 #
 
-from mx import DateTime
-
 from pykota.reporter import BaseReporter, PyKotaReporterError
     
@@ -68,5 +69,5 @@
                 self.report.append('-' * len(header))
                 for (group, grouppquota) in self.tool.storage.getPrinterGroupsAndQuotas(printer, self.ugnames) :
-                    (pages, money, name, reached, soft, hard, balance, datelimit, lifepagecounter, lifetimepaid) = self.getQuota(group, grouppquota)
+                    (pages, money, name, reached, pagecounter, soft, hard, balance, datelimit, lifepagecounter, lifetimepaid) = self.getQuota(group, grouppquota)
                     self.report.append("%-9.9s %s %7i %7s %7s %10s %-10.10s %8i %10s" % (name, reached, pagecounter, soft, hard, balance, datelimit, lifepagecounter, lifetimepaid))
                     total += pages
Index: /pykota/trunk/pykota/accounters/stupid.py
===================================================================
--- /pykota/trunk/pykota/accounters/stupid.py (revision 1239)
+++ /pykota/trunk/pykota/accounters/stupid.py (revision 1240)
@@ -22,6 +22,6 @@
 #
 # $Log$
-# Revision 1.9  2003/12/27 15:43:36  uid67467
-# Savannah is back online...
+# Revision 1.10  2003/12/27 16:49:25  uid67467
+# Should be ok now.
 #
 # Revision 1.8  2003/11/23 19:01:37  jalet
Index: /pykota/trunk/pykota/accounters/external.py
===================================================================
--- /pykota/trunk/pykota/accounters/external.py (revision 1239)
+++ /pykota/trunk/pykota/accounters/external.py (revision 1240)
@@ -22,6 +22,6 @@
 #
 # $Log$
-# Revision 1.11  2003/12/27 15:43:36  uid67467
-# Savannah is back online...
+# Revision 1.12  2003/12/27 16:49:25  uid67467
+# Should be ok now.
 #
 # Revision 1.10  2003/11/23 19:01:36  jalet
Index: /pykota/trunk/pykota/accounters/querying.py
===================================================================
--- /pykota/trunk/pykota/accounters/querying.py (revision 1239)
+++ /pykota/trunk/pykota/accounters/querying.py (revision 1240)
@@ -22,6 +22,6 @@
 #
 # $Log$
-# Revision 1.9  2003/12/27 15:43:36  uid67467
-# Savannah is back online...
+# Revision 1.10  2003/12/27 16:49:25  uid67467
+# Should be ok now.
 #
 # Revision 1.8  2003/11/21 14:28:46  jalet
@@ -70,5 +70,6 @@
         """Returns the printer's internal page counter."""
         global MAXTRIES, TIMETOSLEEP
-        for i in range(MAXTRIES) :
+        self.filter.logdebug("Reading printer's internal page counter...")
+        for dummy in range(MAXTRIES) :
             try :
                 counter = self.requester.getPrinterPageCounter(self.filter.printerhostname)
@@ -82,4 +83,5 @@
                 break
             time.sleep(TIMETOSLEEP)    
+        self.filter.logdebug("Printer's internal page counter value is : %s" % str(counter))
         return counter    
         
Index: /pykota/trunk/pykota/logger.py
===================================================================
--- /pykota/trunk/pykota/logger.py (revision 1218)
+++ /pykota/trunk/pykota/logger.py (revision 1240)
@@ -22,4 +22,7 @@
 #
 # $Log$
+# Revision 1.11  2003/12/27 16:49:25  uid67467
+# Should be ok now.
+#
 # Revision 1.10  2003/11/25 22:37:22  jalet
 # Small code move
@@ -69,5 +72,5 @@
     __str__ = __repr__
 
-def openLogger(pykotatool, backend) :
+def openLogger(backend) :
     """Returns the appropriate logger subsystem object."""
     try :
@@ -76,3 +79,3 @@
         raise PyKotaLoggingError, _("Unsupported logging subsystem %s") % backend
     else :    
-        return getattr(loggingbackend, "Logger")()
+        return loggingbackend.Logger()
Index: /pykota/trunk/pykota/requester.py
===================================================================
--- /pykota/trunk/pykota/requester.py (revision 1219)
+++ /pykota/trunk/pykota/requester.py (revision 1240)
@@ -22,4 +22,7 @@
 #
 # $Log$
+# Revision 1.11  2003/12/27 16:49:25  uid67467
+# Should be ok now.
+#
 # Revision 1.10  2003/11/25 23:46:40  jalet
 # Don't try to verify if module name is valid, Python does this better than us.
@@ -75,3 +78,3 @@
         raise PyKotaRequesterError, _("Unsupported requester backend %s") % backend
     else :    
-        return getattr(requesterbackend, "Requester")(printername, args)
+        return requesterbackend.Requester(printername, args)
Index: /pykota/trunk/pykota/storages/pgstorage.py
===================================================================
--- /pykota/trunk/pykota/storages/pgstorage.py (revision 1228)
+++ /pykota/trunk/pykota/storages/pgstorage.py (revision 1240)
@@ -22,4 +22,7 @@
 #
 # $Log$
+# Revision 1.25  2003/12/27 16:49:25  uid67467
+# Should be ok now.
+#
 # Revision 1.24  2003/11/29 22:02:14  jalet
 # Don't try to retrieve the user print quota information if current printer
@@ -392,4 +395,15 @@
         return groupsandquotas
         
+    def getParentPrinters(self, printer) :    
+        """Get all the printer groups this printer is a member of."""
+        pgroups = []
+        result = self.doSearch("SELECT printername FROM printergroupsmembers JOIN printers ON groupid=id WHERE printerid=%s;" % self.doQuote(printer.ident))
+        if result :
+            for record in result :
+                parentprinter = self.getPrinter(record.get("printername"))
+                if parentprinter.Exists :
+                    pgroups.append(parentprinter)
+        return pgroups
+        
     def addPrinter(self, printername) :        
         """Adds a printer to the quota storage, returns it."""
@@ -468,5 +482,5 @@
                 self.doModify("INSERT INTO jobhistory (userid, printerid, jobid, pagecounter, action, jobsize, jobprice, filename, title, copies, options) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)" % (self.doQuote(user.ident), self.doQuote(printer.ident), self.doQuote(jobid), self.doQuote(pagecounter), self.doQuote(action), self.doQuote(jobsize), self.doQuote(jobprice), self.doQuote(filename), self.doQuote(title), self.doQuote(copies), self.doQuote(options)))
             else :    
-                self.doModify("INSERT INTO jobhistory (userid, printerid, jobid, pagecounter, action, filename, title, copies, options) VALUES (%s, %s, %s, %s, %s)" % (self.doQuote(user.ident), self.doQuote(printer.ident), self.doQuote(jobid), self.doQuote(pagecounter), self.doQuote(action), self.doQuote(filename), self.doQuote(title), self.doQuote(copies), self.doQuote(options)))
+                self.doModify("INSERT INTO jobhistory (userid, printerid, jobid, pagecounter, action, filename, title, copies, options) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)" % (self.doQuote(user.ident), self.doQuote(printer.ident), self.doQuote(jobid), self.doQuote(pagecounter), self.doQuote(action), self.doQuote(filename), self.doQuote(title), self.doQuote(copies), self.doQuote(options)))
         else :        
             # here we explicitly want to reset jobsize to NULL if needed
Index: /pykota/trunk/pykota/storages/ldapstorage.py
===================================================================
--- /pykota/trunk/pykota/storages/ldapstorage.py (revision 1228)
+++ /pykota/trunk/pykota/storages/ldapstorage.py (revision 1240)
@@ -22,4 +22,7 @@
 #
 # $Log$
+# Revision 1.41  2003/12/27 16:49:25  uid67467
+# Should be ok now.
+#
 # Revision 1.40  2003/11/29 22:02:14  jalet
 # Don't try to retrieve the user print quota information if current printer
@@ -166,4 +169,5 @@
 #
 
+import types
 import time
 import md5
@@ -211,4 +215,14 @@
         return md5.md5("%s" % time.time()).hexdigest()
         
+    def normalizeFields(self, fields) :    
+        """Ensure all items are lists."""
+        for (k, v) in fields.items() :
+            if type(v) not in (types.TupleType, types.ListType) :
+                if not v :
+                    del fields[k]
+                else :    
+                    fields[k] = [ v ]
+        return fields        
+        
     def beginTransaction(self) :    
         """Starts a transaction."""
@@ -237,4 +251,5 @@
     def doAdd(self, dn, fields) :
         """Adds an entry in the LDAP directory."""
+        fields = self.normalizeFields(fields)
         try :
             self.tool.logdebug("QUERY : ADD(%s, %s)" % (dn, str(fields)))
@@ -255,4 +270,5 @@
     def doModify(self, dn, fields, ignoreold=1) :
         """Modifies an entry in the LDAP directory."""
+        fields = self.normalizeFields(fields)
         try :
             oldentry = self.doSearch("objectClass=*", base=dn, scope=ldap.SCOPE_BASE)
@@ -545,4 +561,15 @@
         return groupsandquotas
         
+    def getParentPrinters(self, printer) :    
+        """Get all the printer groups this printer is a member of."""
+        pgroups = []
+        result = self.doSearch("(&(objectClass=pykotaPrinter)(uniqueMember=%s))" % printer.ident, ["pykotaPrinterName"], base=self.info["printerbase"])
+        if result :
+            for (printerid, fields) in result :
+                parentprinter = self.getPrinter(fields.get("pykotaPrinterName")[0])
+                if parentprinter.Exists :
+                    pgroups.append(parentprinter)
+        return pgroups
+        
     def addPrinter(self, printername) :        
         """Adds a printer to the quota storage, returns it."""
@@ -679,5 +706,5 @@
         """Sets the date limit permanently for a user print quota."""
         fields = {
-                   "pykotaDateLimit" : "%04i-%02i-%02i %02i:%02i:%02i" % (datelimit.year, datelimit.month, datelimit.day, datelimit.hour, datelimit.minute, datelimit.second),
+                   "pykotaDateLimit" : datelimit,
                  }
         return self.doModify(userpquota.ident, fields)
@@ -686,5 +713,5 @@
         """Sets the date limit permanently for a group print quota."""
         fields = {
-                   "pykotaDateLimit" : "%04i-%02i-%02i %02i:%02i:%02i" % (datelimit.year, datelimit.month, datelimit.day, datelimit.hour, datelimit.minute, datelimit.second),
+                   "pykotaDateLimit" : datelimit,
                  }
         return self.doModify(grouppquota.ident, fields)
Index: /pykota/trunk/pykota/reporter.py
===================================================================
--- /pykota/trunk/pykota/reporter.py (revision 1239)
+++ /pykota/trunk/pykota/reporter.py (revision 1240)
@@ -22,6 +22,6 @@
 #
 # $Log$
-# Revision 1.5  2003/12/27 15:43:36  uid67467
-# Savannah is back online...
+# Revision 1.6  2003/12/27 16:49:25  uid67467
+# Should be ok now.
 #
 # Revision 1.4  2003/12/02 14:40:21  jalet
@@ -41,4 +41,6 @@
 #
 #
+
+from mx import DateTime
 
 class PyKotaReporterError(Exception):
@@ -96,17 +98,20 @@
                 datelimit = "DENY"
                 reached = "+B"
+            elif balance <= self.tool.config.getPoorMan() :
+                datelimit = "WARNING"
+                reached = "?B"
             else :    
                 datelimit = ""
                 reached = "-B"
         else :
-            if quota.DateLimit is not None :
+            if (quota.HardLimit is not None) and (pagecounter >= quota.HardLimit) :    
+                datelimit = "DENY"
+            elif (quota.HardLimit is None) and (quota.SoftLimit is not None) and (pagecounter >= quota.SoftLimit) :
+                datelimit = "DENY"
+            elif quota.DateLimit is not None :
                 now = DateTime.now()
                 datelimit = DateTime.ISO.ParseDateTime(quota.DateLimit)
                 if now >= datelimit :
                     datelimit = "DENY"
-            elif (quota.HardLimit is not None) and (pagecounter >= quota.HardLimit) :    
-                datelimit = "DENY"
-            elif (quota.HardLimit is None) and (quota.SoftLimit is not None) and (pagecounter >= quota.SoftLimit) :
-                datelimit = "DENY"
             else :    
                 datelimit = ""
@@ -124,3 +129,3 @@
         raise PyKotaReporterError, _("Unsupported reporter backend %s") % reporttype
     else :    
-        return getattr(reporterbackend, "Reporter")(tool, printers, ugnames, isgroup)
+        return reporterbackend.Reporter(tool, printers, ugnames, isgroup)
Index: /pykota/trunk/pykota/accounter.py
===================================================================
--- /pykota/trunk/pykota/accounter.py (revision 1239)
+++ /pykota/trunk/pykota/accounter.py (revision 1240)
@@ -22,6 +22,6 @@
 #
 # $Log$
-# Revision 1.8  2003/12/27 15:43:36  uid67467
-# Savannah is back online...
+# Revision 1.9  2003/12/27 16:49:25  uid67467
+# Should be ok now.
 #
 # Revision 1.7  2003/11/25 23:46:40  jalet
@@ -144,4 +144,9 @@
         return action
         
+    def computeJobSize(self) :    
+        """Must be overriden in children classes."""
+        raise RuntimeError, "AccounterBase.computeJobSize() must be overriden !"
+        
+        
 def openAccounter(kotafilter) :
     """Returns a connection handle to the appropriate accounter."""
@@ -152,3 +157,3 @@
         raise PyKotaAccounterError, _("Unsupported accounter backend %s") % backend
     else :    
-        return getattr(accounterbackend, "Accounter")(kotafilter, args)
+        return accounterbackend.Accounter(kotafilter, args)
Index: /pykota/trunk/pykota/storage.py
===================================================================
--- /pykota/trunk/pykota/storage.py (revision 1219)
+++ /pykota/trunk/pykota/storage.py (revision 1240)
@@ -22,4 +22,7 @@
 #
 # $Log$
+# Revision 1.29  2003/12/27 16:49:25  uid67467
+# Should be ok now.
+#
 # Revision 1.28  2003/11/25 23:46:40  jalet
 # Don't try to verify if module name is valid, Python does this better than us.
@@ -248,4 +251,5 @@
         self.HardLimit = None
         self.DateLimit = None
+        self.ParentPrintersUserPQuota = (user.Exists and printer.Exists and parent.getParentPrintersUserPQuota(self)) or []
         
     def setDateLimit(self, datelimit) :    
@@ -268,20 +272,20 @@
     def increasePagesUsage(self, nbpages) :
         """Increase the value of used pages and money."""
-        if nbpages :
-            jobprice = (float(self.Printer.PricePerPage or 0.0) * nbpages) + float(self.Printer.PricePerJob or 0.0)
-            newpagecounter = int(self.PageCounter or 0) + nbpages
-            newlifepagecounter = int(self.LifePageCounter or 0) + nbpages
-            self.parent.beginTransaction()
-            try :
-                if jobprice : # optimization : don't access the database if unneeded.
-                    self.User.consumeAccountBalance(jobprice)
-                self.parent.writeUserPQuotaPagesCounters(self, newpagecounter, newlifepagecounter)
-            except PyKotaStorageError, msg :    
-                self.parent.rollbackTransaction()
-                raise PyKotaStorageError, msg
-            else :    
-                self.parent.commitTransaction()
-                self.PageCounter = newpagecounter
-                self.LifePageCounter = newlifepagecounter
+        jobprice = (float(self.Printer.PricePerPage or 0.0) * nbpages) + float(self.Printer.PricePerJob or 0.0)
+        self.parent.beginTransaction()
+        try :
+            if nbpages :
+                self.User.consumeAccountBalance(jobprice)
+                for upq in [ self ] + self.ParentPrintersUserPQuota :
+                    newpagecounter = int(upq.PageCounter or 0) + nbpages
+                    newlifepagecounter = int(upq.LifePageCounter or 0) + nbpages
+                    self.parent.writeUserPQuotaPagesCounters(upq, newpagecounter, newlifepagecounter)
+                    upq.PageCounter = newpagecounter
+                    upq.LifePageCounter = newlifepagecounter
+        except PyKotaStorageError, msg :    
+            self.parent.rollbackTransaction()
+            raise PyKotaStorageError, msg
+        else :    
+            self.parent.commitTransaction()
         
 class StorageGroupPQuota(StorageObject) :
@@ -344,4 +348,8 @@
             self.tool.logdebug("Caching enabled.")
             self.caches = { "USERS" : {}, "GROUPS" : {}, "PRINTERS" : {}, "USERPQUOTAS" : {}, "GROUPPQUOTAS" : {}, "JOBS" : {}, "LASTJOBS" : {} }
+        
+    def close(self) :    
+        """Must be overriden in children classes."""
+        raise RuntimeError, "BaseStorage.close() must be overriden !"
         
     def __del__(self) :        
@@ -441,4 +449,11 @@
         return user.Groups   
         
+    def getParentPrintersUserPQuota(self, userpquota) :     
+        """Returns all user print quota on the printer and its parents."""
+        upquotas = [ ]
+        for printer in self.getParentPrinters(userpquota.Printer) :
+            upquotas.append(self.getUserPQuota(userpquota.User, printer))
+        return upquotas        
+        
 def openConnection(pykotatool) :
     """Returns a connection handle to the appropriate Quota Storage Database."""
@@ -454,4 +469,4 @@
         admin = backendinfo["storageadmin"] or backendinfo["storageuser"]
         adminpw = backendinfo["storageadminpw"] or backendinfo["storageuserpw"]
-        return getattr(storagebackend, "Storage")(pykotatool, host, database, admin, adminpw)
+        return storagebackend.Storage(pykotatool, host, database, admin, adminpw)
 
Index: /pykota/trunk/pykota/version.py
===================================================================
--- /pykota/trunk/pykota/version.py (revision 1239)
+++ /pykota/trunk/pykota/version.py (revision 1240)
@@ -22,5 +22,5 @@
 #
 
-__version__ = "1.16alpha17_unofficial"
+__version__ = "1.16alpha20_unofficial"
 
 __doc__ = """PyKota : a complete Printing Quota Solution for CUPS and LPRng."""
Index: /pykota/trunk/cgi-bin/printquota.cgi
===================================================================
--- /pykota/trunk/cgi-bin/printquota.cgi (revision 1239)
+++ /pykota/trunk/cgi-bin/printquota.cgi (revision 1240)
@@ -1,3 +1,3 @@
-#! /usr/bin/python2.2
+#! /usr/bin/python
 
 # PyKota Print Quota Reports generator
@@ -23,6 +23,6 @@
 #
 # $Log$
-# Revision 1.17  2003/12/27 15:43:36  uid67467
-# Savannah is back online...
+# Revision 1.18  2003/12/27 16:49:25  uid67467
+# Should be ok now.
 #
 # Revision 1.16  2003/12/02 14:40:20  jalet
Index: /pykota/trunk/bin/waitprinter.sh
===================================================================
--- /pykota/trunk/bin/waitprinter.sh (revision 1096)
+++ /pykota/trunk/bin/waitprinter.sh (revision 1240)
@@ -14,5 +14,7 @@
 # $Id$
 #
-
+# Fix by Matt Hyclak :
+sleep 3 
+#
 until snmpget -v1 -c public -Ov $1 HOST-RESOURCES-MIB::hrPrinterStatus.1 | grep -i idle >/dev/null ; do 
    sleep 1 ; 
Index: /pykota/trunk/bin/cupspykota
===================================================================
--- /pykota/trunk/bin/cupspykota (revision 1222)
+++ /pykota/trunk/bin/cupspykota (revision 1240)
@@ -24,4 +24,11 @@
 #
 # $Log$
+# Revision 1.17  2003/12/27 16:49:25  uid67467
+# Should be ok now.
+#
+# Revision 1.17  2003/12/06 08:54:29  jalet
+# Code simplifications.
+# Added many debugging messages.
+#
 # Revision 1.16  2003/11/26 20:43:29  jalet
 # Inadvertantly introduced a bug, which is fixed.
@@ -84,5 +91,4 @@
 import os
 import popen2
-import time
 import cStringIO
 import shlex
@@ -187,4 +193,5 @@
         if user.Exists :
             # Is the current user allowed to print at all ?
+            thebackend.logdebug("Checking user %s's quota on printer %s" % (user.Name, printer.Name))
             action = thebackend.warnUserPQuota(thebackend.storage.getUserPQuota(user, printer))
         elif policy == "EXTERNAL" :                
@@ -203,4 +210,5 @@
         # pass the job untouched to the underlying layer
         # and starts accounting at the same time
+        thebackend.logdebug("Job accounting begins.")
         thebackend.accounter.beginJob(printer, user)
         
@@ -222,4 +230,5 @@
         
         # And launch it
+        thebackend.logdebug("Starting real backend %s with args %s" % (realbackend, " ".join([os.environ["DEVICE_URI"]] + sys.argv[1:])))
         subprocess = PyKotaPopen3([realbackend] + sys.argv[1:], capturestderr=1, bufsize=0, arg0=os.environ["DEVICE_URI"])
         
@@ -248,4 +257,6 @@
         endinput = 0
         status = -1
+        inputclosed = 0
+        thebackend.logdebug("Entering streams polling loop...")
         while status == -1 :
             # First check if original backend is still alive
@@ -263,4 +274,5 @@
             availablefds = pollster.poll()
             for (fd, mask) in availablefds :
+                # thebackend.logdebug("file: %i    mask: %04x" % (fd, mask))
                 if mask & select.POLLOUT :
                     # We can write
@@ -303,15 +315,10 @@
                         # standard output
                         if outdata :                
-                            try :
-                                os.write(stdoutfno, outdata)
-                                outdata = ""
-                            except :    
-                                pass
-                        try :        
-                            pollster.unregister(fromcfno)        
-                        except KeyError :    
-                            pass
-                        else :    
-                            os.close(fromcfno)
+                            os.write(stdoutfno, outdata)
+                            outdata = ""
+                        # We are no more interested in this file descriptor        
+                        pollster.unregister(fromcfno)        
+                        os.close(fromcfno)
+                        thebackend.logdebug("Real backend's stdout ends.")
                     elif fd == cerrfno :    
                         # Original CUPS backend has finished 
@@ -319,18 +326,12 @@
                         if errdata :                
                             # Try to write remaining info (normally "Ready to print.")
-                            try :
-                                os.write(stderrfno, errdata)
-                                errdata = ""
-                            except :    
-                                pass
+                            os.write(stderrfno, errdata)
+                            errdata = ""
                         # We are no more interested in this file descriptor        
-                        try :        
-                            pollster.unregister(cerrfno)        
-                        except KeyError :    
-                            pass
-                        else :    
-                            os.close(cerrfno)
+                        pollster.unregister(cerrfno)        
+                        os.close(cerrfno)
+                        thebackend.logdebug("Real backend's stderr ends.")
                         
-            if endinput :            
+            if endinput and not inputclosed :            
                 # We deal with remaining input datas here
                 # because EOF can happen in two different
@@ -338,21 +339,17 @@
                 # code, nor making functions.
                 if indata :                
-                    try :
-                        os.write(tocfno, indata)
-                        indata = ""
-                    except :    
-                        pass
-                # Again, we're not interested in this file descriptor        
-                # anymore.
-                try :        
-                    pollster.unregister(tocfno)        
-                except KeyError :    
-                    pass
-                else :    
-                    os.close(tocfno)
+                    os.write(tocfno, indata)
+                    indata = ""
+                # Again, we're not interested in this file descriptor anymore.
+                pollster.unregister(tocfno)        
+                os.close(tocfno)
+                inputclosed = 1
+                thebackend.logdebug("Input data ends.")
                 
         # Input file was a real file, we have to close it.    
         if mustclose :
             infile.close()
+            
+        thebackend.logdebug("Exiting streams polling loop...")
             
         # Check exit code of original CUPS backend.    
@@ -365,7 +362,9 @@
     # stops accounting. 
     thebackend.accounter.endJob(printer, user)
+    thebackend.logdebug("Job accounting ends.")
         
     # retrieve the job size    
     jobsize = thebackend.accounter.getJobSize()
+    thebackend.logdebug("Job size : %i" % jobsize)
     
     # update the quota for the current user on this printer 
@@ -374,8 +373,10 @@
         userquota = thebackend.storage.getUserPQuota(user, printer)
         if userquota.Exists :
+            thebackend.logdebug("Updating user %s's quota on printer %s" % (user.Name, printer.Name))
             userquota.increasePagesUsage(jobsize)
     
     # adds the current job to history    
     printer.addJobToHistory(thebackend.jobid, user, thebackend.accounter.getLastPageCounter(), action, jobsize, jobprice, thebackend.preserveinputfile, thebackend.title, thebackend.copies, thebackend.options)
+    thebackend.logdebug("Job added to history.")
     
     return retcode # return (retcode or gotSigTerm) shouldn't be needed
Index: /pykota/trunk/bin/pykota
===================================================================
--- /pykota/trunk/bin/pykota (revision 1221)
+++ /pykota/trunk/bin/pykota (revision 1240)
@@ -24,4 +24,7 @@
 #
 # $Log$
+# Revision 1.48  2003/12/27 16:49:25  uid67467
+# Should be ok now.
+#
 # Revision 1.47  2003/11/26 19:17:35  jalet
 # Printing on a printer not present in the Quota Storage now results
@@ -274,4 +277,5 @@
         if user.Exists :
             # Now does the accounting and act depending on the result
+            thefilter.logdebug("Does accounting for user %s on printer %s." % (user.Name, printer.Name))
             action = thefilter.accounter.doAccounting(printer, user)
         
@@ -279,4 +283,5 @@
             if action == "DENY" :
                 # No, just die cleanly
+                thefilter.logdebug("Printing is denied.")
                 return thefilter.removeJob()
         elif policy == "EXTERNAL" :                
@@ -285,4 +290,5 @@
         
         # pass the job untouched to the underlying layer
+        thefilter.logdebug("Passing input data to next filter.")
         thefilter.accounter.filterInput(thefilter.inputfile)      
     
Index: /pykota/trunk/initscripts/ldap/pykota.schema
===================================================================
--- /pykota/trunk/initscripts/ldap/pykota.schema (revision 1223)
+++ /pykota/trunk/initscripts/ldap/pykota.schema (revision 1240)
@@ -189,5 +189,5 @@
         DESC 'PyKota Printer'
         MUST ( cn )
-        MAY  ( pykotaPrinterName $ pykotaPricePerPage $ pykotaPricePerJob ) )
+        MAY  ( pykotaPrinterName $ pykotaPricePerPage $ pykotaPricePerJob $ uniqueMember ) )
         
 # pykotaUserPQuota        
Index: /pykota/trunk/initscripts/postgresql/pykota-postgresql.sql
===================================================================
--- /pykota/trunk/initscripts/postgresql/pykota-postgresql.sql (revision 1203)
+++ /pykota/trunk/initscripts/postgresql/pykota-postgresql.sql (revision 1240)
@@ -20,4 +20,7 @@
 --
 -- $Log$
+-- Revision 1.7  2003/12/27 16:49:25  uid67467
+-- Should be ok now.
+--
 -- Revision 1.6  2003/11/23 19:01:36  jalet
 -- Job price added to history
@@ -110,6 +113,6 @@
 CREATE TABLE jobhistory(id SERIAL PRIMARY KEY NOT NULL,
                         jobid TEXT,
-                        userid INT4 REFERENCES users(id),
-                        printerid INT4 REFERENCES printers(id),
+                        userid INT4,
+                        printerid INT4,
                         pagecounter INT4 DEFAULT 0,
                         jobsize INT4,
@@ -120,5 +123,6 @@
                         copies INT4,
                         options TEXT,
-                        jobdate TIMESTAMP DEFAULT now());
+                        jobdate TIMESTAMP DEFAULT now(),
+                        CONSTRAINT checkUserPQuota FOREIGN KEY (userid, printerid) REFERENCES userpquota(userid, printerid));
 CREATE INDEX jobhistory_p_id_ix ON jobhistory (printerid);
 CREATE INDEX jobhistory_pd_id_ix ON jobhistory (printerid, jobdate);
@@ -141,13 +145,20 @@
                            userid INT4 REFERENCES users(id),
                            PRIMARY KEY (groupid, userid));
+                           
+--                         
+-- Create the printer groups relationship
+--
+CREATE TABLE printergroupsmembers(groupid INT4 REFERENCES printers(id),
+                           printerid INT4 REFERENCES printers(id),
+                           PRIMARY KEY (groupid, printerid));
 
 --                        
 -- Set some ACLs                        
 --
-REVOKE ALL ON users, groups, printers, userpquota, grouppquota, groupsmembers, jobhistory FROM public;                        
+REVOKE ALL ON users, groups, printers, userpquota, grouppquota, groupsmembers, printergroupsmembers, jobhistory FROM public;                        
 REVOKE ALL ON users_id_seq, groups_id_seq, printers_id_seq, userpquota_id_seq, grouppquota_id_seq, jobhistory_id_seq FROM public;
 
-GRANT SELECT, INSERT, UPDATE, DELETE, REFERENCES ON users, groups, printers, userpquota, grouppquota, groupsmembers, jobhistory TO pykotaadmin;
+GRANT SELECT, INSERT, UPDATE, DELETE, REFERENCES ON users, groups, printers, userpquota, grouppquota, groupsmembers, printergroupsmembers, jobhistory TO pykotaadmin;
 GRANT SELECT, UPDATE ON users_id_seq, groups_id_seq, printers_id_seq, userpquota_id_seq, grouppquota_id_seq, jobhistory_id_seq TO pykotaadmin;
-GRANT SELECT ON users, groups, printers, userpquota, grouppquota, groupsmembers, jobhistory TO pykotauser;
+GRANT SELECT ON users, groups, printers, userpquota, grouppquota, groupsmembers, printergroupsmembers, jobhistory TO pykotauser;
 
Index: /pykota/trunk/initscripts/postgresql/upgrade-to-1.16.sql
===================================================================
--- /pykota/trunk/initscripts/postgresql/upgrade-to-1.16.sql (revision 1203)
+++ /pykota/trunk/initscripts/postgresql/upgrade-to-1.16.sql (revision 1240)
@@ -20,4 +20,7 @@
 --
 -- $Log$
+-- Revision 1.3  2003/12/27 16:49:25  uid67467
+-- Should be ok now.
+--
 -- Revision 1.2  2003/11/23 19:01:36  jalet
 -- Job price added to history
@@ -48,4 +51,22 @@
 ALTER TABLE jobhistory ADD COLUMN options TEXT;
 
+--
+-- Remove bad integrity rules
+-- and replace them with a new one
+--
+ALTER TABLE jobhistory DROP CONSTRAINT "$1";
+ALTER TABLE jobhistory DROP CONSTRAINT "$2";
+ALTER TABLE jobhistory ADD CONSTRAINT checkUserPQuota FOREIGN KEY (userid, printerid) REFERENCES userpquota (userid, printerid);
+
+-- 
+-- Add new tables
+--
+--                         
+-- Create the printer groups relationship
+--
+CREATE TABLE printergroupsmembers(groupid INT4 REFERENCES printers(id),
+                           printerid INT4 REFERENCES printers(id),
+                           PRIMARY KEY (groupid, printerid));
+
 --                         
 -- Now add some indexes
@@ -56,2 +77,9 @@
 CREATE UNIQUE INDEX grouppquota_up_id_ix ON grouppquota (groupid, printerid);
 
+-- 
+-- And now sets some ACLs
+-- 
+REVOKE ALL ON printergroupsmembers FROM public;                        
+GRANT SELECT, INSERT, UPDATE, DELETE, REFERENCES ON printergroupsmembers TO pykotaadmin;
+GRANT SELECT ON printergroupsmembers TO pykotauser;
+
Index: /pykota/trunk/MANIFEST.in
===================================================================
--- /pykota/trunk/MANIFEST.in (revision 1226)
+++ /pykota/trunk/MANIFEST.in (revision 1240)
@@ -1,3 +1,3 @@
-include COPYING TODO NEWS CREDITS SECURITY MANIFEST.in clean.sh bin/cupspykota bin/pykota bin/edpykota bin/warnpykota bin/repykota bin/pykotme bin/waitprinter.sh bin/papwaitprinter.sh bin/README 
+include COPYING TODO NEWS CREDITS SECURITY MANIFEST.in clean.sh bin/cupspykota bin/pykota bin/edpykota bin/warnpykota bin/repykota bin/pykotme bin/waitprinter.sh bin/papwaitprinter.sh bin/mailandpopup.sh bin/README contributed/pagecount.pl
 recursive-include po README *.po *.mo *.pot
 recursive-include man README *.1
Index: /pykota/trunk/NEWS
===================================================================
--- /pykota/trunk/NEWS (revision 1239)
+++ /pykota/trunk/NEWS (revision 1240)
@@ -22,4 +22,39 @@
 PyKota NEWS :
 
+    - 1.16alpha20 :
+    
+        - Fixed several small bugs with the help of PyChecker.
+        
+    - 1.16alpha19 :
+     
+        - Fixed a bug in PostgreSQL backend's code.
+        
+    - 1.16alpha18 :
+    
+        - Initial code for printer groups support. Printer groups have
+          to be added with external tools like psql or gq for example.
+          Printer groups seems to work fine with users. Untested with user 
+          groups for now.
+
+        - Several minor visual problems fixed in reports.
+        
+        - Fixed a bug in LDAP date handling.
+        
+        - Fix for LDAP problem with some combination of OpenLDAP + Python-LDAP
+          + some particular settings.
+          
+        - mailandpopup.sh shell script added. See sample configuration file
+          for details.
+        
+        - Several environment variables are now exported when executing 
+          external commands (accounters, requesters, mailers) :
+           
+            PYKOTAUSERNAME => user who is printing
+            PYKOTAPRINTERNAME => printer on which the job is being sent
+            PYKOTATITLE => Job's title
+            PYKOTAFILENAME => Job's filename
+            PYKOTACOPIES => number of copies asked
+            PYKOTAOPTIONS => print command options (-o with CUPS)
+          
     - 1.16alpha17 :
     
Index: /pykota/trunk/po/en/pykota.po
===================================================================
--- /pykota/trunk/po/en/pykota.po (revision 1239)
+++ /pykota/trunk/po/en/pykota.po (revision 1240)
@@ -21,6 +21,6 @@
 #
 # $Log$
-# Revision 1.37  2003/12/27 15:43:36  uid67467
-# Savannah is back online...
+# Revision 1.38  2003/12/27 16:49:25  uid67467
+# Should be ok now.
 #
 # Revision 1.36  2003/11/18 23:43:11  jalet
Index: /pykota/trunk/po/pt/pykota.po
===================================================================
--- /pykota/trunk/po/pt/pykota.po (revision 1237)
+++ /pykota/trunk/po/pt/pykota.po (revision 1240)
@@ -21,4 +21,7 @@
 #
 # $Log$
+# Revision 1.2  2003/12/27 16:49:25  uid67467
+# Should be ok now.
+#
 # Revision 1.1  2003/12/03 18:49:37  jalet
 # Portugues translation added thanks to Carlos Fernandes
@@ -83,6 +86,6 @@
 msgstr "Impressora %s n�encontrada"
 
-msgid "*** Report for %s quota on printer %s"
-msgstr "*** Relatorio de quota %s para a impressora %s"
+msgid "Report for %s quota on printer %s"
+msgstr "Relatorio de quota %s para a impressora %s"
 
 msgid "Error in page count value %i for user %s on printer %s"
Index: /pykota/trunk/po/es/pykota.po
===================================================================
--- /pykota/trunk/po/es/pykota.po (revision 1239)
+++ /pykota/trunk/po/es/pykota.po (revision 1240)
@@ -21,9 +21,6 @@
 #
 # $Log$
-# Revision 1.5  2003/12/27 15:43:36  uid67467
-# Savannah is back online...
-#
-# Revision 1.4  2003/12/03 18:51:09  jalet
-# Fixed spanish translation
+# Revision 1.6  2003/12/27 16:49:25  uid67467
+# Should be ok now.
 #
 # Revision 1.3  2003/11/24 16:52:53  jalet
@@ -133,13 +130,8 @@
 "\n"
 "\n"
-"Please contact your system administrator :\n"
-"\n"
-"\t%s - <%s>\n"
-msgstr ""
-"\n"
-"\n"
 "Por favor contacte a su administrador del sistema:\n"
 "\n"
 "\t%s - <%s>\n"
+msgstr ""
 
 msgid "Invalid grace delay %s"
Index: /pykota/trunk/po/fr/pykota.po
===================================================================
--- /pykota/trunk/po/fr/pykota.po (revision 1239)
+++ /pykota/trunk/po/fr/pykota.po (revision 1240)
@@ -21,6 +21,6 @@
 #
 # $Log$
-# Revision 1.36  2003/12/27 15:43:36  uid67467
-# Savannah is back online...
+# Revision 1.37  2003/12/27 16:49:25  uid67467
+# Should be ok now.
 #
 # Revision 1.35  2003/11/18 23:43:12  jalet
Index: /pykota/trunk/po/pykota.pot
===================================================================
--- /pykota/trunk/po/pykota.pot (revision 1239)
+++ /pykota/trunk/po/pykota.pot (revision 1240)
@@ -21,6 +21,6 @@
 #
 # $Log$
-# Revision 1.37  2003/12/27 15:43:36  uid67467
-# Savannah is back online...
+# Revision 1.38  2003/12/27 16:49:25  uid67467
+# Should be ok now.
 #
 # Revision 1.36  2003/11/18 23:43:11  jalet
