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

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

Fixed encoding problems for a payment's description

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