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

Revision 3291, 100.5 kB (checked in by jerome, 16 years ago)

Database backends now convert from and to unicode instead of UTF-8.
The data dumper now expects unicode datas from the database.

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