# PyKota
# -*- coding: ISO-8859-15 -*-
#
# PyKota : Print Quotas for CUPS and LPRng
#
# (c) 2003, 2004, 2005, 2006, 2007 Jerome Alet <alet@librelogiciel.com>
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
# 
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# $Id: storage.py 3133 2007-01-17 22:19:42Z jerome $
#
#

"""This module is the database abstraction layer for PyKota."""

import os
import imp
from mx import DateTime

class PyKotaStorageError(Exception):
    """An exception for database related stuff."""
    def __init__(self, message = ""):
        self.message = message
        Exception.__init__(self, message)
    def __repr__(self):
        return self.message
    __str__ = __repr__
        
        
class StorageObject :
    """Object present in the database."""
    def __init__(self, parent) :
        "Initialize minimal data."""
        self.parent = parent
        self.ident = None
        self.Description = None
        self.isDirty = False
        self.Exists = False
        
    def setDescription(self, description=None) :
        """Sets the object's description."""
        if description is not None :
            self.Description = str(description)
            self.isDirty = True    
            
    def save(self) :        
        """Saves the object to the database."""
        if self.isDirty :
            getattr(self.parent, "save%s" % self.__class__.__name__[7:])(self)
            self.isDirty = False
            
        
class StorageUser(StorageObject) :        
    """User class."""
    def __init__(self, parent, name) :
        StorageObject.__init__(self, parent)
        self.Name = name
        self.LimitBy = None
        self.AccountBalance = None
        self.LifeTimePaid = None
        self.Email = None
        self.OverCharge = 1.0
        self.Payments = [] # TODO : maybe handle this smartly for SQL, for now just don't retrieve them
        self.PaymentsBacklog = []
        
    def consumeAccountBalance(self, amount) :     
        """Consumes an amount of money from the user's account balance."""
        self.parent.decreaseUserAccountBalance(self, amount)
        self.AccountBalance = float(self.AccountBalance or 0.0) - amount
        
    def setAccountBalance(self, balance, lifetimepaid, comment="") :
        """Sets the user's account balance in case he pays more money."""
        diff = float(lifetimepaid or 0.0) - float(self.LifeTimePaid or 0.0)
        self.AccountBalance = balance
        self.LifeTimePaid = lifetimepaid
        if diff :
            self.PaymentsBacklog.append((diff, comment))
        self.isDirty = True
        
    def save(self) :    
        """Saves an user and flush its payments backlog."""
        for (value, comment) in self.PaymentsBacklog :
            self.parent.writeNewPayment(self, value, comment)
        self.PaymentsBacklog = []    
        StorageObject.save(self)    
        
    def setLimitBy(self, limitby) :    
        """Sets the user's limiting factor."""
        try :
            limitby = limitby.lower()
        except AttributeError :    
            limitby = "quota"
        if limitby in ["quota", "balance", \
                       "noquota", "noprint", "nochange"] :
            self.LimitBy = limitby
            self.isDirty = True
        
    def setOverChargeFactor(self, factor) :    
        """Sets the user's overcharging coefficient."""
        self.OverCharge = factor
        self.isDirty = True
        
    def setEmail(self, email) :    
        """Sets the user's email address."""
        self.Email = email
        self.isDirty = True
        
    def delete(self) :    
        """Deletes an user from the database."""
        self.parent.deleteUser(self)
        self.parent.flushEntry("USERS", self.Name)
        if self.parent.usecache :
            for (k, v) in self.parent.caches["USERPQUOTAS"].items() :
                if v.User.Name == self.Name :
                    self.parent.flushEntry("USERPQUOTAS", "%s@%s" % (v.User.Name, v.Printer.Name))
        self.Exists = False
        self.isDirty = False            
        
    def refund(self, amount, reason) :
        """Refunds a number of credits to an user."""
        self.consumeAccountBalance(-amount)
        self.parent.writeNewPayment(self, -amount, reason)
        
        
class StorageGroup(StorageObject) :        
    """User class."""
    def __init__(self, parent, name) :
        StorageObject.__init__(self, parent)
        self.Name = name
        self.LimitBy = None
        self.AccountBalance = None
        self.LifeTimePaid = None
        
    def setLimitBy(self, limitby) :    
        """Sets the user's limiting factor."""
        try :
            limitby = limitby.lower()
        except AttributeError :    
            limitby = "quota"
        if limitby in ["quota", "balance", "noquota"] :
            self.LimitBy = limitby
            self.isDirty = True
        
    def addUserToGroup(self, user) :
        """Adds an user to an users group."""
        self.parent.addUserToGroup(user, self)
        
    def delUserFromGroup(self, user) :
        """Removes an user from an users group."""
        self.parent.delUserFromGroup(user, self)
        
    def delete(self) :    
        """Deletes a group from the database."""
        self.parent.deleteGroup(self)
        self.parent.flushEntry("GROUPS", self.Name)
        if self.parent.usecache :
            for (k, v) in self.parent.caches["GROUPPQUOTAS"].items() :
                if v.Group.Name == self.Name :
                    self.parent.flushEntry("GROUPPQUOTAS", "%s@%s" % (v.Group.Name, v.Printer.Name))
        self.Exists = False
        self.isDirty = False            
        
        
class StoragePrinter(StorageObject) :
    """Printer class."""
    def __init__(self, parent, name) :
        StorageObject.__init__(self, parent)
        self.Name = name
        self.PricePerPage = None
        self.PricePerJob = None
        self.MaxJobSize = None
        self.PassThrough = None
        
    def __getattr__(self, name) :    
        """Delays data retrieval until it's really needed."""
        if name == "LastJob" : 
            self.LastJob = self.parent.getPrinterLastJob(self)
            self.parent.tool.logdebug("Lazy retrieval of last job for printer %s" % self.Name)
            return self.LastJob
        elif name == "Coefficients" :    
            self.Coefficients = self.parent.tool.config.getPrinterCoefficients(self.Name)
            self.parent.tool.logdebug("Lazy retrieval of coefficients for printer %s : %s" % (self.Name, self.Coefficients))
            return self.Coefficients
        else :
            raise AttributeError, name
            
    def addJobToHistory(self, jobid, user, pagecounter, action, jobsize=None, jobprice=None, filename=None, title=None, copies=None, options=None, clienthost=None, jobsizebytes=None, jobmd5sum=None, jobpages=None, jobbilling=None, precomputedsize=None, precomputedprice=None) :
        """Adds a job to the printer's history."""
        self.parent.writeJobNew(self, user, jobid, pagecounter, action, jobsize, jobprice, filename, title, copies, options, clienthost, jobsizebytes, jobmd5sum, jobpages, jobbilling, precomputedsize, precomputedprice)
        # TODO : update LastJob object ? Probably not needed.
        
    def addPrinterToGroup(self, printer) :    
        """Adds a printer to a printer group."""
        if (printer not in self.parent.getParentPrinters(self)) and (printer.ident != self.ident) :
            self.parent.writePrinterToGroup(self, printer)
            # TODO : reset cached value for printer parents, or add new parent to cached value
            
    def delPrinterFromGroup(self, printer) :    
        """Deletes a printer from a printer group."""
        self.parent.removePrinterFromGroup(self, printer)
        # TODO : reset cached value for printer parents, or add new parent to cached value
        
    def setPrices(self, priceperpage = None, priceperjob = None) :    
        """Sets the printer's prices."""
        if priceperpage is None :
            priceperpage = self.PricePerPage or 0.0
        else :    
            self.PricePerPage = float(priceperpage)
        if priceperjob is None :    
            priceperjob = self.PricePerJob or 0.0
        else :    
            self.PricePerJob = float(priceperjob)
        self.isDirty = True    
        
    def setPassThrough(self, passthrough) :
        """Sets the printer's passthrough mode."""
        self.PassThrough = passthrough
        self.isDirty = True
        
    def setMaxJobSize(self, maxjobsize) :
        """Sets the printer's maximal job size."""
        self.MaxJobSize = maxjobsize
        self.isDirty = True
        
    def delete(self) :    
        """Deletes a printer from the database."""
        self.parent.deletePrinter(self)
        self.parent.flushEntry("PRINTERS", self.Name)
        if self.parent.usecache :
            for (k, v) in self.parent.caches["USERPQUOTAS"].items() :
                if v.Printer.Name == self.Name :
                    self.parent.flushEntry("USERPQUOTAS", "%s@%s" % (v.User.Name, v.Printer.Name))
            for (k, v) in self.parent.caches["GROUPPQUOTAS"].items() :
                if v.Printer.Name == self.Name :
                    self.parent.flushEntry("GROUPPQUOTAS", "%s@%s" % (v.Group.Name, v.Printer.Name))
        self.Exists = False
        self.isDirty = False            
        
        
class StorageUserPQuota(StorageObject) :
    """User Print Quota class."""
    def __init__(self, parent, user, printer) :
        StorageObject.__init__(self, parent)
        self.User = user
        self.Printer = printer
        self.PageCounter = None
        self.LifePageCounter = None
        self.SoftLimit = None
        self.HardLimit = None
        self.DateLimit = None
        self.WarnCount = None
        self.MaxJobSize = None
        
    def __getattr__(self, name) :    
        """Delays data retrieval until it's really needed."""
        if name == "ParentPrintersUserPQuota" : 
            self.ParentPrintersUserPQuota = (self.User.Exists and self.Printer.Exists and self.parent.getParentPrintersUserPQuota(self)) or []
            return self.ParentPrintersUserPQuota
        else :
            raise AttributeError, name
        
    def setDateLimit(self, datelimit) :    
        """Sets the date limit for this quota."""
        datelimit = DateTime.ISO.ParseDateTime(str(datelimit)[:19])
        date = "%04i-%02i-%02i %02i:%02i:%02i" % (datelimit.year, datelimit.month, datelimit.day, datelimit.hour, datelimit.minute, datelimit.second)
        self.parent.writeUserPQuotaDateLimit(self, date)
        self.DateLimit = date
        
    def setLimits(self, softlimit, hardlimit) :    
        """Sets the soft and hard limit for this quota."""
        self.SoftLimit = softlimit
        self.HardLimit = hardlimit
        self.DateLimit = None
        self.WarnCount = 0
        self.isDirty = True
        
    def setUsage(self, used) :
        """Sets the PageCounter and LifePageCounter to used, or if used is + or - prefixed, changes the values of {Life,}PageCounter by that amount."""
        vused = int(used)
        if used.startswith("+") or used.startswith("-") :
            self.PageCounter += vused
            self.LifePageCounter += vused
        else :
            self.PageCounter = self.LifePageCounter = vused
        self.DateLimit = None
        self.WarnCount = 0
        self.isDirty = 1

    def incDenyBannerCounter(self) :
        """Increment the deny banner counter for this user quota."""
        self.parent.increaseUserPQuotaWarnCount(self)
        self.WarnCount = (self.WarnCount or 0) + 1
        
    def resetDenyBannerCounter(self) :
        """Resets the deny banner counter for this user quota."""
        self.parent.writeUserPQuotaWarnCount(self, 0)
        self.WarnCount = 0
        
    def reset(self) :    
        """Resets page counter to 0."""
        self.PageCounter = 0
        self.DateLimit = None
        self.isDirty = True
        
    def hardreset(self) :    
        """Resets actual and life time page counters to 0."""
        self.PageCounter = self.LifePageCounter = 0
        self.DateLimit = None
        self.isDirty = True
        
    def computeJobPrice(self, jobsize, inkusage=[]) :    
        """Computes the job price as the sum of all parent printers' prices + current printer's ones."""
        totalprice = 0.0    
        if jobsize :
            if self.User.OverCharge != 0.0 :    # optimization, but TODO : beware of rounding errors
                for upq in [ self ] + self.ParentPrintersUserPQuota :
                    totalprice += float(upq.Printer.PricePerJob or 0.0)
                    pageprice = float(upq.Printer.PricePerPage or 0.0)
                    if not inkusage :
                        totalprice += (jobsize * pageprice)
                    else :    
                        for pageindex in range(jobsize) :
                            try :
                                usage = inkusage[pageindex]
                            except IndexError :    
                                self.parent.tool.logdebug("No ink usage information. Using base cost of %f credits for page %i." % (pageprice, pageindex+1))
                                totalprice += pageprice
                            else :    
                                coefficients = self.Printer.Coefficients
                                for (ink, value) in usage.items() :
                                    coefvalue = coefficients.get(ink, 1.0)
                                    coefprice = (coefvalue * pageprice) / 100.0
                                    inkprice = coefprice * value
                                    self.parent.tool.logdebug("Applying coefficient %f for color %s (used at %f%% on page %i) to base cost %f gives %f" % (coefvalue, ink, value, pageindex+1, pageprice, inkprice))
                                    totalprice += inkprice
        if self.User.OverCharge != 1.0 : # TODO : beware of rounding errors
            overcharged = totalprice * self.User.OverCharge        
            self.parent.tool.logdebug("Overcharging %s by a factor of %s ===> User %s will be charged for %s credits." % (totalprice, self.User.OverCharge, self.User.Name, overcharged))
            return overcharged
        else :    
            return totalprice
            
    def increasePagesUsage(self, jobsize, inkusage=[]) :
        """Increase the value of used pages and money."""
        jobprice = self.computeJobPrice(jobsize, inkusage)
        if jobsize :
            if jobprice :
                self.User.consumeAccountBalance(jobprice)
            for upq in [ self ] + self.ParentPrintersUserPQuota :
                self.parent.increaseUserPQuotaPagesCounters(upq, jobsize)
                upq.PageCounter = int(upq.PageCounter or 0) + jobsize
                upq.LifePageCounter = int(upq.LifePageCounter or 0) + jobsize
        return jobprice
        
    def delete(self) :    
        """Deletes an user print quota entry from the database."""
        self.parent.deleteUserPQuota(self)
        if self.parent.usecache :
            self.parent.flushEntry("USERPQUOTAS", "%s@%s" % (self.User.Name, self.Printer.Name))
        self.Exists = False
        self.isDirty = False
        
    def refund(self, nbpages) :    
        """Refunds a number of pages to an user on a particular printer."""
        self.parent.increaseUserPQuotaPagesCounters(self, -nbpages)
        self.PageCounter = int(self.PageCounter or 0) - nbpages
        self.LifePageCounter = int(self.LifePageCounter or 0) - nbpages
        
        
class StorageGroupPQuota(StorageObject) :
    """Group Print Quota class."""
    def __init__(self, parent, group, printer) :
        StorageObject.__init__(self, parent)
        self.Group = group
        self.Printer = printer
        self.PageCounter = None
        self.LifePageCounter = None
        self.SoftLimit = None
        self.HardLimit = None
        self.DateLimit = None
        self.MaxJobSize = None
        
    def __getattr__(self, name) :    
        """Delays data retrieval until it's really needed."""
        if name == "ParentPrintersGroupPQuota" : 
            self.ParentPrintersGroupPQuota = (self.Group.Exists and self.Printer.Exists and self.parent.getParentPrintersGroupPQuota(self)) or []
            return self.ParentPrintersGroupPQuota
        else :
            raise AttributeError, name
        
    def reset(self) :    
        """Resets page counter to 0."""
        for user in self.parent.getGroupMembers(self.Group) :
            uq = self.parent.getUserPQuota(user, self.Printer)
            uq.reset()
            uq.save()
        self.PageCounter = 0
        self.DateLimit = None
        self.isDirty = True
        
    def hardreset(self) :    
        """Resets actual and life time page counters to 0."""
        for user in self.parent.getGroupMembers(self.Group) :
            uq = self.parent.getUserPQuota(user, self.Printer)
            uq.hardreset()
            uq.save()
        self.PageCounter = self.LifePageCounter = 0
        self.DateLimit = None
        self.isDirty = True
        
    def setDateLimit(self, datelimit) :    
        """Sets the date limit for this quota."""
        datelimit = DateTime.ISO.ParseDateTime(str(datelimit)[:19])
        date = "%04i-%02i-%02i %02i:%02i:%02i" % (datelimit.year, \
                                                  datelimit.month, \
                                                  datelimit.day, \
                                                  datelimit.hour, \
                                                  datelimit.minute, \
                                                  datelimit.second)
        self.parent.writeGroupPQuotaDateLimit(self, date)
        self.DateLimit = date
        
    def setLimits(self, softlimit, hardlimit) :    
        """Sets the soft and hard limit for this quota."""
        self.SoftLimit = softlimit
        self.HardLimit = hardlimit
        self.DateLimit = None
        self.isDirty = True
        
    def delete(self) :    
        """Deletes a group print quota entry from the database."""
        self.parent.deleteGroupPQuota(self)
        if self.parent.usecache :
            self.parent.flushEntry("GROUPPQUOTAS", "%s@%s" % (self.Group.Name, self.Printer.Name))
        self.Exists = False
        self.isDirty = False
        
        
class StorageJob(StorageObject) :
    """Printer's Job class."""
    def __init__(self, parent) :
        StorageObject.__init__(self, parent)
        self.UserName = None
        self.PrinterName = None
        self.JobId = None
        self.PrinterPageCounter = None
        self.JobSizeBytes = None
        self.JobSize = None
        self.JobAction = None
        self.JobDate = None
        self.JobPrice = None
        self.JobFileName = None
        self.JobTitle = None
        self.JobCopies = None
        self.JobOptions = None
        self.JobHostName = None
        self.JobMD5Sum = None
        self.JobPages = None
        self.JobBillingCode = None
        self.PrecomputedJobSize = None
        self.PrecomputedJobPrice = None
        
    def __getattr__(self, name) :    
        """Delays data retrieval until it's really needed."""
        if name == "User" : 
            self.User = self.parent.getUser(self.UserName)
            return self.User
        elif name == "Printer" :    
            self.Printer = self.parent.getPrinter(self.PrinterName)
            return self.Printer
        else :
            raise AttributeError, name
            
    def refund(self, reason) :        
        """Refund a particular print job."""
        if (not self.JobSize) or (self.JobAction in ("DENY", "CANCEL", "REFUND")) :
            return
            
        basereason = _("Refunded %i pages and %.3f credits by %s (%s) on %s") \
                        % (self.JobSize,
                           self.JobPrice,
                           self.parent.tool.originalUserName,
                           os.getlogin(),
                           str(DateTime.now())[:19])
        if reason :                                               
            reason = "%s : %s" % (basereason, reason)
        else :    
            reason = basereason
        self.parent.tool.logdebug("Refunding job %s..." % self.ident)    
        self.parent.beginTransaction()
        try :
            if self.JobBillingCode :
                bcode = self.parent.getBillingCode(self.JobBillingCode)
                bcode.refund(self.JobSize, self.JobPrice)
                
            if self.User.Exists :
                self.User.refund(self.JobPrice, reason)
                if self.Printer.Exists :    
                    upq = self.parent.getUserPQuota(self.User, self.Printer)    
                    if upq.Exists :
                        upq.refund(self.JobSize)
            self.parent.refundJob(self.ident)
        except :        
            self.parent.rollbackTransaction()
            self.parent.tool.logdebug("Error while refunding job %s." % self.ident)
            raise
        else :    
            self.parent.commitTransaction()
            self.parent.tool.logdebug("Job %s refunded." % self.ident)
        
        
class StorageLastJob(StorageJob) :
    """Printer's Last Job class."""
    def __init__(self, parent, printer) :
        StorageJob.__init__(self, parent)
        self.PrinterName = printer.Name # not needed
        self.Printer = printer
        
        
class StorageBillingCode(StorageObject) :
    """Billing code class."""
    def __init__(self, parent, name) :
        StorageObject.__init__(self, parent)
        self.BillingCode = name
        self.PageCounter = None
        self.Balance = None
        
    def delete(self) :    
        """Deletes the billing code from the database."""
        self.parent.deleteBillingCode(self)
        self.parent.flushEntry("BILLINGCODES", self.BillingCode)
        self.isDirty = False
        self.Exists = False
        
    def reset(self, balance=0.0, pagecounter=0) :    
        """Resets the pagecounter and balance for this billing code."""
        self.Balance = balance
        self.PageCounter = pagecounter
        self.isDirty = True
        
    def consume(self, pages, price) :
        """Consumes some pages and credits for this billing code."""
        if pages :
            self.parent.consumeBillingCode(self, pages, price)
            self.PageCounter += pages
            self.Balance -= price
           
    def refund(self, pages, price) :
        """Refunds a particular billing code."""
        self.consume(-pages, -price)
        
        
class BaseStorage :
    def __init__(self, pykotatool) :
        """Opens the storage connection."""
        self.closed = 1
        self.tool = pykotatool
        self.usecache = pykotatool.config.getCaching()
        self.disablehistory = pykotatool.config.getDisableHistory()
        self.privacy = pykotatool.config.getPrivacy()
        if self.privacy :
            pykotatool.logdebug("Jobs' title, filename and options will be hidden because of privacy concerns.")
        if self.usecache :
            self.tool.logdebug("Caching enabled.")
            self.caches = { "USERS" : {}, \
                            "GROUPS" : {}, \
                            "PRINTERS" : {}, \
                            "USERPQUOTAS" : {}, \
                            "GROUPPQUOTAS" : {}, \
                            "JOBS" : {}, \
                            "LASTJOBS" : {}, \
                            "BILLINGCODES" : {} }
        
    def close(self) :    
        """Must be overriden in children classes."""
        raise RuntimeError, "BaseStorage.close() must be overriden !"
        
    def __del__(self) :        
        """Ensures that the database connection is closed."""
        self.close()
        
    def getFromCache(self, cachetype, key) :
        """Tries to extract something from the cache."""
        if self.usecache :
            entry = self.caches[cachetype].get(key)
            if entry is not None :
                self.tool.logdebug("Cache hit (%s->%s)" % (cachetype, key))
            else :    
                self.tool.logdebug("Cache miss (%s->%s)" % (cachetype, key))
            return entry    
            
    def cacheEntry(self, cachetype, key, value) :        
        """Puts an entry in the cache."""
        if self.usecache and getattr(value, "Exists", 0) :
            self.caches[cachetype][key] = value
            self.tool.logdebug("Cache store (%s->%s)" % (cachetype, key))
            
    def flushEntry(self, cachetype, key) :        
        """Removes an entry from the cache."""
        if self.usecache :
            try :
                del self.caches[cachetype][key]
            except KeyError :    
                pass
            else :    
                self.tool.logdebug("Cache flush (%s->%s)" % (cachetype, key))
            
    def getUser(self, username) :        
        """Returns the user from cache."""
        user = self.getFromCache("USERS", username)
        if user is None :
            user = self.getUserFromBackend(username)
            self.cacheEntry("USERS", username, user)
        return user    
        
    def getGroup(self, groupname) :        
        """Returns the group from cache."""
        group = self.getFromCache("GROUPS", groupname)
        if group is None :
            group = self.getGroupFromBackend(groupname)
            self.cacheEntry("GROUPS", groupname, group)
        return group    
        
    def getPrinter(self, printername) :        
        """Returns the printer from cache."""
        printer = self.getFromCache("PRINTERS", printername)
        if printer is None :
            printer = self.getPrinterFromBackend(printername)
            self.cacheEntry("PRINTERS", printername, printer)
        return printer    
        
    def getUserPQuota(self, user, printer) :        
        """Returns the user quota information from cache."""
        useratprinter = "%s@%s" % (user.Name, printer.Name)
        upquota = self.getFromCache("USERPQUOTAS", useratprinter)
        if upquota is None :
            upquota = self.getUserPQuotaFromBackend(user, printer)
            self.cacheEntry("USERPQUOTAS", useratprinter, upquota)
        return upquota    
        
    def getGroupPQuota(self, group, printer) :        
        """Returns the group quota information from cache."""
        groupatprinter = "%s@%s" % (group.Name, printer.Name)
        gpquota = self.getFromCache("GROUPPQUOTAS", groupatprinter)
        if gpquota is None :
            gpquota = self.getGroupPQuotaFromBackend(group, printer)
            self.cacheEntry("GROUPPQUOTAS", groupatprinter, gpquota)
        return gpquota    
        
    def getPrinterLastJob(self, printer) :        
        """Extracts last job information for a given printer from cache."""
        lastjob = self.getFromCache("LASTJOBS", printer.Name)
        if lastjob is None :
            lastjob = self.getPrinterLastJobFromBackend(printer)
            self.cacheEntry("LASTJOBS", printer.Name, lastjob)
        return lastjob    
        
    def getBillingCode(self, label) :        
        """Returns the user from cache."""
        code = self.getFromCache("BILLINGCODES", label)
        if code is None :
            code = self.getBillingCodeFromBackend(label)
            self.cacheEntry("BILLINGCODES", label, code)
        return code
        
    def getParentPrinters(self, printer) :    
        """Extracts parent printers information for a given printer from cache."""
        if self.usecache :
            if not hasattr(printer, "Parents") :
                self.tool.logdebug("Cache miss (%s->Parents)" % printer.Name)
                printer.Parents = self.getParentPrintersFromBackend(printer)
                self.tool.logdebug("Cache store (%s->Parents)" % printer.Name)
            else :
                self.tool.logdebug("Cache hit (%s->Parents)" % printer.Name)
        else :        
            printer.Parents = self.getParentPrintersFromBackend(printer)
        for parent in printer.Parents[:] :    
            printer.Parents.extend(self.getParentPrinters(parent))
        uniquedic = {}    
        for parent in printer.Parents :
            uniquedic[parent.Name] = parent
        printer.Parents = uniquedic.values()    
        return printer.Parents
        
    def getGroupMembers(self, group) :        
        """Returns the group's members list from in-group cache."""
        if self.usecache :
            if not hasattr(group, "Members") :
                self.tool.logdebug("Cache miss (%s->Members)" % group.Name)
                group.Members = self.getGroupMembersFromBackend(group)
                self.tool.logdebug("Cache store (%s->Members)" % group.Name)
            else :
                self.tool.logdebug("Cache hit (%s->Members)" % group.Name)
        else :        
            group.Members = self.getGroupMembersFromBackend(group)
        return group.Members    
        
    def getUserGroups(self, user) :        
        """Returns the user's groups list from in-user cache."""
        if self.usecache :
            if not hasattr(user, "Groups") :
                self.tool.logdebug("Cache miss (%s->Groups)" % user.Name)
                user.Groups = self.getUserGroupsFromBackend(user)
                self.tool.logdebug("Cache store (%s->Groups)" % user.Name)
            else :
                self.tool.logdebug("Cache hit (%s->Groups)" % user.Name)
        else :        
            user.Groups = self.getUserGroupsFromBackend(user)
        return user.Groups   
        
    def getParentPrintersUserPQuota(self, userpquota) :     
        """Returns all user print quota on the printer and all its parents recursively."""
        upquotas = [ ]
        for printer in self.getParentPrinters(userpquota.Printer) :
            upq = self.getUserPQuota(userpquota.User, printer)
            if upq.Exists :
                upquotas.append(upq)
        return upquotas        
        
    def getParentPrintersGroupPQuota(self, grouppquota) :     
        """Returns all group print quota on the printer and all its parents recursively."""
        gpquotas = [ ]
        for printer in self.getParentPrinters(grouppquota.Printer) :
            gpq = self.getGroupPQuota(grouppquota.Group, printer)
            if gpq.Exists :
                gpquotas.append(gpq)
        return gpquotas        
        
    def databaseToUserCharset(self, text) :
        """Converts from database format (UTF-8) to user's charset."""
        return self.tool.UTF8ToUserCharset(text)
        
    def userCharsetToDatabase(self, text) :
        """Converts from user's charset to database format (UTF-8)."""
        return self.tool.userCharsetToUTF8(text)
        
    def cleanDates(self, startdate, enddate) :    
        """Clean the dates to create a correct filter."""
        if startdate :    
            startdate = startdate.strip().lower()
        if enddate :    
            enddate = enddate.strip().lower()
        if (not startdate) and (not enddate) :    
            return (None, None)
            
        now = DateTime.now()    
        nameddates = ('yesterday', 'today', 'now', 'tomorrow')
        datedict = { "start" : startdate, "end" : enddate }    
        for limit in datedict.keys() :
            dateval = datedict[limit]
            if dateval :
                for name in nameddates :
                    if dateval.startswith(name) :
                        try :
                            offset = int(dateval[len(name):])
                        except :    
                            offset = 0
                        dateval = dateval[:len(name)]    
                        if limit == "start" :
                            if dateval == "yesterday" :
                                dateval = (now - 1 + offset).Format("%Y%m%d000000")
                            elif dateval == "today" :
                                dateval = (now + offset).Format("%Y%m%d000000")
                            elif dateval == "now" :
                                dateval = (now + offset).Format("%Y%m%d%H%M%S")
                            else : # tomorrow
                                dateval = (now + 1 + offset).Format("%Y%m%d000000")
                        else :
                            if dateval == "yesterday" :
                                dateval = (now - 1 + offset).Format("%Y%m%d235959")
                            elif dateval == "today" :
                                dateval = (now + offset).Format("%Y%m%d235959")
                            elif dateval == "now" :
                                dateval = (now + offset).Format("%Y%m%d%H%M%S")
                            else : # tomorrow
                                dateval = (now + 1 + offset).Format("%Y%m%d235959")
                        break
                        
                if not dateval.isdigit() :
                    dateval = None
                else :    
                    lgdateval = len(dateval)
                    if lgdateval == 4 :
                        if limit == "start" : 
                            dateval = "%s0101 00:00:00" % dateval
                        else :  
                            dateval = "%s1231 23:59:59" % dateval
                    elif lgdateval == 6 :
                        if limit == "start" : 
                            dateval = "%s01 00:00:00" % dateval
                        else :  
                            mxdate = DateTime.ISO.ParseDateTime("%s01 00:00:00" % dateval)
                            dateval = "%s%02i 23:59:59" % (dateval, mxdate.days_in_month)
                    elif lgdateval == 8 :
                        if limit == "start" : 
                            dateval = "%s 00:00:00" % dateval
                        else :  
                            dateval = "%s 23:59:59" % dateval
                    elif lgdateval == 10 :
                        if limit == "start" : 
                            dateval = "%s %s:00:00" % (dateval[:8], dateval[8:])
                        else :  
                            dateval = "%s %s:59:59" % (dateval[:8], dateval[8:])
                    elif lgdateval == 12 :
                        if limit == "start" : 
                            dateval = "%s %s:%s:00" % (dateval[:8], dateval[8:10], dateval[10:])
                        else :  
                            dateval = "%s %s:%s:59" % (dateval[:8], dateval[8:10], dateval[10:])
                    elif lgdateval == 14 :        
                        dateval = "%s %s:%s:%s" % (dateval[:8], dateval[8:10], dateval[10:12], dateval[12:])
                    else :    
                        dateval = None
                    try :    
                        DateTime.ISO.ParseDateTime(dateval[:19])
                    except :    
                        dateval = None
                datedict[limit] = dateval    
        (start, end) = (datedict["start"], datedict["end"])
        if start and end and (start > end) :
            (start, end) = (end, start)
        try :    
            if len(start) == 17 :
                start = "%s-%s-%s %s" % (start[0:4], start[4:6], start[6:8], start[9:])
        except TypeError :        
            pass
            
        try :    
            if len(end) == 17 :
                end = "%s-%s-%s %s" % (end[0:4], end[4:6], end[6:8], end[9:])
        except TypeError :        
            pass
            
        return (start, end)    
        
def openConnection(pykotatool) :
    """Returns a connection handle to the appropriate database."""
    backendinfo = pykotatool.config.getStorageBackend()
    backend = backendinfo["storagebackend"]
    try :
        storagebackend = imp.load_source("storagebackend", 
                                         os.path.join(os.path.dirname(__file__),
                                                      "storages",
                                                      "%s.py" % backend.lower()))
    except ImportError :
        raise PyKotaStorageError, _("Unsupported quota storage backend %s") % backend
    else :    
        host = backendinfo["storageserver"]
        database = backendinfo["storagename"]
        admin = backendinfo["storageadmin"] or backendinfo["storageuser"]
        adminpw = backendinfo["storageadminpw"] or backendinfo["storageuserpw"]
        return storagebackend.Storage(pykotatool, host, database, admin, adminpw)

