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

Revision 3251, 35.7 kB (checked in by jerome, 17 years ago)

Fixed a problem with superfast servers :-)

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