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

Revision 3244, 100.9 kB (checked in by jerome, 17 years ago)

Fixed the same problem as yesterday, elsewhere.

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