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

Revision 3013, 35.4 kB (checked in by jerome, 18 years ago)

Killed another item from the TODO list

  • 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, 2005, 2006 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20#
21# $Id$
22#
23#
24
25import sys
26import os
27import pwd
28import fnmatch
29import getopt
30import smtplib
31import gettext
32import locale
33import socket
34import time
35from email.MIMEText import MIMEText
36from email.Header import Header
37import email.Utils
38
39from mx import DateTime
40
41try :
42    import chardet
43except ImportError :   
44    def detectCharset(text) :
45        """Fakes a charset detection if the chardet module is not installed."""
46        return "ISO-8859-15"
47else :   
48    def detectCharset(text) :
49        """Uses the chardet module to workaround CUPS lying to us."""
50        return chardet.detect(text)["encoding"]
51
52from pykota import config, storage, logger
53from pykota.version import __version__, __author__, __years__, __gplblurb__
54
55def N_(message) :
56    """Fake translation marker for translatable strings extraction."""
57    return message
58
59class PyKotaToolError(Exception):
60    """An exception for PyKota related stuff."""
61    def __init__(self, message = ""):
62        self.message = message
63        Exception.__init__(self, message)
64    def __repr__(self):
65        return self.message
66    __str__ = __repr__
67   
68class PyKotaCommandLineError(PyKotaToolError) :   
69    """An exception for Pykota command line tools."""
70    pass
71   
72def crashed(message="Bug in PyKota") :   
73    """Minimal crash method."""
74    import traceback
75    lines = []
76    for line in traceback.format_exception(*sys.exc_info()) :
77        lines.extend([l for l in line.split("\n") if l])
78    msg = "ERROR: ".join(["%s\n" % l for l in (["ERROR: PyKota v%s" % __version__, message] + lines)])
79    sys.stderr.write(msg)
80    sys.stderr.flush()
81    return msg
82
83class Percent :
84    """A class to display progress."""
85    def __init__(self, app, size=None) :
86        """Initializes the engine."""
87        self.app = app
88        self.size = None
89        if size :
90            self.setSize(size)
91        self.previous = None
92        self.before = time.time()
93       
94    def setSize(self, size) :     
95        """Sets the total size."""
96        self.number = 0
97        self.size = size
98        if size :
99            self.factor = 100.0 / float(size)
100       
101    def display(self, msg) :   
102        """Displays the value."""
103        self.app.display(msg)
104       
105    def oneMore(self) :   
106        """Increments internal counter."""
107        if self.size :
108            self.number += 1
109            percent = "%.02f" % (float(self.number) * self.factor)
110            if percent != self.previous : # optimize for large number of items
111                self.display("\r%s%%" % percent)
112                self.previous = percent
113           
114    def done(self) :         
115        """Displays the 'done' message."""
116        after = time.time()
117        if self.size :
118            speed = self.size / (after - self.before)
119            self.display("\r100.00%%\r        \r%s. %s : %.2f %s.\n" \
120                     % (_("Done"), _("Average speed"), speed, _("entries per second")))
121        else :             
122            self.display("\r100.00%%\r        \r%s.\n" % _("Done"))
123       
124class Tool :
125    """Base class for tools with no database access."""
126    def __init__(self, lang="", charset=None, doc="PyKota v%(__version__)s (c) %(__years__)s %(__author__)s") :
127        """Initializes the command line tool."""
128        # did we drop priviledges ?
129        self.privdropped = 0
130       
131        # locale stuff
132        self.defaultToCLocale = 0
133        try :
134            locale.setlocale(locale.LC_ALL, lang)
135        except (locale.Error, IOError) :
136            # locale.setlocale(locale.LC_ALL, "C")
137            self.defaultToCLocale = 1
138        try :
139            gettext.install("pykota")
140        except :
141            gettext.NullTranslations().install()
142           
143        # We can force the charset.   
144        # The CHARSET environment variable is set by CUPS when printing.
145        # Else we use the current locale's one.
146        # If nothing is set, we use ISO-8859-15 widely used in western Europe.
147        self.localecharset = None
148        try :
149            try :
150                self.localecharset = locale.nl_langinfo(locale.CODESET)
151            except AttributeError :   
152                try :
153                    self.localecharset = locale.getpreferredencoding()
154                except AttributeError :   
155                    try :
156                        self.localecharset = locale.getlocale()[1]
157                        self.localecharset = self.localecharset or locale.getdefaultlocale()[1]
158                    except ValueError :   
159                        pass        # Unknown locale, strange...
160        except locale.Error :           
161            pass
162        self.charset = charset or os.environ.get("CHARSET") or self.localecharset or "ISO-8859-15"
163   
164        # pykota specific stuff
165        self.documentation = doc
166       
167    def deferredInit(self) :       
168        """Deferred initialization."""
169        # try to find the configuration files in user's 'pykota' home directory.
170        try :
171            self.pykotauser = pwd.getpwnam("pykota")
172        except KeyError :   
173            self.pykotauser = None
174            confdir = "/etc/pykota"
175            missingUser = 1
176        else :   
177            confdir = self.pykotauser[5]
178            missingUser = 0
179           
180        self.config = config.PyKotaConfig(confdir)
181        self.debug = self.config.getDebug()
182        self.smtpserver = self.config.getSMTPServer()
183        self.maildomain = self.config.getMailDomain()
184        self.logger = logger.openLogger(self.config.getLoggingBackend())
185           
186        # now drop priviledge if possible
187        self.dropPriv()   
188       
189        # We NEED this here, even when not in an accounting filter/backend   
190        self.softwareJobSize = 0
191        self.softwareJobPrice = 0.0
192       
193        if self.defaultToCLocale :
194            self.printInfo("Incorrect locale settings. PyKota falls back to the default locale.", "warn")
195        if missingUser :     
196            self.printInfo("The 'pykota' system account is missing. Configuration files were searched in /etc/pykota instead.", "warn")
197       
198        self.logdebug("Charset detected from locale settings : %s" % self.localecharset)
199        self.logdebug("Charset in use : %s" % self.charset)
200        arguments = " ".join(['"%s"' % arg for arg in sys.argv])
201        self.logdebug("Command line arguments : %s" % arguments)
202       
203    def dropPriv(self) :   
204        """Drops priviledges."""
205        uid = os.geteuid()
206        try :
207            self.originalUserName = pwd.getpwuid(uid)[0]
208        except (KeyError, IndexError), msg :   
209            self.printInfo(_("Strange problem with uid(%s) : %s") % (uid, msg), "warn")
210            self.originalUserName = None
211        else :
212            if uid :
213                self.logdebug(_("Running as user '%s'.") % self.originalUserName)
214            else :
215                if self.pykotauser is None :
216                    self.logdebug(_("No user named 'pykota'. Not dropping priviledges."))
217                else :   
218                    try :
219                        os.setegid(self.pykotauser[3])
220                        os.seteuid(self.pykotauser[2])
221                    except OSError, msg :   
222                        self.printInfo(_("Impossible to drop priviledges : %s") % msg, "warn")
223                    else :   
224                        self.logdebug(_("Priviledges dropped. Now running as user 'pykota'."))
225                        self.privdropped = 1
226           
227    def regainPriv(self) :   
228        """Drops priviledges."""
229        if self.privdropped :
230            try :
231                os.seteuid(0)
232                os.setegid(0)
233            except OSError, msg :   
234                self.printInfo(_("Impossible to regain priviledges : %s") % msg, "warn")
235            else :   
236                self.logdebug(_("Regained priviledges. Now running as root."))
237                self.privdropped = 0
238       
239    def getCharset(self) :   
240        """Returns the charset in use."""
241        return self.charset
242       
243    def UTF8ToUserCharset(self, text) :
244        """Converts from UTF-8 to user's charset."""
245        if text is None :
246            return None
247        try :
248            return text.decode("UTF-8").encode(self.charset, "replace") 
249        except (UnicodeError, AttributeError) :   
250            try :
251                # Maybe already in Unicode ?
252                return text.encode(self.charset, "replace") 
253            except (UnicodeError, AttributeError) :
254                # Try to autodetect the charset
255                return text.decode(detectCharset(text), "replace").encode(self.charset, "replace")
256       
257    def userCharsetToUTF8(self, text) :
258        """Converts from user's charset to UTF-8."""
259        if text is None :
260            return None
261        try :
262            # We don't necessarily trust the default charset, because
263            # xprint sends us titles in UTF-8 but CUPS gives us an ISO-8859-1 charset !
264            # So we first try to see if the text is already in UTF-8 or not, and
265            # if it is, we delete characters which can't be converted to the user's charset,
266            # then convert back to UTF-8. PostgreSQL 7.3.x used to reject some unicode characters,
267            # this is fixed by the ugly line below :
268            return text.decode("UTF-8").encode(self.charset, "replace").decode(self.charset).encode("UTF-8", "replace")
269        except (UnicodeError, AttributeError) :
270            try :
271                return text.decode(self.charset).encode("UTF-8", "replace") 
272            except (UnicodeError, AttributeError) :   
273                try :
274                    # Maybe already in Unicode ?
275                    return text.encode("UTF-8", "replace") 
276                except (UnicodeError, AttributeError) :
277                    # Try to autodetect the charset
278                    return text.decode(detectCharset(text), "replace").encode("UTF-8", "replace")
279        return newtext
280       
281    def display(self, message) :
282        """Display a message but only if stdout is a tty."""
283        if sys.stdout.isatty() :
284            sys.stdout.write(message)
285            sys.stdout.flush()
286           
287    def logdebug(self, message) :   
288        """Logs something to debug output if debug is enabled."""
289        if self.debug :
290            self.logger.log_message(message, "debug")
291           
292    def printInfo(self, message, level="info") :       
293        """Sends a message to standard error."""
294        sys.stderr.write("%s: %s\n" % (level.upper(), message))
295        sys.stderr.flush()
296       
297    def matchString(self, s, patterns) :
298        """Returns True if the string s matches one of the patterns, else False."""
299        if not patterns :
300            return True # No pattern, always matches.
301        else :   
302            for pattern in patterns :
303                if fnmatch.fnmatchcase(s, pattern) :
304                    return True
305            return False
306       
307    def sanitizeNames(self, options, names) :
308        """Ensures that an user can only see the datas he is allowed to see, by modifying the list of names."""
309        if not self.config.isAdmin :
310            username = pwd.getpwuid(os.geteuid())[0]
311            if not options["list"] :
312                raise PyKotaCommandLineError, "%s : %s" % (username, _("You're not allowed to use this command."))
313            else :
314                if options["groups"] :
315                    user = self.storage.getUser(username)
316                    if user.Exists :
317                        return [ g.Name for g in self.storage.getUserGroups(user) ]
318                return [ username ]
319        elif not names :       
320            return ["*"]
321        else :   
322            return names
323       
324    def display_version_and_quit(self) :
325        """Displays version number, then exists successfully."""
326        try :
327            self.clean()
328        except AttributeError :   
329            pass
330        print __version__
331        sys.exit(0)
332   
333    def display_usage_and_quit(self) :
334        """Displays command line usage, then exists successfully."""
335        try :
336            self.clean()
337        except AttributeError :   
338            pass
339        print _(self.documentation) % globals()
340        print __gplblurb__
341        print
342        print _("Please report bugs to :"), __author__
343        sys.exit(0)
344       
345    def crashed(self, message="Bug in PyKota") :   
346        """Outputs a crash message, and optionally sends it to software author."""
347        msg = crashed(message)
348        fullmessage = "========== Traceback :\n\n%s\n\n========== sys.argv :\n\n%s\n\n========== Environment :\n\n%s\n" % \
349                        (msg, \
350                         "\n".join(["    %s" % repr(a) for a in sys.argv]), \
351                         "\n".join(["    %s=%s" % (k, v) for (k, v) in os.environ.items()]))
352        try :
353            crashrecipient = self.config.getCrashRecipient()
354            if crashrecipient :
355                admin = self.config.getAdminMail("global") # Nice trick, isn't it ?
356                server = smtplib.SMTP(self.smtpserver)
357                msg = MIMEText(fullmessage, _charset=self.charset)
358                msg["Subject"] = str(Header("PyKota v%s crash traceback !" \
359                                        % __version__, charset=self.charset))
360                msg["From"] = admin
361                msg["To"] = crashrecipient
362                msg["Cc"] = admin
363                msg["Date"] = email.Utils.formatdate(localtime=True)
364                server.sendmail(admin, [admin, crashrecipient], msg.as_string())
365                server.quit()
366        except :
367            pass
368        return fullmessage   
369       
370    def parseCommandline(self, argv, short, long, allownothing=0) :
371        """Parses the command line, controlling options."""
372        # split options in two lists: those which need an argument, those which don't need any
373        short = "%sA:" % short
374        long.append("arguments=")
375        withoutarg = []
376        witharg = []
377        lgs = len(short)
378        i = 0
379        while i < lgs :
380            ii = i + 1
381            if (ii < lgs) and (short[ii] == ':') :
382                # needs an argument
383                witharg.append(short[i])
384                ii = ii + 1 # skip the ':'
385            else :
386                # doesn't need an argument
387                withoutarg.append(short[i])
388            i = ii
389               
390        for option in long :
391            if option[-1] == '=' :
392                # needs an argument
393                witharg.append(option[:-1])
394            else :
395                # doesn't need an argument
396                withoutarg.append(option)
397       
398        # then we parse the command line
399        done = 0
400        while not done :
401            # we begin with all possible options unset
402            parsed = {}
403            for option in withoutarg + witharg :
404                parsed[option] = None
405            args = []       # to not break if something unexpected happened
406            try :
407                options, args = getopt.getopt(argv, short, long)
408                if options :
409                    for (o, v) in options :
410                        # we skip the '-' chars
411                        lgo = len(o)
412                        i = 0
413                        while (i < lgo) and (o[i] == '-') :
414                            i = i + 1
415                        o = o[i:]
416                        if o in witharg :
417                            # needs an argument : set it
418                            parsed[o] = v
419                        elif o in withoutarg :
420                            # doesn't need an argument : boolean
421                            parsed[o] = 1
422                        else :
423                            # should never occur
424                            raise PyKotaCommandLineError, "Unexpected problem when parsing command line"
425                elif (not args) and (not allownothing) and sys.stdin.isatty() : # no option and no argument, we display help if we are a tty
426                    self.display_usage_and_quit()
427            except getopt.error, msg :
428                raise PyKotaCommandLineError, str(msg)
429            else :   
430                if parsed["arguments"] or parsed["A"] :
431                    # arguments are in a file, we ignore all other arguments
432                    # and reset the list of arguments to the lines read from
433                    # the file.
434                    argsfile = open(parsed["arguments"] or parsed["A"], "r")
435                    argv = [ l.strip() for l in argsfile.readlines() ]
436                    argsfile.close()
437                    for i in range(len(argv)) :
438                        argi = argv[i]
439                        if argi.startswith('"') and argi.endswith('"') :
440                            argv[i] = argi[1:-1]
441                else :   
442                    done = 1
443        return (parsed, args)
444   
445class PyKotaTool(Tool) :   
446    """Base class for all PyKota command line tools."""
447    def __init__(self, lang="", charset=None, doc="PyKota v%(__version__)s (c) %(__years__)s %(__author__)s") :
448        """Initializes the command line tool and opens the database."""
449        Tool.__init__(self, lang, charset, doc)
450       
451    def deferredInit(self) :   
452        """Deferred initialization."""
453        Tool.deferredInit(self)
454        self.storage = storage.openConnection(self)
455        if self.config.isAdmin : # TODO : We don't know this before, fix this !
456            self.logdebug("Beware : running as a PyKota administrator !")
457        else :   
458            self.logdebug("Don't Panic : running as a mere mortal !")
459       
460    def clean(self) :   
461        """Ensures that the database is closed."""
462        try :
463            self.storage.close()
464        except (TypeError, NameError, AttributeError) :   
465            pass
466           
467    def isValidName(self, name) :
468        """Checks if a user or printer name is valid."""
469        invalidchars = "/@?*,;&|"
470        for c in list(invalidchars) :
471            if c in name :
472                return 0
473        return 1       
474       
475    def sendMessage(self, adminmail, touser, fullmessage) :
476        """Sends an email message containing headers to some user."""
477        try :   
478            server = smtplib.SMTP(self.smtpserver)
479        except socket.error, msg :   
480            self.printInfo(_("Impossible to connect to SMTP server : %s") % msg, "error")
481        else :
482            try :
483                server.sendmail(adminmail, [touser], fullmessage)
484            except smtplib.SMTPException, answer :   
485                for (k, v) in answer.recipients.items() :
486                    self.printInfo(_("Impossible to send mail to %s, error %s : %s") % (k, v[0], v[1]), "error")
487            server.quit()
488       
489    def sendMessageToUser(self, admin, adminmail, user, subject, message) :
490        """Sends an email message to a user."""
491        message += _("\n\nPlease contact your system administrator :\n\n\t%s - <%s>\n") % (admin, adminmail)
492        usermail = user.Email or user.Name
493        if "@" not in usermail :
494            usermail = "%s@%s" % (usermail, self.maildomain or self.smtpserver or "localhost")
495        msg = MIMEText(message, _charset=self.charset)
496        msg["Subject"] = str(Header(subject, charset=self.charset))
497        msg["From"] = adminmail
498        msg["To"] = usermail
499        msg["Date"] = email.Utils.formatdate(localtime=True)
500        self.sendMessage(adminmail, usermail, msg.as_string())
501       
502    def sendMessageToAdmin(self, adminmail, subject, message) :
503        """Sends an email message to the Print Quota administrator."""
504        if "@" not in adminmail :
505            adminmail = "%s@%s" % (adminmail, self.maildomain or self.smtpserver or "localhost")
506        msg = MIMEText(message, _charset=self.charset)
507        msg["Subject"] = str(Header(subject, charset=self.charset))
508        msg["From"] = adminmail
509        msg["To"] = adminmail
510        self.sendMessage(adminmail, adminmail, msg.as_string())
511       
512    def _checkUserPQuota(self, userpquota) :           
513        """Checks the user quota on a printer and deny or accept the job."""
514        # then we check the user's own quota
515        # if we get there we are sure that policy is not EXTERNAL
516        user = userpquota.User
517        printer = userpquota.Printer
518        enforcement = self.config.getPrinterEnforcement(printer.Name)
519        self.logdebug("Checking user %s's quota on printer %s" % (user.Name, printer.Name))
520        (policy, dummy) = self.config.getPrinterPolicy(userpquota.Printer.Name)
521        if not userpquota.Exists :
522            # Unknown userquota
523            if policy == "ALLOW" :
524                action = "POLICY_ALLOW"
525            else :   
526                action = "POLICY_DENY"
527            self.printInfo(_("Unable to match user %s on printer %s, applying default policy (%s)") % (user.Name, printer.Name, action))
528        else :   
529            pagecounter = int(userpquota.PageCounter or 0)
530            if enforcement == "STRICT" :
531                pagecounter += self.softwareJobSize
532            if userpquota.SoftLimit is not None :
533                softlimit = int(userpquota.SoftLimit)
534                if pagecounter < softlimit :
535                    action = "ALLOW"
536                else :   
537                    if userpquota.HardLimit is None :
538                        # only a soft limit, this is equivalent to having only a hard limit
539                        action = "DENY"
540                    else :   
541                        hardlimit = int(userpquota.HardLimit)
542                        if softlimit <= pagecounter < hardlimit :   
543                            now = DateTime.now()
544                            if userpquota.DateLimit is not None :
545                                datelimit = DateTime.ISO.ParseDateTime(str(userpquota.DateLimit))
546                            else :
547                                datelimit = now + self.config.getGraceDelay(printer.Name)
548                                userpquota.setDateLimit(datelimit)
549                            if now < datelimit :
550                                action = "WARN"
551                            else :   
552                                action = "DENY"
553                        else :         
554                            action = "DENY"
555            else :       
556                if userpquota.HardLimit is not None :
557                    # no soft limit, only a hard one.
558                    hardlimit = int(userpquota.HardLimit)
559                    if pagecounter < hardlimit :
560                        action = "ALLOW"
561                    else :     
562                        action = "DENY"
563                else :
564                    # Both are unset, no quota, i.e. accounting only
565                    action = "ALLOW"
566        return action
567   
568    def checkGroupPQuota(self, grouppquota) :   
569        """Checks the group quota on a printer and deny or accept the job."""
570        group = grouppquota.Group
571        printer = grouppquota.Printer
572        enforcement = self.config.getPrinterEnforcement(printer.Name)
573        self.logdebug("Checking group %s's quota on printer %s" % (group.Name, printer.Name))
574        if group.LimitBy and (group.LimitBy.lower() == "balance") : 
575            val = group.AccountBalance or 0.0
576            if enforcement == "STRICT" : 
577                val -= self.softwareJobPrice # use precomputed size.
578            balancezero = self.config.getBalanceZero()
579            if val <= balancezero :
580                action = "DENY"
581            elif val <= self.config.getPoorMan() :   
582                action = "WARN"
583            else :   
584                action = "ALLOW"
585            if (enforcement == "STRICT") and (val == balancezero) :
586                action = "WARN" # we can still print until account is 0
587        else :
588            val = grouppquota.PageCounter or 0
589            if enforcement == "STRICT" :
590                val += int(self.softwareJobSize) # TODO : this is not a fix, problem is elsewhere in grouppquota.PageCounter
591            if grouppquota.SoftLimit is not None :
592                softlimit = int(grouppquota.SoftLimit)
593                if val < softlimit :
594                    action = "ALLOW"
595                else :   
596                    if grouppquota.HardLimit is None :
597                        # only a soft limit, this is equivalent to having only a hard limit
598                        action = "DENY"
599                    else :   
600                        hardlimit = int(grouppquota.HardLimit)
601                        if softlimit <= val < hardlimit :   
602                            now = DateTime.now()
603                            if grouppquota.DateLimit is not None :
604                                datelimit = DateTime.ISO.ParseDateTime(str(grouppquota.DateLimit))
605                            else :
606                                datelimit = now + self.config.getGraceDelay(printer.Name)
607                                grouppquota.setDateLimit(datelimit)
608                            if now < datelimit :
609                                action = "WARN"
610                            else :   
611                                action = "DENY"
612                        else :         
613                            action = "DENY"
614            else :       
615                if grouppquota.HardLimit is not None :
616                    # no soft limit, only a hard one.
617                    hardlimit = int(grouppquota.HardLimit)
618                    if val < hardlimit :
619                        action = "ALLOW"
620                    else :     
621                        action = "DENY"
622                else :
623                    # Both are unset, no quota, i.e. accounting only
624                    action = "ALLOW"
625        return action
626   
627    def checkUserPQuota(self, userpquota) :
628        """Checks the user quota on a printer and all its parents and deny or accept the job."""
629        user = userpquota.User
630        printer = userpquota.Printer
631       
632        # indicates that a warning needs to be sent
633        warned = 0               
634       
635        # first we check any group the user is a member of
636        for group in self.storage.getUserGroups(user) :
637            # No need to check anything if the group is in noquota mode
638            if group.LimitBy != "noquota" :
639                grouppquota = self.storage.getGroupPQuota(group, printer)
640                # for the printer and all its parents
641                for gpquota in [ grouppquota ] + grouppquota.ParentPrintersGroupPQuota :
642                    if gpquota.Exists :
643                        action = self.checkGroupPQuota(gpquota)
644                        if action == "DENY" :
645                            return action
646                        elif action == "WARN" :   
647                            warned = 1
648                       
649        # Then we check the user's account balance
650        # if we get there we are sure that policy is not EXTERNAL
651        (policy, dummy) = self.config.getPrinterPolicy(printer.Name)
652        if user.LimitBy and (user.LimitBy.lower() == "balance") : 
653            self.logdebug("Checking account balance for user %s" % user.Name)
654            if user.AccountBalance is None :
655                if policy == "ALLOW" :
656                    action = "POLICY_ALLOW"
657                else :   
658                    action = "POLICY_DENY"
659                self.printInfo(_("Unable to find user %s's account balance, applying default policy (%s) for printer %s") % (user.Name, action, printer.Name))
660                return action       
661            else :   
662                if user.OverCharge == 0.0 :
663                    self.printInfo(_("User %s will not be charged for printing.") % user.Name)
664                    action = "ALLOW"
665                else :
666                    val = float(user.AccountBalance or 0.0)
667                    enforcement = self.config.getPrinterEnforcement(printer.Name)
668                    if enforcement == "STRICT" : 
669                        val -= self.softwareJobPrice # use precomputed size.
670                    balancezero = self.config.getBalanceZero()   
671                    if val <= balancezero :
672                        action = "DENY"
673                    elif val <= self.config.getPoorMan() :   
674                        action = "WARN"
675                    else :
676                        action = "ALLOW"
677                    if (enforcement == "STRICT") and (val == balancezero) :
678                        action = "WARN" # we can still print until account is 0
679                return action   
680        else :
681            # Then check the user quota on current printer and all its parents.               
682            policyallowed = 0
683            for upquota in [ userpquota ] + userpquota.ParentPrintersUserPQuota :               
684                action = self._checkUserPQuota(upquota)
685                if action in ("DENY", "POLICY_DENY") :
686                    return action
687                elif action == "WARN" :   
688                    warned = 1
689                elif action == "POLICY_ALLOW" :   
690                    policyallowed = 1
691            if warned :       
692                return "WARN"
693            elif policyallowed :   
694                return "POLICY_ALLOW" 
695            else :   
696                return "ALLOW"
697               
698    def externalMailTo(self, cmd, action, user, printer, message) :
699        """Warns the user with an external command."""
700        username = user.Name
701        printername = printer.Name
702        email = user.Email or user.Name
703        if "@" not in email :
704            email = "%s@%s" % (email, self.maildomain or self.smtpserver)
705        os.system(cmd % locals())
706   
707    def formatCommandLine(self, cmd, user, printer) :
708        """Executes an external command."""
709        username = user.Name
710        printername = printer.Name
711        return cmd % locals()
712       
713    def warnGroupPQuota(self, grouppquota) :
714        """Checks a group quota and send messages if quota is exceeded on current printer."""
715        group = grouppquota.Group
716        printer = grouppquota.Printer
717        admin = self.config.getAdmin(printer.Name)
718        adminmail = self.config.getAdminMail(printer.Name)
719        (mailto, arguments) = self.config.getMailTo(printer.Name)
720        if group.LimitBy in ("noquota", "nochange") :
721            action = "ALLOW"
722        else :   
723            action = self.checkGroupPQuota(grouppquota)
724            if action.startswith("POLICY_") :
725                action = action[7:]
726            if action == "DENY" :
727                adminmessage = _("Print Quota exceeded for group %s on printer %s") % (group.Name, printer.Name)
728                self.printInfo(adminmessage)
729                if mailto in [ "BOTH", "ADMIN" ] :
730                    self.sendMessageToAdmin(adminmail, _("Print Quota"), adminmessage)
731                if mailto in [ "BOTH", "USER", "EXTERNAL" ] :
732                    for user in self.storage.getGroupMembers(group) :
733                        if mailto != "EXTERNAL" :
734                            self.sendMessageToUser(admin, adminmail, user, _("Print Quota Exceeded"), self.config.getHardWarn(printer.Name))
735                        else :   
736                            self.externalMailTo(arguments, action, user, printer, self.config.getHardWarn(printer.Name))
737            elif action == "WARN" :   
738                adminmessage = _("Print Quota low for group %s on printer %s") % (group.Name, printer.Name)
739                self.printInfo(adminmessage)
740                if mailto in [ "BOTH", "ADMIN" ] :
741                    self.sendMessageToAdmin(adminmail, _("Print Quota"), adminmessage)
742                if group.LimitBy and (group.LimitBy.lower() == "balance") : 
743                    message = self.config.getPoorWarn()
744                else :     
745                    message = self.config.getSoftWarn(printer.Name)
746                if mailto in [ "BOTH", "USER", "EXTERNAL" ] :
747                    for user in self.storage.getGroupMembers(group) :
748                        if mailto != "EXTERNAL" :
749                            self.sendMessageToUser(admin, adminmail, user, _("Print Quota Exceeded"), message)
750                        else :   
751                            self.externalMailTo(arguments, action, user, printer, message)
752        return action       
753       
754    def warnUserPQuota(self, userpquota) :
755        """Checks a user quota and send him a message if quota is exceeded on current printer."""
756        user = userpquota.User
757        printer = userpquota.Printer
758        admin = self.config.getAdmin(printer.Name)
759        adminmail = self.config.getAdminMail(printer.Name)
760        (mailto, arguments) = self.config.getMailTo(printer.Name)
761       
762        if user.LimitBy in ("noquota", "nochange") :
763            action = "ALLOW"
764        elif user.LimitBy == "noprint" :
765            action = "DENY"
766            message = _("User %s is not allowed to print at this time.") % user.Name
767            self.printInfo(message)
768            if mailto in [ "BOTH", "USER", "EXTERNAL" ] :
769                if mailto != "EXTERNAL" :
770                    self.sendMessageToUser(admin, adminmail, user, _("Printing denied."), message)
771                else :   
772                    self.externalMailTo(arguments, action, user, printer, message)
773            if mailto in [ "BOTH", "ADMIN" ] :
774                self.sendMessageToAdmin(adminmail, _("Print Quota"), message)
775        else :
776            action = self.checkUserPQuota(userpquota)
777            if action.startswith("POLICY_") :
778                action = action[7:]
779               
780            if action == "DENY" :
781                adminmessage = _("Print Quota exceeded for user %s on printer %s") % (user.Name, printer.Name)
782                self.printInfo(adminmessage)
783                if mailto in [ "BOTH", "USER", "EXTERNAL" ] :
784                    message = self.config.getHardWarn(printer.Name)
785                    if mailto != "EXTERNAL" :
786                        self.sendMessageToUser(admin, adminmail, user, _("Print Quota Exceeded"), message)
787                    else :   
788                        self.externalMailTo(arguments, action, user, printer, message)
789                if mailto in [ "BOTH", "ADMIN" ] :
790                    self.sendMessageToAdmin(adminmail, _("Print Quota"), adminmessage)
791            elif action == "WARN" :   
792                adminmessage = _("Print Quota low for user %s on printer %s") % (user.Name, printer.Name)
793                self.printInfo(adminmessage)
794                if mailto in [ "BOTH", "USER", "EXTERNAL" ] :
795                    if user.LimitBy and (user.LimitBy.lower() == "balance") : 
796                        message = self.config.getPoorWarn()
797                    else :     
798                        message = self.config.getSoftWarn(printer.Name)
799                    if mailto != "EXTERNAL" :   
800                        self.sendMessageToUser(admin, adminmail, user, _("Print Quota Low"), message)
801                    else :   
802                        self.externalMailTo(arguments, action, user, printer, message)
803                if mailto in [ "BOTH", "ADMIN" ] :
804                    self.sendMessageToAdmin(adminmail, _("Print Quota"), adminmessage)
805        return action       
806       
Note: See TracBrowser for help on using the browser.