Index: /pykota/trunk/conf/pykota.conf.sample
===================================================================
--- /pykota/trunk/conf/pykota.conf.sample (revision 2307)
+++ /pykota/trunk/conf/pykota.conf.sample (revision 2308)
@@ -851,6 +851,14 @@
 #      one just after the other.
 #
-# Possible values are 'yes' and 'no'
-#
+# Possible values are 'yes', 'no', or any other string.
+# If the string is not recognized as a truth value,
+# it is considered to be a command to launch. 
+# PyKota launches the command and parses its standard
+# output. The special keywords 'ALLOW' and 'DENY' are
+# recognized, allowing an external tool to decide if
+# the job is to be allowed or denied. 
+#
+# denyduplicates : /usr/bin/myowncommand with some arguments
+# denyduplicates : yes
 denyduplicates : no
 
Index: /pykota/trunk/bin/cupspykota
===================================================================
--- /pykota/trunk/bin/cupspykota (revision 2303)
+++ /pykota/trunk/bin/cupspykota (revision 2308)
@@ -239,4 +239,5 @@
             self.logdebug("Precomputed job's size is %s pages, price is %s units" % (self.softwareJobSize, self.softwareJobPrice))
             
+            denyduplicates = self.config.getDenyDuplicates(printer.Name)
             if not self.jobSizeBytes :
                 # if no data to pass to real backend, probably a filter
@@ -245,10 +246,21 @@
                 self.printMoreInfo(user, printer, _("Job contains no data. Printing is denied."), "warn")
                 action = "DENY"
-            elif self.config.getDenyDuplicates(printer.Name) \
+            elif denyduplicates \
                  and printer.LastJob.Exists \
                  and (printer.LastJob.UserName == user.Name) \
                  and (printer.LastJob.JobMD5Sum == self.checksum) :
-                self.printMoreInfo(user, printer, _("Job is a duplicate. Printing is denied."), "warn")
-                action = "DENY" 
+                if denyduplicates == 1 :
+                    self.printMoreInfo(user, printer, _("Job is a duplicate. Printing is denied."), "warn")
+                    action = "DENY"
+                else :    
+                    self.logdebug("Launching subprocess [%s] to see if dupes should be allowed or not." % denyduplicates)
+                    fanswer = os.popen(denyduplicates, "r")
+                    action = fanswer.read().strip().upper()
+                    fanswer.close()
+                    if action == "DENY" :     
+                        self.printMoreInfo(user, printer, _("Job is a duplicate. Printing is denied by subprocess."), "warn")
+                    else :    
+                        self.printMoreInfo(user, printer, _("Job is a duplicate. Printing is allowed by subprocess."), "warn")
+                        action = "ALLOW" # just to be sure, in the case the external command returns something else...
             else :    
                 # checks the user's quota
Index: /pykota/trunk/NEWS
===================================================================
--- /pykota/trunk/NEWS (revision 2307)
+++ /pykota/trunk/NEWS (revision 2308)
@@ -24,4 +24,10 @@
     - 1.23alpha9 :   
     
+        - The denyduplicates directive now accepts a command line as an
+          argument beside yes/no. The command is launched and must
+          print either ALLOW or DENY to its standard output. Then the
+          job gets allowed or denied by PyKota.
+          This will allow user interaction in the near future.
+          
         - Introduced the new 'striptitle' directive to remove ugly prefixes
           from print job's titles whenever a new print job is submitted.
Index: /pykota/trunk/po/sv_SE/pykota.po
===================================================================
--- /pykota/trunk/po/sv_SE/pykota.po (revision 2303)
+++ /pykota/trunk/po/sv_SE/pykota.po (revision 2308)
@@ -1910,4 +1910,10 @@
 msgstr ""
 
+msgid "Job is a duplicate. Printing is denied by subprocess."
+msgstr ""
+
+msgid "Job is a duplicate. Printing is allowed by subprocess."
+msgstr ""
+
 #~ msgid ""
 #~ "Group           used    soft    hard    balance grace         total       "
Index: /pykota/trunk/po/pt/pykota.po
===================================================================
--- /pykota/trunk/po/pt/pykota.po (revision 2303)
+++ /pykota/trunk/po/pt/pykota.po (revision 2308)
@@ -1926,4 +1926,10 @@
 msgstr ""
 
+msgid "Job is a duplicate. Printing is denied by subprocess."
+msgstr ""
+
+msgid "Job is a duplicate. Printing is allowed by subprocess."
+msgstr ""
+
 #~ msgid ""
 #~ "Group           used    soft    hard    balance grace         total       "
Index: /pykota/trunk/po/tr/pykota.po
===================================================================
--- /pykota/trunk/po/tr/pykota.po (revision 2303)
+++ /pykota/trunk/po/tr/pykota.po (revision 2308)
@@ -2254,4 +2254,10 @@
 msgstr ""
 
+msgid "Job is a duplicate. Printing is denied by subprocess."
+msgstr ""
+
+msgid "Job is a duplicate. Printing is allowed by subprocess."
+msgstr ""
+
 #~ msgid ""
 #~ "Internal SNMP accounting asked, but Python-SNMP is not available. Please "
Index: /pykota/trunk/po/pt_BR/pykota.po
===================================================================
--- /pykota/trunk/po/pt_BR/pykota.po (revision 2303)
+++ /pykota/trunk/po/pt_BR/pykota.po (revision 2308)
@@ -1918,4 +1918,10 @@
 msgstr ""
 
+msgid "Job is a duplicate. Printing is denied by subprocess."
+msgstr ""
+
+msgid "Job is a duplicate. Printing is allowed by subprocess."
+msgstr ""
+
 #~ msgid ""
 #~ "Group           used    soft    hard    balance grace         total       "
Index: /pykota/trunk/po/es/pykota.po
===================================================================
--- /pykota/trunk/po/es/pykota.po (revision 2303)
+++ /pykota/trunk/po/es/pykota.po (revision 2308)
@@ -2691,4 +2691,10 @@
 
 msgid "only for payments or history"
+msgstr ""
+
+msgid "Job is a duplicate. Printing is denied by subprocess."
+msgstr ""
+
+msgid "Job is a duplicate. Printing is allowed by subprocess."
 msgstr ""
 
Index: /pykota/trunk/po/fr/pykota.po
===================================================================
--- /pykota/trunk/po/fr/pykota.po (revision 2303)
+++ /pykota/trunk/po/fr/pykota.po (revision 2308)
@@ -2808,2 +2808,12 @@
 msgid "only for payments or history"
 msgstr "seulement pour les paiements ou l'historique"
+
+msgid "Job is a duplicate. Printing is denied by subprocess."
+msgstr ""
+"Le travail d'impression est un doublon. Impression interdite par le sous-"
+"processus."
+
+msgid "Job is a duplicate. Printing is allowed by subprocess."
+msgstr ""
+"Le travail d'impression est un doublon. Impression autoris�par le sous-"
+"processus."
Index: /pykota/trunk/po/de/pykota.po
===================================================================
--- /pykota/trunk/po/de/pykota.po (revision 2303)
+++ /pykota/trunk/po/de/pykota.po (revision 2308)
@@ -2367,4 +2367,10 @@
 
 msgid "only for payments or history"
+msgstr ""
+
+msgid "Job is a duplicate. Printing is denied by subprocess."
+msgstr ""
+
+msgid "Job is a duplicate. Printing is allowed by subprocess."
 msgstr ""
 
Index: /pykota/trunk/po/el_GR/pykota.po
===================================================================
--- /pykota/trunk/po/el_GR/pykota.po (revision 2303)
+++ /pykota/trunk/po/el_GR/pykota.po (revision 2308)
@@ -2337,4 +2337,10 @@
 
 msgid "only for payments or history"
+msgstr ""
+
+msgid "Job is a duplicate. Printing is denied by subprocess."
+msgstr ""
+
+msgid "Job is a duplicate. Printing is allowed by subprocess."
 msgstr ""
 
Index: /pykota/trunk/po/pykota.pot
===================================================================
--- /pykota/trunk/po/pykota.pot (revision 2303)
+++ /pykota/trunk/po/pykota.pot (revision 2308)
@@ -1851,2 +1851,8 @@
 msgid "only for payments or history"
 msgstr ""
+
+msgid "Job is a duplicate. Printing is denied by subprocess."
+msgstr ""
+
+msgid "Job is a duplicate. Printing is allowed by subprocess."
+msgstr ""
Index: /pykota/trunk/po/nb_NO/pykota.po
===================================================================
--- /pykota/trunk/po/nb_NO/pykota.po (revision 2303)
+++ /pykota/trunk/po/nb_NO/pykota.po (revision 2308)
@@ -2291,4 +2291,10 @@
 
 msgid "only for payments or history"
+msgstr ""
+
+msgid "Job is a duplicate. Printing is denied by subprocess."
+msgstr ""
+
+msgid "Job is a duplicate. Printing is allowed by subprocess."
 msgstr ""
 
Index: /pykota/trunk/po/th/pykota.po
===================================================================
--- /pykota/trunk/po/th/pykota.po (revision 2303)
+++ /pykota/trunk/po/th/pykota.po (revision 2308)
@@ -1891,4 +1891,10 @@
 msgstr ""
 
+msgid "Job is a duplicate. Printing is denied by subprocess."
+msgstr ""
+
+msgid "Job is a duplicate. Printing is allowed by subprocess."
+msgstr ""
+
 #~ msgid ""
 #~ "Group           used    soft    hard    balance grace         total       "
Index: /pykota/trunk/po/zh_TW/pykota.po
===================================================================
--- /pykota/trunk/po/zh_TW/pykota.po (revision 2303)
+++ /pykota/trunk/po/zh_TW/pykota.po (revision 2308)
@@ -1885,4 +1885,10 @@
 msgstr ""
 
+msgid "Job is a duplicate. Printing is denied by subprocess."
+msgstr ""
+
+msgid "Job is a duplicate. Printing is allowed by subprocess."
+msgstr ""
+
 #~ msgid ""
 #~ "Internal SNMP accounting asked, but Python-SNMP is not available. Please "
Index: /pykota/trunk/po/it/pykota.po
===================================================================
--- /pykota/trunk/po/it/pykota.po (revision 2303)
+++ /pykota/trunk/po/it/pykota.po (revision 2308)
@@ -1929,4 +1929,10 @@
 msgstr ""
 
+msgid "Job is a duplicate. Printing is denied by subprocess."
+msgstr ""
+
+msgid "Job is a duplicate. Printing is allowed by subprocess."
+msgstr ""
+
 #~ msgid ""
 #~ "Group           used    soft    hard    balance grace         total       "
Index: /pykota/trunk/TODO
===================================================================
--- /pykota/trunk/TODO (revision 2307)
+++ /pykota/trunk/TODO (revision 2308)
@@ -22,14 +22,4 @@
 TODO, in no particular order :
         
-        - Allow 'external' syntax for denyduplicates :
-        
-            in pykota.conf, if denyduplicates is set to :
-  
-              - yes ==> then duplicates are denied silently (with a log)
-              - no (or unset) ==> then duplicates are allowed (without a log)
-              - external(some_command_here) then the command is launched
-                and its output is parsed, it may contain only ALLOW or DENY.
-                And it should handle the dialog with the user, if any.
-                
         - Add a new external hook to allow the overwriting of the job's ticket        
           (mainly username and billing code) at startup time :
Index: /pykota/trunk/pykota/config.py
===================================================================
--- /pykota/trunk/pykota/config.py (revision 2307)
+++ /pykota/trunk/pykota/config.py (revision 2308)
@@ -49,5 +49,12 @@
     def isTrue(self, option) :        
         """Returns 1 if option is set to true, else 0."""
-        if (option is not None) and (option.upper().strip() in ['Y', 'YES', '1', 'ON', 'T', 'TRUE']) :
+        if (option is not None) and (option.strip().upper() in ['Y', 'YES', '1', 'ON', 'T', 'TRUE']) :
+            return 1
+        else :    
+            return 0
+                        
+    def isFalse(self, option) :        
+        """Returns 1 if option is set to false, else 0."""
+        if (option is not None) and (option.strip().upper() in ['N', 'NO', '0', 'OFF', 'F', 'FALSE']) :
             return 1
         else :    
@@ -394,9 +401,17 @@
         
     def getDenyDuplicates(self, printername) :          
-        """Returns 1 if we want to deny duplicate jobs, else 0."""
+        """Returns 1 or a command if we want to deny duplicate jobs, else 0."""
         try : 
-            return self.isTrue(self.getPrinterOption(printername, "denyduplicates"))
+            denyduplicates = self.getPrinterOption(printername, "denyduplicates")
         except PyKotaConfigError :    
             return 0
+        else :    
+            if self.isTrue(denyduplicates) :
+                return 1
+            elif self.isFalse(denyduplicates) :
+                return 0
+            else :    
+                # it's a command to run.
+                return denyduplicates
         
     def getWinbindSeparator(self) :          
