root / pykota / trunk / pykota / tool.py @ 1601

Revision 1601, 50.9 kB (checked in by jalet, 20 years ago)

Missing file... Am I really stupid ?

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
Line 
1# PyKota
2# -*- coding: ISO-8859-15 -*-
3
4# PyKota - Print Quotas for CUPS and LPRng
5#
6# (c) 2003-2004 Jerome Alet <alet@librelogiciel.com>
7# This program is free software; you can redistribute it and/or modify
8# it under the terms of the GNU General Public License as published by
9# the Free Software Foundation; either version 2 of the License, or
10# (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License
18# along with this program; if not, write to the Free Software
19# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
20#
21# $Id$
22#
23# $Log$
24# Revision 1.113  2004/07/17 20:37:27  jalet
25# Missing file... Am I really stupid ?
26#
27# Revision 1.112  2004/07/16 12:22:47  jalet
28# LPRng support early version
29#
30# Revision 1.111  2004/07/06 18:09:42  jalet
31# Reduced the set of invalid characters in names
32#
33# Revision 1.110  2004/07/01 19:56:42  jalet
34# Better dispatching of error messages
35#
36# Revision 1.109  2004/07/01 17:45:49  jalet
37# Added code to handle the description field for printers
38#
39# Revision 1.108  2004/06/24 23:09:30  jalet
40# Also prints read size on last block
41#
42# Revision 1.107  2004/06/23 13:03:28  jalet
43# Catches accounter configuration errors earlier
44#
45# Revision 1.106  2004/06/22 09:31:18  jalet
46# Always send some debug info to CUPS' back channel stream (stderr) as
47# informationnal messages.
48#
49# Revision 1.105  2004/06/21 08:17:38  jalet
50# Added version number in subject message for directive crashrecipient.
51#
52# Revision 1.104  2004/06/18 13:34:49  jalet
53# Now all tracebacks include PyKota's version number
54#
55# Revision 1.103  2004/06/18 13:17:26  jalet
56# Now includes PyKota's version number in messages sent by the crashrecipient
57# directive.
58#
59# Revision 1.102  2004/06/17 13:26:51  jalet
60# Better exception handling code
61#
62# Revision 1.101  2004/06/16 20:56:34  jalet
63# Smarter initialisation code
64#
65# Revision 1.100  2004/06/11 08:16:03  jalet
66# More exceptions catched in case of very early failure.
67#
68# Revision 1.99  2004/06/11 07:07:38  jalet
69# Now detects and logs configuration syntax errors instead of failing without
70# any notice message.
71#
72# Revision 1.98  2004/06/08 19:27:12  jalet
73# Doesn't ignore SIGCHLD anymore
74#
75# Revision 1.97  2004/06/07 22:45:35  jalet
76# Now accepts a job when enforcement is STRICT and predicted account balance
77# is equal to 0.0 : since the job hasn't been printed yet, only its printing
78# will really render balance equal to 0.0, so we should be allowed to print.
79#
80# Revision 1.96  2004/06/05 22:18:04  jalet
81# Now catches some exceptions earlier.
82# storage.py and ldapstorage.py : removed old comments
83#
84# Revision 1.95  2004/06/03 21:50:34  jalet
85# Improved error logging.
86# crashrecipient directive added.
87# Now exports the job's size in bytes too.
88#
89# Revision 1.94  2004/06/03 08:51:03  jalet
90# logs job's size in bytes now
91#
92# Revision 1.93  2004/06/02 21:51:02  jalet
93# Moved the sigterm capturing elsewhere
94#
95# Revision 1.92  2004/06/02 13:21:38  jalet
96# Debug message added
97#
98# Revision 1.91  2004/05/25 05:17:52  jalet
99# Now precomputes the job's size only if current printer's enforcement
100# is "STRICT"
101#
102# Revision 1.90  2004/05/24 22:45:49  jalet
103# New 'enforcement' directive added
104# Polling loop improvements
105#
106# Revision 1.89  2004/05/21 22:02:52  jalet
107# Preliminary work on pre-accounting
108#
109# Revision 1.88  2004/05/18 14:49:20  jalet
110# Big code changes to completely remove the need for "requester" directives,
111# jsut use "hardware(... your previous requester directive's content ...)"
112#
113# Revision 1.87  2004/05/17 19:14:59  jalet
114# Now catches SIGPIPE and SIGCHLD
115#
116# Revision 1.86  2004/05/13 13:59:28  jalet
117# Code simplifications
118#
119# Revision 1.85  2004/05/11 08:26:27  jalet
120# Now catches connection problems to SMTP server
121#
122# Revision 1.84  2004/04/21 08:36:32  jalet
123# Exports the PYKOTASTATUS environment variable when SIGTERM is received.
124#
125# Revision 1.83  2004/04/16 17:03:49  jalet
126# The list of printers groups the current printer is a member of is
127# now exported in the PYKOTAPGROUPS environment variable
128#
129# Revision 1.82  2004/04/13 09:38:03  jalet
130# More work on correct child processes handling
131#
132# Revision 1.81  2004/04/09 22:24:47  jalet
133# Began work on correct handling of child processes when jobs are cancelled by
134# the user. Especially important when an external requester is running for a
135# long time.
136#
137# Revision 1.80  2004/04/06 12:00:21  jalet
138# uninitialized values caused problems
139#
140# Revision 1.79  2004/03/28 21:01:29  jalet
141# PYKOTALIMITBY environment variable is now exported too
142#
143# Revision 1.78  2004/03/08 20:13:25  jalet
144# Allow names to begin with a digit
145#
146# Revision 1.77  2004/03/03 13:10:35  jalet
147# Now catches all smtplib exceptions when there's a problem sending messages
148#
149# Revision 1.76  2004/03/01 14:34:15  jalet
150# PYKOTAPHASE wasn't set at the right time at the end of data transmission
151# to underlying layer (real backend)
152#
153# Revision 1.75  2004/03/01 11:23:25  jalet
154# Pre and Post hooks to external commands are available in the cupspykota
155# backend. Forthe pykota filter they will be implemented real soon now.
156#
157# Revision 1.74  2004/02/26 14:18:07  jalet
158# Should fix the remaining bugs wrt printers groups and users groups.
159#
160# Revision 1.73  2004/02/19 14:20:21  jalet
161# maildomain pykota.conf directive added.
162# Small improvements on mail headers quality.
163#
164# Revision 1.72  2004/01/14 15:51:19  jalet
165# Docstring added.
166#
167# Revision 1.71  2004/01/11 23:22:42  jalet
168# Major code refactoring, it's way cleaner, and now allows automated addition
169# of printers on first print.
170#
171# Revision 1.70  2004/01/08 14:10:32  jalet
172# Copyright year changed.
173#
174# Revision 1.69  2004/01/05 16:02:18  jalet
175# Dots in user, groups and printer names should be allowed.
176#
177# Revision 1.68  2004/01/02 17:38:40  jalet
178# This time it should be better...
179#
180# Revision 1.67  2004/01/02 17:37:09  jalet
181# I'm completely stupid !!! Better to not talk while coding !
182#
183# Revision 1.66  2004/01/02 17:31:26  jalet
184# Forgot to remove some code
185#
186# Revision 1.65  2003/12/06 08:14:38  jalet
187# Added support for CUPS device uris which contain authentication information.
188#
189# Revision 1.64  2003/11/29 22:03:17  jalet
190# Some code refactoring work. New code is not used at this time.
191#
192# Revision 1.63  2003/11/29 20:06:20  jalet
193# Added 'utolower' configuration option to convert all usernames to
194# lowercase when printing. All database accesses are still and will
195# remain case sensitive though.
196#
197# Revision 1.62  2003/11/25 22:37:22  jalet
198# Small code move
199#
200# Revision 1.61  2003/11/25 22:03:28  jalet
201# No more message on stderr when the translation is not available.
202#
203# Revision 1.60  2003/11/25 21:54:05  jalet
204# updated FAQ
205#
206# Revision 1.59  2003/11/25 13:33:43  jalet
207# Puts 'root' instead of '' when printing from CUPS web interface (which
208# gives an empty username)
209#
210# Revision 1.58  2003/11/21 14:28:45  jalet
211# More complete job history.
212#
213# Revision 1.57  2003/11/19 23:19:38  jalet
214# Code refactoring work.
215# Explicit redirection to /dev/null has to be set in external policy now, just
216# like in external mailto.
217#
218# Revision 1.56  2003/11/19 07:40:20  jalet
219# Missing import statement.
220# Better documentation for mailto: external(...)
221#
222# Revision 1.55  2003/11/18 23:43:12  jalet
223# Mailto can be any external command now, as usual.
224#
225# Revision 1.54  2003/10/24 21:52:46  jalet
226# Now can force language when coming from CGI script.
227#
228# Revision 1.53  2003/10/08 21:41:38  jalet
229# External policies for printers works !
230# We can now auto-add users on first print, and do other useful things if needed.
231#
232# Revision 1.52  2003/10/07 09:07:28  jalet
233# Character encoding added to please latest version of Python
234#
235# Revision 1.51  2003/10/06 14:21:41  jalet
236# Test reversed to not retrieve group members when no messages for them.
237#
238# Revision 1.50  2003/10/02 20:23:18  jalet
239# Storage caching mechanism added.
240#
241# Revision 1.49  2003/07/29 20:55:17  jalet
242# 1.14 is out !
243#
244# Revision 1.48  2003/07/21 23:01:56  jalet
245# Modified some messages aout soft limit
246#
247# Revision 1.47  2003/07/16 21:53:08  jalet
248# Really big modifications wrt new configuration file's location and content.
249#
250# Revision 1.46  2003/07/09 20:17:07  jalet
251# Email field added to PostgreSQL schema
252#
253# Revision 1.45  2003/07/08 19:43:51  jalet
254# Configurable warning messages.
255# Poor man's treshold value added.
256#
257# Revision 1.44  2003/07/07 11:49:24  jalet
258# Lots of small fixes with the help of PyChecker
259#
260# Revision 1.43  2003/07/04 09:06:32  jalet
261# Small bug fix wrt undefined "LimitBy" field.
262#
263# Revision 1.42  2003/06/30 12:46:15  jalet
264# Extracted reporting code.
265#
266# Revision 1.41  2003/06/25 14:10:01  jalet
267# Hey, it may work (edpykota --reset excepted) !
268#
269# Revision 1.40  2003/06/10 16:37:54  jalet
270# Deletion of the second user which is not needed anymore.
271# Added a debug configuration field in /etc/pykota.conf
272# All queries can now be sent to the logger in debug mode, this will
273# greatly help improve performance when time for this will come.
274#
275# Revision 1.39  2003/04/29 18:37:54  jalet
276# Pluggable accounting methods (actually doesn't support external scripts)
277#
278# Revision 1.38  2003/04/24 11:53:48  jalet
279# Default policy for unknown users/groups is to DENY printing instead
280# of the previous default to ALLOW printing. This is to solve an accuracy
281# problem. If you set the policy to ALLOW, jobs printed by in nexistant user
282# (from PyKota's POV) will be charged to the next user who prints on the
283# same printer.
284#
285# Revision 1.37  2003/04/24 08:08:27  jalet
286# Debug message forgotten
287#
288# Revision 1.36  2003/04/24 07:59:40  jalet
289# LPRng support now works !
290#
291# Revision 1.35  2003/04/23 22:13:57  jalet
292# Preliminary support for LPRng added BUT STILL UNTESTED.
293#
294# Revision 1.34  2003/04/17 09:26:21  jalet
295# repykota now reports account balances too.
296#
297# Revision 1.33  2003/04/16 12:35:49  jalet
298# Groups quota work now !
299#
300# Revision 1.32  2003/04/16 08:53:14  jalet
301# Printing can now be limited either by user's account balance or by
302# page quota (the default). Quota report doesn't include account balance
303# yet, though.
304#
305# Revision 1.31  2003/04/15 11:30:57  jalet
306# More work done on money print charging.
307# Minor bugs corrected.
308# All tools now access to the storage as priviledged users, repykota excepted.
309#
310# Revision 1.30  2003/04/10 21:47:20  jalet
311# Job history added. Upgrade script neutralized for now !
312#
313# Revision 1.29  2003/03/29 13:45:27  jalet
314# GPL paragraphs were incorrectly (from memory) copied into the sources.
315# Two README files were added.
316# Upgrade script for PostgreSQL pre 1.01 schema was added.
317#
318# Revision 1.28  2003/03/29 13:08:28  jalet
319# Configuration is now expected to be found in /etc/pykota.conf instead of
320# in /etc/cups/pykota.conf
321# Installation script can move old config files to the new location if needed.
322# Better error handling if configuration file is absent.
323#
324# Revision 1.27  2003/03/15 23:01:28  jalet
325# New mailto option in configuration file added.
326# No time to test this tonight (although it should work).
327#
328# Revision 1.26  2003/03/09 23:58:16  jalet
329# Comment
330#
331# Revision 1.25  2003/03/07 22:56:14  jalet
332# 0.99 is out with some bug fixes.
333#
334# Revision 1.24  2003/02/27 23:48:41  jalet
335# Correctly maps PyKota's log levels to syslog log levels
336#
337# Revision 1.23  2003/02/27 22:55:20  jalet
338# WARN log priority doesn't exist.
339#
340# Revision 1.22  2003/02/27 09:09:20  jalet
341# Added a method to match strings against wildcard patterns
342#
343# Revision 1.21  2003/02/17 23:01:56  jalet
344# Typos
345#
346# Revision 1.20  2003/02/17 22:55:01  jalet
347# More options can now be set per printer or globally :
348#
349#       admin
350#       adminmail
351#       gracedelay
352#       requester
353#
354# the printer option has priority when both are defined.
355#
356# Revision 1.19  2003/02/10 11:28:45  jalet
357# Localization
358#
359# Revision 1.18  2003/02/10 01:02:17  jalet
360# External requester is about to work, but I must sleep
361#
362# Revision 1.17  2003/02/09 13:05:43  jalet
363# Internationalization continues...
364#
365# Revision 1.16  2003/02/09 12:56:53  jalet
366# Internationalization begins...
367#
368# Revision 1.15  2003/02/08 22:09:52  jalet
369# Name check method moved here
370#
371# Revision 1.14  2003/02/07 10:42:45  jalet
372# Indentation problem
373#
374# Revision 1.13  2003/02/07 08:34:16  jalet
375# Test wrt date limit was wrong
376#
377# Revision 1.12  2003/02/06 23:20:02  jalet
378# warnpykota doesn't need any user/group name argument, mimicing the
379# warnquota disk quota tool.
380#
381# Revision 1.11  2003/02/06 22:54:33  jalet
382# warnpykota should be ok
383#
384# Revision 1.10  2003/02/06 15:03:11  jalet
385# added a method to set the limit date
386#
387# Revision 1.9  2003/02/06 10:39:23  jalet
388# Preliminary edpykota work.
389#
390# Revision 1.8  2003/02/06 09:19:02  jalet
391# More robust behavior (hopefully) when the user or printer is not managed
392# correctly by the Quota System : e.g. cupsFilter added in ppd file, but
393# printer and/or user not 'yet?' in storage.
394#
395# Revision 1.7  2003/02/06 00:00:45  jalet
396# Now includes the printer name in email messages
397#
398# Revision 1.6  2003/02/05 23:55:02  jalet
399# Cleaner email messages
400#
401# Revision 1.5  2003/02/05 23:45:09  jalet
402# Better DateTime manipulation wrt grace delay
403#
404# Revision 1.4  2003/02/05 23:26:22  jalet
405# Incorrect handling of grace delay
406#
407# Revision 1.3  2003/02/05 22:16:20  jalet
408# DEVICE_URI is undefined outside of CUPS, i.e. for normal command line tools
409#
410# Revision 1.2  2003/02/05 22:10:29  jalet
411# Typos
412#
413# Revision 1.1  2003/02/05 21:28:17  jalet
414# Initial import into CVS
415#
416#
417#
418
419import sys
420import os
421import fnmatch
422import getopt
423import smtplib
424import gettext
425import locale
426import signal
427import socket
428import tempfile
429import ConfigParser
430
431from mx import DateTime
432
433from pykota import version, config, storage, logger, accounter, pdlanalyzer
434
435class PyKotaToolError(Exception):
436    """An exception for PyKota config related stuff."""
437    def __init__(self, message = ""):
438        self.message = message
439        Exception.__init__(self, message)
440    def __repr__(self):
441        return self.message
442    __str__ = __repr__
443   
444def crashed(message) :   
445    """Minimal crash method."""
446    import traceback
447    lines = []
448    for line in traceback.format_exception(*sys.exc_info()) :
449        lines.extend([l for l in line.split("\n") if l])
450    msg = "ERROR: ".join(["%s\n" % l for l in (["ERROR: PyKota v%s" % version.__version__, message] + lines)])
451    sys.stderr.write(msg)
452    sys.stderr.flush()
453    return msg
454
455class PyKotaTool :   
456    """Base class for all PyKota command line tools."""
457    def __init__(self, lang=None, doc="PyKota %s (c) 2003-2004 %s" % (version.__version__, version.__author__)) :
458        """Initializes the command line tool."""
459        # locale stuff
460        try :
461            locale.setlocale(locale.LC_ALL, lang)
462            gettext.install("pykota")
463        except (locale.Error, IOError) :
464            gettext.NullTranslations().install()
465   
466        # pykota specific stuff
467        self.documentation = doc
468        try :
469            self.config = config.PyKotaConfig("/etc/pykota")
470        except ConfigParser.ParsingError, msg :   
471            sys.stderr.write("ERROR: Problem encountered while parsing configuration file : %s\n" % msg)
472            sys.stderr.flush()
473            sys.exit(-1)
474           
475        try :
476            self.debug = self.config.getDebug()
477            self.smtpserver = self.config.getSMTPServer()
478            self.maildomain = self.config.getMailDomain()
479            self.logger = logger.openLogger(self.config.getLoggingBackend())
480            self.storage = storage.openConnection(self)
481        except (config.PyKotaConfigError, logger.PyKotaLoggingError, storage.PyKotaStorageError), msg :
482            self.crashed(msg)
483            raise
484        self.softwareJobSize = 0
485        self.softwareJobPrice = 0.0
486       
487    def logdebug(self, message) :   
488        """Logs something to debug output if debug is enabled."""
489        if self.debug :
490            self.logger.log_message(message, "debug")
491           
492    def printInfo(self, message, level="info") :       
493        """Sends a message to standard error."""
494        sys.stderr.write("%s: %s\n" % (level.upper(), message))
495        sys.stderr.flush()
496       
497    def clean(self) :   
498        """Ensures that the database is closed."""
499        try :
500            self.storage.close()
501        except (TypeError, NameError, AttributeError) :   
502            pass
503           
504    def display_version_and_quit(self) :
505        """Displays version number, then exists successfully."""
506        self.clean()
507        print version.__version__
508        sys.exit(0)
509   
510    def display_usage_and_quit(self) :
511        """Displays command line usage, then exists successfully."""
512        self.clean()
513        print self.documentation
514        sys.exit(0)
515       
516    def crashed(self, message) :   
517        """Outputs a crash message, and optionally sends it to software author."""
518        msg = crashed(message)
519        try :
520            crashrecipient = self.config.getCrashRecipient()
521            if crashrecipient :
522                admin = self.config.getAdminMail("global") # Nice trick, isn't it ?
523                fullmessage = "========== Traceback :\n\n%s\n\n========== sys.argv :\n\n%s\n\n========== Environment :\n\n%s\n" % \
524                                (msg, \
525                                 "\n".join(["    %s" % repr(a) for a in sys.argv]), \
526                                 "\n".join(["    %s=%s" % (k, v) for (k, v) in os.environ.items()]))
527                server = smtplib.SMTP(self.smtpserver)
528                server.sendmail(admin, [admin, crashrecipient], \
529                                       "From: %s\nTo: %s\nCc: %s\nSubject: PyKota v%s crash traceback !\n\n%s" % \
530                                       (admin, crashrecipient, admin, version.__version__, fullmessage))
531                server.quit()
532        except :
533            pass
534       
535    def parseCommandline(self, argv, short, long, allownothing=0) :
536        """Parses the command line, controlling options."""
537        # split options in two lists: those which need an argument, those which don't need any
538        withoutarg = []
539        witharg = []
540        lgs = len(short)
541        i = 0
542        while i < lgs :
543            ii = i + 1
544            if (ii < lgs) and (short[ii] == ':') :
545                # needs an argument
546                witharg.append(short[i])
547                ii = ii + 1 # skip the ':'
548            else :
549                # doesn't need an argument
550                withoutarg.append(short[i])
551            i = ii
552               
553        for option in long :
554            if option[-1] == '=' :
555                # needs an argument
556                witharg.append(option[:-1])
557            else :
558                # doesn't need an argument
559                withoutarg.append(option)
560       
561        # we begin with all possible options unset
562        parsed = {}
563        for option in withoutarg + witharg :
564            parsed[option] = None
565       
566        # then we parse the command line
567        args = []       # to not break if something unexpected happened
568        try :
569            options, args = getopt.getopt(argv, short, long)
570            if options :
571                for (o, v) in options :
572                    # we skip the '-' chars
573                    lgo = len(o)
574                    i = 0
575                    while (i < lgo) and (o[i] == '-') :
576                        i = i + 1
577                    o = o[i:]
578                    if o in witharg :
579                        # needs an argument : set it
580                        parsed[o] = v
581                    elif o in withoutarg :
582                        # doesn't need an argument : boolean
583                        parsed[o] = 1
584                    else :
585                        # should never occur
586                        raise PyKotaToolError, "Unexpected problem when parsing command line"
587            elif (not args) and (not allownothing) and sys.stdin.isatty() : # no option and no argument, we display help if we are a tty
588                self.display_usage_and_quit()
589        except getopt.error, msg :
590            self.printInfo(msg)
591            self.display_usage_and_quit()
592        return (parsed, args)
593   
594    def isValidName(self, name) :
595        """Checks if a user or printer name is valid."""
596        invalidchars = "/@"
597        for c in invalidchars :
598            if c in name :
599                return 0
600        return 1       
601       
602    def matchString(self, s, patterns) :
603        """Returns 1 if the string s matches one of the patterns, else 0."""
604        for pattern in patterns :
605            if fnmatch.fnmatchcase(s, pattern) :
606                return 1
607        return 0
608       
609    def sendMessage(self, adminmail, touser, fullmessage) :
610        """Sends an email message containing headers to some user."""
611        if "@" not in touser :
612            touser = "%s@%s" % (touser, self.maildomain or self.smtpserver)
613        try :   
614            server = smtplib.SMTP(self.smtpserver)
615        except socket.error, msg :   
616            self.printInfo(_("Impossible to connect to SMTP server : %s") % msg, "error")
617        else :
618            try :
619                server.sendmail(adminmail, [touser], "From: %s\nTo: %s\n%s" % (adminmail, touser, fullmessage))
620            except smtplib.SMTPException, answer :   
621                for (k, v) in answer.recipients.items() :
622                    self.printInfo(_("Impossible to send mail to %s, error %s : %s") % (k, v[0], v[1]), "error")
623            server.quit()
624       
625    def sendMessageToUser(self, admin, adminmail, user, subject, message) :
626        """Sends an email message to a user."""
627        message += _("\n\nPlease contact your system administrator :\n\n\t%s - <%s>\n") % (admin, adminmail)
628        self.sendMessage(adminmail, user.Email or user.Name, "Subject: %s\n\n%s" % (subject, message))
629       
630    def sendMessageToAdmin(self, adminmail, subject, message) :
631        """Sends an email message to the Print Quota administrator."""
632        self.sendMessage(adminmail, adminmail, "Subject: %s\n\n%s" % (subject, message))
633       
634    def _checkUserPQuota(self, userpquota) :           
635        """Checks the user quota on a printer and deny or accept the job."""
636        # then we check the user's own quota
637        # if we get there we are sure that policy is not EXTERNAL
638        user = userpquota.User
639        printer = userpquota.Printer
640        enforcement = self.config.getPrinterEnforcement(printer.Name)
641        self.logdebug("Checking user %s's quota on printer %s" % (user.Name, printer.Name))
642        (policy, dummy) = self.config.getPrinterPolicy(userpquota.Printer.Name)
643        if not userpquota.Exists :
644            # Unknown userquota
645            if policy == "ALLOW" :
646                action = "POLICY_ALLOW"
647            else :   
648                action = "POLICY_DENY"
649            self.printInfo(_("Unable to match user %s on printer %s, applying default policy (%s)") % (user.Name, printer.Name, action))
650        else :   
651            pagecounter = int(userpquota.PageCounter or 0)
652            if enforcement == "STRICT" :
653                pagecounter += self.softwareJobSize
654            if userpquota.SoftLimit is not None :
655                softlimit = int(userpquota.SoftLimit)
656                if pagecounter < softlimit :
657                    action = "ALLOW"
658                else :   
659                    if userpquota.HardLimit is None :
660                        # only a soft limit, this is equivalent to having only a hard limit
661                        action = "DENY"
662                    else :   
663                        hardlimit = int(userpquota.HardLimit)
664                        if softlimit <= pagecounter < hardlimit :   
665                            now = DateTime.now()
666                            if userpquota.DateLimit is not None :
667                                datelimit = DateTime.ISO.ParseDateTime(userpquota.DateLimit)
668                            else :
669                                datelimit = now + self.config.getGraceDelay(printer.Name)
670                                userpquota.setDateLimit(datelimit)
671                            if now < datelimit :
672                                action = "WARN"
673                            else :   
674                                action = "DENY"
675                        else :         
676                            action = "DENY"
677            else :       
678                if userpquota.HardLimit is not None :
679                    # no soft limit, only a hard one.
680                    hardlimit = int(userpquota.HardLimit)
681                    if pagecounter < hardlimit :
682                        action = "ALLOW"
683                    else :     
684                        action = "DENY"
685                else :
686                    # Both are unset, no quota, i.e. accounting only
687                    action = "ALLOW"
688        return action
689   
690    def checkGroupPQuota(self, grouppquota) :   
691        """Checks the group quota on a printer and deny or accept the job."""
692        group = grouppquota.Group
693        printer = grouppquota.Printer
694        enforcement = self.config.getPrinterEnforcement(printer.Name)
695        self.logdebug("Checking group %s's quota on printer %s" % (group.Name, printer.Name))
696        if group.LimitBy and (group.LimitBy.lower() == "balance") : 
697            val = group.AccountBalance
698            if enforcement == "STRICT" : 
699                val -= self.softwareJobPrice # use precomputed size.
700            if val <= 0.0 :
701                action = "DENY"
702            elif val <= self.config.getPoorMan() :   
703                action = "WARN"
704            else :   
705                action = "ALLOW"
706            if (enforcement == "STRICT") and (val == 0.0) :
707                action = "WARN" # we can still print until account is 0
708        else :
709            val = grouppquota.PageCounter
710            if enforcement == "STRICT" :
711                val += self.softwareJobSize
712            if grouppquota.SoftLimit is not None :
713                softlimit = int(grouppquota.SoftLimit)
714                if val < softlimit :
715                    action = "ALLOW"
716                else :   
717                    if grouppquota.HardLimit is None :
718                        # only a soft limit, this is equivalent to having only a hard limit
719                        action = "DENY"
720                    else :   
721                        hardlimit = int(grouppquota.HardLimit)
722                        if softlimit <= val < hardlimit :   
723                            now = DateTime.now()
724                            if grouppquota.DateLimit is not None :
725                                datelimit = DateTime.ISO.ParseDateTime(grouppquota.DateLimit)
726                            else :
727                                datelimit = now + self.config.getGraceDelay(printer.Name)
728                                grouppquota.setDateLimit(datelimit)
729                            if now < datelimit :
730                                action = "WARN"
731                            else :   
732                                action = "DENY"
733                        else :         
734                            action = "DENY"
735            else :       
736                if grouppquota.HardLimit is not None :
737                    # no soft limit, only a hard one.
738                    hardlimit = int(grouppquota.HardLimit)
739                    if val < hardlimit :
740                        action = "ALLOW"
741                    else :     
742                        action = "DENY"
743                else :
744                    # Both are unset, no quota, i.e. accounting only
745                    action = "ALLOW"
746        return action
747   
748    def checkUserPQuota(self, userpquota) :
749        """Checks the user quota on a printer and all its parents and deny or accept the job."""
750        user = userpquota.User
751        printer = userpquota.Printer
752       
753        # indicates that a warning needs to be sent
754        warned = 0               
755       
756        # first we check any group the user is a member of
757        for group in self.storage.getUserGroups(user) :
758            grouppquota = self.storage.getGroupPQuota(group, printer)
759            # for the printer and all its parents
760            for gpquota in [ grouppquota ] + grouppquota.ParentPrintersGroupPQuota :
761                if gpquota.Exists :
762                    action = self.checkGroupPQuota(gpquota)
763                    if action == "DENY" :
764                        return action
765                    elif action == "WARN" :   
766                        warned = 1
767                       
768        # Then we check the user's account balance
769        # if we get there we are sure that policy is not EXTERNAL
770        (policy, dummy) = self.config.getPrinterPolicy(printer.Name)
771        if user.LimitBy and (user.LimitBy.lower() == "balance") : 
772            self.logdebug("Checking account balance for user %s" % user.Name)
773            if user.AccountBalance is None :
774                if policy == "ALLOW" :
775                    action = "POLICY_ALLOW"
776                else :   
777                    action = "POLICY_DENY"
778                self.printInfo(_("Unable to find user %s's account balance, applying default policy (%s) for printer %s") % (user.Name, action, printer.Name))
779                return action       
780            else :   
781                val = float(user.AccountBalance or 0.0)
782                enforcement = self.config.getPrinterEnforcement(printer.Name)
783                if enforcement == "STRICT" : 
784                    val -= self.softwareJobPrice # use precomputed size.
785                if val <= 0.0 :
786                    action = "DENY"
787                elif val <= self.config.getPoorMan() :   
788                    action = "WARN"
789                else :
790                    action = "ALLOW"
791                if (enforcement == "STRICT") and (val == 0.0) :
792                    action = "WARN" # we can still print until account is 0
793                return action   
794        else :
795            # Then check the user quota on current printer and all its parents.               
796            policyallowed = 0
797            for upquota in [ userpquota ] + userpquota.ParentPrintersUserPQuota :               
798                action = self._checkUserPQuota(upquota)
799                if action in ("DENY", "POLICY_DENY") :
800                    return action
801                elif action == "WARN" :   
802                    warned = 1
803                elif action == "POLICY_ALLOW" :   
804                    policyallowed = 1
805            if warned :       
806                return "WARN"
807            elif policyallowed :   
808                return "POLICY_ALLOW" 
809            else :   
810                return "ALLOW"
811               
812    def externalMailTo(self, cmd, action, user, printer, message) :
813        """Warns the user with an external command."""
814        username = user.Name
815        printername = printer.Name
816        email = user.Email or user.Name
817        if "@" not in email :
818            email = "%s@%s" % (email, self.maildomain or self.smtpserver)
819        os.system(cmd % locals())
820   
821    def formatCommandLine(self, cmd, user, printer) :
822        """Executes an external command."""
823        username = user.Name
824        printername = printer.Name
825        return cmd % locals()
826       
827    def warnGroupPQuota(self, grouppquota) :
828        """Checks a group quota and send messages if quota is exceeded on current printer."""
829        group = grouppquota.Group
830        printer = grouppquota.Printer
831        admin = self.config.getAdmin(printer.Name)
832        adminmail = self.config.getAdminMail(printer.Name)
833        (mailto, arguments) = self.config.getMailTo(printer.Name)
834        action = self.checkGroupPQuota(grouppquota)
835        if action.startswith("POLICY_") :
836            action = action[7:]
837        if action == "DENY" :
838            adminmessage = _("Print Quota exceeded for group %s on printer %s") % (group.Name, printer.Name)
839            self.printInfo(adminmessage)
840            if mailto in [ "BOTH", "ADMIN" ] :
841                self.sendMessageToAdmin(adminmail, _("Print Quota"), adminmessage)
842            if mailto in [ "BOTH", "USER", "EXTERNAL" ] :
843                for user in self.storage.getGroupMembers(group) :
844                    if mailto != "EXTERNAL" :
845                        self.sendMessageToUser(admin, adminmail, user, _("Print Quota Exceeded"), self.config.getHardWarn(printer.Name))
846                    else :   
847                        self.externalMailTo(arguments, action, user, printer, message)
848        elif action == "WARN" :   
849            adminmessage = _("Print Quota low for group %s on printer %s") % (group.Name, printer.Name)
850            self.printInfo(adminmessage)
851            if mailto in [ "BOTH", "ADMIN" ] :
852                self.sendMessageToAdmin(adminmail, _("Print Quota"), adminmessage)
853            if group.LimitBy and (group.LimitBy.lower() == "balance") : 
854                message = self.config.getPoorWarn()
855            else :     
856                message = self.config.getSoftWarn(printer.Name)
857            if mailto in [ "BOTH", "USER", "EXTERNAL" ] :
858                for user in self.storage.getGroupMembers(group) :
859                    if mailto != "EXTERNAL" :
860                        self.sendMessageToUser(admin, adminmail, user, _("Print Quota Exceeded"), message)
861                    else :   
862                        self.externalMailTo(arguments, action, user, printer, message)
863        return action       
864       
865    def warnUserPQuota(self, userpquota) :
866        """Checks a user quota and send him a message if quota is exceeded on current printer."""
867        user = userpquota.User
868        printer = userpquota.Printer
869        admin = self.config.getAdmin(printer.Name)
870        adminmail = self.config.getAdminMail(printer.Name)
871        (mailto, arguments) = self.config.getMailTo(printer.Name)
872        action = self.checkUserPQuota(userpquota)
873        if action.startswith("POLICY_") :
874            action = action[7:]
875        if action == "DENY" :
876            adminmessage = _("Print Quota exceeded for user %s on printer %s") % (user.Name, printer.Name)
877            self.printInfo(adminmessage)
878            if mailto in [ "BOTH", "USER", "EXTERNAL" ] :
879                message = self.config.getHardWarn(printer.Name)
880                if mailto != "EXTERNAL" :
881                    self.sendMessageToUser(admin, adminmail, user, _("Print Quota Exceeded"), message)
882                else :   
883                    self.externalMailTo(arguments, action, user, printer, message)
884            if mailto in [ "BOTH", "ADMIN" ] :
885                self.sendMessageToAdmin(adminmail, _("Print Quota"), adminmessage)
886        elif action == "WARN" :   
887            adminmessage = _("Print Quota low for user %s on printer %s") % (user.Name, printer.Name)
888            self.printInfo(adminmessage)
889            if mailto in [ "BOTH", "USER", "EXTERNAL" ] :
890                if user.LimitBy and (user.LimitBy.lower() == "balance") : 
891                    message = self.config.getPoorWarn()
892                else :     
893                    message = self.config.getSoftWarn(printer.Name)
894                if mailto != "EXTERNAL" :   
895                    self.sendMessageToUser(admin, adminmail, user, _("Print Quota Low"), message)
896                else :   
897                    self.externalMailTo(arguments, action, user, printer, message)
898            if mailto in [ "BOTH", "ADMIN" ] :
899                self.sendMessageToAdmin(adminmail, _("Print Quota"), adminmessage)
900        return action       
901       
902class PyKotaFilterOrBackend(PyKotaTool) :   
903    """Class for the PyKota filter or backend."""
904    def __init__(self) :
905        """Initialize local datas from current environment."""
906        # We begin with ignoring signals, we may de-ignore them later on.
907        self.gotSigTerm = 0
908        signal.signal(signal.SIGTERM, signal.SIG_IGN)
909        # signal.signal(signal.SIGCHLD, signal.SIG_IGN)
910        signal.signal(signal.SIGPIPE, signal.SIG_IGN)
911       
912        PyKotaTool.__init__(self)
913        (self.printingsystem, \
914         self.printerhostname, \
915         self.printername, \
916         self.username, \
917         self.jobid, \
918         self.inputfile, \
919         self.copies, \
920         self.title, \
921         self.options, \
922         self.originalbackend) = self.extractInfoFromCupsOrLprng()
923         
924        self.logdebug(_("Printing system %s, args=%s") % (str(self.printingsystem), " ".join(sys.argv)))
925       
926        self.username = self.username or 'root' # when printing test page from CUPS web interface, username is empty
927        if self.config.getUserNameToLower() :
928            self.username = self.username.lower()
929        self.preserveinputfile = self.inputfile 
930        try :
931            self.accounter = accounter.openAccounter(self)
932        except (config.PyKotaConfigError, accounter.PyKotaAccounterError), msg :   
933            self.crashed(msg)
934            raise
935        self.exportJobInfo()
936        self.jobdatastream = self.openJobDataStream()
937        os.environ["PYKOTAJOBSIZEBYTES"] = str(self.jobSizeBytes)
938        self.logdebug("Job size is %s bytes" % self.jobSizeBytes)
939        self.logdebug("Capturing SIGTERM events.")
940        signal.signal(signal.SIGTERM, self.sigterm_handler)
941       
942    def sendBackChannelData(self, message) :   
943        """Sends an informational message to CUPS via back channel stream (stderr)."""
944        self.printInfo("PyKota (PID %s) : %s" % (os.getpid(), message.strip()))
945       
946    def openJobDataStream(self) :   
947        """Opens the file which contains the job's datas."""
948        if self.preserveinputfile is None :
949            # Job comes from sys.stdin, but this is not
950            # seekable and complexifies our task, so create
951            # a temporary file and use it instead
952            self.sendBackChannelData("Duplicating data stream from stdin to temporary file")
953            dummy = 0
954            MEGABYTE = 1024*1024
955            self.jobSizeBytes = 0
956            infile = tempfile.TemporaryFile()
957            while 1 :
958                data = sys.stdin.read(MEGABYTE) 
959                if not data :
960                    break
961                self.jobSizeBytes += len(data)   
962                if not (dummy % 10) :
963                    self.sendBackChannelData("%s bytes read..." % self.jobSizeBytes)
964                dummy += 1   
965                infile.write(data)
966            self.sendBackChannelData("%s bytes read total." % self.jobSizeBytes)
967            infile.flush()   
968            infile.seek(0)
969            return infile
970        else :   
971            # real file, just open it
972            self.sendBackChannelData("Opening data stream %s" % self.preserveinputfile)
973            self.jobSizeBytes = os.stat(self.preserveinputfile)[6]
974            return open(self.preserveinputfile, "rb")
975       
976    def closeJobDataStream(self) :   
977        """Closes the file which contains the job's datas."""
978        self.sendBackChannelData("Closing data stream.")
979        try :
980            self.jobdatastream.close()
981        except :   
982            pass
983       
984    def precomputeJobSize(self) :   
985        """Computes the job size with a software method."""
986        self.logdebug("Precomputing job's size with generic PDL analyzer...")
987        self.jobdatastream.seek(0)
988        try :
989            parser = pdlanalyzer.PDLAnalyzer(self.jobdatastream)
990            jobsize = parser.getJobSize()
991        except pdlanalyzer.PDLAnalyzerError, msg :   
992            # Here we just log the failure, but
993            # we finally ignore it and return 0 since this
994            # computation is just an indication of what the
995            # job's size MAY be.
996            self.printInfo(_("Unable to precompute the job's size with the generic PDL analyzer : %s") % msg, "warn")
997            return 0
998        else :   
999            if ((self.printingsystem == "CUPS") \
1000                and (self.preserveinputfile is not None)) \
1001                or (self.printingsystem != "CUPS") :
1002                return jobsize * self.copies
1003            else :       
1004                return jobsize
1005           
1006    def sigterm_handler(self, signum, frame) :
1007        """Sets an attribute whenever SIGTERM is received."""
1008        self.gotSigTerm = 1
1009        os.environ["PYKOTASTATUS"] = "CANCELLED"
1010        self.printInfo(_("SIGTERM received, job %s cancelled.") % self.jobid)
1011       
1012    def exportJobInfo(self) :   
1013        """Exports job information to the environment."""
1014        os.environ["PYKOTAUSERNAME"] = str(self.username)
1015        os.environ["PYKOTAPRINTERNAME"] = str(self.printername)
1016        os.environ["PYKOTAJOBID"] = str(self.jobid)
1017        os.environ["PYKOTATITLE"] = self.title or ""
1018        os.environ["PYKOTAFILENAME"] = self.preserveinputfile or ""
1019        os.environ["PYKOTACOPIES"] = str(self.copies)
1020        os.environ["PYKOTAOPTIONS"] = self.options or ""
1021   
1022    def exportUserInfo(self, userpquota) :
1023        """Exports user information to the environment."""
1024        os.environ["PYKOTALIMITBY"] = str(userpquota.User.LimitBy)
1025        os.environ["PYKOTABALANCE"] = str(userpquota.User.AccountBalance or 0.0)
1026        os.environ["PYKOTALIFETIMEPAID"] = str(userpquota.User.LifeTimePaid or 0.0)
1027        os.environ["PYKOTAPAGECOUNTER"] = str(userpquota.PageCounter or 0)
1028        os.environ["PYKOTALIFEPAGECOUNTER"] = str(userpquota.LifePageCounter or 0)
1029        os.environ["PYKOTASOFTLIMIT"] = str(userpquota.SoftLimit)
1030        os.environ["PYKOTAHARDLIMIT"] = str(userpquota.HardLimit)
1031        os.environ["PYKOTADATELIMIT"] = str(userpquota.DateLimit)
1032       
1033        # not really an user information, but anyway
1034        # exports the list of printers groups the current
1035        # printer is a member of
1036        os.environ["PYKOTAPGROUPS"] = ",".join([p.Name for p in self.storage.getParentPrinters(userpquota.Printer)])
1037       
1038    def prehook(self, userpquota) :
1039        """Allows plugging of an external hook before the job gets printed."""
1040        prehook = self.config.getPreHook(userpquota.Printer.Name)
1041        if prehook :
1042            self.logdebug("Executing pre-hook [%s]" % prehook)
1043            os.system(prehook)
1044       
1045    def posthook(self, userpquota) :
1046        """Allows plugging of an external hook after the job gets printed and/or denied."""
1047        posthook = self.config.getPostHook(userpquota.Printer.Name)
1048        if posthook :
1049            self.logdebug("Executing post-hook [%s]" % posthook)
1050            os.system(posthook)
1051       
1052    def printInfo(self, message, level="info") :       
1053        """Sends a message to standard error."""
1054        self.logger.log_message("%s" % message, level)
1055       
1056    def extractInfoFromCupsOrLprng(self) :   
1057        """Returns a tuple (printingsystem, printerhostname, printername, username, jobid, filename, title, options, backend).
1058       
1059           Returns (None, None, None, None, None, None, None, None, None, None) if no printing system is recognized.
1060        """
1061        # Try to detect CUPS
1062        if os.environ.has_key("CUPS_SERVERROOT") and os.path.isdir(os.environ.get("CUPS_SERVERROOT", "")) :
1063            if len(sys.argv) == 7 :
1064                inputfile = sys.argv[6]
1065            else :   
1066                inputfile = None
1067               
1068            # check that the DEVICE_URI environment variable's value is
1069            # prefixed with "cupspykota:" otherwise don't touch it.
1070            # If this is the case, we have to remove the prefix from
1071            # the environment before launching the real backend in cupspykota
1072            device_uri = os.environ.get("DEVICE_URI", "")
1073            if device_uri.startswith("cupspykota:") :
1074                fulldevice_uri = device_uri[:]
1075                device_uri = fulldevice_uri[len("cupspykota:"):]
1076                if device_uri.startswith("//") :    # lpd (at least)
1077                    device_uri = device_uri[2:]
1078                os.environ["DEVICE_URI"] = device_uri   # TODO : side effect !
1079            # TODO : check this for more complex urls than ipp://myprinter.dot.com:631/printers/lp
1080            try :
1081                (backend, destination) = device_uri.split(":", 1) 
1082            except ValueError :   
1083                raise PyKotaToolError, "Invalid DEVICE_URI : %s\n" % device_uri
1084            while destination.startswith("/") :
1085                destination = destination[1:]
1086            checkauth = destination.split("@", 1)   
1087            if len(checkauth) == 2 :
1088                destination = checkauth[1]
1089            printerhostname = destination.split("/")[0].split(":")[0]
1090            return ("CUPS", \
1091                    printerhostname, \
1092                    os.environ.get("PRINTER"), \
1093                    sys.argv[2].strip(), \
1094                    sys.argv[1].strip(), \
1095                    inputfile, \
1096                    int(sys.argv[4].strip()), \
1097                    sys.argv[3], \
1098                    sys.argv[5], \
1099                    backend)
1100        else :   
1101            # Try to detect LPRng
1102            # TODO : try to extract filename, job's title, and options if available
1103            jseen = Pseen = nseen = rseen = Kseen = None
1104            for arg in sys.argv :
1105                if arg.startswith("-j") :
1106                    jseen = arg[2:].strip()
1107                elif arg.startswith("-n") :     
1108                    nseen = arg[2:].strip()
1109                elif arg.startswith("-P") :   
1110                    Pseen = arg[2:].strip()
1111                elif arg.startswith("-r") :   
1112                    rseen = arg[2:].strip()
1113                elif arg.startswith("-K") or arg.startswith("-#") :   
1114                    Kseen = int(arg[2:].strip())
1115            if Kseen is None :       
1116                Kseen = 1       # we assume the user wants at least one copy...
1117            if (rseen is None) and jseen and Pseen and nseen :   
1118                self.printInfo(_("Printer hostname undefined, set to 'localhost'"), "warn")
1119                rseen = "localhost"
1120            try :   
1121                df_name = [line[8:] for line in os.environ.get("HF", "").split() if line.startswith("df_name=")][0]
1122            except IndexError :
1123                inputfile = None       
1124            else :   
1125                inputfile = os.path.join(os.environ.get("SPOOL_DIR", "."), df_name)
1126            if jseen and Pseen and nseen and rseen :       
1127                return ("LPRNG", rseen, Pseen, nseen, jseen, inputfile, Kseen, None, None, None)
1128        self.printInfo(_("Printing system unknown, args=%s") % " ".join(sys.argv), "warn")
1129        return (None, None, None, None, None, None, None, None, None, None)   # Unknown printing system
1130       
1131    def getPrinterUserAndUserPQuota(self) :       
1132        """Returns a tuple (policy, printer, user, and user print quota) on this printer.
1133       
1134           "OK" is returned in the policy if both printer, user and user print quota
1135           exist in the Quota Storage.
1136           Otherwise, the policy as defined for this printer in pykota.conf is returned.
1137           
1138           If policy was set to "EXTERNAL" and one of printer, user, or user print quota
1139           doesn't exist in the Quota Storage, then an external command is launched, as
1140           defined in the external policy for this printer in pykota.conf
1141           This external command can do anything, like automatically adding printers
1142           or users, for example, and finally extracting printer, user and user print
1143           quota from the Quota Storage is tried a second time.
1144           
1145           "EXTERNALERROR" is returned in case policy was "EXTERNAL" and an error status
1146           was returned by the external command.
1147        """
1148        for passnumber in range(1, 3) :
1149            printer = self.storage.getPrinter(self.printername)
1150            user = self.storage.getUser(self.username)
1151            userpquota = self.storage.getUserPQuota(user, printer)
1152            if printer.Exists and user.Exists and userpquota.Exists :
1153                policy = "OK"
1154                break
1155            (policy, args) = self.config.getPrinterPolicy(self.printername)
1156            if policy == "EXTERNAL" :   
1157                commandline = self.formatCommandLine(args, user, printer)
1158                if not printer.Exists :
1159                    self.printInfo(_("Printer %s not registered in the PyKota system, applying external policy (%s) for printer %s") % (self.printername, commandline, self.printername))
1160                if not user.Exists :
1161                    self.printInfo(_("User %s not registered in the PyKota system, applying external policy (%s) for printer %s") % (self.username, commandline, self.printername))
1162                if not userpquota.Exists :
1163                    self.printInfo(_("User %s doesn't have quota on printer %s in the PyKota system, applying external policy (%s) for printer %s") % (self.username, self.printername, commandline, self.printername))
1164                if os.system(commandline) :
1165                    self.printInfo(_("External policy %s for printer %s produced an error. Job rejected. Please check PyKota's configuration files.") % (commandline, self.printername), "error")
1166                    policy = "EXTERNALERROR"
1167                    break
1168            else :       
1169                if not printer.Exists :
1170                    self.printInfo(_("Printer %s not registered in the PyKota system, applying default policy (%s)") % (self.printername, policy))
1171                if not user.Exists :
1172                    self.printInfo(_("User %s not registered in the PyKota system, applying default policy (%s) for printer %s") % (self.username, policy, self.printername))
1173                if not userpquota.Exists :
1174                    self.printInfo(_("User %s doesn't have quota on printer %s in the PyKota system, applying default policy (%s)") % (self.username, self.printername, policy))
1175                break
1176        if policy == "EXTERNAL" :   
1177            if not printer.Exists :
1178                self.printInfo(_("Printer %s still not registered in the PyKota system, job will be rejected") % self.printername)
1179            if not user.Exists :
1180                self.printInfo(_("User %s still not registered in the PyKota system, job will be rejected on printer %s") % (self.username, self.printername))
1181            if not userpquota.Exists :
1182                self.printInfo(_("User %s still doesn't have quota on printer %s in the PyKota system, job will be rejected") % (self.username, self.printername))
1183        return (policy, printer, user, userpquota)
1184       
1185    def mainWork(self) :   
1186        """Main work is done here."""
1187        (policy, printer, user, userpquota) = self.getPrinterUserAndUserPQuota()
1188        # TODO : check for last user's quota in case pykota filter is used with querying
1189        if policy == "EXTERNALERROR" :
1190            # Policy was 'EXTERNAL' and the external command returned an error code
1191            return self.removeJob()
1192        elif policy == "EXTERNAL" :
1193            # Policy was 'EXTERNAL' and the external command wasn't able
1194            # to add either the printer, user or user print quota
1195            return self.removeJob()
1196        elif policy == "DENY" :   
1197            # Either printer, user or user print quota doesn't exist,
1198            # and the job should be rejected.
1199            return self.removeJob()
1200        else :
1201            if policy not in ("OK", "ALLOW") :
1202                self.printInfo(_("Invalid policy %s for printer %s") % (policy, self.printername))
1203                return self.removeJob()
1204            else :
1205                return self.doWork(policy, printer, user, userpquota)
Note: See TracBrowser for help on using the browser.