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

Revision 2459, 79.5 kB (checked in by jerome, 19 years ago)

Did some work to prepare the integration of MaxJobSize? and PassThrough?
mode for printers.

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