root / pykota / trunk / pykota / storages / ldapstorage.py @ 2686

Revision 2686, 87.0 kB (checked in by jerome, 18 years ago)

Modified pkprinters to improve speed just like I did for pkbcodes earlier.
edpykota had to be modified as well to use the new printer API.
The time spent to modify printers has been almost halved.

  • 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
25#
26# My IANA assigned number, for
27# "Conseil Internet & Logiciels Libres, J�me Alet"
28# is 16868. Use this as a base to create the LDAP schema.
29#
30
31import sys
32import os
33import types
34import time
35import md5
36import base64
37import random
38
39from mx import DateTime
40
41from pykota.storage import PyKotaStorageError, BaseStorage, StorageObject, \
42                           StorageUser, StorageGroup, StoragePrinter, \
43                           StorageJob, StorageLastJob, StorageUserPQuota, \
44                           StorageGroupPQuota, StorageBillingCode
45
46try :
47    import ldap
48    import ldap.modlist
49except ImportError :   
50    raise PyKotaStorageError, "This python version (%s) doesn't seem to have the python-ldap module installed correctly." % sys.version.split()[0]
51else :   
52    try :
53        from ldap.cidict import cidict
54    except ImportError :   
55        import UserDict
56        sys.stderr.write("ERROR: PyKota requires a newer version of python-ldap. Workaround activated. Please upgrade python-ldap !\n")
57        class cidict(UserDict.UserDict) :
58            pass # Fake it all, and don't care for case insensitivity : users who need it will have to upgrade.
59   
60class Storage(BaseStorage) :
61    def __init__(self, pykotatool, host, dbname, user, passwd) :
62        """Opens the LDAP connection."""
63        self.savedtool = pykotatool
64        self.savedhost = host
65        self.saveddbname = dbname
66        self.saveduser = user
67        self.savedpasswd = passwd
68        self.secondStageInit()
69       
70    def secondStageInit(self) :   
71        """Second stage initialisation."""
72        BaseStorage.__init__(self, self.savedtool)
73        self.info = self.tool.config.getLDAPInfo()
74        message = ""
75        for tryit in range(3) :
76            try :
77                self.tool.logdebug("Trying to open database (host=%s, dbname=%s, user=%s)..." % (self.savedhost, self.saveddbname, self.saveduser))
78                self.database = ldap.initialize(self.savedhost) 
79                if self.info["ldaptls"] :
80                    # we want TLS
81                    ldap.set_option(ldap.OPT_X_TLS_CACERTFILE, self.info["cacert"])
82                    self.database.set_option(ldap.OPT_X_TLS, ldap.OPT_X_TLS_DEMAND)
83                    self.database.start_tls_s()
84                self.database.simple_bind_s(self.saveduser, self.savedpasswd)
85                self.basedn = self.saveddbname
86            except ldap.SERVER_DOWN :   
87                message = "LDAP backend for PyKota seems to be down !"
88                self.tool.printInfo("%s" % message, "error")
89                self.tool.printInfo("Trying again in 2 seconds...", "warn")
90                time.sleep(2)
91            except ldap.LDAPError :   
92                message = "Unable to connect to LDAP server %s as %s." % (self.savedhost, self.saveduser)
93                self.tool.printInfo("%s" % message, "error")
94                self.tool.printInfo("Trying again in 2 seconds...", "warn")
95                time.sleep(2)
96            else :   
97                self.useldapcache = self.tool.config.getLDAPCache()
98                if self.useldapcache :
99                    self.tool.logdebug("Low-Level LDAP Caching enabled.")
100                    self.ldapcache = {} # low-level cache specific to LDAP backend
101                self.closed = 0
102                self.tool.logdebug("Database opened (host=%s, dbname=%s, user=%s)" % (self.savedhost, self.saveddbname, self.saveduser))
103                return # All is fine here.
104        raise PyKotaStorageError, message         
105           
106    def close(self) :   
107        """Closes the database connection."""
108        if not self.closed :
109            self.database.unbind_s()
110            self.closed = 1
111            self.tool.logdebug("Database closed.")
112       
113    def genUUID(self) :   
114        """Generates an unique identifier.
115       
116           TODO : this one is not unique accross several print servers, but should be sufficient for testing.
117        """
118        return md5.md5("%s-%s" % (time.time(), random.random())).hexdigest()
119       
120    def normalizeFields(self, fields) :   
121        """Ensure all items are lists."""
122        for (k, v) in fields.items() :
123            if type(v) not in (types.TupleType, types.ListType) :
124                if not v :
125                    del fields[k]
126                else :   
127                    fields[k] = [ v ]
128        return fields       
129       
130    def beginTransaction(self) :   
131        """Starts a transaction."""
132        self.tool.logdebug("Transaction begins... WARNING : No transactions in LDAP !")
133       
134    def commitTransaction(self) :   
135        """Commits a transaction."""
136        self.tool.logdebug("Transaction committed. WARNING : No transactions in LDAP !")
137       
138    def rollbackTransaction(self) :     
139        """Rollbacks a transaction."""
140        self.tool.logdebug("Transaction aborted. WARNING : No transaction in LDAP !")
141       
142    def doSearch(self, key, fields=None, base="", scope=ldap.SCOPE_SUBTREE, flushcache=0) :
143        """Does an LDAP search query."""
144        message = ""
145        for tryit in range(3) :
146            try :
147                base = base or self.basedn
148                if self.useldapcache :
149                    # Here we overwrite the fields the app want, to try and
150                    # retrieve ALL user defined attributes ("*")
151                    # + the createTimestamp attribute, needed by job history
152                    #
153                    # This may not work with all LDAP servers
154                    # but works at least in OpenLDAP (2.1.25)
155                    # and iPlanet Directory Server (5.1 SP3)
156                    fields = ["*", "createTimestamp"]         
157                   
158                if self.useldapcache and (not flushcache) and (scope == ldap.SCOPE_BASE) and self.ldapcache.has_key(base) :
159                    entry = self.ldapcache[base]
160                    self.tool.logdebug("LDAP cache hit %s => %s" % (base, entry))
161                    result = [(base, entry)]
162                else :
163                    self.tool.logdebug("QUERY : Filter : %s, BaseDN : %s, Scope : %s, Attributes : %s" % (key, base, scope, fields))
164                    result = self.database.search_s(base, scope, key, fields)
165            except ldap.NO_SUCH_OBJECT, msg :       
166                raise PyKotaStorageError, (_("Search base %s doesn't seem to exist. Probable misconfiguration. Please double check /etc/pykota/pykota.conf : %s") % (base, msg))
167            except ldap.LDAPError, msg :   
168                message = (_("Search for %s(%s) from %s(scope=%s) returned no answer.") % (key, fields, base, scope)) + " : %s" % str(msg)
169                self.tool.printInfo("LDAP error : %s" % message, "error")
170                self.tool.printInfo("LDAP connection will be closed and reopened.", "warn")
171                self.close()
172                self.secondStageInit()
173            else :     
174                self.tool.logdebug("QUERY : Result : %s" % result)
175                result = [ (dn, cidict(attrs)) for (dn, attrs) in result ]
176                if self.useldapcache :
177                    for (dn, attributes) in result :
178                        self.tool.logdebug("LDAP cache store %s => %s" % (dn, attributes))
179                        self.ldapcache[dn] = attributes
180                return result
181        raise PyKotaStorageError, message
182           
183    def doAdd(self, dn, fields) :
184        """Adds an entry in the LDAP directory."""
185        fields = self.normalizeFields(cidict(fields))
186        message = ""
187        for tryit in range(3) :
188            try :
189                self.tool.logdebug("QUERY : ADD(%s, %s)" % (dn, str(fields)))
190                entry = ldap.modlist.addModlist(fields)
191                self.tool.logdebug("%s" % entry)
192                self.database.add_s(dn, entry)
193            except ldap.LDAPError, msg :
194                message = (_("Problem adding LDAP entry (%s, %s)") % (dn, str(fields))) + " : %s" % str(msg)
195                self.tool.printInfo("LDAP error : %s" % message, "error")
196                self.tool.printInfo("LDAP connection will be closed and reopened.", "warn")
197                self.close()
198                self.secondStageInit()
199            else :
200                if self.useldapcache :
201                    self.tool.logdebug("LDAP cache add %s => %s" % (dn, fields))
202                    self.ldapcache[dn] = fields
203                return dn
204        raise PyKotaStorageError, message
205           
206    def doDelete(self, dn) :
207        """Deletes an entry from the LDAP directory."""
208        message = ""
209        for tryit in range(3) :
210            try :
211                self.tool.logdebug("QUERY : Delete(%s)" % dn)
212                self.database.delete_s(dn)
213            except ldap.LDAPError, msg :
214                message = (_("Problem deleting LDAP entry (%s)") % dn) + " : %s" % str(msg)
215                self.tool.printInfo("LDAP error : %s" % message, "error")
216                self.tool.printInfo("LDAP connection will be closed and reopened.", "warn")
217                self.close()
218                self.secondStageInit()
219            else :   
220                if self.useldapcache :
221                    try :
222                        self.tool.logdebug("LDAP cache del %s" % dn)
223                        del self.ldapcache[dn]
224                    except KeyError :   
225                        pass
226                return       
227        raise PyKotaStorageError, message
228           
229    def doModify(self, dn, fields, ignoreold=1, flushcache=0) :
230        """Modifies an entry in the LDAP directory."""
231        fields = cidict(fields)
232        for tryit in range(3) :
233            try :
234                # TODO : take care of, and update LDAP specific cache
235                if self.useldapcache and not flushcache :
236                    if self.ldapcache.has_key(dn) :
237                        old = self.ldapcache[dn]
238                        self.tool.logdebug("LDAP cache hit %s => %s" % (dn, old))
239                        oldentry = {}
240                        for (k, v) in old.items() :
241                            if k != "createTimestamp" :
242                                oldentry[k] = v
243                    else :   
244                        self.tool.logdebug("LDAP cache miss %s" % dn)
245                        oldentry = self.doSearch("objectClass=*", base=dn, scope=ldap.SCOPE_BASE)[0][1]
246                else :       
247                    oldentry = self.doSearch("objectClass=*", base=dn, scope=ldap.SCOPE_BASE, flushcache=flushcache)[0][1]
248                for (k, v) in fields.items() :
249                    if type(v) == type({}) :
250                        try :
251                            oldvalue = v["convert"](oldentry.get(k, [0])[0])
252                        except ValueError :   
253                            self.tool.logdebug("Error converting %s with %s(%s)" % (oldentry.get(k), k, v))
254                            oldvalue = 0
255                        if v["operator"] == '+' :
256                            newvalue = oldvalue + v["value"]
257                        else :   
258                            newvalue = oldvalue - v["value"]
259                        fields[k] = str(newvalue)
260                fields = self.normalizeFields(fields)
261                self.tool.logdebug("QUERY : Modify(%s, %s ==> %s)" % (dn, oldentry, fields))
262                entry = ldap.modlist.modifyModlist(oldentry, fields, ignore_oldexistent=ignoreold)
263                modentry = []
264                for (mop, mtyp, mval) in entry :
265                    if mtyp and (mtyp.lower() != "createtimestamp") :
266                        modentry.append((mop, mtyp, mval))
267                self.tool.logdebug("MODIFY : %s ==> %s ==> %s" % (fields, entry, modentry))
268                if modentry :
269                    self.database.modify_s(dn, modentry)
270            except ldap.LDAPError, msg :
271                message = (_("Problem modifying LDAP entry (%s, %s)") % (dn, fields)) + " : %s" % str(msg)
272                self.tool.printInfo("LDAP error : %s" % message, "error")
273                self.tool.printInfo("LDAP connection will be closed and reopened.", "warn")
274                self.close()
275                self.secondStageInit()
276            else :
277                if self.useldapcache :
278                    cachedentry = self.ldapcache[dn]
279                    for (mop, mtyp, mval) in entry :
280                        if mop in (ldap.MOD_ADD, ldap.MOD_REPLACE) :
281                            cachedentry[mtyp] = mval
282                        else :
283                            try :
284                                del cachedentry[mtyp]
285                            except KeyError :   
286                                pass
287                    self.tool.logdebug("LDAP cache update %s => %s" % (dn, cachedentry))
288                return dn
289        raise PyKotaStorageError, message
290           
291    def filterNames(self, records, attribute, patterns=None) :
292        """Returns a list of 'attribute' from a list of records.
293       
294           Logs any missing attribute.
295        """   
296        result = []
297        for (dn, record) in records :
298            attrval = record.get(attribute, [None])[0]
299            if attrval is None :
300                self.tool.printInfo("Object %s has no %s attribute !" % (dn, attribute), "error")
301            else :
302                attrval = self.databaseToUserCharset(attrval)
303                if patterns :
304                    if (not isinstance(patterns, type([]))) and (not isinstance(patterns, type(()))) :
305                        patterns = [ patterns ]
306                    if self.tool.matchString(attrval, patterns) :   
307                        result.append(attrval)
308                else :   
309                    result.append(attrval)
310        return result       
311               
312    def getAllBillingCodes(self, billingcode=None) :   
313        """Extracts all billing codes or only the billing codes matching the optional parameter."""
314        ldapfilter = "objectClass=pykotaBilling"
315        result = self.doSearch(ldapfilter, ["pykotaBillingCode"], base=self.info["billingcodebase"])
316        if result :
317            return [self.databaseToUserCharset(bc) for bc in self.filterNames(result, "pykotaBillingCode", billingcode)]
318        else :   
319            return []
320       
321    def getAllPrintersNames(self, printername=None) :   
322        """Extracts all printer names or only the printers' names matching the optional parameter."""
323        ldapfilter = "objectClass=pykotaPrinter"
324        result = self.doSearch(ldapfilter, ["pykotaPrinterName"], base=self.info["printerbase"])
325        if result :
326            return self.filterNames(result, "pykotaPrinterName", printername)
327        else :   
328            return []
329       
330    def getAllUsersNames(self, username=None) :   
331        """Extracts all user names or only the users' names matching the optional parameter."""
332        ldapfilter = "objectClass=pykotaAccount"
333        result = self.doSearch(ldapfilter, ["pykotaUserName"], base=self.info["userbase"])
334        if result :
335            return self.filterNames(result, "pykotaUserName", username)
336        else :   
337            return []
338       
339    def getAllGroupsNames(self, groupname=None) :   
340        """Extracts all group names or only the groups' names matching the optional parameter."""
341        ldapfilter = "objectClass=pykotaGroup"
342        result = self.doSearch(ldapfilter, ["pykotaGroupName"], base=self.info["groupbase"])
343        if result :
344            return self.filterNames(result, "pykotaGroupName", groupname)
345        else :   
346            return []
347       
348    def getUserNbJobsFromHistory(self, user) :
349        """Returns the number of jobs the user has in history."""
350        result = self.doSearch("(&(pykotaUserName=%s)(objectClass=pykotaJob))" % self.userCharsetToDatabase(user.Name), None, base=self.info["jobbase"])
351        return len(result)
352       
353    def getUserFromBackend(self, username) :   
354        """Extracts user information given its name."""
355        user = StorageUser(self, username)
356        username = self.userCharsetToDatabase(username)
357        result = self.doSearch("(&(objectClass=pykotaAccount)(|(pykotaUserName=%s)(%s=%s)))" % (username, self.info["userrdn"], username), ["pykotaUserName", "pykotaLimitBy", self.info["usermail"], "pykotaOverCharge"], base=self.info["userbase"])
358        if result :
359            fields = result[0][1]
360            user.ident = result[0][0]
361            user.Email = fields.get(self.info["usermail"], [None])[0]
362            user.LimitBy = fields.get("pykotaLimitBy", ["quota"])[0]
363            user.OverCharge = float(fields.get("pykotaOverCharge", [1.0])[0])
364            result = self.doSearch("(&(objectClass=pykotaAccountBalance)(|(pykotaUserName=%s)(%s=%s)))" % (username, self.info["balancerdn"], username), ["pykotaBalance", "pykotaLifeTimePaid", "pykotaPayments"], base=self.info["balancebase"])
365            if not result :
366                raise PyKotaStorageError, _("No pykotaAccountBalance object found for user %s. Did you create LDAP entries manually ?") % username
367            else :
368                fields = result[0][1]
369                user.idbalance = result[0][0]
370                user.AccountBalance = fields.get("pykotaBalance")
371                if user.AccountBalance is not None :
372                    if user.AccountBalance[0].upper() == "NONE" :
373                        user.AccountBalance = None
374                    else :   
375                        user.AccountBalance = float(user.AccountBalance[0])
376                user.AccountBalance = user.AccountBalance or 0.0       
377                user.LifeTimePaid = fields.get("pykotaLifeTimePaid")
378                if user.LifeTimePaid is not None :
379                    if user.LifeTimePaid[0].upper() == "NONE" :
380                        user.LifeTimePaid = None
381                    else :   
382                        user.LifeTimePaid = float(user.LifeTimePaid[0])
383                user.LifeTimePaid = user.LifeTimePaid or 0.0       
384                user.Payments = []
385                for payment in fields.get("pykotaPayments", []) :
386                    try :
387                        (date, amount, description) = payment.split(" # ")
388                    except ValueError :
389                        # Payment with no description (old Payment)
390                        (date, amount) = payment.split(" # ")
391                        description = ""
392                    else :   
393                        description = self.databaseToUserCharset(base64.decodestring(description))
394                    user.Payments.append((date, float(amount), description))
395            user.Exists = 1
396        return user
397       
398    def getGroupFromBackend(self, groupname) :   
399        """Extracts group information given its name."""
400        group = StorageGroup(self, groupname)
401        groupname = self.userCharsetToDatabase(groupname)
402        result = self.doSearch("(&(objectClass=pykotaGroup)(|(pykotaGroupName=%s)(%s=%s)))" % (groupname, self.info["grouprdn"], groupname), ["pykotaGroupName", "pykotaLimitBy"], base=self.info["groupbase"])
403        if result :
404            fields = result[0][1]
405            group.ident = result[0][0]
406            group.Name = fields.get("pykotaGroupName", [self.databaseToUserCharset(groupname)])[0] 
407            group.LimitBy = fields.get("pykotaLimitBy", ["quota"])[0]
408            group.AccountBalance = 0.0
409            group.LifeTimePaid = 0.0
410            for member in self.getGroupMembers(group) :
411                if member.Exists :
412                    group.AccountBalance += member.AccountBalance
413                    group.LifeTimePaid += member.LifeTimePaid
414            group.Exists = 1
415        return group
416       
417    def getPrinterFromBackend(self, printername) :       
418        """Extracts printer information given its name : returns first matching printer."""
419        printer = StoragePrinter(self, printername)
420        printername = self.userCharsetToDatabase(printername)
421        result = self.doSearch("(&(objectClass=pykotaPrinter)(|(pykotaPrinterName=%s)(%s=%s)))" \
422                      % (printername, self.info["printerrdn"], printername), \
423                        ["pykotaPrinterName", "pykotaPricePerPage", \
424                         "pykotaPricePerJob", "pykotaMaxJobSize", \
425                         "pykotaPassThrough", "uniqueMember", "description"], \
426                      base=self.info["printerbase"])
427        if result :
428            fields = result[0][1]       # take only first matching printer, ignore the rest
429            printer.ident = result[0][0]
430            printer.Name = fields.get("pykotaPrinterName", [self.databaseToUserCharset(printername)])[0] 
431            printer.PricePerJob = float(fields.get("pykotaPricePerJob", [0.0])[0])
432            printer.PricePerPage = float(fields.get("pykotaPricePerPage", [0.0])[0])
433            printer.MaxJobSize = int(fields.get("pykotaMaxJobSize", [0])[0])
434            printer.PassThrough = fields.get("pykotaPassThrough", [None])[0]
435            if printer.PassThrough in (1, "1", "t", "true", "TRUE", "True") :
436                printer.PassThrough = 1
437            else :
438                printer.PassThrough = 0
439            printer.uniqueMember = fields.get("uniqueMember", [])
440            printer.Description = self.databaseToUserCharset(fields.get("description", [""])[0]) 
441            printer.Exists = 1
442        return printer   
443       
444    def getUserPQuotaFromBackend(self, user, printer) :       
445        """Extracts a user print quota."""
446        userpquota = StorageUserPQuota(self, user, printer)
447        if printer.Exists and user.Exists :
448            if self.info["userquotabase"].lower() == "user" :
449                base = user.ident
450            else :   
451                base = self.info["userquotabase"]
452            result = self.doSearch("(&(objectClass=pykotaUserPQuota)(pykotaUserName=%s)(pykotaPrinterName=%s))" % \
453                                      (self.userCharsetToDatabase(user.Name), self.userCharsetToDatabase(printer.Name)), \
454                                      ["pykotaPageCounter", "pykotaLifePageCounter", "pykotaSoftLimit", "pykotaHardLimit", "pykotaDateLimit", "pykotaWarnCount"], \
455                                      base=base)
456            if result :
457                fields = result[0][1]
458                userpquota.ident = result[0][0]
459                userpquota.PageCounter = int(fields.get("pykotaPageCounter", [0])[0])
460                userpquota.LifePageCounter = int(fields.get("pykotaLifePageCounter", [0])[0])
461                userpquota.WarnCount = int(fields.get("pykotaWarnCount", [0])[0])
462                userpquota.SoftLimit = fields.get("pykotaSoftLimit")
463                if userpquota.SoftLimit is not None :
464                    if userpquota.SoftLimit[0].upper() == "NONE" :
465                        userpquota.SoftLimit = None
466                    else :   
467                        userpquota.SoftLimit = int(userpquota.SoftLimit[0])
468                userpquota.HardLimit = fields.get("pykotaHardLimit")
469                if userpquota.HardLimit is not None :
470                    if userpquota.HardLimit[0].upper() == "NONE" :
471                        userpquota.HardLimit = None
472                    elif userpquota.HardLimit is not None :   
473                        userpquota.HardLimit = int(userpquota.HardLimit[0])
474                userpquota.DateLimit = fields.get("pykotaDateLimit")
475                if userpquota.DateLimit is not None :
476                    if userpquota.DateLimit[0].upper() == "NONE" : 
477                        userpquota.DateLimit = None
478                    else :   
479                        userpquota.DateLimit = userpquota.DateLimit[0]
480                userpquota.Exists = 1
481        return userpquota
482       
483    def getGroupPQuotaFromBackend(self, group, printer) :       
484        """Extracts a group print quota."""
485        grouppquota = StorageGroupPQuota(self, group, printer)
486        if group.Exists :
487            if self.info["groupquotabase"].lower() == "group" :
488                base = group.ident
489            else :   
490                base = self.info["groupquotabase"]
491            result = self.doSearch("(&(objectClass=pykotaGroupPQuota)(pykotaGroupName=%s)(pykotaPrinterName=%s))" % \
492                                      (self.userCharsetToDatabase(group.Name), self.userCharsetToDatabase(printer.Name)), \
493                                      ["pykotaSoftLimit", "pykotaHardLimit", "pykotaDateLimit"], \
494                                      base=base)
495            if result :
496                fields = result[0][1]
497                grouppquota.ident = result[0][0]
498                grouppquota.SoftLimit = fields.get("pykotaSoftLimit")
499                if grouppquota.SoftLimit is not None :
500                    if grouppquota.SoftLimit[0].upper() == "NONE" :
501                        grouppquota.SoftLimit = None
502                    else :   
503                        grouppquota.SoftLimit = int(grouppquota.SoftLimit[0])
504                grouppquota.HardLimit = fields.get("pykotaHardLimit")
505                if grouppquota.HardLimit is not None :
506                    if grouppquota.HardLimit[0].upper() == "NONE" :
507                        grouppquota.HardLimit = None
508                    else :   
509                        grouppquota.HardLimit = int(grouppquota.HardLimit[0])
510                grouppquota.DateLimit = fields.get("pykotaDateLimit")
511                if grouppquota.DateLimit is not None :
512                    if grouppquota.DateLimit[0].upper() == "NONE" : 
513                        grouppquota.DateLimit = None
514                    else :   
515                        grouppquota.DateLimit = grouppquota.DateLimit[0]
516                grouppquota.PageCounter = 0
517                grouppquota.LifePageCounter = 0
518                usernamesfilter = "".join(["(pykotaUserName=%s)" % self.userCharsetToDatabase(member.Name) for member in self.getGroupMembers(group)])
519                if usernamesfilter :
520                    usernamesfilter = "(|%s)" % usernamesfilter
521                if self.info["userquotabase"].lower() == "user" :
522                    base = self.info["userbase"]
523                else :
524                    base = self.info["userquotabase"]
525                result = self.doSearch("(&(objectClass=pykotaUserPQuota)(pykotaPrinterName=%s)%s)" % \
526                                          (self.userCharsetToDatabase(printer.Name), usernamesfilter), \
527                                          ["pykotaPageCounter", "pykotaLifePageCounter"], base=base)
528                if result :
529                    for userpquota in result :   
530                        grouppquota.PageCounter += int(userpquota[1].get("pykotaPageCounter", [0])[0] or 0)
531                        grouppquota.LifePageCounter += int(userpquota[1].get("pykotaLifePageCounter", [0])[0] or 0)
532                grouppquota.Exists = 1
533        return grouppquota
534       
535    def getPrinterLastJobFromBackend(self, printer) :       
536        """Extracts a printer's last job information."""
537        lastjob = StorageLastJob(self, printer)
538        pname = self.userCharsetToDatabase(printer.Name)
539        result = self.doSearch("(&(objectClass=pykotaLastjob)(|(pykotaPrinterName=%s)(%s=%s)))" % \
540                                  (pname, self.info["printerrdn"], pname), \
541                                  ["pykotaLastJobIdent"], \
542                                  base=self.info["lastjobbase"])
543        if result :
544            lastjob.lastjobident = result[0][0]
545            lastjobident = result[0][1]["pykotaLastJobIdent"][0]
546            result = None
547            try :
548                result = self.doSearch("objectClass=pykotaJob", [ "pykotaJobSizeBytes", 
549                                                                  "pykotaHostName", 
550                                                                  "pykotaUserName", 
551                                                                  "pykotaPrinterName", 
552                                                                  "pykotaJobId", 
553                                                                  "pykotaPrinterPageCounter", 
554                                                                  "pykotaJobSize", 
555                                                                  "pykotaAction", 
556                                                                  "pykotaJobPrice", 
557                                                                  "pykotaFileName", 
558                                                                  "pykotaTitle", 
559                                                                  "pykotaCopies", 
560                                                                  "pykotaOptions", 
561                                                                  "pykotaBillingCode", 
562                                                                  "pykotaPages", 
563                                                                  "pykotaMD5Sum", 
564                                                                  "pykotaPrecomputedJobSize",
565                                                                  "pykotaPrecomputedJobPrice",
566                                                                  "createTimestamp" ], 
567                                                                base="cn=%s,%s" % (lastjobident, self.info["jobbase"]), scope=ldap.SCOPE_BASE)
568            except PyKotaStorageError :   
569                pass # Last job entry exists, but job probably doesn't exist anymore.
570            if result :
571                fields = result[0][1]
572                lastjob.ident = result[0][0]
573                lastjob.JobId = fields.get("pykotaJobId")[0]
574                lastjob.UserName = self.databaseToUserCharset(fields.get("pykotaUserName")[0])
575                lastjob.PrinterPageCounter = int(fields.get("pykotaPrinterPageCounter", [0])[0])
576                try :
577                    lastjob.JobSize = int(fields.get("pykotaJobSize", [0])[0])
578                except ValueError :   
579                    lastjob.JobSize = None
580                try :   
581                    lastjob.JobPrice = float(fields.get("pykotaJobPrice", [0.0])[0])
582                except ValueError :   
583                    lastjob.JobPrice = None
584                lastjob.JobAction = fields.get("pykotaAction", [""])[0]
585                lastjob.JobFileName = self.databaseToUserCharset(fields.get("pykotaFileName", [""])[0]) 
586                lastjob.JobTitle = self.databaseToUserCharset(fields.get("pykotaTitle", [""])[0]) 
587                lastjob.JobCopies = int(fields.get("pykotaCopies", [0])[0])
588                lastjob.JobOptions = self.databaseToUserCharset(fields.get("pykotaOptions", [""])[0]) 
589                lastjob.JobHostName = fields.get("pykotaHostName", [""])[0]
590                lastjob.JobSizeBytes = fields.get("pykotaJobSizeBytes", [0L])[0]
591                lastjob.JobBillingCode = self.databaseToUserCharset(fields.get("pykotaBillingCode", [None])[0])
592                lastjob.JobMD5Sum = fields.get("pykotaMD5Sum", [None])[0]
593                lastjob.JobPages = fields.get("pykotaPages", [""])[0]
594                try :
595                    lastjob.PrecomputedJobSize = int(fields.get("pykotaPrecomputedJobSize", [0])[0])
596                except ValueError :   
597                    lastjob.PrecomputedJobSize = None
598                try :   
599                    lastjob.PrecomputedJobPrice = float(fields.get("pykotaPrecomputedJobPrice", [0.0])[0])
600                except ValueError :   
601                    lastjob.PrecomputedJobPrice = None
602                if lastjob.JobTitle == lastjob.JobFileName == lastjob.JobOptions == "hidden" :
603                    (lastjob.JobTitle, lastjob.JobFileName, lastjob.JobOptions) = (_("Hidden because of privacy concerns"),) * 3
604                date = fields.get("createTimestamp", ["19700101000000Z"])[0] # It's in UTC !
605                mxtime = DateTime.strptime(date[:14], "%Y%m%d%H%M%S").localtime()
606                lastjob.JobDate = mxtime.strftime("%Y%m%d %H:%M:%S")
607                lastjob.Exists = 1
608        return lastjob
609       
610    def getGroupMembersFromBackend(self, group) :       
611        """Returns the group's members list."""
612        groupmembers = []
613        gname = self.userCharsetToDatabase(group.Name)
614        result = self.doSearch("(&(objectClass=pykotaGroup)(|(pykotaGroupName=%s)(%s=%s)))" % \
615                                  (gname, self.info["grouprdn"], gname), \
616                                  [self.info["groupmembers"]], \
617                                  base=self.info["groupbase"])
618        if result :
619            for username in result[0][1].get(self.info["groupmembers"], []) :
620                groupmembers.append(self.getUser(self.databaseToUserCharset(username)))
621        return groupmembers       
622       
623    def getUserGroupsFromBackend(self, user) :       
624        """Returns the user's groups list."""
625        groups = []
626        uname = self.userCharsetToDatabase(user.Name)
627        result = self.doSearch("(&(objectClass=pykotaGroup)(%s=%s))" % \
628                                  (self.info["groupmembers"], uname), \
629                                  [self.info["grouprdn"], "pykotaGroupName", "pykotaLimitBy"], \
630                                  base=self.info["groupbase"])
631        if result :
632            for (groupid, fields) in result :
633                groupname = self.databaseToUserCharset((fields.get("pykotaGroupName", [None]) or fields.get(self.info["grouprdn"], [None]))[0])
634                group = self.getFromCache("GROUPS", groupname)
635                if group is None :
636                    group = StorageGroup(self, groupname)
637                    group.ident = groupid
638                    group.LimitBy = fields.get("pykotaLimitBy")
639                    if group.LimitBy is not None :
640                        group.LimitBy = group.LimitBy[0]
641                    else :   
642                        group.LimitBy = "quota"
643                    group.AccountBalance = 0.0
644                    group.LifeTimePaid = 0.0
645                    for member in self.getGroupMembers(group) :
646                        if member.Exists :
647                            group.AccountBalance += member.AccountBalance
648                            group.LifeTimePaid += member.LifeTimePaid
649                    group.Exists = 1
650                    self.cacheEntry("GROUPS", group.Name, group)
651                groups.append(group)
652        return groups       
653       
654    def getParentPrintersFromBackend(self, printer) :   
655        """Get all the printer groups this printer is a member of."""
656        pgroups = []
657        result = self.doSearch("(&(objectClass=pykotaPrinter)(uniqueMember=%s))" % \
658                                  printer.ident, \
659                                  ["pykotaPrinterName"], \
660                                  base=self.info["printerbase"])
661        if result :
662            for (printerid, fields) in result :
663                if printerid != printer.ident : # In case of integrity violation.
664                    parentprinter = self.getPrinter(self.databaseToUserCharset(fields.get("pykotaPrinterName")[0]))
665                    if parentprinter.Exists :
666                        pgroups.append(parentprinter)
667        return pgroups
668       
669    def getMatchingPrinters(self, printerpattern) :
670        """Returns the list of all printers for which name matches a certain pattern."""
671        printers = []
672        # see comment at the same place in pgstorage.py
673        result = self.doSearch("objectClass=pykotaPrinter", \
674                                  ["pykotaPrinterName", "pykotaPricePerPage", "pykotaPricePerJob", "pykotaMaxJobSize", "pykotaPassThrough", "uniqueMember", "description"], \
675                                  base=self.info["printerbase"])
676        if result :
677            patterns = printerpattern.split(",")
678            for (printerid, fields) in result :
679                printername = self.databaseToUserCharset(fields.get("pykotaPrinterName", [""])[0] or fields.get(self.info["printerrdn"], [""])[0])
680                if self.tool.matchString(printername, patterns) :
681                    printer = StoragePrinter(self, printername)
682                    printer.ident = printerid
683                    printer.PricePerJob = float(fields.get("pykotaPricePerJob", [0.0])[0] or 0.0)
684                    printer.PricePerPage = float(fields.get("pykotaPricePerPage", [0.0])[0] or 0.0)
685                    printer.MaxJobSize = int(fields.get("pykotaMaxJobSize", [0])[0])
686                    printer.PassThrough = fields.get("pykotaPassThrough", [None])[0]
687                    if printer.PassThrough in (1, "1", "t", "true", "TRUE", "True") :
688                        printer.PassThrough = 1
689                    else :
690                        printer.PassThrough = 0
691                    printer.uniqueMember = fields.get("uniqueMember", [])
692                    printer.Description = self.databaseToUserCharset(fields.get("description", [""])[0]) 
693                    printer.Exists = 1
694                    printers.append(printer)
695                    self.cacheEntry("PRINTERS", printer.Name, printer)
696        return printers       
697       
698    def getMatchingUsers(self, userpattern) :
699        """Returns the list of all users for which name matches a certain pattern."""
700        users = []
701        # see comment at the same place in pgstorage.py
702        result = self.doSearch("objectClass=pykotaAccount", \
703                                  ["pykotaUserName", "pykotaLimitBy", self.info["usermail"], "pykotaOverCharge"], \
704                                  base=self.info["userbase"])
705        if result :
706            patterns = userpattern.split(",")
707            for (userid, fields) in result :
708                username = self.databaseToUserCharset(fields.get("pykotaUserName", [""])[0] or fields.get(self.info["userrdn"], [""])[0])
709                if self.tool.matchString(username, patterns) :
710                    user = StorageUser(self, username)
711                    user.ident = userid
712                    user.Email = fields.get(self.info["usermail"], [None])[0]
713                    user.LimitBy = fields.get("pykotaLimitBy", ["quota"])[0]
714                    user.OverCharge = float(fields.get("pykotaOverCharge", [1.0])[0])
715                    uname = self.userCharsetToDatabase(username)
716                    result = self.doSearch("(&(objectClass=pykotaAccountBalance)(|(pykotaUserName=%s)(%s=%s)))" % \
717                                              (uname, self.info["balancerdn"], uname), \
718                                              ["pykotaBalance", "pykotaLifeTimePaid", "pykotaPayments"], \
719                                              base=self.info["balancebase"])
720                    if not result :
721                        raise PyKotaStorageError, _("No pykotaAccountBalance object found for user %s. Did you create LDAP entries manually ?") % username
722                    else :
723                        fields = result[0][1]
724                        user.idbalance = result[0][0]
725                        user.AccountBalance = fields.get("pykotaBalance")
726                        if user.AccountBalance is not None :
727                            if user.AccountBalance[0].upper() == "NONE" :
728                                user.AccountBalance = None
729                            else :   
730                                user.AccountBalance = float(user.AccountBalance[0])
731                        user.AccountBalance = user.AccountBalance or 0.0       
732                        user.LifeTimePaid = fields.get("pykotaLifeTimePaid")
733                        if user.LifeTimePaid is not None :
734                            if user.LifeTimePaid[0].upper() == "NONE" :
735                                user.LifeTimePaid = None
736                            else :   
737                                user.LifeTimePaid = float(user.LifeTimePaid[0])
738                        user.LifeTimePaid = user.LifeTimePaid or 0.0       
739                        user.Payments = []
740                        for payment in fields.get("pykotaPayments", []) :
741                            try :
742                                (date, amount, description) = payment.split(" # ")
743                            except ValueError :
744                                # Payment with no description (old Payment)
745                                (date, amount) = payment.split(" # ")
746                                description = ""
747                            else :   
748                                description = self.databaseToUserCharset(base64.decodestring(description))
749                            user.Payments.append((date, float(amount), description))
750                    user.Exists = 1
751                    users.append(user)
752                    self.cacheEntry("USERS", user.Name, user)
753        return users       
754       
755    def getMatchingGroups(self, grouppattern) :
756        """Returns the list of all groups for which name matches a certain pattern."""
757        groups = []
758        # see comment at the same place in pgstorage.py
759        result = self.doSearch("objectClass=pykotaGroup", \
760                                  ["pykotaGroupName", "pykotaLimitBy"], \
761                                  base=self.info["groupbase"])
762        if result :
763            patterns = grouppattern.split(",")
764            for (groupid, fields) in result :
765                groupname = self.databaseToUserCharset(fields.get("pykotaGroupName", [""])[0] or fields.get(self.info["grouprdn"], [""])[0])
766                if self.tool.matchString(groupname, patterns) :
767                    group = StorageGroup(self, groupname)
768                    group.ident = groupid
769                    group.Name = fields.get("pykotaGroupName", [self.databaseToUserCharset(groupname)])[0] 
770                    group.LimitBy = fields.get("pykotaLimitBy", ["quota"])[0]
771                    group.AccountBalance = 0.0
772                    group.LifeTimePaid = 0.0
773                    for member in self.getGroupMembers(group) :
774                        if member.Exists :
775                            group.AccountBalance += member.AccountBalance
776                            group.LifeTimePaid += member.LifeTimePaid
777                    group.Exists = 1
778        return groups
779       
780    def getPrinterUsersAndQuotas(self, printer, names=["*"]) :       
781        """Returns the list of users who uses a given printer, along with their quotas."""
782        usersandquotas = []
783        pname = self.userCharsetToDatabase(printer.Name)
784        names = [self.userCharsetToDatabase(n) for n in names]
785        if self.info["userquotabase"].lower() == "user" :
786           base = self.info["userbase"]
787        else :
788           base = self.info["userquotabase"]
789        result = self.doSearch("(&(objectClass=pykotaUserPQuota)(pykotaPrinterName=%s)(|%s))" % \
790                                  (pname, "".join(["(pykotaUserName=%s)" % uname for uname in names])), \
791                                  ["pykotaUserName", "pykotaPageCounter", "pykotaLifePageCounter", "pykotaSoftLimit", "pykotaHardLimit", "pykotaDateLimit", "pykotaWarnCount"], \
792                                  base=base)
793        if result :
794            for (userquotaid, fields) in result :
795                user = self.getUser(self.databaseToUserCharset(fields.get("pykotaUserName")[0]))
796                userpquota = StorageUserPQuota(self, user, printer)
797                userpquota.ident = userquotaid
798                userpquota.PageCounter = int(fields.get("pykotaPageCounter", [0])[0])
799                userpquota.LifePageCounter = int(fields.get("pykotaLifePageCounter", [0])[0])
800                userpquota.WarnCount = int(fields.get("pykotaWarnCount", [0])[0])
801                userpquota.SoftLimit = fields.get("pykotaSoftLimit")
802                if userpquota.SoftLimit is not None :
803                    if userpquota.SoftLimit[0].upper() == "NONE" :
804                        userpquota.SoftLimit = None
805                    else :   
806                        userpquota.SoftLimit = int(userpquota.SoftLimit[0])
807                userpquota.HardLimit = fields.get("pykotaHardLimit")
808                if userpquota.HardLimit is not None :
809                    if userpquota.HardLimit[0].upper() == "NONE" :
810                        userpquota.HardLimit = None
811                    elif userpquota.HardLimit is not None :   
812                        userpquota.HardLimit = int(userpquota.HardLimit[0])
813                userpquota.DateLimit = fields.get("pykotaDateLimit")
814                if userpquota.DateLimit is not None :
815                    if userpquota.DateLimit[0].upper() == "NONE" : 
816                        userpquota.DateLimit = None
817                    else :   
818                        userpquota.DateLimit = userpquota.DateLimit[0]
819                userpquota.Exists = 1
820                usersandquotas.append((user, userpquota))
821                self.cacheEntry("USERPQUOTAS", "%s@%s" % (user.Name, printer.Name), userpquota)
822        usersandquotas.sort(lambda x, y : cmp(x[0].Name, y[0].Name))           
823        return usersandquotas
824               
825    def getPrinterGroupsAndQuotas(self, printer, names=["*"]) :       
826        """Returns the list of groups which uses a given printer, along with their quotas."""
827        groupsandquotas = []
828        pname = self.userCharsetToDatabase(printer.Name)
829        names = [self.userCharsetToDatabase(n) for n in names]
830        if self.info["groupquotabase"].lower() == "group" :
831           base = self.info["groupbase"]
832        else :
833           base = self.info["groupquotabase"]
834        result = self.doSearch("(&(objectClass=pykotaGroupPQuota)(pykotaPrinterName=%s)(|%s))" % \
835                                  (pname, "".join(["(pykotaGroupName=%s)" % gname for gname in names])), \
836                                  ["pykotaGroupName"], \
837                                  base=base)
838        if result :
839            for (groupquotaid, fields) in result :
840                group = self.getGroup(self.databaseToUserCharset(fields.get("pykotaGroupName")[0]))
841                grouppquota = self.getGroupPQuota(group, printer)
842                groupsandquotas.append((group, grouppquota))
843        groupsandquotas.sort(lambda x, y : cmp(x[0].Name, y[0].Name))           
844        return groupsandquotas
845       
846    def addPrinter(self, printername) :       
847        """Adds a printer to the quota storage, returns it."""
848        printername = self.userCharsetToDatabase(printername)
849        fields = { self.info["printerrdn"] : printername,
850                   "objectClass" : ["pykotaObject", "pykotaPrinter"],
851                   "cn" : printername,
852                   "pykotaPrinterName" : printername,
853                   "pykotaPricePerPage" : "0.0",
854                   "pykotaPricePerJob" : "0.0",
855                   "pykotaMaxJobSize" : "0",
856                   "pykotaPassThrough" : "0",
857                 } 
858        dn = "%s=%s,%s" % (self.info["printerrdn"], printername, self.info["printerbase"])
859        self.doAdd(dn, fields)
860        return self.getPrinter(printername)
861       
862    def addUser(self, user) :       
863        """Adds a user to the quota storage, returns it."""
864        uname = self.userCharsetToDatabase(user.Name)
865        newfields = {
866                       "pykotaUserName" : uname,
867                       "pykotaLimitBy" : (user.LimitBy or "quota"),
868                       "pykotaOverCharge" : str(user.OverCharge),
869                    }   
870                       
871        if user.Email :
872            newfields.update({self.info["usermail"]: user.Email})
873        mustadd = 1
874        if self.info["newuser"].lower() != 'below' :
875            try :
876                (where, action) = [s.strip() for s in self.info["newuser"].split(",")]
877            except ValueError :
878                (where, action) = (self.info["newuser"].strip(), "fail")
879            result = self.doSearch("(&(objectClass=%s)(%s=%s))" % \
880                                      (where, self.info["userrdn"], uname), \
881                                      None, \
882                                      base=self.info["userbase"])
883            if result :
884                (dn, fields) = result[0]
885                oc = fields.get("objectClass", fields.get("objectclass", []))
886                oc.extend(["pykotaAccount", "pykotaAccountBalance"])
887                fields.update(newfields)
888                fields.update({ "pykotaBalance" : str(user.AccountBalance or 0.0),
889                                "pykotaLifeTimePaid" : str(user.LifeTimePaid or 0.0), })   
890                self.doModify(dn, fields)
891                mustadd = 0
892            else :
893                message = _("Unable to find an existing objectClass %s entry with %s=%s to attach pykotaAccount objectClass") % (where, self.info["userrdn"], user.Name)
894                if action.lower() == "warn" :   
895                    self.tool.printInfo(_("%s. A new entry will be created instead.") % message, "warn")
896                else : # 'fail' or incorrect setting
897                    raise PyKotaStorageError, "%s. Action aborted. Please check your configuration." % message
898               
899        if mustadd :
900            if self.info["userbase"] == self.info["balancebase"] :           
901                fields = { self.info["userrdn"] : uname,
902                           "objectClass" : ["pykotaObject", "pykotaAccount", "pykotaAccountBalance"],
903                           "cn" : uname,
904                           "pykotaBalance" : str(user.AccountBalance or 0.0),
905                           "pykotaLifeTimePaid" : str(user.LifeTimePaid or 0.0), 
906                         } 
907            else :             
908                fields = { self.info["userrdn"] : uname,
909                           "objectClass" : ["pykotaObject", "pykotaAccount"],
910                           "cn" : uname,
911                         } 
912            fields.update(newfields)         
913            dn = "%s=%s,%s" % (self.info["userrdn"], uname, self.info["userbase"])
914            self.doAdd(dn, fields)
915            if self.info["userbase"] != self.info["balancebase"] :           
916                fields = { self.info["balancerdn"] : uname,
917                           "objectClass" : ["pykotaObject", "pykotaAccountBalance"],
918                           "cn" : uname,
919                           "pykotaBalance" : str(user.AccountBalance or 0.0),
920                           "pykotaLifeTimePaid" : str(user.LifeTimePaid or 0.0), 
921                         } 
922                dn = "%s=%s,%s" % (self.info["balancerdn"], uname, self.info["balancebase"])
923                self.doAdd(dn, fields)
924           
925        return self.getUser(user.Name)
926       
927    def addGroup(self, group) :       
928        """Adds a group to the quota storage, returns it."""
929        gname = self.userCharsetToDatabase(group.Name)
930        newfields = { 
931                      "pykotaGroupName" : gname,
932                      "pykotaLimitBy" : (group.LimitBy or "quota"),
933                    } 
934        mustadd = 1
935        if self.info["newgroup"].lower() != 'below' :
936            try :
937                (where, action) = [s.strip() for s in self.info["newgroup"].split(",")]
938            except ValueError :
939                (where, action) = (self.info["newgroup"].strip(), "fail")
940            result = self.doSearch("(&(objectClass=%s)(%s=%s))" % \
941                                      (where, self.info["grouprdn"], gname), \
942                                      None, \
943                                      base=self.info["groupbase"])
944            if result :
945                (dn, fields) = result[0]
946                oc = fields.get("objectClass", fields.get("objectclass", []))
947                oc.extend(["pykotaGroup"])
948                fields.update(newfields)
949                self.doModify(dn, fields)
950                mustadd = 0
951            else :
952                message = _("Unable to find an existing entry to attach pykotaGroup objectclass %s") % group.Name
953                if action.lower() == "warn" :   
954                    self.tool.printInfo("%s. A new entry will be created instead." % message, "warn")
955                else : # 'fail' or incorrect setting
956                    raise PyKotaStorageError, "%s. Action aborted. Please check your configuration." % message
957               
958        if mustadd :
959            fields = { self.info["grouprdn"] : gname,
960                       "objectClass" : ["pykotaObject", "pykotaGroup"],
961                       "cn" : gname,
962                     } 
963            fields.update(newfields)         
964            dn = "%s=%s,%s" % (self.info["grouprdn"], gname, self.info["groupbase"])
965            self.doAdd(dn, fields)
966        return self.getGroup(group.Name)
967       
968    def addUserToGroup(self, user, group) :   
969        """Adds an user to a group."""
970        if user.Name not in [u.Name for u in self.getGroupMembers(group)] :
971            result = self.doSearch("objectClass=pykotaGroup", None, base=group.ident, scope=ldap.SCOPE_BASE)   
972            if result :
973                fields = result[0][1]
974                if not fields.has_key(self.info["groupmembers"]) :
975                    fields[self.info["groupmembers"]] = []
976                fields[self.info["groupmembers"]].append(self.userCharsetToDatabase(user.Name))
977                self.doModify(group.ident, fields)
978                group.Members.append(user)
979               
980    def addUserPQuota(self, user, printer) :
981        """Initializes a user print quota on a printer."""
982        uuid = self.genUUID()
983        uname = self.userCharsetToDatabase(user.Name)
984        pname = self.userCharsetToDatabase(printer.Name)
985        fields = { "cn" : uuid,
986                   "objectClass" : ["pykotaObject", "pykotaUserPQuota"],
987                   "pykotaUserName" : uname,
988                   "pykotaPrinterName" : pname,
989                   "pykotaDateLimit" : "None",
990                   "pykotaPageCounter" : "0",
991                   "pykotaLifePageCounter" : "0",
992                   "pykotaWarnCount" : "0",
993                 } 
994        if self.info["userquotabase"].lower() == "user" :
995            dn = "cn=%s,%s" % (uuid, user.ident)
996        else :   
997            dn = "cn=%s,%s" % (uuid, self.info["userquotabase"])
998        self.doAdd(dn, fields)
999        return self.getUserPQuota(user, printer)
1000       
1001    def addGroupPQuota(self, group, printer) :
1002        """Initializes a group print quota on a printer."""
1003        uuid = self.genUUID()
1004        gname = self.userCharsetToDatabase(group.Name)
1005        pname = self.userCharsetToDatabase(printer.Name)
1006        fields = { "cn" : uuid,
1007                   "objectClass" : ["pykotaObject", "pykotaGroupPQuota"],
1008                   "pykotaGroupName" : gname,
1009                   "pykotaPrinterName" : pname,
1010                   "pykotaDateLimit" : "None",
1011                 } 
1012        if self.info["groupquotabase"].lower() == "group" :
1013            dn = "cn=%s,%s" % (uuid, group.ident)
1014        else :   
1015            dn = "cn=%s,%s" % (uuid, self.info["groupquotabase"])
1016        self.doAdd(dn, fields)
1017        return self.getGroupPQuota(group, printer)
1018       
1019    def savePrinter(self, printer) :   
1020        """Saves the printer to the database in a single operation."""
1021        fields = {
1022                   "pykotaPassThrough" : (printer.PassThrough and "t") or "f",
1023                   "pykotaMaxJobSize" : (printer.MaxJobSize and str(printer.MaxJobSize)) or "0",
1024                   "description" : self.userCharsetToDatabase(printer.Description or ""),
1025                   "pykotaPricePerPage" : str(printer.PricePerPage),
1026                   "pykotaPricePerJob" : str(printer.PricePerJob),
1027                 }
1028        self.doModify(printer.ident, fields)
1029       
1030    def writeUserOverCharge(self, user, factor) :
1031        """Sets the user's overcharging coefficient."""
1032        fields = {
1033                   "pykotaOverCharge" : str(factor),
1034                 }
1035        self.doModify(user.ident, fields)
1036       
1037    def writeUserLimitBy(self, user, limitby) :   
1038        """Sets the user's limiting factor."""
1039        fields = {
1040                   "pykotaLimitBy" : limitby,
1041                 }
1042        self.doModify(user.ident, fields)         
1043       
1044    def writeGroupLimitBy(self, group, limitby) :   
1045        """Sets the group's limiting factor."""
1046        fields = {
1047                   "pykotaLimitBy" : limitby,
1048                 }
1049        self.doModify(group.ident, fields)         
1050       
1051    def writeUserPQuotaDateLimit(self, userpquota, datelimit) :   
1052        """Sets the date limit permanently for a user print quota."""
1053        fields = {
1054                   "pykotaDateLimit" : datelimit,
1055                 }
1056        return self.doModify(userpquota.ident, fields)
1057           
1058    def writeGroupPQuotaDateLimit(self, grouppquota, datelimit) :   
1059        """Sets the date limit permanently for a group print quota."""
1060        fields = {
1061                   "pykotaDateLimit" : datelimit,
1062                 }
1063        return self.doModify(grouppquota.ident, fields)
1064       
1065    def increaseUserPQuotaPagesCounters(self, userpquota, nbpages) :   
1066        """Increase page counters for a user print quota."""
1067        fields = {
1068                   "pykotaPageCounter" : { "operator" : "+", "value" : nbpages, "convert" : int },
1069                   "pykotaLifePageCounter" : { "operator" : "+", "value" : nbpages, "convert" : int },
1070                 }
1071        return self.doModify(userpquota.ident, fields)         
1072       
1073    def writeUserPQuotaPagesCounters(self, userpquota, newpagecounter, newlifepagecounter) :   
1074        """Sets the new page counters permanently for a user print quota."""
1075        fields = {
1076                   "pykotaPageCounter" : str(newpagecounter),
1077                   "pykotaLifePageCounter" : str(newlifepagecounter),
1078                   "pykotaDateLimit" : None,
1079                   "pykotaWarnCount" : "0",
1080                 } 
1081        return self.doModify(userpquota.ident, fields)         
1082       
1083    def decreaseUserAccountBalance(self, user, amount) :   
1084        """Decreases user's account balance from an amount."""
1085        fields = {
1086                   "pykotaBalance" : { "operator" : "-", "value" : amount, "convert" : float },
1087                 }
1088        return self.doModify(user.idbalance, fields, flushcache=1)         
1089       
1090    def writeUserAccountBalance(self, user, newbalance, newlifetimepaid=None) :   
1091        """Sets the new account balance and eventually new lifetime paid."""
1092        fields = {
1093                   "pykotaBalance" : str(newbalance),
1094                 }
1095        if newlifetimepaid is not None :
1096            fields.update({ "pykotaLifeTimePaid" : str(newlifetimepaid) })
1097        return self.doModify(user.idbalance, fields)         
1098           
1099    def writeNewPayment(self, user, amount, comment="") :
1100        """Adds a new payment to the payments history."""
1101        payments = []
1102        for payment in user.Payments :
1103            payments.append("%s # %s # %s" % (payment[0], str(payment[1]), base64.encodestring(self.userCharsetToDatabase(payment[2])).strip()))
1104        payments.append("%s # %s # %s" % (str(DateTime.now()), str(amount), base64.encodestring(self.userCharsetToDatabase(comment)).strip()))
1105        fields = {
1106                   "pykotaPayments" : payments,
1107                 }
1108        return self.doModify(user.idbalance, fields)         
1109       
1110    def writeLastJobSize(self, lastjob, jobsize, jobprice) :       
1111        """Sets the last job's size permanently."""
1112        fields = {
1113                   "pykotaJobSize" : str(jobsize),
1114                   "pykotaJobPrice" : str(jobprice),
1115                 }
1116        self.doModify(lastjob.ident, fields)         
1117       
1118    def writeJobNew(self, printer, user, jobid, 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) :
1119        """Adds a job in a printer's history."""
1120        uname = self.userCharsetToDatabase(user.Name)
1121        pname = self.userCharsetToDatabase(printer.Name)
1122        if (not self.disablehistory) or (not printer.LastJob.Exists) :
1123            uuid = self.genUUID()
1124            dn = "cn=%s,%s" % (uuid, self.info["jobbase"])
1125        else :   
1126            uuid = printer.LastJob.ident[3:].split(",")[0]
1127            dn = printer.LastJob.ident
1128        if self.privacy :   
1129            # For legal reasons, we want to hide the title, filename and options
1130            title = filename = options = "hidden"
1131        fields = {
1132                   "objectClass" : ["pykotaObject", "pykotaJob"],
1133                   "cn" : uuid,
1134                   "pykotaUserName" : uname,
1135                   "pykotaPrinterName" : pname,
1136                   "pykotaJobId" : jobid,
1137                   "pykotaPrinterPageCounter" : str(pagecounter),
1138                   "pykotaAction" : action,
1139                   "pykotaFileName" : ((filename is None) and "None") or self.userCharsetToDatabase(filename), 
1140                   "pykotaTitle" : ((title is None) and "None") or self.userCharsetToDatabase(title), 
1141                   "pykotaCopies" : str(copies), 
1142                   "pykotaOptions" : ((options is None) and "None") or self.userCharsetToDatabase(options), 
1143                   "pykotaHostName" : str(clienthost), 
1144                   "pykotaJobSizeBytes" : str(jobsizebytes),
1145                   "pykotaMD5Sum" : str(jobmd5sum),
1146                   "pykotaPages" : jobpages,            # don't add this attribute if it is not set, so no string conversion
1147                   "pykotaBillingCode" : self.userCharsetToDatabase(jobbilling), # don't add this attribute if it is not set, so no string conversion
1148                   "pykotaPrecomputedJobSize" : str(precomputedsize),
1149                   "pykotaPrecomputedJobPrice" : str(precomputedprice),
1150                 }
1151        if (not self.disablehistory) or (not printer.LastJob.Exists) :
1152            if jobsize is not None :         
1153                fields.update({ "pykotaJobSize" : str(jobsize), "pykotaJobPrice" : str(jobprice) })
1154            self.doAdd(dn, fields)
1155        else :   
1156            # here we explicitly want to reset jobsize to 'None' if needed
1157            fields.update({ "pykotaJobSize" : str(jobsize), "pykotaJobPrice" : str(jobprice) })
1158            self.doModify(dn, fields)
1159           
1160        if printer.LastJob.Exists :
1161            fields = {
1162                       "pykotaLastJobIdent" : uuid,
1163                     }
1164            self.doModify(printer.LastJob.lastjobident, fields)         
1165        else :   
1166            lastjuuid = self.genUUID()
1167            lastjdn = "cn=%s,%s" % (lastjuuid, self.info["lastjobbase"])
1168            fields = {
1169                       "objectClass" : ["pykotaObject", "pykotaLastJob"],
1170                       "cn" : lastjuuid,
1171                       "pykotaPrinterName" : pname,
1172                       "pykotaLastJobIdent" : uuid,
1173                     } 
1174            self.doAdd(lastjdn, fields)         
1175           
1176    def writeUserPQuotaLimits(self, userpquota, softlimit, hardlimit) :
1177        """Sets soft and hard limits for a user quota."""
1178        fields = { 
1179                   "pykotaSoftLimit" : str(softlimit),
1180                   "pykotaHardLimit" : str(hardlimit),
1181                   "pykotaDateLimit" : "None",
1182                   "pykotaWarnCount" : "0",
1183                 }
1184        self.doModify(userpquota.ident, fields)
1185       
1186    def writeUserPQuotaWarnCount(self, userpquota, warncount) :
1187        """Sets the warn counter value for a user quota."""
1188        fields = { 
1189                   "pykotaWarnCount" : str(warncount or 0),
1190                 }
1191        self.doModify(userpquota.ident, fields)
1192       
1193    def increaseUserPQuotaWarnCount(self, userpquota) :
1194        """Increases the warn counter value for a user quota."""
1195        fields = {
1196                   "pykotaWarnCount" : { "operator" : "+", "value" : 1, "convert" : int },
1197                 }
1198        return self.doModify(userpquota.ident, fields)         
1199       
1200    def writeGroupPQuotaLimits(self, grouppquota, softlimit, hardlimit) :
1201        """Sets soft and hard limits for a group quota on a specific printer."""
1202        fields = { 
1203                   "pykotaSoftLimit" : str(softlimit),
1204                   "pykotaHardLimit" : str(hardlimit),
1205                   "pykotaDateLimit" : "None",
1206                 }
1207        self.doModify(grouppquota.ident, fields)
1208           
1209    def writePrinterToGroup(self, pgroup, printer) :
1210        """Puts a printer into a printer group."""
1211        if printer.ident not in pgroup.uniqueMember :
1212            pgroup.uniqueMember.append(printer.ident)
1213            fields = {
1214                       "uniqueMember" : pgroup.uniqueMember
1215                     } 
1216            self.doModify(pgroup.ident, fields)         
1217           
1218    def removePrinterFromGroup(self, pgroup, printer) :
1219        """Removes a printer from a printer group."""
1220        try :
1221            pgroup.uniqueMember.remove(printer.ident)
1222        except ValueError :   
1223            pass
1224        else :   
1225            fields = {
1226                       "uniqueMember" : pgroup.uniqueMember,
1227                     } 
1228            self.doModify(pgroup.ident, fields)         
1229           
1230    def retrieveHistory(self, user=None, printer=None, hostname=None, billingcode=None, limit=100, start=None, end=None) :
1231        """Retrieves all print jobs for user on printer (or all) between start and end date, limited to first 100 results."""
1232        precond = "(objectClass=pykotaJob)"
1233        where = []
1234        if user is not None :
1235            where.append("(pykotaUserName=%s)" % self.userCharsetToDatabase(user.Name))
1236        if printer is not None :
1237            where.append("(pykotaPrinterName=%s)" % self.userCharsetToDatabase(printer.Name))
1238        if hostname is not None :
1239            where.append("(pykotaHostName=%s)" % hostname)
1240        if billingcode is not None :
1241            where.append("(pykotaBillingCode=%s)" % self.userCharsetToDatabase(billingcode))
1242        if where :   
1243            where = "(&%s)" % "".join([precond] + where)
1244        else :   
1245            where = precond
1246        jobs = []   
1247        result = self.doSearch(where, fields=[ "pykotaJobSizeBytes", 
1248                                               "pykotaHostName", 
1249                                               "pykotaUserName", 
1250                                               "pykotaPrinterName", 
1251                                               "pykotaJobId", 
1252                                               "pykotaPrinterPageCounter", 
1253                                               "pykotaAction", 
1254                                               "pykotaJobSize", 
1255                                               "pykotaJobPrice", 
1256                                               "pykotaFileName", 
1257                                               "pykotaTitle", 
1258                                               "pykotaCopies", 
1259                                               "pykotaOptions", 
1260                                               "pykotaBillingCode", 
1261                                               "pykotaPages", 
1262                                               "pykotaMD5Sum", 
1263                                               "pykotaPrecomputedJobSize",
1264                                               "pykotaPrecomputedJobPrice",
1265                                               "createTimestamp" ], 
1266                                      base=self.info["jobbase"])
1267        if result :
1268            for (ident, fields) in result :
1269                job = StorageJob(self)
1270                job.ident = ident
1271                job.JobId = fields.get("pykotaJobId")[0]
1272                job.PrinterPageCounter = int(fields.get("pykotaPrinterPageCounter", [0])[0] or 0)
1273                try :
1274                    job.JobSize = int(fields.get("pykotaJobSize", [0])[0])
1275                except ValueError :   
1276                    job.JobSize = None
1277                try :   
1278                    job.JobPrice = float(fields.get("pykotaJobPrice", [0.0])[0])
1279                except ValueError :
1280                    job.JobPrice = None
1281                job.JobAction = fields.get("pykotaAction", [""])[0]
1282                job.JobFileName = self.databaseToUserCharset(fields.get("pykotaFileName", [""])[0]) 
1283                job.JobTitle = self.databaseToUserCharset(fields.get("pykotaTitle", [""])[0]) 
1284                job.JobCopies = int(fields.get("pykotaCopies", [0])[0])
1285                job.JobOptions = self.databaseToUserCharset(fields.get("pykotaOptions", [""])[0]) 
1286                job.JobHostName = fields.get("pykotaHostName", [""])[0]
1287                job.JobSizeBytes = fields.get("pykotaJobSizeBytes", [0L])[0]
1288                job.JobBillingCode = self.databaseToUserCharset(fields.get("pykotaBillingCode", [None])[0])
1289                job.JobMD5Sum = fields.get("pykotaMD5Sum", [None])[0]
1290                job.JobPages = fields.get("pykotaPages", [""])[0]
1291                try :
1292                    job.PrecomputedJobSize = int(fields.get("pykotaPrecomputedJobSize", [0])[0])
1293                except ValueError :   
1294                    job.PrecomputedJobSize = None
1295                try :   
1296                    job.PrecomputedJobPrice = float(fields.get("pykotaPrecomputedJobPrice", [0.0])[0])
1297                except ValueError :
1298                    job.PrecomputedJobPrice = None
1299                if job.JobTitle == job.JobFileName == job.JobOptions == "hidden" :
1300                    (job.JobTitle, job.JobFileName, job.JobOptions) = (_("Hidden because of privacy concerns"),) * 3
1301                date = fields.get("createTimestamp", ["19700101000000Z"])[0] # It's in UTC !
1302                mxtime = DateTime.strptime(date[:14], "%Y%m%d%H%M%S").localtime()
1303                job.JobDate = mxtime.strftime("%Y%m%d %H:%M:%S")
1304                if ((start is None) and (end is None)) or \
1305                   ((start is None) and (job.JobDate <= end)) or \
1306                   ((end is None) and (job.JobDate >= start)) or \
1307                   ((job.JobDate >= start) and (job.JobDate <= end)) :
1308                    job.UserName = self.databaseToUserCharset(fields.get("pykotaUserName")[0])
1309                    job.PrinterName = self.databaseToUserCharset(fields.get("pykotaPrinterName")[0])
1310                    job.Exists = 1
1311                    jobs.append(job)
1312            jobs.sort(lambda x, y : cmp(y.JobDate, x.JobDate))       
1313            if limit :   
1314                jobs = jobs[:int(limit)]
1315        return jobs
1316       
1317    def deleteUser(self, user) :   
1318        """Completely deletes an user from the Quota Storage."""
1319        uname = self.userCharsetToDatabase(user.Name)
1320        todelete = []   
1321        result = self.doSearch("(&(objectClass=pykotaJob)(pykotaUserName=%s))" % uname, base=self.info["jobbase"])
1322        for (ident, fields) in result :
1323            todelete.append(ident)
1324        if self.info["userquotabase"].lower() == "user" :
1325            base = self.info["userbase"]
1326        else :
1327            base = self.info["userquotabase"]
1328        result = self.doSearch("(&(objectClass=pykotaUserPQuota)(pykotaUserName=%s))" % uname, \
1329                                  ["pykotaPrinterName", "pykotaUserName"], \
1330                                  base=base)
1331        for (ident, fields) in result :
1332            # ensure the user print quota entry will be deleted
1333            todelete.append(ident)
1334           
1335            # if last job of current printer was printed by the user
1336            # to delete, we also need to delete the printer's last job entry.
1337            printer = self.getPrinter(self.databaseToUserCharset(fields["pykotaPrinterName"][0]))
1338            if printer.LastJob.UserName == user.Name :
1339                todelete.append(printer.LastJob.lastjobident)
1340           
1341        for ident in todelete :   
1342            self.doDelete(ident)
1343           
1344        result = self.doSearch("objectClass=pykotaAccount", None, base=user.ident, scope=ldap.SCOPE_BASE)   
1345        if result :
1346            fields = result[0][1]
1347            for k in fields.keys() :
1348                if k.startswith("pykota") :
1349                    del fields[k]
1350                elif k.lower() == "objectclass" :   
1351                    todelete = []
1352                    for i in range(len(fields[k])) :
1353                        if fields[k][i].startswith("pykota") : 
1354                            todelete.append(i)
1355                    todelete.sort()       
1356                    todelete.reverse()
1357                    for i in todelete :
1358                        del fields[k][i]
1359            if fields.get("objectClass") or fields.get("objectclass") :
1360                self.doModify(user.ident, fields, ignoreold=0)       
1361            else :   
1362                self.doDelete(user.ident)
1363        result = self.doSearch("(&(objectClass=pykotaAccountBalance)(pykotaUserName=%s))" % \
1364                                   uname, \
1365                                   ["pykotaUserName"], \
1366                                   base=self.info["balancebase"])
1367        for (ident, fields) in result :
1368            self.doDelete(ident)
1369       
1370    def deleteGroup(self, group) :   
1371        """Completely deletes a group from the Quota Storage."""
1372        gname = self.userCharsetToDatabase(group.Name)
1373        if self.info["groupquotabase"].lower() == "group" :
1374            base = self.info["groupbase"]
1375        else :
1376            base = self.info["groupquotabase"]
1377        result = self.doSearch("(&(objectClass=pykotaGroupPQuota)(pykotaGroupName=%s))" % \
1378                                  gname, \
1379                                  ["pykotaGroupName"], \
1380                                  base=base)
1381        for (ident, fields) in result :
1382            self.doDelete(ident)
1383        result = self.doSearch("objectClass=pykotaGroup", None, base=group.ident, scope=ldap.SCOPE_BASE)   
1384        if result :
1385            fields = result[0][1]
1386            for k in fields.keys() :
1387                if k.startswith("pykota") :
1388                    del fields[k]
1389                elif k.lower() == "objectclass" :   
1390                    todelete = []
1391                    for i in range(len(fields[k])) :
1392                        if fields[k][i].startswith("pykota") : 
1393                            todelete.append(i)
1394                    todelete.sort()       
1395                    todelete.reverse()
1396                    for i in todelete :
1397                        del fields[k][i]
1398            if fields.get("objectClass") or fields.get("objectclass") :
1399                self.doModify(group.ident, fields, ignoreold=0)       
1400            else :   
1401                self.doDelete(group.ident)
1402               
1403    def deletePrinter(self, printer) :   
1404        """Completely deletes a printer from the Quota Storage."""
1405        pname = self.userCharsetToDatabase(printer.Name)
1406        result = self.doSearch("(&(objectClass=pykotaLastJob)(pykotaPrinterName=%s))" % pname, base=self.info["lastjobbase"])
1407        for (ident, fields) in result :
1408            self.doDelete(ident)
1409        result = self.doSearch("(&(objectClass=pykotaJob)(pykotaPrinterName=%s))" % pname, base=self.info["jobbase"])
1410        for (ident, fields) in result :
1411            self.doDelete(ident)
1412        if self.info["groupquotabase"].lower() == "group" :
1413            base = self.info["groupbase"]
1414        else :
1415            base = self.info["groupquotabase"]
1416        result = self.doSearch("(&(objectClass=pykotaGroupPQuota)(pykotaPrinterName=%s))" % pname, base=base)
1417        for (ident, fields) in result :
1418            self.doDelete(ident)
1419        if self.info["userquotabase"].lower() == "user" :
1420            base = self.info["userbase"]
1421        else :
1422            base = self.info["userquotabase"]
1423        result = self.doSearch("(&(objectClass=pykotaUserPQuota)(pykotaPrinterName=%s))" % pname, base=base)
1424        for (ident, fields) in result :
1425            self.doDelete(ident)
1426        for parent in self.getParentPrinters(printer) : 
1427            try :
1428                parent.uniqueMember.remove(printer.ident)
1429            except ValueError :   
1430                pass
1431            else :   
1432                fields = {
1433                           "uniqueMember" : parent.uniqueMember,
1434                         } 
1435                self.doModify(parent.ident, fields)         
1436        self.doDelete(printer.ident)   
1437       
1438    def deleteBillingCode(self, code) :
1439        """Deletes a billing code from the Quota Storage (no entries are deleted from the history)"""
1440        self.doDelete(code.ident)
1441       
1442    def extractPrinters(self, extractonly={}) :
1443        """Extracts all printer records."""
1444        pname = extractonly.get("printername")
1445        entries = [p for p in [self.getPrinter(name) for name in self.getAllPrintersNames(pname)] if p.Exists]
1446        if entries :
1447            result = [ ("dn", "printername", "priceperpage", "priceperjob", "description", "maxjobsize", "passthrough") ]
1448            for entry in entries :
1449                if entry.PassThrough in (1, "1", "t", "true", "T", "TRUE", "True") :
1450                    passthrough = "t"
1451                else :   
1452                    passthrough = "f"
1453                result.append((entry.ident, entry.Name, entry.PricePerPage, entry.PricePerJob, entry.Description, entry.MaxJobSize, passthrough))
1454            return result 
1455       
1456    def extractUsers(self, extractonly={}) :
1457        """Extracts all user records."""
1458        uname = extractonly.get("username")
1459        entries = [u for u in [self.getUser(name) for name in self.getAllUsersNames(uname)] if u.Exists]
1460        if entries :
1461            result = [ ("dn", "username", "balance", "lifetimepaid", "limitby", "email") ]
1462            for entry in entries :
1463                result.append((entry.ident, entry.Name, entry.AccountBalance, entry.LifeTimePaid, entry.LimitBy, entry.Email))
1464            return result 
1465       
1466    def extractBillingcodes(self, extractonly={}) :
1467        """Extracts all billing codes records."""
1468        billingcode = extractonly.get("billingcode")
1469        entries = [b for b in [self.getBillingCode(label) for label in self.getAllBillingCodes(billingcode)] if b.Exists]
1470        if entries :
1471            result = [ ("dn", "billingcode", "balance", "pagecounter", "description") ]
1472            for entry in entries :
1473                result.append((entry.ident, entry.BillingCode, entry.Balance, entry.PageCounter, entry.Description))
1474            return result 
1475       
1476    def extractGroups(self, extractonly={}) :
1477        """Extracts all group records."""
1478        gname = extractonly.get("groupname")
1479        entries = [g for g in [self.getGroup(name) for name in self.getAllGroupsNames(gname)] if g.Exists]
1480        if entries :
1481            result = [ ("dn", "groupname", "limitby", "balance", "lifetimepaid") ]
1482            for entry in entries :
1483                result.append((entry.ident, entry.Name, entry.LimitBy, entry.AccountBalance, entry.LifeTimePaid))
1484            return result 
1485       
1486    def extractPayments(self, extractonly={}) :
1487        """Extracts all payment records."""
1488        uname = extractonly.get("username")
1489        entries = [u for u in [self.getUser(name) for name in self.getAllUsersNames(uname)] if u.Exists]
1490        if entries :
1491            result = [ ("username", "amount", "date", "description") ]
1492            for entry in entries :
1493                for (date, amount, description) in entry.Payments :
1494                    result.append((entry.Name, amount, date, description))
1495            return result       
1496       
1497    def extractUpquotas(self, extractonly={}) :
1498        """Extracts all userpquota records."""
1499        pname = extractonly.get("printername")
1500        entries = [p for p in [self.getPrinter(name) for name in self.getAllPrintersNames(pname)] if p.Exists]
1501        if entries :
1502            result = [ ("username", "printername", "dn", "userdn", "printerdn", "lifepagecounter", "pagecounter", "softlimit", "hardlimit", "datelimit") ]
1503            uname = extractonly.get("username")
1504            for entry in entries :
1505                for (user, userpquota) in self.getPrinterUsersAndQuotas(entry, names=[uname or "*"]) :
1506                    result.append((user.Name, entry.Name, userpquota.ident, user.ident, entry.ident, userpquota.LifePageCounter, userpquota.PageCounter, userpquota.SoftLimit, userpquota.HardLimit, userpquota.DateLimit))
1507            return result
1508       
1509    def extractGpquotas(self, extractonly={}) :
1510        """Extracts all grouppquota records."""
1511        pname = extractonly.get("printername")
1512        entries = [p for p in [self.getPrinter(name) for name in self.getAllPrintersNames(pname)] if p.Exists]
1513        if entries :
1514            result = [ ("groupname", "printername", "dn", "groupdn", "printerdn", "lifepagecounter", "pagecounter", "softlimit", "hardlimit", "datelimit") ]
1515            gname = extractonly.get("groupname")
1516            for entry in entries :
1517                for (group, grouppquota) in self.getPrinterGroupsAndQuotas(entry, names=[gname or "*"]) :
1518                    result.append((group.Name, entry.Name, grouppquota.ident, group.ident, entry.ident, grouppquota.LifePageCounter, grouppquota.PageCounter, grouppquota.SoftLimit, grouppquota.HardLimit, grouppquota.DateLimit))
1519            return result
1520       
1521    def extractUmembers(self, extractonly={}) :
1522        """Extracts all user groups members."""
1523        gname = extractonly.get("groupname")
1524        entries = [g for g in [self.getGroup(name) for name in self.getAllGroupsNames(gname)] if g.Exists]
1525        if entries :
1526            result = [ ("groupname", "username", "groupdn", "userdn") ]
1527            uname = extractonly.get("username")
1528            for entry in entries :
1529                for member in entry.Members :
1530                    if (uname is None) or (member.Name == uname) :
1531                        result.append((entry.Name, member.Name, entry.ident, member.ident))
1532            return result       
1533               
1534    def extractPmembers(self, extractonly={}) :
1535        """Extracts all printer groups members."""
1536        pname = extractonly.get("printername")
1537        entries = [p for p in [self.getPrinter(name) for name in self.getAllPrintersNames(pname)] if p.Exists]
1538        if entries :
1539            result = [ ("pgroupname", "printername", "pgroupdn", "printerdn") ]
1540            pgname = extractonly.get("pgroupname")
1541            for entry in entries :
1542                for parent in self.getParentPrinters(entry) :
1543                    if (pgname is None) or (parent.Name == pgname) :
1544                        result.append((parent.Name, entry.Name, parent.ident, entry.ident))
1545            return result       
1546       
1547    def extractHistory(self, extractonly={}) :
1548        """Extracts all jobhistory records."""
1549        uname = extractonly.get("username")
1550        if uname :
1551            user = self.getUser(uname)
1552        else :   
1553            user = None
1554        pname = extractonly.get("printername")
1555        if pname :
1556            printer = self.getPrinter(pname)
1557        else :   
1558            printer = None
1559        startdate = extractonly.get("start")
1560        enddate = extractonly.get("end")
1561        (startdate, enddate) = self.cleanDates(startdate, enddate)
1562        entries = self.retrieveHistory(user, printer, hostname=extractonly.get("hostname"), billingcode=extractonly.get("billingcode"), limit=None, start=startdate, end=enddate)
1563        if entries :
1564            result = [ ("username", "printername", "dn", "jobid", "pagecounter", "jobsize", "action", "jobdate", "filename", "title", "copies", "options", "jobprice", "hostname", "jobsizebytes", "md5sum", "pages", "billingcode", "precomputedjobsize", "precomputedjobprice") ] 
1565            for entry in entries :
1566                result.append((entry.UserName, entry.PrinterName, entry.ident, entry.JobId, entry.PrinterPageCounter, entry.JobSize, entry.JobAction, entry.JobDate, entry.JobFileName, entry.JobTitle, entry.JobCopies, entry.JobOptions, entry.JobPrice, entry.JobHostName, entry.JobSizeBytes, entry.JobMD5Sum, entry.JobPages, entry.JobBillingCode, entry.PrecomputedJobSize, entry.PrecomputedJobPrice)) 
1567            return result
1568           
1569    def getBillingCodeFromBackend(self, label) :
1570        """Extracts billing code information given its label : returns first matching billing code."""
1571        code = StorageBillingCode(self, label)
1572        ulabel = self.userCharsetToDatabase(label)
1573        result = self.doSearch("(&(objectClass=pykotaBilling)(pykotaBillingCode=%s))" % \
1574                                  ulabel, \
1575                                  ["pykotaBillingCode", "pykotaBalance", "pykotaPageCounter", "description"], \
1576                                  base=self.info["billingcodebase"])
1577        if result :
1578            fields = result[0][1]       # take only first matching code, ignore the rest
1579            code.ident = result[0][0]
1580            code.BillingCode = self.databaseToUserCharset(fields.get("pykotaBillingCode", [ulabel])[0])
1581            code.PageCounter = int(fields.get("pykotaPageCounter", [0])[0])
1582            code.Balance = float(fields.get("pykotaBalance", [0.0])[0])
1583            code.Description = self.databaseToUserCharset(fields.get("description", [""])[0]) 
1584            code.Exists = 1
1585        return code   
1586       
1587    def addBillingCode(self, label) :
1588        """Adds a billing code to the quota storage, returns it."""
1589        uuid = self.genUUID()
1590        dn = "cn=%s,%s" % (uuid, self.info["billingcodebase"])
1591        fields = { "objectClass" : ["pykotaObject", "pykotaBilling"],
1592                   "cn" : uuid,
1593                   "pykotaBillingCode" : self.userCharsetToDatabase(label),
1594                   "pykotaPageCounter" : "0",
1595                   "pykotaBalance" : "0.0",
1596                 } 
1597        self.doAdd(dn, fields)
1598        return self.getBillingCode(label)
1599       
1600    def saveBillingCode(self, code) :
1601        """Sets the new description for a billing code."""
1602        fields = {
1603                   "description" : self.userCharsetToDatabase(code.Description or ""), 
1604                   "pykotaPageCounter" : str(code.PageCounter),
1605                   "pykotaBalance" : str(code.Balance),
1606                 }
1607        self.doModify(code.ident, fields)
1608           
1609    def getMatchingBillingCodes(self, billingcodepattern) :
1610        """Returns the list of all billing codes which match a certain pattern."""
1611        codes = []
1612        result = self.doSearch("objectClass=pykotaBilling", \
1613                                ["pykotaBillingCode", "description", "pykotaPageCounter", "pykotaBalance"], \
1614                                base=self.info["billingcodebase"])
1615        if result :
1616            patterns = billingcodepattern.split(",")
1617            for (codeid, fields) in result :
1618                codename = self.databaseToUserCharset(fields.get("pykotaBillingCode", [""])[0])
1619                if self.tool.matchString(codename, patterns) :
1620                    code = StorageBillingCode(self, codename)
1621                    code.ident = codeid
1622                    code.PageCounter = int(fields.get("pykotaPageCounter", [0])[0])
1623                    code.Balance = float(fields.get("pykotaBalance", [0.0])[0])
1624                    code.Description = self.databaseToUserCharset(fields.get("description", [""])[0]) 
1625                    code.Exists = 1
1626                    codes.append(code)
1627                    self.cacheEntry("BILLINGCODES", code.BillingCode, code)
1628        return codes       
1629       
1630    def consumeBillingCode(self, code, pagecounter, balance) :
1631        """Consumes from a billing code."""
1632        fields = {
1633                   "pykotaBalance" : { "operator" : "-", "value" : balance, "convert" : float },
1634                   "pykotaPageCounter" : { "operator" : "+", "value" : pagecounter, "convert" : int },
1635                 }
1636        return self.doModify(code.ident, fields)         
Note: See TracBrowser for help on using the browser.